diff --git a/modules/sdk-coin-ton/src/lib/transaction.ts b/modules/sdk-coin-ton/src/lib/transaction.ts index 4a97edaaf1..fbd1e4443f 100644 --- a/modules/sdk-coin-ton/src/lib/transaction.ts +++ b/modules/sdk-coin-ton/src/lib/transaction.ts @@ -18,6 +18,7 @@ export class Transaction extends BaseTransaction { expireTime: number; sender: string; publicKey: string; + isV3ContractMessage: boolean; protected unsignedMessage: string; protected finalMessage: string; @@ -26,6 +27,7 @@ export class Transaction extends BaseTransaction { this.bounceable = false; this.fromAddressBounceable = true; this.toAddressBounceable = true; + this.isV3ContractMessage = false; } canSign(key: BaseKey): boolean { @@ -93,7 +95,9 @@ export class Transaction extends BaseTransaction { // expireAt should be set as per the provided arg value, regardless of the seqno message.bits.writeUint(expireAt, 32); message.bits.writeUint(seqno, 32); - message.bits.writeUint(0, 8); // op + if (!this.isV3ContractMessage) { + message.bits.writeUint(0, 8); // op + } return message; } @@ -140,7 +144,7 @@ export class Transaction extends BaseTransaction { body.writeCell(signingMessage); let stateInit; - if (seqno === 0) { + if (seqno === 0 && !this.isV3ContractMessage) { const WalletClass = TonWeb.Wallets.all['v4R2']; const wallet = new WalletClass(new TonWeb.HttpProvider(), { publicKey: TonWeb.utils.hexToBytes(this.publicKey), @@ -266,10 +270,14 @@ export class Transaction extends BaseTransaction { const seqno = slice.loadUint(32).toNumber(); const op = slice.loadUint(8).toNumber(); - if (op !== 0) throw new Error('invalid op'); + if (op !== 3) { + if (op !== 0) throw new Error('invalid op'); - const sendMode = slice.loadUint(8).toNumber(); - if (sendMode !== 3) throw new Error('invalid sendMode'); + const sendMode = slice.loadUint(8).toNumber(); + if (sendMode !== 3) throw new Error('invalid sendMode'); + } else { + this.isV3ContractMessage = true; + } let order = slice.loadRef(); diff --git a/modules/sdk-coin-ton/src/lib/transactionBuilder.ts b/modules/sdk-coin-ton/src/lib/transactionBuilder.ts index 757218fe27..147aac1c10 100644 --- a/modules/sdk-coin-ton/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-ton/src/lib/transactionBuilder.ts @@ -179,4 +179,9 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { this.transaction.publicKey = key; return this; } + + isV3ContractMessage(bool: boolean): TransactionBuilder { + this.transaction.isV3ContractMessage = bool; + return this; + } } diff --git a/modules/sdk-coin-ton/test/resources/ton.ts b/modules/sdk-coin-ton/test/resources/ton.ts index 01b71635d1..108e3c6e06 100644 --- a/modules/sdk-coin-ton/test/resources/ton.ts +++ b/modules/sdk-coin-ton/test/resources/ton.ts @@ -10,6 +10,7 @@ export const privateKeys = { prvKey4: 'ba4c313bcf830b825adaa3ae08cfde86e79e15a84e6fdc3b1fe35a6bb82d9f22', prvKey5: 'fdb9ea1bb7f0120ce6eb7b047ac6744c4298f277756330e18dbd5419590a1ef2', prvKey6: 'f66d30357e6a4180e9ab41865f0f347cbaffe60f9d499fe6c2f27046327652f6', + prvKey7: '339f9b7c8b039e1f71cc28c5fa30db29913624e2d53d4323a2664d3cfb5b07a4', }; export const addresses = { @@ -24,6 +25,13 @@ export const addresses = { ], }; +export const vestingContract = { + address: 'UQD0eii2CN6p_hznXcqBFlHHbcBQShW2Vj1EaIK2MnxVrWC6', + addressBounceable: 'EQD0eii2CN6p_hznXcqBFlHHbcBQShW2Vj1EaIK2MnxVrT1_', + publicKey: '339f9b7c8b039e1f71cc28c5fa30db29913624e2d53d4323a2664d3cfb5b07a4', + signature: '4K2iXQ5G5lU/1TKhfQn4xaAgL7sK91mFQb6OixkRwpZFJCYSbhmDJqLJfWRVRjfbL+cX4HjC7siqsGMDUgh4DQ==', +}; + export const sender = { address: 'UQAbJug-k-tufWMjEC1RKSM0iiJTDUcYkC7zWANHrkT55Afg', publicKey: 'c0c3b9dc09932121ee351b2448c50a3ae2571b12951245c85f3bd95d5e7a06f8', @@ -71,6 +79,21 @@ export const signedSendTransaction = { }, }; +export const v3CompatibleSignedSendTransaction = { + txBounceable: + 'te6cckEBAgEAqAAB34gB6PRRbBG9U/w5zruVAiyjjtuAoJQrbKx6iNEFbGT4q1oHBW0S6HI3Mqn+qZUL6E/GLQEBfdhXuswqDfR0WMiOFLIpITCTcMwZNRZL6yKqMb7Zfzi/A8YXdkVVgxgakEPAaU1NGLtH0CDAAAAAGBwBAGZiAGcJlmF0UvErDsi5Rs21SP70rP1K36wtjBImqtbV96EuHMS0AAAAAAAAAAAAAAAAAAAiW72E', + txIdBounceable: '4i1GCyN5IkQQ-vESvNl4Wp1ejp7LfazRlNWzUbtGwSA=', + bounceableSignable: 'lOEOTzPXnPotTTHi7xgivFNUHH+xUgq/nKpaP/bK+Xo=', + recipient: { + address: 'EQDOEyzC6KXiVh2Rco2bapH96Vn6lb9YWxgkTVWtq-9CXL0m', + amount: '10000000', + }, + recipientNonBounceable: { + address: 'UQDOEyzC6KXiVh2Rco2bapH96Vn6lb9YWxgkTVWtq-9CXODj', + amount: '10000000', + }, +}; + export const signedTokenSendTransaction = { tx: 'te6cckECGgEABB0AAuGIAVSGb+UGjjP3lvt+zFA8wouI3McEd6CKbO2TwcZ3OfLKGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF9NTAQHUHhbX00VGZ3d2r8hbJxuz7PaxmuCOJ6kgckppQAFmQgABT9LR3Iqffskp0J9gWYO8Azlnb33BCMj8FqIUIGxGOZpiWgAAAAAAAAAAAAAAAAABGAGuD4p+pQAAAAAAAAAAQ7msoAgA/BGdBi/R01erquxJOvPgGKclBawUs3MAi0/IdctKQz8AKpDN/KDRxn7y32/ZigeYUXEbmOCO9BFNnbJ4OM7nPllGHoSBGQAkAAAAAGpldHRvbiB0ZXN0aW5nwHtw7A==', tx2: 'te6cckECGgEABBQAAuGIAVSGb+UGjjP3lvt+zFA8wouI3McEd6CKbO2TwcZ3OfLKGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF9NTAQHUHhbX00VGZ3d2r8hbJxuz7PaxmuCOJ6kgckppQAFoYgABT9LR3Iqffskp0J9gWYO8Azlnb33BCMj8FqIUIGxGOaAX14QAAAAAAAAAAAAAAAAAARgBrg+KfqUAAAAAAAAAAEO5rKAIAPwRnQYv0dNXq6rsSTrz4BinJQWsFLNzAItPyHXLSkM/ACqQzfyg0cZ+8t9v2YoHmFFxG5jgjvQRTZ2yeDjO5z5ZRzEtARkAEAAAAAB0ZXN0MfeoHg==', diff --git a/modules/sdk-coin-ton/test/unit/transferBuilder.ts b/modules/sdk-coin-ton/test/unit/transferBuilder.ts index 732346b8d9..bd52d67d9e 100644 --- a/modules/sdk-coin-ton/test/unit/transferBuilder.ts +++ b/modules/sdk-coin-ton/test/unit/transferBuilder.ts @@ -212,4 +212,51 @@ describe('Ton Transfer Builder', () => { const txJson2 = tx2.toJson(); txJson2.destinationAlias.should.equal(otherFormat); }); + + it('should build a transfer tx compatible with v3 wallet contract', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sender(testData.vestingContract.address); + txBuilder.sequenceNumber(3); + txBuilder.expireTime(1761215512); + txBuilder.send(testData.v3CompatibleSignedSendTransaction.recipient); + txBuilder.isV3ContractMessage(true); + txBuilder.bounceable(true); + txBuilder.addSignature( + { pub: testData.vestingContract.publicKey }, + Buffer.from(testData.vestingContract.signature, 'base64') + ); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.Send); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.vestingContract.address, + value: testData.v3CompatibleSignedSendTransaction.recipient.amount, + coin: 'tton', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.v3CompatibleSignedSendTransaction.recipient.address, + value: testData.v3CompatibleSignedSendTransaction.recipient.amount, + coin: 'tton', + }); + }); + + it('should build a v3 compatible send transaction using raw transaction hex', async function () { + const txBuilder = factory.from(testData.v3CompatibleSignedSendTransaction.txBounceable); + const builtTx = await txBuilder.build(); + const jsonTx = builtTx.toJson(); + should.equal(builtTx.type, TransactionType.Send); + should.equal( + builtTx.signablePayload.toString('base64'), + testData.v3CompatibleSignedSendTransaction.bounceableSignable + ); + should.equal(builtTx.id, testData.v3CompatibleSignedSendTransaction.txIdBounceable); + jsonTx.sender.should.equal(testData.vestingContract.addressBounceable); + jsonTx.destination.should.equal(testData.v3CompatibleSignedSendTransaction.recipient.address); + jsonTx.destinationAlias.should.equal(testData.v3CompatibleSignedSendTransaction.recipientNonBounceable.address); + jsonTx.amount.should.equal(testData.v3CompatibleSignedSendTransaction.recipient.amount); + jsonTx.seqno.should.equal(3); + jsonTx.expirationTime.should.equal(1761215512); + jsonTx.bounceable.should.equal(true); + }); });