diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bc05bc7c..c94282d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: services: docker: - image: docker:19.03.12 + image: docker:24.0.5 options: --privileged ports: - 5432:5432 @@ -32,13 +32,20 @@ jobs: - name: Set up Docker Compose run: | + sudo curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose docker-compose -f docker-compose-test.yml up --build -d + - name: Run start-test-components.sh for web client tests + run: | + cd clients/tests/web + chmod +x start-test-components.sh + ./start-test-components.sh - name: Wait for services to be ready run: | sleep 80 # Adjust time as necessary for services to initialize - - name: Verify Bitcoin daemon Service with Curl + - name: Verify Bitcoin daemon Service and create wallet run: | - container_id=$(docker ps -qf "name=mercurylayer_bitcoind_1") + container_id=$(docker ps -qf "name=mercurylayer-bitcoind-1") echo "Container ID: $container_id" docker logs $container_id wallet_name="new_wallet" @@ -47,19 +54,54 @@ jobs: echo "New Wallet Address: $address" docker exec $container_id bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress 101 "$address" docker exec $container_id bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress bcrt1pcngfxjdkf4r2h26k52dh5nunxg8m68uf4lkfhmfjvjj6agfkm5jqmftw4e 0.0001 + - name: Create wallet for esplora + run: | + container_id=$(docker ps -qf "name=esplora-container") + echo "Container ID: $container_id" + docker logs $container_id + wallet_name="esplora_wallet" + docker exec $container_id cli createwallet $wallet_name + address=$(docker exec $container_id cli getnewaddress $wallet_name) + echo "New Wallet Address: $address" + docker exec $container_id cli generatetoaddress 101 "$address" + docker exec $container_id cli sendtoaddress bcrt1pcngfxjdkf4r2h26k52dh5nunxg8m68uf4lkfhmfjvjj6agfkm5jqmftw4e 0.0001 + - name: Wait for services to be ready + run: | + sleep 60 # Wait for lnd to sync with latest block + - name: Verify LND nodes and create a channel between them + run: | + container_id_alice=$(docker ps -qf "name=mercurylayer-alice-1") + echo "Container ID: $container_id_alice" + container_id_bob=$(docker ps -qf "name=mercurylayer-bob-1") + echo "Container ID: $container_id_bob" + docker logs $container_id_alice + docker logs $container_id_bob + identity_pubkey_bob=$(docker exec $container_id_bob lncli -n regtest getinfo | jq -r '.identity_pubkey') + echo "Pubkey: $identity_pubkey_bob" + docker exec $container_id_alice lncli -n regtest connect $identity_pubkey_bob@bob:9735 + docker exec $container_id_alice lncli -n regtest listpeers + address=$(docker exec $container_id_bob lncli -n regtest newaddress p2wkh | jq -r '.address') + container_id_bitcoind=$(docker ps -qf "name=mercurylayer-bitcoind-1") + docker exec $container_id_bitcoind bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress $address 0.5 + docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -generate 6 + identity_pubkey_alice=$(docker exec $container_id_alice lncli -n regtest getinfo | jq -r '.identity_pubkey') + docker exec $container_id_bob lncli -n regtest openchannel $identity_pubkey_alice 100000 + docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -generate 5 + docker logs $container_id_alice + docker logs $container_id_bob - name: Verify ElectrumX Service with Curl run: | - container_id=$(docker ps -qf "name=mercurylayer_electrs_1") + container_id=$(docker ps -qf "name=mercurylayer-electrs-1") echo "Container ID: $container_id" docker logs $container_id - name: Verify Enclave Service with Curl run: | - container_id=$(docker ps -qf "name=mercurylayer_enclave-sgx_1") + container_id=$(docker ps -qf "name=mercurylayer-enclave-sgx-1") echo "Container ID: $container_id" docker logs $container_id - name: Verify Mercury Service with Curl run: | - container_id=$(docker ps -qf "name=mercurylayer_mercury_1") + container_id=$(docker ps -qf "name=mercurylayer-mercury-1") echo "Container ID: $container_id" docker logs $container_id docker exec $container_id \ @@ -71,14 +113,23 @@ jobs: -H "Content-Type: application/json" \ -d '{"statechain_id":"550e8400e29b41d4a716446655440000"}' docker logs $(docker ps -qf "name=enclave") + - name: Verify Node Service with Curl + run: | + curl -X POST http://0.0.0.0:3000/deposit_amount \ + -H 'Content-Type: application/json' \ + -d '{"address":"bcrt1pkygl356c6fvk6ptx72c64hrjkhcxecj4hjzfzc430svzczuv6m0s42lvwx","amount":1000}' + - name: Verify Esplora Service with Curl + run: | + curl http://0.0.0.0:8094/regtest/api/blocks/tip/height + docker logs $(docker ps -qf "name=esplora-container") - name: Check connectivity between containers run: | # Get container IDs - enclave_container=$(docker ps -qf "name=mercurylayer_enclave-sgx_1") - mercury_container=$(docker ps -qf "name=mercurylayer_mercury_1") + enclave_container=$(docker ps -qf "name=mercurylayer-enclave-sgx-1") + mercury_container=$(docker ps -qf "name=mercurylayer-mercury-1") - # Check if mercurylayer_mercury_1 can reach mercurylayer_enclave-sgx_1 - docker exec $mercury_container curl -v http://mercurylayer_enclave-sgx_1:18080/get_public_key \ + # Check if mercurylayer-mercury-1 can reach mercurylayer-enclave-sgx-1 + docker exec $mercury_container curl -v http://mercurylayer-enclave-sgx-1:18080/get_public_key \ -H "Content-Type: application/json" \ -d '{"statechain_id":"550e8400e29b41d4a716446655440000"}' @@ -88,7 +139,7 @@ jobs: -H "Content-Type: application/json" \ -d '{"statechain_id":"550e8400e29b41d4a716446655440000"}' - docker inspect mercurylayer_mercury_1 + docker inspect mercurylayer-mercury-1 - name: Set up Node.js uses: actions/setup-node@v2 with: @@ -98,15 +149,32 @@ jobs: run: | cd clients/apps/nodejs npm install + npm install mocha -g + - name: Install Node.js dependencies for web client + run: | + cd clients/tests/web + npm install + npx playwright install + npx playwright install-deps - name: Install Node.js dependencies for lib run: | cd clients/libs/nodejs npm install + - name: Install Node.js dependencies for web + run: | + cd clients/libs/web + npm install + - name: Run web client Tests + run: | + cd clients/tests/web + sudo chmod -R 755 ./data_bitcoin_regtest + npx vitest --browser.name=chromium --browser.headless - name: Run Client-Side Tests run: | cd clients/apps/nodejs node test_basic_workflow2.js node test_atomic_swap.js + mocha ./test/tb04-simple-lightning-latch.mjs --exit - name: Tear Down run: | docker-compose -f docker-compose-test.yml down diff --git a/clients/apps/nodejs/test/tb04-simple-lightning-latch.mjs b/clients/apps/nodejs/test/tb04-simple-lightning-latch.mjs index cf07e8ca..0ed4e187 100644 --- a/clients/apps/nodejs/test/tb04-simple-lightning-latch.mjs +++ b/clients/apps/nodejs/test/tb04-simple-lightning-latch.mjs @@ -3,40 +3,371 @@ import client_config from '../client_config.js'; import mercurynodejslib from 'mercurynodejslib'; import { CoinStatus } from 'mercurynodejslib/coin_enum.js'; import crypto from 'crypto'; -import { createWallet, removeDatabase, getnewaddress, generateBlock, depositCoin } from './test-utils.mjs'; +import { sleep, createWallet, depositCoin, generateInvoice, payHoldInvoice, payInvoice, settleInvoice } from '../test_utils.js'; describe('TB04 - Lightning Latch', function() { + this.timeout(30000); context('Simple Transfer', () => { it('should complete successfully', async () => { - + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_1"; + let wallet_2_name = "w_ln_2"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + const usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin = listCoins[0]; + + expect(coin.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddress.transfer_receive, false, paymentHash.batchId); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.true; + expect(transferReceiveResult.receivedStatechainIds).empty; + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin.statechain_id); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin.statechain_id, paymentHash.batchId); + + const hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + + expect(hash).to.equal(paymentHash.hash); + }) + }) + + context('The sender tries to get the pre-image before the batch is unlocked should fail', () => { + it('should complete successfully', async () => { + + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_3"; + let wallet_2_name = "w_ln_4"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + + const amount = 10000; + let token = undefined; + let tokenId = undefined; + let depositInfo = undefined; + let tokenList = undefined; + let usedToken = undefined; + let listCoins = undefined; + + token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + tokenId = token.token_id; + + depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin1 = listCoins[0]; + + expect(coin1.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash1 = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin1.statechain_id); + + token = await mercurynodejslib.newToken(clientConfig, wallet_2_name); + tokenId = token.token_id; + + depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_2_name, amount); + + tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_2_name); + usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_2_name, amount, depositInfo); + + listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_2_name); + + expect(listCoins.length).to.equal(1); + + const coin2 = listCoins[0]; + + expect(coin2.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash2 = await mercurynodejslib.paymentHash(clientConfig, wallet_2_name, coin2.statechain_id); + + const transferAddress1 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_1_name, null); + const transferAddress2 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin1.statechain_id, transferAddress1.transfer_receive, false, paymentHash1.batchId); + await mercurynodejslib.transferSend(clientConfig, wallet_2_name, coin2.statechain_id, transferAddress2.transfer_receive, false, paymentHash2.batchId); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_1_name); + + expect(transferReceiveResult.isThereBatchLocked).is.true; + expect(transferReceiveResult.receivedStatechainIds).empty; + + try { + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin1.statechain_id, paymentHash1.batchId); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'Request failed with status code 404'; + expect(error.message).to.equal(expectedMessage); + } + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin1.statechain_id); + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_2_name, coin2.statechain_id); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin1.statechain_id, paymentHash1.batchId); + + const hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + + expect(hash).to.equal(paymentHash1.hash); + }) + }) + + context('Statecoin sender can recover (resend their coin) after batch timeout without completion', () => { + it('should complete successfully', async () => { + const clientConfig = client_config.load(); - await removeDatabase(clientConfig); + let wallet_1_name = "w_ln_5"; + let wallet_2_name = "w_ln_6"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + + const amount = 10000; + let token = undefined; + let tokenId = undefined; + let depositInfo = undefined; + let tokenList = undefined; + let usedToken = undefined; + let listCoins = undefined; + + token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + tokenId = token.token_id; + + depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin1 = listCoins[0]; + + expect(coin1.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash1 = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin1.statechain_id); + + token = await mercurynodejslib.newToken(clientConfig, wallet_2_name); + tokenId = token.token_id; + + depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_2_name, amount); + + tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_2_name); + usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_2_name, amount, depositInfo); + + listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_2_name); + + expect(listCoins.length).to.equal(1); + + const coin2 = listCoins[0]; + + expect(coin2.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash2 = await mercurynodejslib.paymentHash(clientConfig, wallet_2_name, coin2.statechain_id); + + const transferAddress1 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_1_name, null); + const transferAddress2 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin1.statechain_id, transferAddress1.transfer_receive, false, paymentHash1.batchId); + await mercurynodejslib.transferSend(clientConfig, wallet_2_name, coin2.statechain_id, transferAddress2.transfer_receive, false, paymentHash1.batchId); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_1_name); + + expect(transferReceiveResult.isThereBatchLocked).is.true; + expect(transferReceiveResult.receivedStatechainIds).empty; + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin1.statechain_id); + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_2_name, coin2.statechain_id); + + await sleep(20000); + + let errorMessage; + console.error = (msg) => { + errorMessage = msg; + }; + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + // Assert the captured error message + const expectedMessage = 'Failed to update transfer message'; + expect(errorMessage).contains(expectedMessage); + + const transferAddress3 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_1_name, null); + const transferAddress4 = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin1.statechain_id, transferAddress3.transfer_receive, false, paymentHash2.batchId); + await mercurynodejslib.transferSend(clientConfig, wallet_2_name, coin2.statechain_id, transferAddress4.transfer_receive, false, paymentHash2.batchId); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_1_name); + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin1.statechain_id); + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_2_name, coin2.statechain_id); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin1.statechain_id, paymentHash1.batchId); + + const hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + + expect(hash).to.equal(paymentHash1.hash); + }) + }) + + context('Statecoin trade with invoice creation, payment and settlement', () => { + it('should complete successfully', async () => { - let wallet_1_name = "w1"; - let wallet_2_name = "w2"; - let wallet1 = await createWallet(clientConfig, wallet_1_name); - let wallet2 = await createWallet(clientConfig, wallet_2_name); + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_7"; + let wallet_2_name = "w_ln_8"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); - const token = await mercurynodejslib.newToken(clientConfig, wallet1.name); + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); const tokenId = token.token_id; const amount = 10000; - const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); - const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet1.name); + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); const usedToken = tokenList.find(token => token.token_id === tokenId); expect(usedToken.spent).is.true; - await depositCoin(depositInfo.deposit_address, amount); + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin = listCoins[0]; + + expect(coin.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const invoice = await generateInvoice(paymentHash.hash, amount); + + payInvoice(invoice.payment_request); + + const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddress.transfer_receive, false, paymentHash.batchId); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.true; + expect(transferReceiveResult.receivedStatechainIds).empty; + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin.statechain_id); - const coreWalletAddress = await getnewaddress(); + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); - await generateBlock(clientConfig.confirmationTarget, coreWalletAddress); + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin.statechain_id, paymentHash.batchId); + + const hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + + expect(hash).to.equal(paymentHash.hash); + + await settleInvoice(preimage); + }) + }) + + context('Receiver tries to transfer invoice amount to another invoice before preimage retrieval should fail', () => { + it('should complete successfully', async () => { + + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_9"; + let wallet_2_name = "w_ln_10"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; - const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet1.name); + const amount = 60000; + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + const usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); expect(listCoins.length).to.equal(1); @@ -44,35 +375,220 @@ describe('TB04 - Lightning Latch', function() { expect(coin.status).to.equal(CoinStatus.CONFIRMED); - const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet1.name, coin.statechain_id); + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const invoice = await generateInvoice(paymentHash.hash, amount); - const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet2.name, null); + payHoldInvoice(invoice.payment_request); - await mercurynodejslib.transferSend(clientConfig, wallet1.name, coin.statechain_id, transferAddress.transfer_receive, false, paymentHash.batchId); + const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddress.transfer_receive, false, paymentHash.batchId); const hashFromServer = await mercurynodejslib.getPaymentHash(clientConfig, paymentHash.batchId); expect(hashFromServer).to.equal(paymentHash.hash); - let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet2.name); + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); expect(transferReceiveResult.isThereBatchLocked).is.true; expect(transferReceiveResult.receivedStatechainIds).empty; - await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet1.name, coin.statechain_id); + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin.statechain_id); - transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet2.name); + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); expect(transferReceiveResult.isThereBatchLocked).is.false; expect(transferReceiveResult.receivedStatechainIds).not.empty; - const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet1.name, coin.statechain_id, paymentHash.batchId); + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin.statechain_id, paymentHash.batchId); const hash = crypto.createHash('sha256') .update(Buffer.from(preimage, 'hex')) .digest('hex') expect(hash).to.equal(paymentHash.hash); + + const paymentHashSecond = "b1f55a2f2eabb08ed9d6e15a053a6ac84d04d1c017de5a42caaec98b8d2ff738" + const invoiceSecond = await generateInvoice(paymentHashSecond, amount); + + try { + await payInvoice(invoiceSecond.payment_request); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }) + }) + + context('Statecoin sender sends coin without batch_id (receiver should still be able to receive, but no pre-image revealed)', () => { + it('should complete successfully', async () => { + + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_11"; + let wallet_2_name = "w_ln_12"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + const usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin = listCoins[0]; + + expect(coin.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddress.transfer_receive, false, null); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin.statechain_id); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).is.empty; + + let hash; + try { + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin.statechain_id, paymentHash.batchId); + hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + expect(hash).to.equal(paymentHash.hash); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }) + }) + + context('Sender sends coin without batch_id, and then resends to a different address (to attempt to steal), and then attempts to retrieve the pre-image, should fail (and LN payment cannot be claimed)', () => { + it('should complete successfully', async () => { + + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_13"; + let wallet_2_name = "w_ln_14"; + let wallet_3_name = "w_ln_15"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + await createWallet(clientConfig, wallet_3_name); + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + const usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin = listCoins[0]; + + expect(coin.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const transferAddress = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddress.transfer_receive, false, null); + + const transferAddressSecond = await mercurynodejslib.newTransferAddress(clientConfig, wallet_3_name, null); + + await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transferAddressSecond.transfer_receive, false, null); + + let transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_3_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).not.empty; + + await mercurynodejslib.confirmPendingInvoice(clientConfig, wallet_1_name, coin.statechain_id); + + transferReceiveResult = await mercurynodejslib.transferReceive(clientConfig, wallet_2_name); + + expect(transferReceiveResult.isThereBatchLocked).is.false; + expect(transferReceiveResult.receivedStatechainIds).is.empty; + + let hash; + try { + const { preimage } = await mercurynodejslib.retrievePreImage(clientConfig, wallet_1_name, coin.statechain_id, paymentHash.batchId); + hash = crypto.createHash('sha256') + .update(Buffer.from(preimage, 'hex')) + .digest('hex') + expect(hash).to.equal(paymentHash.hash); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }) + }) + + context('Coin receiver creates a non hold invoice, and sends to sender (i.e. an invoice with the a different payment hash). Sender should be able to determine this.', () => { + it('should complete successfully', async () => { + + // await removeDatabase(); + const clientConfig = client_config.load(); + let wallet_1_name = "w_ln_16"; + await createWallet(clientConfig, wallet_1_name); + + const token = await mercurynodejslib.newToken(clientConfig, wallet_1_name); + const tokenId = token.token_id; + + const amount = 10000; + const depositInfo = await mercurynodejslib.getDepositBitcoinAddress(clientConfig, wallet_1_name, amount); + + const tokenList = await mercurynodejslib.getWalletTokens(clientConfig, wallet_1_name); + const usedToken = tokenList.find(token => token.token_id === tokenId); + + expect(usedToken.spent).is.true; + + await depositCoin(clientConfig, wallet_1_name, amount, depositInfo); + + const listCoins = await mercurynodejslib.listStatecoins(clientConfig, wallet_1_name); + + expect(listCoins.length).to.equal(1); + + const coin = listCoins[0]; + + expect(coin.status).to.equal(CoinStatus.CONFIRMED); + + const paymentHash = await mercurynodejslib.paymentHash(clientConfig, wallet_1_name, coin.statechain_id); + + const paymentHashSecond = "a3b5f72d4e8cb07cd9a6e17c054a7ac84d05e1c018fe5b43cbbef98a9d3ff839" + const invoiceSecond = await generateInvoice(paymentHashSecond, amount); + + const isInvoiceValid = await mercurynodejslib.verifyInvoice(clientConfig, paymentHash.batchId, invoiceSecond.payment_request); + expect(isInvoiceValid).is.false; }) }) }) diff --git a/clients/apps/nodejs/test_atomic_swap.js b/clients/apps/nodejs/test_atomic_swap.js index c3bb5f3c..6156a89f 100644 --- a/clients/apps/nodejs/test_atomic_swap.js +++ b/clients/apps/nodejs/test_atomic_swap.js @@ -773,79 +773,79 @@ async function atomicSwapWithSecondPartySteal(clientConfig, wallet_1_name, walle const clientConfig = client_config.load(); // Successful test - all transfers complete within batch_time complete. - let wallet_27_name = "w27"; - let wallet_28_name = "w28"; - let wallet_29_name = "w29"; - let wallet_30_name = "w30"; - await createWallet(clientConfig, wallet_27_name); - await createWallet(clientConfig, wallet_28_name); - await createWallet(clientConfig, wallet_29_name); - await createWallet(clientConfig, wallet_30_name); - await atomicSwapSuccess(clientConfig, wallet_27_name, wallet_28_name, wallet_29_name, wallet_30_name); + let wallet_1_name = "w_atomic_1"; + let wallet_2_name = "w_atomic_2"; + let wallet_3_name = "w_atomic_3"; + let wallet_4_name = "w_atomic_4"; + await createWallet(clientConfig, wallet_1_name); + await createWallet(clientConfig, wallet_2_name); + await createWallet(clientConfig, wallet_3_name); + await createWallet(clientConfig, wallet_4_name); + await atomicSwapSuccess(clientConfig, wallet_1_name, wallet_2_name, wallet_3_name, wallet_4_name); console.log("Completed test for Successful test - all transfers complete within batch_time complete."); // Second party performs transfer-sender with incorrect or missing batch-id. First party should still receive OK. - let wallet_31_name = "w31"; - let wallet_32_name = "w32"; - let wallet_33_name = "w33"; - let wallet_34_name = "w34"; - await createWallet(clientConfig, wallet_31_name); - await createWallet(clientConfig, wallet_32_name); - await createWallet(clientConfig, wallet_33_name); - await createWallet(clientConfig, wallet_34_name); - await atomicSwapWithSecondBatchIdMissing(clientConfig, wallet_31_name, wallet_32_name, wallet_33_name, wallet_34_name); + let wallet_5_name = "w_atomic_5"; + let wallet_6_name = "w_atomic_6"; + let wallet_7_name = "w_atomic_7"; + let wallet_8_name = "w_atomic_8"; + await createWallet(clientConfig, wallet_5_name); + await createWallet(clientConfig, wallet_6_name); + await createWallet(clientConfig, wallet_7_name); + await createWallet(clientConfig, wallet_8_name); + await atomicSwapWithSecondBatchIdMissing(clientConfig, wallet_5_name, wallet_6_name, wallet_7_name, wallet_8_name); console.log("Completed test for Second party performs transfer-sender with incorrect or missing batch-id. First party should still receive OK."); // First party performs transfer-sender without batch_id. - let wallet_35_name = "w35"; - let wallet_36_name = "w36"; - let wallet_37_name = "w37"; - let wallet_38_name = "w38"; - await createWallet(clientConfig, wallet_35_name); - await createWallet(clientConfig, wallet_36_name); - await createWallet(clientConfig, wallet_37_name); - await createWallet(clientConfig, wallet_38_name); - await atomicSwapWithoutFirstBatchId(clientConfig, wallet_35_name, wallet_36_name, wallet_37_name, wallet_38_name); + let wallet_9_name = "w_atomic_9"; + let wallet_10_name = "w_atomic_10"; + let wallet_11_name = "w_atomic_11"; + let wallet_12_name = "w_atomic_12"; + await createWallet(clientConfig, wallet_9_name); + await createWallet(clientConfig, wallet_10_name); + await createWallet(clientConfig, wallet_11_name); + await createWallet(clientConfig, wallet_12_name); + await atomicSwapWithoutFirstBatchId(clientConfig, wallet_9_name, wallet_10_name, wallet_11_name, wallet_12_name); console.log("Completed test for First party performs transfer-sender without batch_id."); // One party doesn't complete transfer-receiver before the timeout. // Both wallets should be able to repeat transfer-sender and transfer-receiver back to new addresses without error, // after the timeout. - let wallet_39_name = "w39"; - let wallet_40_name = "w40"; - let wallet_41_name = "w41"; - let wallet_42_name = "w42"; - await createWallet(clientConfig, wallet_39_name); - await createWallet(clientConfig, wallet_40_name); - await createWallet(clientConfig, wallet_41_name); - await createWallet(clientConfig, wallet_42_name); - await atomicSwapWithTimeout(clientConfig, wallet_39_name, wallet_40_name, wallet_41_name, wallet_42_name); + let wallet_13_name = "w_atomic_13"; + let wallet_14_name = "w_atomic_14"; + let wallet_15_name = "w_atomic_15"; + let wallet_16_name = "w_atomic_16"; + await createWallet(clientConfig, wallet_13_name); + await createWallet(clientConfig, wallet_14_name); + await createWallet(clientConfig, wallet_15_name); + await createWallet(clientConfig, wallet_16_name); + await atomicSwapWithTimeout(clientConfig, wallet_13_name, wallet_14_name, wallet_15_name, wallet_16_name); console.log("Completed test for One party doesn't complete transfer-receiver before the timeout."); // First party tries to steal within timeout // they perform transfer-sender a second time sending back to one of their own addresses - should fail. - let wallet_43_name = "w43"; - let wallet_44_name = "w44"; - let wallet_45_name = "w45"; - let wallet_46_name = "w46"; - await createWallet(clientConfig, wallet_43_name); - await createWallet(clientConfig, wallet_44_name); - await createWallet(clientConfig, wallet_45_name); - await createWallet(clientConfig, wallet_46_name); - await atomicSwapWithFirstPartySteal(clientConfig, wallet_43_name, wallet_44_name, wallet_45_name, wallet_46_name); + let wallet_17_name = "w_atomic_17"; + let wallet_18_name = "w_atomic_18"; + let wallet_19_name = "w_atomic_19"; + let wallet_20_name = "w_atomic_20"; + await createWallet(clientConfig, wallet_17_name); + await createWallet(clientConfig, wallet_18_name); + await createWallet(clientConfig, wallet_19_name); + await createWallet(clientConfig, wallet_20_name); + await atomicSwapWithFirstPartySteal(clientConfig, wallet_17_name, wallet_18_name, wallet_19_name, wallet_20_name); console.log("Completed test for First party tries to steal within timeout"); // Second party tries to steal within timeout // they perform transfer-sender a second time sending back to one of their own addresses - should fail. - let wallet_47_name = "w47"; - let wallet_48_name = "w48"; - let wallet_49_name = "w49"; - let wallet_50_name = "w50"; - await createWallet(clientConfig, wallet_47_name); - await createWallet(clientConfig, wallet_48_name); - await createWallet(clientConfig, wallet_49_name); - await createWallet(clientConfig, wallet_50_name); - await atomicSwapWithSecondPartySteal(clientConfig, wallet_47_name, wallet_48_name, wallet_49_name, wallet_50_name); + let wallet_21_name = "w_atomic_21"; + let wallet_22_name = "w_atomic_22"; + let wallet_23_name = "w_atomic_23"; + let wallet_24_name = "w_atomic_24"; + await createWallet(clientConfig, wallet_21_name); + await createWallet(clientConfig, wallet_22_name); + await createWallet(clientConfig, wallet_23_name); + await createWallet(clientConfig, wallet_24_name); + await atomicSwapWithSecondPartySteal(clientConfig, wallet_21_name, wallet_22_name, wallet_23_name, wallet_24_name); console.log("Completed test for Second party tries to steal within timeout"); process.exit(0); // Exit successfully diff --git a/clients/apps/nodejs/test_basic_workflow2.js b/clients/apps/nodejs/test_basic_workflow2.js index 75bf20ad..faf64184 100644 --- a/clients/apps/nodejs/test_basic_workflow2.js +++ b/clients/apps/nodejs/test_basic_workflow2.js @@ -1,5 +1,3 @@ -const util = require('node:util'); -const exec = util.promisify(require('node:child_process').exec); const assert = require('node:assert/strict'); const mercurynodejslib = require('mercurynodejslib'); const { CoinStatus } = require('mercurynodejslib/coin_enum'); @@ -397,7 +395,7 @@ async function interruptBeforeSignFirst(clientConfig, wallet_1_name, wallet_2_na let transfer_address = await mercurynodejslib.newTransferAddress(clientConfig, wallet_2_name, null); - console.log("Disconnect mercurylayer_mercury_1 from network"); + console.log("Disconnect mercurylayer-mercury-1 from network"); await disconnectMercuryServer(); try { @@ -408,7 +406,7 @@ async function interruptBeforeSignFirst(clientConfig, wallet_1_name, wallet_2_na assert(error.message.includes("connect ECONNREFUSED 0.0.0.0:8000"), `Unexpected error message: ${error.message}`); } - console.log("Connect mercurylayer_mercury_1 from network"); + console.log("Connect mercurylayer-mercury-1 from network"); await connectMercuryServer(); } @@ -449,7 +447,7 @@ const new_transaction = async(clientConfig, electrumClient, coin, toAddress, isW const serverPartialSigRequest = partialSigRequest.partial_signature_request_payload; - console.log("Disconnect mercurylayer_mercury_1 from network"); + console.log("Disconnect mercurylayer-mercury-1 from network"); await disconnectMercuryServer(); let serverPartialSig; @@ -463,7 +461,7 @@ const new_transaction = async(clientConfig, electrumClient, coin, toAddress, isW `Unexpected error message: ${error.message}`); } - console.log("Connect mercurylayer_mercury_1 from network"); + console.log("Connect mercurylayer-mercury-1 from network"); await connectMercuryServer(); } @@ -632,7 +630,7 @@ async function interruptSignWithElectrumUnavailability(clientConfig, wallet_1_na await sleep(5000); // wait for Electrum to disconnect - console.log("Disconnect mercurylayer_electrs_1 from network"); + console.log("Disconnect mercurylayer-electrs-1 from network"); await disconnectElectr(); try { @@ -643,7 +641,7 @@ async function interruptSignWithElectrumUnavailability(clientConfig, wallet_1_na assert(error.message.includes("connect ECONNREFUSED 0.0.0.0:50001"), `Unexpected error message: ${error.message}`); } - console.log("Connect mercurylayer_electrs_1 from network"); + console.log("Connect mercurylayer-electrs-1 from network"); await connectElectr(); await sleep(5000); // wait for Electrum to connect @@ -692,7 +690,7 @@ async function interruptTransferReceiveWithElectrumUnavailability(clientConfig, await sleep(5000); // wait for Electrum to disconnect - console.log("Disconnect mercurylayer_electrs_1 from network"); + console.log("Disconnect mercurylayer-electrs-1 from network"); await disconnectElectr(); try { @@ -703,7 +701,7 @@ async function interruptTransferReceiveWithElectrumUnavailability(clientConfig, assert(error.message.includes("connect ECONNREFUSED 0.0.0.0:50001"), `Unexpected error message: ${error.message}`); } - console.log("Connect mercurylayer_electrs_1 from network"); + console.log("Connect mercurylayer-electrs-1 from network"); await connectElectr(); await sleep(5000); // wait for Electrum to connect @@ -750,7 +748,7 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon coin = await mercurynodejslib.transferSend(clientConfig, wallet_1_name, coin.statechain_id, transfer_address.transfer_receive, false, null); - console.log("Disconnect mercurylayer_mercury_1 from network"); + console.log("Disconnect mercurylayer-mercury-1 from network"); await disconnectMercuryServer(); try { @@ -761,7 +759,7 @@ async function interruptTransferReceiveWithMercuryServerUnavailability(clientCon assert(error.message.includes("connect ECONNREFUSED 0.0.0.0:8000"), `Unexpected error message: ${error.message}`); } - console.log("Connect mercurylayer_mercury_1 from network"); + console.log("Connect mercurylayer-mercury-1 from network"); await connectMercuryServer(); } diff --git a/clients/apps/nodejs/test_utils.js b/clients/apps/nodejs/test_utils.js index e6972cc0..fe1ece6b 100644 --- a/clients/apps/nodejs/test_utils.js +++ b/clients/apps/nodejs/test_utils.js @@ -54,7 +54,7 @@ const getElectrumClient = async (clientConfig) => { } const generateBlock = async (numBlocks) => { - const generateBlockCommand = `docker exec $(docker ps -qf "name=mercurylayer_bitcoind_1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress ${numBlocks} "bcrt1qgh48u8aj4jvjkalc28lqujyx2wveck4jsm59x9"`; + const generateBlockCommand = `docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress ${numBlocks} "bcrt1qgh48u8aj4jvjkalc28lqujyx2wveck4jsm59x9"`; await exec(generateBlockCommand); // console.log(`Generated ${numBlocks} blocks`); @@ -74,7 +74,7 @@ const depositCoin = async (clientConfig, wallet_name, amount, deposit_info) => { // Sending Bitcoin using bitcoin-cli try { - const sendBitcoinCommand = `docker exec $(docker ps -qf "name=mercurylayer_bitcoind_1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress ${deposit_info.deposit_address} ${amountInBtc}`; + const sendBitcoinCommand = `docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress ${deposit_info.deposit_address} ${amountInBtc}`; await exec(sendBitcoinCommand); // console.log(`Sent ${amountInBtc} BTC to ${deposit_info.deposit_address}`); await generateBlock(3); @@ -85,19 +85,61 @@ const depositCoin = async (clientConfig, wallet_name, amount, deposit_info) => { } const disconnectMercuryServer = async () => { - await exec("docker network disconnect mercurylayer_default mercurylayer_mercury_1"); + await exec("docker network disconnect mercurylayer_default mercurylayer-mercury-1"); } const connectMercuryServer = async () => { - await exec("docker network connect mercurylayer_default mercurylayer_mercury_1"); + await exec("docker network connect mercurylayer_default mercurylayer-mercury-1"); } const disconnectElectr = async () => { - await exec("docker network disconnect mercurylayer_default mercurylayer_electrs_1"); + await exec("docker network disconnect mercurylayer_default mercurylayer-electrs-1"); } const connectElectr = async () => { - await exec("docker network connect mercurylayer_default mercurylayer_electrs_1"); + await exec("docker network connect mercurylayer_default mercurylayer-electrs-1"); +} + +const generateInvoice = async (paymentHash, amountInSats) => { + + const generateInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-alice-1") lncli -n regtest addholdinvoice ${paymentHash} --amt ${amountInSats}`; + const { stdout, stderr } = await exec(generateInvoiceCommand); + if (stderr) { + console.error('Error:', stderr); + return null; + } + + try { + const response = JSON.parse(stdout.trim()); + return response; + } catch (error) { + console.error('Error parsing JSON:', error); + return null; + } +} + +const payInvoice = async (paymentRequest) => { + + const payInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-bob-1") lncli -n regtest payinvoice --force ${paymentRequest}`; + const { stdout, stderr } = await exec(payInvoiceCommand); + if (stderr) { + console.error('Error:', stderr); + return null; + } + console.log('stdout:', stdout.trim()); + return stdout.trim(); +} + +const payHoldInvoice = (paymentRequest) => { + + const payInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-bob-1") lncli -n regtest payinvoice --force ${paymentRequest}`; + exec(payInvoiceCommand); +} + +const settleInvoice = async (preimage) => { + + const settleInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-alice-1") lncli -n regtest settleinvoice ${preimage}`; + await exec(settleInvoiceCommand); } module.exports = { @@ -111,5 +153,9 @@ module.exports = { connectElectr, disconnectElectr, connectMercuryServer, - disconnectMercuryServer + disconnectMercuryServer , + generateInvoice, + payInvoice, + payHoldInvoice, + settleInvoice }; diff --git a/clients/libs/nodejs/index.js b/clients/libs/nodejs/index.js index 069ca45b..3a46ddc5 100644 --- a/clients/libs/nodejs/index.js +++ b/clients/libs/nodejs/index.js @@ -16,6 +16,7 @@ const sqlite_manager = require('./sqlite_manager'); const { v4: uuidv4 } = require('uuid'); const wallet_manager = require('./wallet'); +const lightningPayReq = require("bolt11"); const getDatabase = async (clientConfig) => { const databaseFile = clientConfig.databaseFile; @@ -247,6 +248,14 @@ const retrievePreImage = async (clientConfig, walletName, statechainId, batchId) return preImage; } +const verifyInvoice = async (clientConfig, batchId, paymentRequest) => { + + const decodedInvoice = lightningPayReq.decode(paymentRequest); + let paymentHash = await getPaymentHash(clientConfig, batchId); + + return paymentHash === decodedInvoice.tagsObject.payment_hash; +} + const getPaymentHash = async (clientConfig, batchId) => { return await lightningLatch.getPaymentHash(clientConfig, batchId); @@ -266,5 +275,6 @@ module.exports = { paymentHash, confirmPendingInvoice, retrievePreImage, + verifyInvoice, getPaymentHash }; diff --git a/clients/libs/nodejs/package.json b/clients/libs/nodejs/package.json index 955545c7..9a45dcdb 100644 --- a/clients/libs/nodejs/package.json +++ b/clients/libs/nodejs/package.json @@ -13,6 +13,7 @@ "@mempool/electrum-client": "^1.1.9", "axios": "^1.5.1", "bitcoinjs-lib": "^6.1.5", + "bolt11": "^1.4.1", "commander": "^11.1.0", "config": "^3.3.9", "mercury-wasm": "file:../../../wasm/node_pkg/debug", diff --git a/clients/libs/web/main.js b/clients/libs/web/main.js index 5da25a5b..8d3abec7 100644 --- a/clients/libs/web/main.js +++ b/clients/libs/web/main.js @@ -9,6 +9,7 @@ import transfer_send from './transfer_send.js'; import transfer_receive from './transfer_receive.js'; import lightningLatch from './lightning-latch.js'; import { v4 as uuidv4 } from 'uuid'; +import { decodeInvoice } from '../../tests/web/test-utils.js'; const greet = async () => { @@ -127,6 +128,16 @@ const getPaymentHash = async (clientConfig, batchId) => { return await lightningLatch.getPaymentHash(clientConfig, batchId); } +const verifyInvoice = async (clientConfig, batchId, paymentRequest) => { + + const decodedInvoice = await decodeInvoice(paymentRequest); + let paymentHash = await getPaymentHash(clientConfig, batchId); + console.log("Decoded invoice: ", decodedInvoice); + + const paymentHashFromInvoice = decodedInvoice.tags.find(tag => tag.tagName === "payment_hash")?.data; + return paymentHash === paymentHashFromInvoice; +} + export default { greet, createWallet, @@ -141,5 +152,6 @@ export default { paymentHash, confirmPendingInvoice, retrievePreImage, - getPaymentHash + getPaymentHash, + verifyInvoice } diff --git a/clients/libs/web/vite.config.js b/clients/libs/web/vite.config.js index a1a47266..275a30da 100644 --- a/clients/libs/web/vite.config.js +++ b/clients/libs/web/vite.config.js @@ -1,6 +1,5 @@ import { defineConfig } from 'vite' import { resolve } from 'path' -import { nodePolyfills } from 'vite-plugin-node-polyfills' // https://vitejs.dev/config/ export default defineConfig({ diff --git a/clients/tests/web/ClientConfig.js b/clients/tests/web/ClientConfig.js index a0023153..1dd7ea9c 100644 --- a/clients/tests/web/ClientConfig.js +++ b/clients/tests/web/ClientConfig.js @@ -8,8 +8,8 @@ // }; const clientConfig = { - esploraServer: "http://localhost:8094/regtest", - statechainEntity: "http://127.0.0.1:8000", + esploraServer: "http://0.0.0.0:8094/regtest", + statechainEntity: "http://0.0.0.0:8000", network: "regtest", feeRateTolerance: 5, confirmationTarget: 2, diff --git a/clients/tests/web/package.json b/clients/tests/web/package.json index 466c1efb..66de7bfe 100644 --- a/clients/tests/web/package.json +++ b/clients/tests/web/package.json @@ -14,6 +14,7 @@ "dependencies": { "axios": "^1.7.2", "body-parser": "^1.20.2", + "bolt11": "^1.4.1", "cors": "^2.8.5", "express": "^4.19.2", "install": "^0.13.0", diff --git a/clients/tests/web/server-regtest.cjs b/clients/tests/web/server-regtest.cjs index b68d64e6..de77656b 100644 --- a/clients/tests/web/server-regtest.cjs +++ b/clients/tests/web/server-regtest.cjs @@ -5,6 +5,7 @@ const cors = require('cors') const port = 3000 const util = require('node:util'); const exec = util.promisify(require('node:child_process').exec); +const lightningPayReq = require('bolt11'); app.use(bodyParser.json()) app.use(cors()) @@ -30,6 +31,32 @@ async function depositCoin(amount, address) { await exec(sendBitcoinCommand); } +const generateInvoice = async (paymentHash, amountInSats) => { + const generateInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-alice-1") lncli -n regtest addholdinvoice ${paymentHash} --amt ${amountInSats}`; + + const { stdout, stderr } = await exec(generateInvoiceCommand); + if (stderr) { + console.error('Error:', stderr); + return null; + } + return stdout.trim(); +} + +const payInvoice = async (paymentRequest) => { + const payInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-bob-1") lncli -n regtest payinvoice --force ${paymentRequest}`; + await exec(payInvoiceCommand); +} + +const payHoldInvoice = (paymentRequest) => { + const payInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-bob-1") lncli -n regtest payinvoice --force ${paymentRequest}`; + exec(payInvoiceCommand); +} + +const settleInvoice = async (preimage) => { + const settleInvoiceCommand = `docker exec $(docker ps -qf "name=mercurylayer-alice-1") lncli -n regtest settleinvoice ${preimage}`; + await exec(settleInvoiceCommand); +} + app.post('/deposit_amount', async (req, res) => { const { address, amount } = req.body @@ -63,6 +90,101 @@ app.post('/generate_blocks', async (req, res) => { } }) +app.post('/generate_invoice', async (req, res) => { + const { paymentHash, amountInSats } = req.body + + if (typeof paymentHash === 'string' && Number.isInteger(amountInSats)) { + console.log(`Generating invoice ...`) + + try { + const invoice = await generateInvoice(paymentHash, amountInSats); + res.status(200).send({ message: 'Invoice generated successfully', invoice }) + } catch (error) { + console.log(error.message); + res.status(500).send({ message: error.message }) + } + + } else { + res.status(400).send({ message: 'Invalid input' }) + } +}) + +app.post('/pay_invoice', async (req, res) => { + const { paymentRequest } = req.body + + if (typeof paymentRequest === 'string') { + console.log(`Paying invoice ...`) + + try { + await payInvoice(paymentRequest); + } catch (error) { + console.log(error.message); + res.status(500).send({ message: error.message }) + } + + res.status(200).send({ message: 'Invoice paid successfully' }) + } else { + res.status(400).send({ message: 'Invalid input' }) + } +}) + +app.post('/pay_holdinvoice', async (req, res) => { + const { paymentRequest } = req.body + + if (typeof paymentRequest === 'string') { + console.log(`Paying invoice ...`) + + try { + payHoldInvoice(paymentRequest); + } catch (error) { + console.log(error.message); + res.status(500).send({ message: error.message }) + } + + res.status(200).send({ message: 'Invoice paid successfully' }) + } else { + res.status(400).send({ message: 'Invalid input' }) + } +}) + +app.post('/settle_invoice', async (req, res) => { + const { preimage } = req.body + + if (typeof preimage === 'string') { + console.log(`Settling invoice ...`) + + try { + await settleInvoice(preimage); + } catch (error) { + console.log(error.message); + res.status(500).send({ message: error.message }) + } + + res.status(200).send({ message: 'Invoice settled successfully' }) + } else { + res.status(400).send({ message: 'Invalid input' }) + } +}) + +app.post('/decode_invoice', async (req, res) => { + const { paymentRequest } = req.body + + if (typeof paymentRequest === 'string') { + console.log(`Decoding invoice ...`) + + try { + const invoice = lightningPayReq.decode(paymentRequest); + res.status(200).send({ message: 'Invoice generated successfully', invoice }) + } catch (error) { + console.log(error.message); + res.status(500).send({ message: error.message }) + } + + } else { + res.status(400).send({ message: 'Invalid input' }) + } +}) + app.listen(port, () => { console.log(`Docker server listening on port ${port}`) }) \ No newline at end of file diff --git a/clients/tests/web/start-test-components.sh b/clients/tests/web/start-test-components.sh index f530b563..51f9e540 100755 --- a/clients/tests/web/start-test-components.sh +++ b/clients/tests/web/start-test-components.sh @@ -18,16 +18,18 @@ cleanup() { trap cleanup SIGINT SIGTERM # Run node server in the background and capture its PID +npm install node server-regtest.cjs & NODE_PID=$! # Run docker command in the background without -it flags -docker run --name esplora-container -p 50001:50001 -p 8094:80 \ +docker run --name esplora-container -p 50002:50002 -p 8094:80 \ --volume $PWD/data_bitcoin_regtest:/data \ --env CORS_ALLOW='*' --rm \ blockstream/esplora \ /srv/explorer/run.sh bitcoin-regtest explorer & DOCKER_PID=$! + # Wait for both processes to finish -wait $NODE_PID $DOCKER_PID +# wait $NODE_PID $DOCKER_PID diff --git a/clients/tests/web/test-utils.js b/clients/tests/web/test-utils.js index 7ea3f9b8..9b953c45 100644 --- a/clients/tests/web/test-utils.js +++ b/clients/tests/web/test-utils.js @@ -5,7 +5,7 @@ const generateBlocks = async (blocks) => { blocks }; - const url = `http://localhost:3000/generate_blocks`; + const url = `http://0.0.0.0:3000/generate_blocks`; let response = await axios.post(url, body); @@ -21,7 +21,7 @@ const depositCoin = async (address, amount) => { amount }; - const url = `http://localhost:3000/deposit_amount`; + const url = `http://0.0.0.0:3000/deposit_amount`; let response = await axios.post(url, body); @@ -37,4 +37,89 @@ const testEsplora = async () => { console.log(block_header); } -export { generateBlocks, depositCoin }; +const sleep = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const generateInvoice = async (paymentHash, amountInSats) => { + + const body = { + paymentHash, + amountInSats + }; + + const url = `http://0.0.0.0:3000/generate_invoice`; + + let response = await axios.post(url, body); + + if (response.status == 200) { + const invoice = JSON.parse(response.data.invoice); + return invoice; + } else { + throw new Error(`Failed to generate invoice`); + } +} + +const payInvoice = async (paymentRequest) => { + + const body = { + paymentRequest + }; + + const url = `http://0.0.0.0:3000/pay_invoice`; + + let response = await axios.post(url, body); + + if (response.status != 200) { + throw new Error(`Failed to pay invoice`); + } +} + +const payHoldInvoice = async (paymentRequest) => { + + const body = { + paymentRequest + }; + + const url = `http://0.0.0.0:3000/pay_holdinvoice`; + + let response = await axios.post(url, body); + + if (response.status != 200) { + throw new Error(`Failed to pay hold invoice`); + } +} + +const settleInvoice = async (preimage) => { + + const body = { + preimage + }; + + const url = `http://0.0.0.0:3000/settle_invoice`; + + let response = await axios.post(url, body); + + if (response.status != 200) { + throw new Error(`Failed to settle invoice`); + } +} + +const decodeInvoice = async (paymentRequest) => { + const body = { + paymentRequest + }; + + const url = `http://0.0.0.0:3000/decode_invoice`; + + let response = await axios.post(url, body); + + if (response.status == 200) { + const invoice = response.data.invoice; + return invoice; + } else { + throw new Error(`Failed to decode invoice`); + } +} + +export { generateBlocks, depositCoin, sleep, generateInvoice, payInvoice, payHoldInvoice, settleInvoice, decodeInvoice }; diff --git a/clients/tests/web/test/tb03-simple-atomic-transfer.test.js b/clients/tests/web/test/tb03-simple-atomic-transfer.test.js index a93adf0b..e4de80c6 100644 --- a/clients/tests/web/test/tb03-simple-atomic-transfer.test.js +++ b/clients/tests/web/test/tb03-simple-atomic-transfer.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin } from '../test-utils.js'; +import { generateBlocks, depositCoin, sleep } from '../test-utils.js'; describe('TB03 - Simple Atomic Transfer', () => { test("expected flow", async () => { @@ -95,4 +95,521 @@ describe('TB03 - Simple Atomic Transfer', () => { await mercuryweblib.withdrawCoin(clientConfig, wallet4.name, statechainId2, toAddress, null, null); }); -}, 50000); \ No newline at end of file +}, 50000); + +describe('TB03 - Atomic swap with second batchid missing', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03"); + localStorage.removeItem("mercury-layer:wallet2_tb03"); + localStorage.removeItem("mercury-layer:wallet3_tb03"); + localStorage.removeItem("mercury-layer:wallet4_tb03"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); + let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + const toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, ""); + + let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(false); + + const transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + + expect(transferReceive4.isThereBatchLocked).toBe(false); + + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(false); + + const toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId1, toAddress, null, null); + + await mercuryweblib.withdrawCoin(clientConfig, wallet4.name, statechainId2, toAddress, null, null); + + }); +}, 50000); + +describe('TB03 - Atomic swap without first batchid', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03"); + localStorage.removeItem("mercury-layer:wallet2_tb03"); + localStorage.removeItem("mercury-layer:wallet3_tb03"); + localStorage.removeItem("mercury-layer:wallet4_tb03"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); + let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + const toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, null); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + + let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(false); + + const transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + + expect(transferReceive4.isThereBatchLocked).toBe(false); + + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(false); + + const toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId1, toAddress, null, null); + + await mercuryweblib.withdrawCoin(clientConfig, wallet4.name, statechainId2, toAddress, null, null); + + }); +}, 50000); + +describe('TB03 - Atomic swap with timeout', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03"); + localStorage.removeItem("mercury-layer:wallet2_tb03"); + localStorage.removeItem("mercury-layer:wallet3_tb03"); + localStorage.removeItem("mercury-layer:wallet4_tb03"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); + let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + let toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + let toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + + let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(true); + + await sleep(20000); + + let errorMessage; + console.error = (msg) => { + errorMessage = msg; + }; + + let transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + + // Assert the captured error message + const expectedMessage = 'Failed to update transfer message'; + expect(errorMessage).contains(expectedMessage); + + toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(true); + + transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + + expect(transferReceive4.isThereBatchLocked).toBe(false); + + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + + expect(transferReceive3.isThereBatchLocked).toBe(false); + + const toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId1, toAddress, null, null); + + await mercuryweblib.withdrawCoin(clientConfig, wallet4.name, statechainId2, toAddress, null, null); + + }); +}, 50000); + +describe('TB03 - Atomic swap with first party steal', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03"); + localStorage.removeItem("mercury-layer:wallet2_tb03"); + localStorage.removeItem("mercury-layer:wallet3_tb03"); + localStorage.removeItem("mercury-layer:wallet4_tb03"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); + let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + const toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + + const toAddress3_for_steal = await mercuryweblib.newTransferAddress(wallet3.name, true); + + try { + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3_for_steal.transfer_receive, false, toAddress3.batch_id); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'Request failed'; + expect(error.message).contains(expectedMessage); + } + + let transferReceive3 = undefined; + try { + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + await sleep(3000); + + try { + const transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + + try { + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + }); +}, 50000); + +describe('TB03 - Atomic swap with second party steal', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03"); + localStorage.removeItem("mercury-layer:wallet2_tb03"); + localStorage.removeItem("mercury-layer:wallet3_tb03"); + localStorage.removeItem("mercury-layer:wallet4_tb03"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); + let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + const toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + + const toAddress4_for_steal = await mercuryweblib.newTransferAddress(wallet4.name, true); + + try { + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4_for_steal.transfer_receive, false, toAddress4.batch_id); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'Request failed'; + expect(error.message).contains(expectedMessage); + } + + let transferReceive3 = undefined; + try { + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + await sleep(3000); + + try { + const transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + + try { + transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'num_sigs is not correct'; + expect(error.message).contains(expectedMessage); + } + }); +}, 50000); diff --git a/clients/tests/web/test/tb04-simple-lightning-latch.test.js b/clients/tests/web/test/tb04-simple-lightning-latch.test.js index c92b26cb..e5b7c158 100644 --- a/clients/tests/web/test/tb04-simple-lightning-latch.test.js +++ b/clients/tests/web/test/tb04-simple-lightning-latch.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin } from '../test-utils.js'; +import { generateBlocks, depositCoin, sleep, generateInvoice, payInvoice, payHoldInvoice, settleInvoice, decodeInvoice } from '../test-utils.js'; async function sha256(preimage) { let buffer; @@ -109,4 +109,596 @@ describe('TB04 - Simple Lightning Latch', () => { expect(hashPreImage).toEqual(paymentHash.hash); }); -}, 50000); \ No newline at end of file +}, 50000); + +describe('TB04 - The sender tries to get the pre-image before the batch is unlocked should fail', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash1 = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId1); + const paymentHash2 = await mercuryweblib.paymentHash(clientConfig, wallet2.name, statechainId2); + + let transferAddress1 = await mercuryweblib.newTransferAddress(wallet2.name); + let transferAddress2 = await mercuryweblib.newTransferAddress(wallet1.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, transferAddress1.transfer_receive, false, paymentHash1.batchId ); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, transferAddress2.transfer_receive, false, paymentHash2.batchId); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(true); + + try { + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId1, paymentHash1.batchId); + } catch (error) { + // Assert the captured error message + const expectedMessage = 'Request failed with status code 404'; + expect(error.message).to.equal(expectedMessage); + } + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId1); + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet2.name, statechainId2); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + let toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId1, toAddress, null, null); + + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId1, paymentHash1.batchId); + + let hashPreImage = await sha256(preimage); + + expect(hashPreImage).toEqual(paymentHash1.hash); + }); +}, 50000); + +describe('TB04 - Statecoin sender can recover (resend their coin) after batch timeout without completion', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + await mercuryweblib.newToken(clientConfig, wallet2.name); + + const amount = 1000; + + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + + const statechainId1 = result1.statechain_id; + const statechainId2 = result2.statechain_id; + + let isDepositInMempool1 = false; + let isDepositConfirmed1 = false; + + let isDepositInMempool2 = false; + let isDepositConfirmed2 = false; + + let areBlocksGenerated = false; + + await depositCoin(result1.deposit_address, amount); + await depositCoin(result2.deposit_address, amount); + + while (!isDepositConfirmed2 || !isDepositConfirmed1) { + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool1) { + isDepositInMempool1 = true; + } else if (coin.statechain_id === statechainId1 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed1 = true; + } + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool2) { + isDepositInMempool2 = true; + } else if (coin.statechain_id === statechainId2 && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed2 = true; + } + } + + if (isDepositInMempool1 && isDepositInMempool2 && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash1 = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId1); + const paymentHash2 = await mercuryweblib.paymentHash(clientConfig, wallet2.name, statechainId2); + + let transferAddress1 = await mercuryweblib.newTransferAddress(wallet2.name); + let transferAddress2 = await mercuryweblib.newTransferAddress(wallet1.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, transferAddress1.transfer_receive, false, paymentHash1.batchId ); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, transferAddress2.transfer_receive, false, paymentHash1.batchId); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(true); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId1); + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet2.name, statechainId2); + + await sleep(20000); + + let errorMessage; + console.error = (msg) => { + errorMessage = msg; + }; + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + // Assert the captured error message + const expectedMessage = 'Failed to update transfer message'; + expect(errorMessage).contains(expectedMessage); + + let transferAddress3 = await mercuryweblib.newTransferAddress(wallet2.name); + let transferAddress4 = await mercuryweblib.newTransferAddress(wallet1.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, transferAddress3.transfer_receive, false, paymentHash2.batchId ); + await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, transferAddress4.transfer_receive, false, paymentHash2.batchId); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet1.name); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId1); + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet2.name, statechainId2); + + expect(transferReceive.isThereBatchLocked).toBe(true); + + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId1, paymentHash1.batchId); + + let hashPreImage = await sha256(preimage); + + expect(hashPreImage).toEqual(paymentHash1.hash); + }); +}, 50000); + +describe('TB04 - Statecoin trade with invoice creation, payment and settlement', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + + const amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId); + + const invoice = await generateInvoice(paymentHash.hash, amount); + + payInvoice(invoice.payment_request); + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId, transferAddress.transfer_receive, false, paymentHash.batchId ); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(true); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + let toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId, paymentHash.batchId); + + let hashPreImage = await sha256(preimage); + + expect(hashPreImage).toEqual(paymentHash.hash); + + await settleInvoice(preimage); + }); +}, 50000); + +describe('TB04 - Receiver tries to transfer invoice amount to another invoice before preimage retrieval should fail', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + + const amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId); + + const invoice = await generateInvoice(paymentHash.hash, amount); + + await payHoldInvoice(invoice.payment_request); + console.log(invoice); + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId, transferAddress.transfer_receive, false, paymentHash.batchId ); + + const hashFromServer = await mercuryweblib.getPaymentHash(clientConfig, paymentHash.batchId); + + expect(hashFromServer).to.equal(paymentHash.hash); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(true); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + let toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId, paymentHash.batchId); + + let hashPreImage = await sha256(preimage); + + expect(hashPreImage).toEqual(paymentHash.hash); + + const paymentRequest = "lnbcrt10u1pnt70qdpp55w6lwt2w3jc8ekdxu97q2jn6epxstcwqrrl9ks7thmuc48fllqusdqqcqzzsxqyz5vqsp54ay50wuys4sjjmzsxms3hv6pf294rnys8y9k93t8dz6edpqvue5s9qxpqysgqwpp3m8a77cc6nt2yvxvkcql5avzf50ga95dxrtugfv2p5wdqfjy4fd44srh7undwaa7tkju88c0z9zf9ryp5tdr3hv7t9699l4qfpccpp7fqx7" + const invoiceSecond = await decodeInvoice(paymentRequest); + + try { + await payInvoice(invoiceSecond.payment_request); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }); +}, 50000); + +describe('Statecoin sender sends coin without batch_id (receiver should still be able to receive, but no pre-image revealed)', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + + const amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId); + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId, transferAddress.transfer_receive, false, null); + + const hashFromServer = await mercuryweblib.getPaymentHash(clientConfig, paymentHash.batchId); + + expect(hashFromServer).to.equal(paymentHash.hash); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + let toAddress = "bcrt1q805t9k884s5qckkxv7l698hqlz7t6alsfjsqym"; + + await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + + let hashPreImage; + try { + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId, paymentHash.batchId); + hashPreImage = await sha256(preimage); + expect(hashPreImage).toEqual(paymentHash.hash); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }); +}, 50000); + +describe('TB04 - Sender sends coin without batch_id, and then resends to a different address (to attempt to steal), and then attempts to retrieve the pre-image, should fail (and LN payment cannot be claimed)', () => { + test("expected flow", async () => { + localStorage.removeItem("mercury-layer:wallet1_tb04"); + localStorage.removeItem("mercury-layer:wallet2_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + + const amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId); + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId, transferAddress.transfer_receive, false, null); + + const transferAddressSecond = await mercuryweblib.newTransferAddress(wallet3.name); + + await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId, transferAddressSecond.transfer_receive, false, null); + + const hashFromServer = await mercuryweblib.getPaymentHash(clientConfig, paymentHash.batchId); + + expect(hashFromServer).to.equal(paymentHash.hash); + + let transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + await mercuryweblib.confirmPendingInvoice(clientConfig, wallet1.name, statechainId); + + transferReceive = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + + expect(transferReceive.isThereBatchLocked).toBe(false); + + let hashPreImage; + try { + const { preimage } = await mercuryweblib.retrievePreImage(clientConfig, wallet1.name, statechainId, paymentHash.batchId); + hashPreImage = await sha256(preimage); + expect(hashPreImage).toEqual(paymentHash.hash); + } catch (error) { + console.error('Error:', error); + expect(error.message).to.include('failed'); + } + }); +}, 50000); + +describe('Coin receiver creates a non hold invoice, and sends to sender (i.e. an invoice with the a different payment hash). Sender should be able to determine this.', () => { + test("expected flow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb04"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); + + await mercuryweblib.newToken(clientConfig, wallet1.name); + + const amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + const paymentHash = await mercuryweblib.paymentHash(clientConfig, wallet1.name, statechainId); + + const paymentRequest = "lnbcrt10u1pnt70fjpp53gj23vyghz5ggpc3ppkttxkqkywfz2dfwgalsa9ynylwe28lscqqdqqcqzzsxqyz5vqsp5l640fse8wx9773rpxlqdgv95t4swhpeueuta358404q6exvhzk5q9qxpqysgqzytwwj9n6s5dt4jd6vgvy9rmtcq4cwhe4h98asrpe3u3pqp3tqkrcvz5fm6uvr76akr9ml07sz70cdx45d64h9dpmnd2ua5qdpp79rcpqpucyd" + + const isInvoiceValid = await mercuryweblib.verifyInvoice(clientConfig, paymentHash.batchId, paymentRequest); + expect(isInvoiceValid).is.false; + }); +}, 50000); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 7cce6ae7..c629574c 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -3,6 +3,7 @@ version: '3.8' services: postgres: image: postgres + container_name: mercurylayer-postgres-1 environment: POSTGRES_PASSWORD: pgpassword ports: @@ -10,15 +11,19 @@ services: bitcoind: image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507 + container_name: mercurylayer-bitcoind-1 user: root - command: -regtest -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=user:63cf03615adebaa9356591f95b07ec7b$$920588e53f94798bda636acac1b6a77e10e3ee7fe57e414d62f3ee9e580cd27a -fallbackfee=0.0001 + command: -regtest -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=user:63cf03615adebaa9356591f95b07ec7b$$920588e53f94798bda636acac1b6a77e10e3ee7fe57e414d62f3ee9e580cd27a -fallbackfee=0.0001 -zmqpubrawblock=tcp://0.0.0.0:28332 -zmqpubrawtx=tcp://0.0.0.0:28333 ports: - "18443:18443" + - "28332:28332" + - "28333:28333" volumes: - bitcoin_data:/root/.bitcoin electrs: image: getumbrel/electrs:v0.9.4@sha256:b1590ac6cfb0e5b481c6a7af7f0626d76cbb91c63702b0f5c47e2829e9c37997 + container_name: mercurylayer-electrs-1 user: root environment: ELECTRS_LOG_FILTERS: "INFO" @@ -39,6 +44,7 @@ services: build: context: enclave dockerfile: Dockerfiles/SIM/Dockerfile + container_name: mercurylayer-enclave-sgx-1 depends_on: - postgres environment: @@ -58,12 +64,93 @@ services: LH_DECREMENT: 1 CONNECTION_STRING: postgres://postgres:pgpassword@postgres:5432/postgres BATCH_TIMEOUT: 20 - ENCLAVES: '[{"url": "http://mercurylayer_enclave-sgx_1:18080", "allow_deposit": true}]' + ENCLAVES: '[{"url": "http://mercurylayer-enclave-sgx-1:18080", "allow_deposit": true}]' ports: - "8000:8000" depends_on: - postgres + alice: + image: lightninglabs/lndinit:v0.1.21-beta-lnd-v0.18.0-beta + container_name: mercurylayer-alice-1 + user: root + hostname: lnd + entrypoint: + - sh + - -c + - | + if [[ ! -f /data/seed.txt ]]; then + lndinit gen-seed > /data/seed.txt + fi + if [[ ! -f /data/walletpassword.txt ]]; then + lndinit gen-password > /data/walletpassword.txt + fi + lndinit -v init-wallet \ + --secret-source=file \ + --file.seed=/data/seed.txt \ + --file.wallet-password=/data/walletpassword.txt \ + --init-file.output-wallet-dir=/root/.lnd/data/chain/bitcoin/regtest \ + --init-file.validate-password + mkdir -p /data/.lnd + if [ ! -f "/data/.lnd/umbrel-lnd.conf" ]; then + touch "/data/.lnd/umbrel-lnd.conf" + fi + lnd --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=bitcoind --bitcoind.rpcuser=user --bitcoind.rpcpass=pass --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 --configfile=/data/.lnd/umbrel-lnd.conf --wallet-unlock-password-file=/data/walletpassword.txt --wallet-unlock-allow-create + ports: + - "9735:9735" + - "10009:10009" + - "8080:8080" + volumes: + - alice-data:/data/.lnd + restart: unless-stopped + environment: + HOME: /data + command: [ '/init-wallet-k8s.sh' ] + depends_on: + - bitcoind + + bob: + image: lightninglabs/lndinit:v0.1.21-beta-lnd-v0.18.0-beta + container_name: mercurylayer-bob-1 + user: root + hostname: lnd + entrypoint: + - sh + - -c + - | + if [[ ! -f /data/seed.txt ]]; then + lndinit gen-seed > /data/seed.txt + fi + if [[ ! -f /data/walletpassword.txt ]]; then + lndinit gen-password > /data/walletpassword.txt + fi + lndinit -v init-wallet \ + --secret-source=file \ + --file.seed=/data/seed.txt \ + --file.wallet-password=/data/walletpassword.txt \ + --init-file.output-wallet-dir=/root/.lnd/data/chain/bitcoin/regtest \ + --init-file.validate-password + mkdir -p /data/.lnd + if [ ! -f "/data/.lnd/umbrel-lnd.conf" ]; then + touch "/data/.lnd/umbrel-lnd.conf" + fi + lnd --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=bitcoind --bitcoind.rpcuser=user --bitcoind.rpcpass=pass --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 --configfile=/data/.lnd/umbrel-lnd.conf --wallet-unlock-password-file=/data/walletpassword.txt --wallet-unlock-allow-create + ports: + - "9736:9735" + - "10010:10009" + - "8081:8080" + volumes: + - bob-data:/data/.lnd + restart: unless-stopped + environment: + HOME: /data + command: [ '/init-wallet-k8s.sh' ] + depends_on: + - bitcoind + volumes: bitcoin_data: - electrs-data: \ No newline at end of file + electrs-data: + alice-data: + bob-data: + esplora-data: