Skip to content

Commit d0e792a

Browse files
committed
feat(sdk-coin-ton): add setFullWithdrawalMessage() for full unstake
Implements the full-withdrawal path for TON single nominator contracts via text comment "w". When present, the contract drains balance - gas - MIN_TON_FOR_STORAGE automatically. - Add SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT = 'w' constant - Add setFullWithdrawalMessage() to SingleNominatorWithdrawBuilder - Detect "w" comment in transaction parser to set SingleNominatorWithdraw type - Add unit tests for full withdrawal build and round-trip Full withdrawal is inferred by transactionType === SingleNominatorWithdraw && !withdrawAmount, mirroring TonWhales handling. SC-6175 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> TICKET: SC-6175
1 parent 9537fdd commit d0e792a

4 files changed

Lines changed: 77 additions & 0 deletions

File tree

modules/sdk-coin-ton/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export const VESTING_CONTRACT_CODE_B64 =
66
'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';
77
export const TON_WHALES_DEPOSIT_OPCODE = '2077040623';
88
export const TON_WHALES_WITHDRAW_OPCODE = '3665837821';
9+
export const SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT = 'w';

modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
22
import { Recipient, TransactionType } from '@bitgo/sdk-core';
33
import { TransactionBuilder } from './transactionBuilder';
44
import { Transaction } from './transaction';
5+
import { SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT } from './constants';
56

67
export class SingleNominatorWithdrawBuilder extends TransactionBuilder {
78
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -23,6 +24,16 @@ export class SingleNominatorWithdrawBuilder extends TransactionBuilder {
2324
return this;
2425
}
2526

27+
/**
28+
* Sets the message to withdraw everything from the single nominator contract.
29+
* Uses a plain transfer with text comment "w" which instructs the contract to
30+
* drain balance - gas - MIN_TON_FOR_STORAGE automatically.
31+
*/
32+
setFullWithdrawalMessage(): SingleNominatorWithdrawBuilder {
33+
this.transaction.message = SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT;
34+
return this;
35+
}
36+
2637
setMessage(msg: string): SingleNominatorWithdrawBuilder {
2738
throw new Error('Method not implemented.');
2839
}

modules/sdk-coin-ton/src/lib/transaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
VESTING_CONTRACT_WALLET_ID,
1313
TON_WHALES_DEPOSIT_OPCODE,
1414
TON_WHALES_WITHDRAW_OPCODE,
15+
SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT,
1516
} from './constants';
1617

1718
export class Transaction extends BaseTransaction {
@@ -359,6 +360,8 @@ export class Transaction extends BaseTransaction {
359360
this.transactionType = TransactionType.TonWhalesVestingDeposit;
360361
} else if (payload === 'Withdraw') {
361362
this.transactionType = TransactionType.TonWhalesVestingWithdrawal;
363+
} else if (payload === SINGLE_NOMINATOR_WITHDRAW_ALL_COMMENT) {
364+
this.transactionType = TransactionType.SingleNominatorWithdraw;
362365
}
363366
} else if (opcode === 4096) {
364367
const queryId = order.loadUint(64).toNumber();

modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,68 @@ describe('Ton Single Nominator Withdraw Builder', () => {
132132
should.equal(txBounceable.toJson().withdrawAmount, singleNominatorWithdrawAmount);
133133
});
134134

135+
it('should build a full withdrawal tx using setFullWithdrawalMessage', async function () {
136+
const txBuilder = factory.getSingleNominatorWithdrawBuilder();
137+
txBuilder.sender(testData.sender.address);
138+
txBuilder.sequenceNumber(0);
139+
txBuilder.publicKey(testData.sender.publicKey);
140+
txBuilder.expireTime(1234567890);
141+
txBuilder.send(testData.recipients[0]);
142+
txBuilder.setFullWithdrawalMessage();
143+
const tx = await txBuilder.build();
144+
should.equal(tx.type, TransactionType.SingleNominatorWithdraw);
145+
should.equal(tx.toJson().bounceable, false);
146+
should.equal(tx.toJson().withdrawAmount, undefined);
147+
tx.inputs.length.should.equal(1);
148+
tx.inputs[0].should.deepEqual({
149+
address: testData.sender.address,
150+
value: testData.recipients[0].amount,
151+
coin: 'tton',
152+
});
153+
tx.outputs.length.should.equal(1);
154+
tx.outputs[0].should.deepEqual({
155+
address: testData.recipients[0].address,
156+
value: testData.recipients[0].amount,
157+
coin: 'tton',
158+
});
159+
const rawTx = tx.toBroadcastFormat();
160+
// Verify the raw transaction can be parsed back as a full withdrawal
161+
// Full withdrawal is inferred by: transactionType === SingleNominatorWithdraw && !withdrawAmount
162+
const txBuilder2 = factory.from(rawTx);
163+
const tx2 = await txBuilder2.build();
164+
should.equal(tx2.type, TransactionType.SingleNominatorWithdraw);
165+
should.equal(tx2.toJson().withdrawAmount, undefined);
166+
should.equal(tx2.toBroadcastFormat(), rawTx);
167+
});
168+
169+
it('should build a signed full withdrawal tx using add signature', async function () {
170+
const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 });
171+
const publicKey = keyPair.getKeys().pub;
172+
const address = await utils.default.getAddressFromPublicKey(publicKey);
173+
const txBuilder = factory.getSingleNominatorWithdrawBuilder();
174+
txBuilder.sender(address);
175+
txBuilder.sequenceNumber(0);
176+
txBuilder.publicKey(publicKey);
177+
const expireAt = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 7;
178+
txBuilder.expireTime(expireAt);
179+
txBuilder.send(testData.recipients[0]);
180+
txBuilder.setFullWithdrawalMessage();
181+
const tx = await txBuilder.build();
182+
should.equal(tx.type, TransactionType.SingleNominatorWithdraw);
183+
should.equal(tx.toJson().withdrawAmount, undefined);
184+
const signable = tx.signablePayload;
185+
const signature = keyPair.signMessageinUint8Array(signable);
186+
txBuilder.addSignature(keyPair.getKeys(), Buffer.from(signature));
187+
const signedTx = await txBuilder.build();
188+
const builder2 = factory.from(signedTx.toBroadcastFormat());
189+
const tx2 = await builder2.build();
190+
const signature2 = keyPair.signMessageinUint8Array(tx2.signablePayload);
191+
should.equal(Buffer.from(signature).toString('hex'), Buffer.from(signature2).toString('hex'));
192+
should.equal(tx.toBroadcastFormat(), tx2.toBroadcastFormat());
193+
should.equal(tx2.type, TransactionType.SingleNominatorWithdraw);
194+
should.equal(tx2.toJson().withdrawAmount, undefined);
195+
});
196+
135197
xit('should build a signed withdraw tx and submit onchain', async function () {
136198
const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC'));
137199
const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 });

0 commit comments

Comments
 (0)