diff --git a/modules/sdk-coin-ton/src/lib/constants.ts b/modules/sdk-coin-ton/src/lib/constants.ts index 3d0ba69bcb..753d600e43 100644 --- a/modules/sdk-coin-ton/src/lib/constants.ts +++ b/modules/sdk-coin-ton/src/lib/constants.ts @@ -4,3 +4,4 @@ export const JETTON_TRANSFER_OPCODE = 0x0f8a7ea5; export const WITHDRAW_OPCODE = '00001000'; export const VESTING_CONTRACT_CODE_B64 = 'te6cckECHAEAA/sAART/APSkE/S88sgLAQIBIAISAgFIAwUDrNBsIiDXScFgkVvgAdDTAwFxsJFb4PpAMNs8AdMf0z/4S1JAxwUjghCnczrNurCOpGwS2zyCEPdzOs0BcIAYyMsFUATPFiP6AhPLassfyz/JgED7AOMOExQEAc74SlJAxwUDghByWKabuhOwjtGOLAH6QH/IygAC+kQByMoHy//J0PhEECOBAQj0QfhkINdKwgAglQHUMNAB3rMS5oIQ8limmzJwgBjIywVQBM8WI/oCE8tqyx/LP8mAQPsA2zySXwPiGwIBIAYPAgEgBwoCAW4ICQAZrc52omhAIGuQ64X/wAAZrx32omhAEGuQ64WPwAIBYgsMAUutNG2eNvwiRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQBMCAWoNDgAPol+1E0NcLH4BL6LHbPPpEAcjKB8v/ydD4RIEBCPQKb6ExhMCASAQEQEpukYts8+EX4RvhH+Ej4SfhK+Ev4RIEwINuYRts82zyBMVA7jygwjXGCDTH9Mf0x8C+CO78mTtRNDTH9Mf0/8wWrryoVAzuvKiAvkBQDP5EPKj+ADbPCDXSsABjpntRO1F7UeRW+1n7WXtZI6C2zztQe3xAfL/kTDi+EGk+GHbPBMUGwB+7UTQ0x8B+GHTHwH4YtP/Afhj9AQB+GTUAdDTPwH4ZdMfAfhm0x8B+GfTHwH4aPoAAfhp+kAB+Gr6QAH4a9HRAlzTB9TR+CPbPCDCAI6bIsAD8uBkIdDTA/pAMfpA+EpSIMcFs5JfBOMNkTDiAfsAFRYAYPhF+EagUhC8kjBw4PhF+EigUhC5kzD4SeD4SfhJ+EUTofhHqQT4RvhHqQQQI6mEoQP6IfpEAcjKB8v/ydD4RIEBCPQKb6Exj18zAXKwwALy4GUB+gAxcdch+gAx+gAx0z8x0x8x0wABwADy4GbTAAGT1DDQ3iFx2zyOKjHTHzAgghBOc3RLuiGCEEdldCS6sSGCEFZ0Q3C6sQGCEFZvdGW6sfLgZ+MOcJJfA+IgwgAYFxoC6gFw2zyObSDXScIAjmPTHyHAACKDC7qxIoEQAbqxIoIQR9VDkbqxIoIQWV8HvLqxIoIQafswbLqxIoIQVm90ZbqxIoIQVnRDcLqx8uBnAcAAIddJwgCwjhXTBzAgwGQhwHexIcBEsQHAV7Hy4GiRMOKRMOLjDRgZAEQB+kQBw/+SW3DgAfgzIG6SW3Dg0CDXSYMHuZJbcODXC/+6ABrTHzCCEFZvdGW68uBnAA6TcvsCkTDiAGb4SPhH+Eb4RcjLP8sfyx/LH/hJ+gL4Ss8W+EvPFsn4RPhD+EL4QcjLH8sfy//0AMzJ7VSo1+S9'; +export const TON_WHALES_DEPOSIT_OPCODE = '2077040623'; diff --git a/modules/sdk-coin-ton/src/lib/tonWhalesDepositBuilder.ts b/modules/sdk-coin-ton/src/lib/tonWhalesDepositBuilder.ts new file mode 100644 index 0000000000..872568860f --- /dev/null +++ b/modules/sdk-coin-ton/src/lib/tonWhalesDepositBuilder.ts @@ -0,0 +1,43 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { Recipient, TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; +import { TON_WHALES_DEPOSIT_OPCODE } from './constants'; + +export class TonWhalesDepositBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.TonWhalesDeposit; + } + + setDepositMessage(queryId?: string): TonWhalesDepositBuilder { + // Deposit payload is just OpCode + QueryId. + // The Amount is in the transaction value (recipient.amount) + // The Gas Limit is hardcoded in Transaction.ts build() + const qId = queryId || '0000000000000000'; + this.transaction.message = TON_WHALES_DEPOSIT_OPCODE + qId; + return this; + } + + setDepositAmount(amount: string): TonWhalesDepositBuilder { + if (!this.transaction.recipient) { + this.transaction.recipient = { address: '', amount: amount }; + } else { + this.transaction.recipient.amount = amount; + } + return this; + } + + send(recipient: Recipient): TonWhalesDepositBuilder { + this.transaction.recipient = recipient; + return this; + } + + setMessage(msg: string): TonWhalesDepositBuilder { + throw new Error('Method not implemented.'); + } +} diff --git a/modules/sdk-coin-ton/src/lib/transaction.ts b/modules/sdk-coin-ton/src/lib/transaction.ts index 3088bc7558..6e9f9609f7 100644 --- a/modules/sdk-coin-ton/src/lib/transaction.ts +++ b/modules/sdk-coin-ton/src/lib/transaction.ts @@ -5,7 +5,13 @@ import { Cell } from 'tonweb/dist/types/boc/cell'; import { BaseKey, BaseTransaction, Entry, Recipient, TransactionRecipient, TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { TransactionExplanation, TxData } from './iface'; -import { WITHDRAW_OPCODE, WALLET_ID, JETTON_TRANSFER_OPCODE, VESTING_CONTRACT_WALLET_ID } from './constants'; +import { + WITHDRAW_OPCODE, + WALLET_ID, + JETTON_TRANSFER_OPCODE, + VESTING_CONTRACT_WALLET_ID, + TON_WHALES_DEPOSIT_OPCODE, +} from './constants'; export class Transaction extends BaseTransaction { public recipient: Recipient; @@ -127,6 +133,11 @@ export class Transaction extends BaseTransaction { payloadCell.bits.writeUint(parseInt(WITHDRAW_OPCODE, 16), 32); payloadCell.bits.writeUint(parseInt(queryId, 16), 64); payloadCell.bits.writeCoins(new BN(withdrawAmount)); + } else if (payload.length >= 26 && payload.substring(0, 10) === TON_WHALES_DEPOSIT_OPCODE) { + const queryId = payload.substring(10, 26); + payloadCell.bits.writeUint(parseInt(TON_WHALES_DEPOSIT_OPCODE, 10), 32); + payloadCell.bits.writeUint(parseInt(queryId, 16), 64); + payloadCell.bits.writeCoins(TonWeb.utils.toNano('1')); } else { payloadCell.bits.writeUint(0, 32); payloadCell.bits.writeString(payload); @@ -369,6 +380,13 @@ export class Transaction extends BaseTransaction { forwardTonAmount: forwardTonAmount.toString(), message: message, }; + } else if (opcode === parseInt(TON_WHALES_DEPOSIT_OPCODE, 10)) { + this.transactionType = TransactionType.TonWhalesDeposit; + const queryId = order.loadUint(64).toNumber(); + // This is the gas limit, which must be read to advance the cursor + // We do not need to store it + order.loadCoins(); + payload = TON_WHALES_DEPOSIT_OPCODE + queryId.toString(16).padStart(16, '0'); } else { payload = ''; } diff --git a/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts index dfcfc18822..c6b354db2c 100644 --- a/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts @@ -6,6 +6,7 @@ import { SingleNominatorWithdrawBuilder } from './singleNominatorWithdrawBuilder import { Transaction } from './transaction'; import { TokenTransferBuilder } from './tokenTransferBuilder'; import { TokenTransaction } from './tokenTransaction'; +import { TonWhalesDepositBuilder } from './tonWhalesDepositBuilder'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -37,6 +38,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { case TransactionType.SendToken: builder = this.getTokenTransferBuilder(); break; + case TransactionType.TonWhalesDeposit: + builder = this.getTonWhalesDepositBuilder(); + break; default: throw new InvalidTransactionError('unsupported transaction'); } @@ -70,4 +74,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); } + + getTonWhalesDepositBuilder(): TonWhalesDepositBuilder { + return new TonWhalesDepositBuilder(this._coinConfig); + } } diff --git a/modules/sdk-coin-ton/test/resources/ton.ts b/modules/sdk-coin-ton/test/resources/ton.ts index 108e3c6e06..bc82ecef89 100644 --- a/modules/sdk-coin-ton/test/resources/ton.ts +++ b/modules/sdk-coin-ton/test/resources/ton.ts @@ -146,3 +146,21 @@ export const signedSingleNominatorWithdrawTransaction = { amount: '123400000', }, }; + +export const signedTonWhalesDepositTransaction = { + recipient: { + //https://testnet.tonscan.org/address/kQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_l7mg + address: 'EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq', + amount: '10000000000', // 10 TON + }, + // This is the raw TX from sandboxing a deposit to Ton Whales + tx: 'te6cckEBAgEAvAAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwFf6OLyGMsPoPXNPLUqMoUZTIrdu2maNNUK52q+Wa0BJhNq9e/qHXYsF9xU5TYbOsZt1EBGJf1GpkumdgXj0/4CU1NGLtKFdHwAAAC4AAcAQCLYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6gSoF8gAAAAAAAAAAAAAAAAAAB7zR/vAAAAAGlCugJDuaygCErRw2Y=', + seqno: 92, + queryId: '000000006942ba02', + expireTime: 1765980734, + sender: 'EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ', + publicKey: '9d6d3714aeb1f007f6e6aa728f79fdd005ea2c7ad459b2f54d73f9e672426230', + signature: + 'aff471790c6587d07ae69e5a9519428ca6456eddb4cd1a6a8573b55f2cd6809309b57af7f50ebb160bee2a729b0d9d6336ea202312fea35325d33b02f1e9ff01', + bounceable: true, +}; diff --git a/modules/sdk-coin-ton/test/unit/tonWhalesDepositBuilder.ts b/modules/sdk-coin-ton/test/unit/tonWhalesDepositBuilder.ts new file mode 100644 index 0000000000..9ff3245b71 --- /dev/null +++ b/modules/sdk-coin-ton/test/unit/tonWhalesDepositBuilder.ts @@ -0,0 +1,93 @@ +import should from 'should'; +import { TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilderFactory } from '../../src'; // Adjust path as needed +import { coins } from '@bitgo/statics'; +import * as testData from '../resources/ton'; +import { TON_WHALES_DEPOSIT_OPCODE } from '../../src/lib/constants'; + +describe('Ton Whales Deposit Builder', () => { + const factory = new TransactionBuilderFactory(coins.get('tton')); + const fixture = testData.signedTonWhalesDepositTransaction; + + it('should parse a raw transaction and extract correct parameters', async function () { + const txBuilder = factory.from(fixture.tx); + const builtTx = await txBuilder.build(); + const jsonTx = builtTx.toJson(); + + // Verify Business Logic Fields + should.equal(builtTx.type, TransactionType.TonWhalesDeposit); + should.equal(jsonTx.amount, fixture.recipient.amount); + should.equal(jsonTx.destination, fixture.recipient.address); + should.equal(jsonTx.sender, fixture.sender); + + // Verify Network Constraints + should.equal(jsonTx.seqno, fixture.seqno); + should.equal(jsonTx.expirationTime, fixture.expireTime); + should.equal(jsonTx.bounceable, fixture.bounceable); + + // Verify Payload Structure (OpCode Check) + const msg = builtTx['message'] || ''; + should.equal(msg.startsWith(TON_WHALES_DEPOSIT_OPCODE), true); + }); + + it('should parse and rebuild the transaction resulting in the same hex', async function () { + const txBuilder = factory.from(fixture.tx); + const builtTx = await txBuilder.build(); + + // Verify the parser extracted the signature + const signature = builtTx.signature[0]; + should.exist(signature); + signature.should.not.be.empty(); + + // Rebuild from the parsed object + const builder2 = factory.from(builtTx.toBroadcastFormat()); + const builtTx2 = await builder2.build(); + + // The output of the second build should match the original raw transaction + should.equal(builtTx2.toBroadcastFormat(), fixture.tx); + should.equal(builtTx2.type, TransactionType.TonWhalesDeposit); + }); + + it('should build a transaction from scratch that byte-for-byte matches the raw fixture', async function () { + const builder = factory.getTonWhalesDepositBuilder(); + + // Set Header Info from Fixture + builder.sender(fixture.sender); + builder.publicKey(fixture.publicKey); + builder.sequenceNumber(fixture.seqno); + builder.expireTime(fixture.expireTime); + builder.bounceable(fixture.bounceable); + + // Set Staking Info from Fixture + builder.send({ + address: fixture.recipient.address, + amount: fixture.recipient.amount, + }); + builder.setDepositAmount(fixture.recipient.amount); + + // Set the specific QueryID from Fixture so binary hash matches + builder.setDepositMessage(fixture.queryId); + + // Attach Signature from Fixture (Mocking the HSM signing process) + if (fixture.signature) { + builder.addSignature({ pub: fixture.publicKey }, Buffer.from(fixture.signature, 'hex')); + } + + // Build Signed Transaction + const signedBuiltTx = await builder.build(); + + // Final Assertion: Byte-for-byte equality with the Sandbox output + should.equal(signedBuiltTx.toBroadcastFormat(), fixture.tx); + should.equal(signedBuiltTx.type, TransactionType.TonWhalesDeposit); + }); + + it('should parse the bounceable flag correctly', async function () { + const txBuilder = factory.from(fixture.tx); + const tx = await txBuilder.build(); + + // The fixture is set to true, so the parser must reflect that + const isBounceable = tx.toJson().bounceable; + should.equal(isBounceable, fixture.bounceable); + should.equal(typeof isBounceable, 'boolean'); + }); +}); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index 6433f84c05..7153f7195f 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -119,6 +119,10 @@ export enum TransactionType { // flrp ImportToC, + + // ton whales + TonWhalesDeposit, + TonWhalesWithdraw, } /**