diff --git a/modules/sdk-coin-ton/src/lib/constants.ts b/modules/sdk-coin-ton/src/lib/constants.ts index 1a87b5cc8b..7b10ae78b1 100644 --- a/modules/sdk-coin-ton/src/lib/constants.ts +++ b/modules/sdk-coin-ton/src/lib/constants.ts @@ -6,3 +6,4 @@ 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'; export const TON_WHALES_WITHDRAW_OPCODE = '3665837821'; +export const SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT = 'w'; diff --git a/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts b/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts index 6ecc38e68d..ee104de608 100644 --- a/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts +++ b/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts @@ -2,6 +2,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { Recipient, TransactionType } from '@bitgo/sdk-core'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction'; +import { SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT } from './constants'; export class SingleNominatorWithdrawBuilder extends TransactionBuilder { constructor(_coinConfig: Readonly) { @@ -23,6 +24,16 @@ export class SingleNominatorWithdrawBuilder extends TransactionBuilder { return this; } + /** + * Sets the message to withdraw everything from the single nominator contract. + * Uses a plain transfer with text comment "w" which instructs the contract to + * drain balance - gas - MIN_TON_FOR_STORAGE automatically. + */ + setFullWithdrawalMessage(): SingleNominatorWithdrawBuilder { + this.transaction.message = SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT; + return this; + } + setMessage(msg: string): SingleNominatorWithdrawBuilder { 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 2c251a2adb..38f51bcea7 100644 --- a/modules/sdk-coin-ton/src/lib/transaction.ts +++ b/modules/sdk-coin-ton/src/lib/transaction.ts @@ -12,6 +12,7 @@ import { VESTING_CONTRACT_WALLET_ID, TON_WHALES_DEPOSIT_OPCODE, TON_WHALES_WITHDRAW_OPCODE, + SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT, } from './constants'; export class Transaction extends BaseTransaction { @@ -359,6 +360,8 @@ export class Transaction extends BaseTransaction { this.transactionType = TransactionType.TonWhalesVestingDeposit; } else if (payload === 'Withdraw') { this.transactionType = TransactionType.TonWhalesVestingWithdrawal; + } else if (payload === SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT) { + this.transactionType = TransactionType.SingleNominatorWithdraw; } } else if (opcode === 4096) { const queryId = order.loadUint(64).toNumber(); diff --git a/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts b/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts index c387285985..e0fa773420 100644 --- a/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts +++ b/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts @@ -132,6 +132,68 @@ describe('Ton Single Nominator Withdraw Builder', () => { should.equal(txBounceable.toJson().withdrawAmount, singleNominatorWithdrawAmount); }); + it('should build a full withdrawal tx using setFullWithdrawalMessage', async function () { + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(testData.sender.address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(testData.sender.publicKey); + txBuilder.expireTime(1234567890); + txBuilder.send(testData.recipients[0]); + txBuilder.setFullWithdrawalMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx.toJson().bounceable, false); + should.equal(tx.toJson().withdrawAmount, undefined); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender.address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + const rawTx = tx.toBroadcastFormat(); + // Verify the raw transaction can be parsed back as a full withdrawal + // Full withdrawal is inferred by: transactionType === SingleNominatorWithdraw && !withdrawAmount + const txBuilder2 = factory.from(rawTx); + const tx2 = await txBuilder2.build(); + should.equal(tx2.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx2.toJson().withdrawAmount, undefined); + should.equal(tx2.toBroadcastFormat(), rawTx); + }); + + it('should build a signed full withdrawal tx using add signature', async function () { + const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 }); + const publicKey = keyPair.getKeys().pub; + const address = await utils.default.getAddressFromPublicKey(publicKey); + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(publicKey); + const expireAt = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 7; + txBuilder.expireTime(expireAt); + txBuilder.send(testData.recipients[0]); + txBuilder.setFullWithdrawalMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx.toJson().withdrawAmount, undefined); + const signable = tx.signablePayload; + const signature = keyPair.signMessageinUint8Array(signable); + txBuilder.addSignature(keyPair.getKeys(), Buffer.from(signature)); + const signedTx = await txBuilder.build(); + const builder2 = factory.from(signedTx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const signature2 = keyPair.signMessageinUint8Array(tx2.signablePayload); + should.equal(Buffer.from(signature).toString('hex'), Buffer.from(signature2).toString('hex')); + should.equal(tx.toBroadcastFormat(), tx2.toBroadcastFormat()); + should.equal(tx2.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx2.toJson().withdrawAmount, undefined); + }); + xit('should build a signed withdraw tx and submit onchain', async function () { const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC')); const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 });