diff --git a/clients/apps/nodejs/test_basic_workflow2.js b/clients/apps/nodejs/test_basic_workflow2.js index fd74d1be..8f432928 100644 --- a/clients/apps/nodejs/test_basic_workflow2.js +++ b/clients/apps/nodejs/test_basic_workflow2.js @@ -58,7 +58,7 @@ const getElectrumClient = async (clientConfig) => { async function generateBlock(numBlocks) { const generateBlockCommand = `docker exec $(docker ps -qf "name=mercurylayer_bitcoind_1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress ${numBlocks} "bcrt1qgh48u8aj4jvjkalc28lqujyx2wveck4jsm59x9"`; - exec(generateBlockCommand); + await exec(generateBlockCommand); console.log(`Generated ${numBlocks} blocks`); const clientConfig = client_config.load(); @@ -868,6 +868,201 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon await exec("docker network connect mercurylayer_default mercurylayer_mercury_1"); } +async function transferSendAtCoinExpiry(clientConfig, wallet_1_name, wallet_2_name) { + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const deposit_info = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + let tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + + let usedToken = tokenList.find(token => token.token_id === tokenId); + + assert(usedToken.spent); + + await depositCoin(clientConfig, wallet_1_name, amount, deposit_info); + + let coin = undefined; + + console.log("coin: ", coin); + + while (!coin) { + const list_coins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + let coinsWithStatechainId = list_coins.filter(c => { + return c.statechain_id === deposit_info.statechain_id && c.status === CoinStatus.CONFIRMED; + }); + + if (coinsWithStatechainId.length === 0) { + console.log("Waiting for coin to be confirmed..."); + console.log(`Check the address ${deposit_info.deposit_address} ...\n`); + await sleep(5000); + generateBlock(1); + continue; + } + + coin = coinsWithStatechainId[0]; + break; + } + + console.log("coin: ", coin); + + const electrumClient = await getElectrumClient(clientConfig); + + const blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) + const currentBlockheight = blockHeader.height; + + const blocksToBeGenerated = coin.locktime - currentBlockheight; + await generateBlock(blocksToBeGenerated); + + let transfer_address = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + try { + coin = await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transfer_address.transfer_receive); + assert.fail("Expected error when transferring expired coin, but no error was thrown"); + } catch (error) { + console.log("Expected error received: ", error.message); + assert(error.message.includes("The coin is expired."), + `Unexpected error message: ${error.message}`); + } +} + +async function transferReceiveAtCoinExpiry(clientConfig, wallet_1_name, wallet_2_name) { + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const deposit_info = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + let tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + + let usedToken = tokenList.find(token => token.token_id === tokenId); + + assert(usedToken.spent); + + await depositCoin(clientConfig, wallet_1_name, amount, deposit_info); + + let coin = undefined; + + console.log("coin: ", coin); + + while (!coin) { + const list_coins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + let coinsWithStatechainId = list_coins.filter(c => { + return c.statechain_id === deposit_info.statechain_id && c.status === CoinStatus.CONFIRMED; + }); + + if (coinsWithStatechainId.length === 0) { + console.log("Waiting for coin to be confirmed..."); + console.log(`Check the address ${deposit_info.deposit_address} ...\n`); + await sleep(5000); + generateBlock(1); + continue; + } + + coin = coinsWithStatechainId[0]; + break; + } + + console.log("coin: ", coin); + + let transfer_address = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + coin = await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transfer_address.transfer_receive); + + const electrumClient = await getElectrumClient(clientConfig); + + const blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) + const currentBlockheight = blockHeader.height; + + const blocksToBeGenerated = coin.locktime - currentBlockheight; + await generateBlock(blocksToBeGenerated); + + let received_statechain_ids = undefined; + + let errorMessage; + console.error = (msg) => { + errorMessage = msg; + }; + + received_statechain_ids = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + // Assert the captured error message + const expectedMessage = 'The coin is expired.'; + assert.ok(errorMessage.includes(expectedMessage)); + console.log("received_statechain_ids: ", received_statechain_ids); + + assert(received_statechain_ids.length > 0); + assert(received_statechain_ids[0] == coin.statechain_id); +} + +async function transferSendCoinExpiryBySending(clientConfig, wallet_1_name, wallet_2_name) { + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const deposit_info = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + let tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + + let usedToken = tokenList.find(token => token.token_id === tokenId); + + assert(usedToken.spent); + + await depositCoin(clientConfig, wallet_1_name, amount, deposit_info); + + let coin = undefined; + + console.log("coin: ", coin); + + while (!coin) { + const list_coins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + let coinsWithStatechainId = list_coins.filter(c => { + return c.statechain_id === deposit_info.statechain_id && c.status === CoinStatus.CONFIRMED; + }); + + if (coinsWithStatechainId.length === 0) { + console.log("Waiting for coin to be confirmed..."); + console.log(`Check the address ${deposit_info.deposit_address} ...\n`); + await sleep(5000); + generateBlock(1); + continue; + } + + coin = coinsWithStatechainId[0]; + break; + } + + console.log("coin: ", coin); + + const electrumClient = await getElectrumClient(clientConfig); + + const blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) + const currentBlockheight = blockHeader.height; + + const serverInfo = await utils.infoConfig(clientConfig, electrumClient); + + const blocksToBeGenerated = coin.locktime - currentBlockheight - serverInfo.interval; + await generateBlock(blocksToBeGenerated); + + let transfer_address = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + try { + coin = await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transfer_address.transfer_receive); + assert.fail("Expected error when transferring expired coin, but no error was thrown"); + } catch (error) { + console.log("Expected error received: ", error.message); + assert(error.message.includes("The coin is expired."), + `Unexpected error message: ${error.message}`); + } +} + (async () => { try { @@ -878,7 +1073,7 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon await createWallet(clientConfig, wallet_1_name); await createWallet(clientConfig, wallet_2_name); await walletTransfersToItselfAndWithdraw(clientConfig, wallet_1_name); - await walletTransfersToAnotherAndBroadcastsBackupTx(clientConfig, wallet_1_name, wallet_2_name); + // await walletTransfersToAnotherAndBroadcastsBackupTx(clientConfig, wallet_1_name, wallet_2_name); // Deposit, repeat send @@ -887,7 +1082,6 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon await createWallet(clientConfig, wallet_3_name); await createWallet(clientConfig, wallet_4_name); await depositAndRepeatSend(clientConfig, wallet_3_name); - await walletTransfersToAnotherAndBroadcastsBackupTx(clientConfig, wallet_3_name, wallet_4_name); console.log("Completed test for Deposit, repeat send"); // Transfer-sender after transfer-receiver @@ -949,6 +1143,45 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon await createWallet(clientConfig, wallet_18_name); await walletTransfersToItselfTillLocktimeReachesBlockHeightAndWithdraw(clientConfig, wallet_18_name); console.log("Completed test for Deposit, iterative self transfer"); + + // Send backup tx before expiry + let wallet_19_name = "w19"; + let wallet_20_name = "w20"; + await createWallet(clientConfig, wallet_19_name); + await createWallet(clientConfig, wallet_20_name); + try { + await walletTransfersToAnotherAndBroadcastsBackupTx(clientConfig, wallet_19_name, wallet_20_name) + assert.fail("Expected error when sending backup tx before expiry, but no error was thrown"); + } catch (error) { + console.log("Expected error received: ", error.message); + assert(error.message.includes("The coin is not expired yet."), + `Unexpected error message: ${error.message}`); + } + console.log("Completed test for send backup tx before expiry"); + + // Transfer-sender of coin at expiry + let wallet_21_name = "w21"; + let wallet_22_name = "w22"; + await createWallet(clientConfig, wallet_21_name); + await createWallet(clientConfig, wallet_22_name); + await transferSendAtCoinExpiry(clientConfig, wallet_21_name, wallet_22_name); + console.log("Completed test for Transfer-sender of coin at expiry"); + + // Transfer-receive of coin at expiry + let wallet_23_name = "w23"; + let wallet_24_name = "w24"; + await createWallet(clientConfig, wallet_23_name); + await createWallet(clientConfig, wallet_24_name); + await transferReceiveAtCoinExpiry(clientConfig, wallet_23_name, wallet_24_name); + console.log("Completed test for Transfer-receive of coin at expiry"); + + // Transfer-sender of coin that will make it expired by sending + let wallet_25_name = "w25"; + let wallet_26_name = "w26"; + await createWallet(clientConfig, wallet_25_name); + await createWallet(clientConfig, wallet_26_name); + await transferSendCoinExpiryBySending(clientConfig, wallet_25_name, wallet_26_name); + console.log("Completed test for Transfer-sender of coin that will make it expired by sending"); process.exit(0); // Exit successfully } catch (error) { diff --git a/clients/libs/nodejs/broadcast_backup_tx.js b/clients/libs/nodejs/broadcast_backup_tx.js index 00b02fe4..67ad7eda 100644 --- a/clients/libs/nodejs/broadcast_backup_tx.js +++ b/clients/libs/nodejs/broadcast_backup_tx.js @@ -42,6 +42,13 @@ const execute = async (clientConfig, electrumClient, db, walletName, statechainI throw new Error(`Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is ${coin.status}`); } + const blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) + const currentBlockheight = blockHeader.height; + + if (currentBlockheight <= coin.locktime) { + throw new Error(`The coin is not expired yet. Coin locktime is ${coin.locktime} and current blockheight is ${currentBlockheight}`); + } + const backupTx = mercury_wasm.latestBackuptxPaysToUserpubkey(backupTxs, coin, wallet.network); if (!backupTx) { diff --git a/clients/libs/nodejs/transfer_receive.js b/clients/libs/nodejs/transfer_receive.js index bd3b4d10..ddc1714b 100644 --- a/clients/libs/nodejs/transfer_receive.js +++ b/clients/libs/nodejs/transfer_receive.js @@ -26,6 +26,14 @@ const execute = async (clientConfig, electrumClient, db, wallet_name) => { const serverInfo = await utils.infoConfig(clientConfig, electrumClient); + let blockHeader = undefined; + try { + blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) + } catch (error) { + throw new Error("Error getting block height from electrs server"); + } + const currentBlockheight = blockHeader.height; + let uniqueAuthPubkeys = new Set(); wallet.coins.forEach(coin => { @@ -88,6 +96,10 @@ const execute = async (clientConfig, electrumClient, db, wallet_name) => { continue; } } + + if (currentBlockheight >= coin.locktime) { + console.error(`The coin is expired. Coin locktime is ${coin.locktime} and current blockheight is ${currentBlockheight}`); + } } } diff --git a/clients/libs/nodejs/transfer_send.js b/clients/libs/nodejs/transfer_send.js index 3d06cb7e..52063def 100644 --- a/clients/libs/nodejs/transfer_send.js +++ b/clients/libs/nodejs/transfer_send.js @@ -42,7 +42,9 @@ const execute = async (clientConfig, electrumClient, db, walletName, statechainI const blockHeader = await electrumClient.request('blockchain.headers.subscribe'); // request(promise) const currentBlockheight = blockHeader.height; - if (currentBlockheight > coin.locktime) { + const serverInfo = await utils.infoConfig(clientConfig, electrumClient); + + if (currentBlockheight + serverInfo.interval >= coin.locktime) { throw new Error(`The coin is expired. Coin locktime is ${coin.locktime} and current blockheight is ${currentBlockheight}`); } @@ -63,8 +65,6 @@ const execute = async (clientConfig, electrumClient, db, walletName, statechainI const new_x1 = await get_new_x1(clientConfig, statechain_id, signed_statechain_id, new_auth_pubkey, batchId); - const serverInfo = await utils.infoConfig(clientConfig, electrumClient); - let feeRateSatsPerByte = (serverInfo.fee_rate_sats_per_byte > clientConfig.maxFeeRate) ? clientConfig.maxFeeRate: serverInfo.fee_rate_sats_per_byte; const signed_tx = await transaction.new_transaction(