|
| 1 | +import { BitGoAPI } from '@bitgo/sdk-api'; |
| 2 | +import { TestBitGo } from '@bitgo/sdk-test'; |
| 3 | +import * as assert from 'assert'; |
| 4 | +import 'should'; |
| 5 | +import { Wallet } from '../../../../src/bitgo/wallet/wallet'; |
| 6 | +import { WalletSignTransactionOptions } from '../../../../src/bitgo/wallet/iWallet'; |
| 7 | +import { BaseCoin, BitGoBase } from 'modules/sdk-core/src'; |
| 8 | +import { Tbtc } from '@bitgo/sdk-coin-btc'; |
| 9 | +import nock from 'nock'; |
| 10 | +import { common } from '@bitgo/sdk-core'; |
| 11 | + |
| 12 | +describe('Wallet signTransaction with verifyTxParams', function () { |
| 13 | + let realWallet: Wallet; |
| 14 | + |
| 15 | + beforeEach(function () { |
| 16 | + const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' }); |
| 17 | + bitgo.initializeTestVars(); |
| 18 | + bitgo.safeRegister('tbtc', Tbtc.createInstance); |
| 19 | + const basecoin = bitgo.coin('tbtc'); |
| 20 | + |
| 21 | + // Real wallet data from tbtc testnet |
| 22 | + const realWalletData = { |
| 23 | + id: '6840948b17e91662b782d55bbf988c4e', |
| 24 | + coin: 'tbtc', |
| 25 | + label: 'Test: User & Backup Signing', |
| 26 | + m: 2, |
| 27 | + n: 3, |
| 28 | + keys: [ |
| 29 | + '6840947d037fdb798e0bf860e52cc4a8', |
| 30 | + '6840947e7c18efe3b0b77e9a75308aab', |
| 31 | + '68409480bdf143f4a1d32474acc09baa', |
| 32 | + ], |
| 33 | + multisigType: 'onchain', |
| 34 | + type: 'hot', |
| 35 | + balance: 329034, |
| 36 | + balanceString: '329034', |
| 37 | + confirmedBalance: 329034, |
| 38 | + confirmedBalanceString: '329034', |
| 39 | + spendableBalance: 329034, |
| 40 | + spendableBalanceString: '329034', |
| 41 | + }; |
| 42 | + |
| 43 | + realWallet = new Wallet(bitgo as unknown as BitGoBase, basecoin as unknown as BaseCoin, realWalletData); |
| 44 | + }); |
| 45 | + |
| 46 | + it('should fail verification when expected recipient does not match actual transaction recipient', async function () { |
| 47 | + // Real transaction hex that sends to tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp |
| 48 | + const realTxHex = |
| 49 | + '70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000'; |
| 50 | + |
| 51 | + const txPrebuild = { |
| 52 | + txHex: realTxHex, |
| 53 | + walletId: '6840948b17e91662b782d55bbf988c4e', |
| 54 | + }; |
| 55 | + |
| 56 | + // Verification parameters with wrong expected recipient |
| 57 | + const verifyTxParams = { |
| 58 | + txParams: { |
| 59 | + recipients: [ |
| 60 | + { |
| 61 | + address: '2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n', // Expected recipient |
| 62 | + amount: '10000', // Expected amount |
| 63 | + }, |
| 64 | + ], |
| 65 | + type: 'send', |
| 66 | + }, |
| 67 | + }; |
| 68 | + const bgUrl = common.Environments['test'].uri; |
| 69 | + nock(bgUrl).get(`/api/v2/tbtc/key/${realWallet.keyIds()[0]}`).reply(200, { |
| 70 | + id: '6840947d037fdb798e0bf860e52cc4a8', |
| 71 | + pub: 'pub', |
| 72 | + encryptedPrv: |
| 73 | + 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', |
| 74 | + }); |
| 75 | + |
| 76 | + nock(bgUrl).get(`/api/v2/tbtc/key/${realWallet.keyIds()[1]}`).reply(200, { |
| 77 | + id: realWallet.keyIds()[1], |
| 78 | + pub: 'pub', |
| 79 | + encryptedPrv: |
| 80 | + 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', |
| 81 | + }); |
| 82 | + |
| 83 | + nock(bgUrl).get(`/api/v2/tbtc/key/${realWallet.keyIds()[2]}`).reply(200, { |
| 84 | + id: realWallet.keyIds()[2], |
| 85 | + pub: 'pub', |
| 86 | + encryptedPrv: |
| 87 | + 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', |
| 88 | + }); |
| 89 | + |
| 90 | + const signParams: WalletSignTransactionOptions = { |
| 91 | + txPrebuild, |
| 92 | + verifyTxParams, |
| 93 | + prv: 'test-private-key', |
| 94 | + }; |
| 95 | + |
| 96 | + try { |
| 97 | + await realWallet.signTransaction(signParams); |
| 98 | + assert.fail('Should have thrown verification error'); |
| 99 | + } catch (error) { |
| 100 | + assert.ok( |
| 101 | + error.message.includes('recipient address mismatch'), |
| 102 | + `Error message should contain 'recipient address mismatch', got: ${error.message}` |
| 103 | + ); |
| 104 | + assert.ok( |
| 105 | + error.message.includes('2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n'), |
| 106 | + `Error message should contain '2Muux9UnVFCiGaYbX8D8FTsKaErkhLXRX5n', got: ${error.message}` |
| 107 | + ); |
| 108 | + assert.ok( |
| 109 | + error.message.includes('tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp'), |
| 110 | + `Error message should contain 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', got: ${error.message}` |
| 111 | + ); |
| 112 | + } |
| 113 | + |
| 114 | + // Verify that verifyTransaction was called with correct parameters |
| 115 | + mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce; |
| 116 | + const verifyCall = mockBaseCoinReal.verifyTransaction.getCall(0); |
| 117 | + const verifyCallArgs = verifyCall.args[0]; |
| 118 | + |
| 119 | + verifyCallArgs.should.have.property('txPrebuild'); |
| 120 | + verifyCallArgs.txPrebuild.should.have.property('txHex', realTxHex); |
| 121 | + verifyCallArgs.txPrebuild.should.have.property('walletId', '6840948b17e91662b782d55bbf988c4e'); |
| 122 | + verifyCallArgs.should.have.property('txParams', verifyTxParams.txParams); |
| 123 | + verifyCallArgs.should.have.property('verification', verifyTxParams.verification); |
| 124 | + verifyCallArgs.should.have.property('wallet', realWallet); |
| 125 | + verifyCallArgs.should.have.property('walletType', 'onchain'); |
| 126 | + |
| 127 | + // Verify that signTransaction was not called due to verification failure |
| 128 | + mockBaseCoinReal.signTransaction.should.not.have.been.called; |
| 129 | + }); |
| 130 | + |
| 131 | + it('should pass verification when expected recipient matches actual transaction recipient', async function () { |
| 132 | + // Same real transaction hex |
| 133 | + const realTxHex = |
| 134 | + '70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000'; |
| 135 | + |
| 136 | + const txPrebuild = { |
| 137 | + txHex: realTxHex, |
| 138 | + walletId: '6840948b17e91662b782d55bbf988c4e', |
| 139 | + }; |
| 140 | + |
| 141 | + // Verification parameters with correct expected recipient |
| 142 | + const verifyTxParams = { |
| 143 | + txParams: { |
| 144 | + recipients: [ |
| 145 | + { |
| 146 | + address: 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', // Correct recipient |
| 147 | + amount: '10000', // Expected amount |
| 148 | + }, |
| 149 | + ], |
| 150 | + type: 'send', |
| 151 | + }, |
| 152 | + verification: { |
| 153 | + disableNetworking: true, |
| 154 | + }, |
| 155 | + }; |
| 156 | + |
| 157 | + const signParams: WalletSignTransactionOptions = { |
| 158 | + txPrebuild, |
| 159 | + verifyTxParams, |
| 160 | + prv: 'test-private-key', |
| 161 | + }; |
| 162 | + |
| 163 | + // Mock presignTransaction to return the same params |
| 164 | + mockBaseCoinReal.presignTransaction.resolves(signParams); |
| 165 | + |
| 166 | + // Mock verifyTransaction to succeed |
| 167 | + mockBaseCoinReal.verifyTransaction.resolves(true); |
| 168 | + |
| 169 | + // Mock signTransaction to return a signed transaction |
| 170 | + mockBaseCoinReal.signTransaction.resolves({ |
| 171 | + txHex: realTxHex, |
| 172 | + halfSigned: {}, |
| 173 | + }); |
| 174 | + |
| 175 | + const result = await realWallet.signTransaction(signParams); |
| 176 | + |
| 177 | + // Verify that verifyTransaction was called |
| 178 | + mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce; |
| 179 | + |
| 180 | + // Verify that signTransaction was called after successful verification |
| 181 | + mockBaseCoinReal.signTransaction.should.have.been.calledOnce; |
| 182 | + |
| 183 | + // Verify the result |
| 184 | + result.should.have.property('txHex', realTxHex); |
| 185 | + }); |
| 186 | + |
| 187 | + it('should handle amount verification in addition to address verification', async function () { |
| 188 | + const realTxHex = |
| 189 | + '70736274ff0100890100000001e082ee5f3be60a260bd181d86cbc3ed1f2f53f6e33572138c3220a461d64e13d0100000000fdffffff021027000000000000220020670840f165692923c8e8709d271e467894807bc70bcc49f5475889c789fd9c9375d50400000000002251205ae846b2c844e131cefcf635e8566a683a3f96c97410c43413f919cc9247e182000000004f010488b21e000000000000000000940a6f6c2d84214ba69e48354858dd8e4df2b0f36d51b6721516172c4b56922402c8d26710504a5a7965c8fce430057418802bc5a06e1987ede6897fb40e0a66b5044b8de8914f010488b21e0000000000000000009c6426c55cb8e0b186f7f776b42cb4de8118be401cce288bcb1ef70457f4072c0397f25ebd3f03c85333b1ffc14e04d051b476a5e48e4e0a5e999c0cf163c3178704758af64b4f010488b21e000000000000000000ab9a6eea233e963b74fd79afd9c71d467fb1ce1d91fe1681e2b7332e203baae6033e70fb09c6eb45d08aff2f067060c6345143918961d5bee0e1d8037b77001925047b156bd00001012b8e02050000000000225120a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f609001030400000000211686f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d671500758af64b000000000000000029000000110000002116b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14815004b8de89100000000000000002900000011000000011720cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7011820b558f0176c61865e960bbd14bff1e251b99cebb81fea61c0f931452dc029778648fc05424954474f01a385cb8dc799daaffb15a3e311e1465b3ce990017034ec4e1e2056575a3f6090cd77b37b43fe3ae9cdcb266faaf4b443766269f3182f6bb40f3a9db93f8384d7420286f3450713d04e8ac343e576c1df80c5a1fca4e45aff6ee6447ceeb4d0d51d6703b7d96086e8b6763162b7deb2a0149d104d01a2ddad547f290822d304dd6bd14800000105200fc5866bddca6f779664a6910eda9af586287893c805b717b90af777cb5b2a0501068e01c04420ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b37317ad206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ac01c044206c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf9547245ad2005e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91ac210705e1bca7220c83cc2d6a2c596f8fe00b9bc084bee5667cab06fa3354f1565a91350177065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b8de8910000000000000000290000001200000021076c5e41b4813b4b0112e41738dea9b866d30c5f34832961120a5bfddaf954724555020e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a3277065e65a3639ada742d46b3d282980784549337c12f015385b727711d98b4374b156bd0000000000000000029000000120000002107ac366be7d240d82968539bdab6fc81fdb016f0b73d720afd1bfdd50bc8b3731735010e7b30f7ec73ccbc76d8e08bc35c7b4837defce7f184f658c95b94bdec831a32758af64b0000000000000000290000001200000000'; |
| 190 | + |
| 191 | + const txPrebuild = { |
| 192 | + txHex: realTxHex, |
| 193 | + walletId: '6840948b17e91662b782d55bbf988c4e', |
| 194 | + }; |
| 195 | + |
| 196 | + // Verification parameters with correct address but wrong amount |
| 197 | + const verifyTxParams = { |
| 198 | + txParams: { |
| 199 | + recipients: [ |
| 200 | + { |
| 201 | + address: 'tb1qvuyyput9dy5j8j8gwzwjw8jx0z2gq778p0xyna28tzyu0z0anjfs3pf2mp', // Correct recipient |
| 202 | + amount: '50000', // Wrong amount (actual is 10000 satoshis) |
| 203 | + }, |
| 204 | + ], |
| 205 | + type: 'send', |
| 206 | + }, |
| 207 | + verification: { |
| 208 | + disableNetworking: true, |
| 209 | + }, |
| 210 | + }; |
| 211 | + |
| 212 | + const signParams: WalletSignTransactionOptions = { |
| 213 | + txPrebuild, |
| 214 | + verifyTxParams, |
| 215 | + prv: 'test-private-key', |
| 216 | + }; |
| 217 | + |
| 218 | + // Mock presignTransaction to return the same params |
| 219 | + mockBaseCoinReal.presignTransaction.resolves(signParams); |
| 220 | + |
| 221 | + // Mock verifyTransaction to fail with amount mismatch error |
| 222 | + mockBaseCoinReal.verifyTransaction.rejects( |
| 223 | + new Error('Transaction verification failed: amount mismatch. Expected 50000 but transaction sends 10000') |
| 224 | + ); |
| 225 | + |
| 226 | + try { |
| 227 | + await realWallet.signTransaction(signParams); |
| 228 | + assert.fail('Should have thrown verification error'); |
| 229 | + } catch (error) { |
| 230 | + assert.ok( |
| 231 | + error.message.includes('amount mismatch'), |
| 232 | + `Error message should contain 'amount mismatch', got: ${error.message}` |
| 233 | + ); |
| 234 | + assert.ok( |
| 235 | + error.message.includes('Expected 50000'), |
| 236 | + `Error message should contain 'Expected 50000', got: ${error.message}` |
| 237 | + ); |
| 238 | + assert.ok( |
| 239 | + error.message.includes('sends 10000'), |
| 240 | + `Error message should contain 'sends 10000', got: ${error.message}` |
| 241 | + ); |
| 242 | + } |
| 243 | + |
| 244 | + // Verify that verifyTransaction was called |
| 245 | + mockBaseCoinReal.verifyTransaction.should.have.been.calledOnce; |
| 246 | + |
| 247 | + // Verify that signTransaction was not called due to verification failure |
| 248 | + mockBaseCoinReal.signTransaction.should.not.have.been.called; |
| 249 | + }); |
| 250 | +}); |
0 commit comments