Skip to content

Commit 5916e05

Browse files
committed
Merge commit '223be659bc9610e5bcefbbe4ea5849baa153457c' into rel/latest
2 parents 0d618e6 + 223be65 commit 5916e05

File tree

7 files changed

+244
-1
lines changed

7 files changed

+244
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export const WITHDRAW_OPCODE = '00001000';
55
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';
8+
export const TON_WHALES_WITHDRAW_OPCODE = '3665837821';
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { Recipient, TransactionType } from '@bitgo/sdk-core';
3+
import { TransactionBuilder } from './transactionBuilder';
4+
import { Transaction } from './transaction';
5+
import { TON_WHALES_WITHDRAW_OPCODE } from './constants';
6+
7+
export class TonWhalesWithdrawalBuilder extends TransactionBuilder {
8+
constructor(_coinConfig: Readonly<CoinConfig>) {
9+
super(_coinConfig);
10+
this._transaction = new Transaction(_coinConfig);
11+
}
12+
13+
protected get transactionType(): TransactionType {
14+
return TransactionType.TonWhalesWithdrawal;
15+
}
16+
17+
/**
18+
* Sets the payload for the withdrawal request.
19+
* Structure: OpCode (32) + QueryId (64) + GasLimit (Coins) + UnstakeAmount (Coins)
20+
* * @param unstakeAmount The amount of NanoTON to unstake (inside payload)
21+
* @param unstakeAmount The amount to unstake
22+
* @param queryId Optional custom query ID
23+
*/
24+
setWithdrawalMessage(unstakeAmount: string, queryId?: string): TonWhalesWithdrawalBuilder {
25+
const qId = queryId || '0000000000000000';
26+
27+
this.transaction.message = TON_WHALES_WITHDRAW_OPCODE + qId + unstakeAmount;
28+
return this;
29+
}
30+
31+
/**
32+
* Sets the message to withdraw EVERYTHING from the pool.
33+
* This sets the unstakeAmount to "0", which is the specific signal for full withdrawal.
34+
*/
35+
setFullWithdrawalMessage(queryId?: string): TonWhalesWithdrawalBuilder {
36+
return this.setWithdrawalMessage('0', queryId);
37+
}
38+
39+
/**
40+
* Sets the value attached to the transaction (The Fees).
41+
* NOTE: This is NOT the unstake amount. This is the fee paid to the pool
42+
* to process the request (e.g. withdrawFee + receiptPrice).
43+
* * @param amount NanoTON amount to attach to the message
44+
*/
45+
setForwardAmount(amount: string): TonWhalesWithdrawalBuilder {
46+
if (!this.transaction.recipient) {
47+
this.transaction.recipient = { address: '', amount: amount };
48+
} else {
49+
this.transaction.recipient.amount = amount;
50+
}
51+
return this;
52+
}
53+
54+
send(recipient: Recipient): TonWhalesWithdrawalBuilder {
55+
this.transaction.recipient = recipient;
56+
return this;
57+
}
58+
59+
setMessage(msg: string): TonWhalesWithdrawalBuilder {
60+
throw new Error('Use setWithdrawalMessage for specific payload construction');
61+
}
62+
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
JETTON_TRANSFER_OPCODE,
1212
VESTING_CONTRACT_WALLET_ID,
1313
TON_WHALES_DEPOSIT_OPCODE,
14+
TON_WHALES_WITHDRAW_OPCODE,
1415
} from './constants';
1516

1617
export class Transaction extends BaseTransaction {
@@ -137,7 +138,21 @@ export class Transaction extends BaseTransaction {
137138
const queryId = payload.substring(10, 26);
138139
payloadCell.bits.writeUint(parseInt(TON_WHALES_DEPOSIT_OPCODE, 10), 32);
139140
payloadCell.bits.writeUint(parseInt(queryId, 16), 64);
141+
// The Ton Whales protocol requires a specific 'gas limit' field in the payload
142+
// structure (OpCode -> QueryId -> GasLimit -> Amount).
140143
payloadCell.bits.writeCoins(TonWeb.utils.toNano('1'));
144+
} else if (payload.length >= 26 && payload.substring(0, 10) === TON_WHALES_WITHDRAW_OPCODE) {
145+
const queryId = payload.substring(10, 26);
146+
const amountStr = payload.substring(26);
147+
148+
payloadCell.bits.writeUint(parseInt(TON_WHALES_WITHDRAW_OPCODE, 10), 32);
149+
payloadCell.bits.writeUint(new BN(queryId, 16), 64);
150+
// The Ton Whales protocol requires a specific 'gas limit' field in the payload
151+
// structure (OpCode -> QueryId -> GasLimit -> Amount).
152+
// We hardcode 1 TON here to match the Deposit implementation and ensure
153+
// sufficient gas for the pool to process the request.
154+
payloadCell.bits.writeCoins(TonWeb.utils.toNano('1'));
155+
payloadCell.bits.writeCoins(new BN(amountStr));
141156
} else {
142157
payloadCell.bits.writeUint(0, 32);
143158
payloadCell.bits.writeString(payload);
@@ -387,6 +402,18 @@ export class Transaction extends BaseTransaction {
387402
// We do not need to store it
388403
order.loadCoins();
389404
payload = TON_WHALES_DEPOSIT_OPCODE + queryId.toString(16).padStart(16, '0');
405+
} else if (opcode === parseInt(TON_WHALES_WITHDRAW_OPCODE, 10)) {
406+
this.transactionType = TransactionType.TonWhalesWithdrawal;
407+
408+
const queryId = order.loadUint(64).toNumber();
409+
order.loadCoins(); // Skip Gas (Hardcoded in builder)
410+
const amount = order.loadCoins();
411+
412+
withdrawAmount = amount.toString(); // Decimal String
413+
414+
// Reconstruct Payload: Decimal Op + Hex Query + Decimal Amount
415+
const queryHex = new BN(queryId).toString(16).padStart(16, '0');
416+
payload = TON_WHALES_WITHDRAW_OPCODE + queryHex + withdrawAmount;
390417
} else {
391418
payload = '';
392419
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Transaction } from './transaction';
77
import { TokenTransferBuilder } from './tokenTransferBuilder';
88
import { TokenTransaction } from './tokenTransaction';
99
import { TonWhalesDepositBuilder } from './tonWhalesDepositBuilder';
10+
import { TonWhalesWithdrawalBuilder } from './tonWhalesWithdrawalBuilder';
1011

1112
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1213
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -41,6 +42,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4142
case TransactionType.TonWhalesDeposit:
4243
builder = this.getTonWhalesDepositBuilder();
4344
break;
45+
case TransactionType.TonWhalesWithdrawal:
46+
builder = this.getTonWhalesWithdrawalBuilder();
47+
break;
4448
default:
4549
throw new InvalidTransactionError('unsupported transaction');
4650
}
@@ -78,4 +82,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
7882
getTonWhalesDepositBuilder(): TonWhalesDepositBuilder {
7983
return new TonWhalesDepositBuilder(this._coinConfig);
8084
}
85+
86+
getTonWhalesWithdrawalBuilder(): TonWhalesWithdrawalBuilder {
87+
return new TonWhalesWithdrawalBuilder(this._coinConfig);
88+
}
8189
}

modules/sdk-coin-ton/test/resources/ton.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,39 @@ export const signedTonWhalesDepositTransaction = {
164164
'aff471790c6587d07ae69e5a9519428ca6456eddb4cd1a6a8573b55f2cd6809309b57af7f50ebb160bee2a729b0d9d6336ea202312fea35325d33b02f1e9ff01',
165165
bounceable: true,
166166
};
167+
168+
export const signedTonWhalesWithdrawalTransaction = {
169+
recipient: {
170+
//https://testnet.tonscan.org/address/kQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_l7mg
171+
address: 'EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq',
172+
amount: '200000000', // 10 TON
173+
},
174+
withdrawAmount: '10000000000',
175+
// This is the raw TX from sandboxing a withdrawal request to Ton Whales
176+
tx: 'te6cckEBAgEAwAAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwGzbdqzqRjzzou/GIUqqqdZn7Tevr+oSawF529ibEgSoxfcezGF5GW4oF6/Ws+4OanMgBwMVCe0GIEK3GSTzCIaU1NGLtKVSvAAAAC6AAcAQCUYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6BfXhAAAAAAAAAAAAAAAAAAANqAPv0AAAAAaUqlPEO5rKAFAlQL5ACKp3CI',
177+
seqno: 93,
178+
queryId: '00000000694aa53c',
179+
expireTime: 1766499704,
180+
sender: 'EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ',
181+
publicKey: '9d6d3714aeb1f007f6e6aa728f79fdd005ea2c7ad459b2f54d73f9e672426230',
182+
signature:
183+
'd9b6ed59d48c79e745df8c42955553accfda6f5f5fd424d602f3b7b1362409518bee3d98c2f232dc502f5fad67dc1cd4e6400e062a13da0c40856e3249e6110d',
184+
bounceable: true,
185+
};
186+
187+
export const signedTonWhalesFullWithdrawalTransaction = {
188+
recipient: {
189+
address: 'EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq',
190+
amount: '200000000', // 0.2 TON (Fee)
191+
},
192+
withdrawAmount: '0', // 0 means Full Withdraw
193+
tx: 'te6cckEBAgEAuwAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwHSrLxEIwA9nyfxKqom8MsGbPCL5SfwqGDzHyYnKzJwU8ecNqb6xkB7u9gBwBrZdO3NvecF44nXe2Lm/+OL8Z4aU1NGLtKVgg4AAAC8AAcAQCKYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6BfXhAAAAAAAAAAAAAAAAAAANqAPv0AAAAAaUrAy0O5rKAAudrTIw==',
194+
seqno: 94,
195+
queryId: '00000000694ac0cb',
196+
expireTime: 1766506759,
197+
sender: 'EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ',
198+
publicKey: '9d6d3714aeb1f007f6e6aa728f79fdd005ea2c7ad459b2f54d73f9e672426230',
199+
signature:
200+
'e9565e2211801ecf93f8955513786583367845f293f85430798f931395993829e3ce1b537d63203dddec00e00d6cba76e6def382f1c4ebbdb1737ff1c5f8cf0d',
201+
bounceable: true,
202+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import should from 'should';
2+
import { TransactionType } from '@bitgo/sdk-core';
3+
import { TransactionBuilderFactory } from '../../src'; // Adjust path as needed
4+
import { coins } from '@bitgo/statics';
5+
import * as testData from '../resources/ton';
6+
import { TON_WHALES_WITHDRAW_OPCODE } from '../../src/lib/constants';
7+
8+
describe('Ton Whales Withdrawal Builder', () => {
9+
const factory = new TransactionBuilderFactory(coins.get('tton'));
10+
11+
// Define the scenarios we want to test
12+
const scenarios = [
13+
{
14+
name: 'Partial Withdrawal (10 TON)',
15+
fixture: testData.signedTonWhalesWithdrawalTransaction,
16+
},
17+
{
18+
name: 'Full Withdrawal (Amount 0)',
19+
fixture: testData.signedTonWhalesFullWithdrawalTransaction,
20+
},
21+
];
22+
23+
scenarios.forEach((scenario) => {
24+
describe(scenario.name, () => {
25+
const fixture = scenario.fixture;
26+
27+
it('should parse a raw transaction and extract correct parameters', async function () {
28+
const txBuilder = factory.from(fixture.tx);
29+
const builtTx = await txBuilder.build();
30+
const jsonTx = builtTx.toJson();
31+
32+
// Verify Business Logic Fields
33+
should.equal(builtTx.type, TransactionType.TonWhalesWithdrawal);
34+
35+
// NOTE: In withdrawals, recipient.amount is the FEE, withdrawAmount is the STAKE
36+
should.equal(jsonTx.amount, fixture.recipient.amount);
37+
should.equal(jsonTx.withdrawAmount, fixture.withdrawAmount);
38+
39+
should.equal(jsonTx.destination, fixture.recipient.address);
40+
should.equal(jsonTx.sender, fixture.sender);
41+
42+
// Verify Network Constraints
43+
should.equal(jsonTx.seqno, fixture.seqno);
44+
should.equal(jsonTx.expirationTime, fixture.expireTime);
45+
should.equal(jsonTx.bounceable, fixture.bounceable);
46+
47+
// Verify Payload Structure
48+
// Logic: DecimalOpCode + HexQueryId + DecimalAmount
49+
const msg = builtTx['message'] || '';
50+
should.equal(msg.startsWith(TON_WHALES_WITHDRAW_OPCODE), true);
51+
52+
// Ensure the payload ENDS with the decimal amount (either "1000..." or "0")
53+
should.equal(msg.endsWith(fixture.withdrawAmount), true);
54+
});
55+
56+
it('should parse and rebuild the transaction resulting in the same hex', async function () {
57+
const txBuilder = factory.from(fixture.tx);
58+
const builtTx = await txBuilder.build();
59+
60+
// Verify the parser extracted the signature
61+
const signature = builtTx.signature[0];
62+
should.exist(signature);
63+
signature.should.not.be.empty();
64+
65+
// Rebuild from the parsed object
66+
const builder2 = factory.from(builtTx.toBroadcastFormat());
67+
const builtTx2 = await builder2.build();
68+
69+
// The output of the second build should match the original raw transaction
70+
should.equal(builtTx2.toBroadcastFormat(), fixture.tx);
71+
should.equal(builtTx2.type, TransactionType.TonWhalesWithdrawal);
72+
});
73+
74+
it('should build a transaction from scratch that byte-for-byte matches the raw fixture', async function () {
75+
// Get the specific Withdrawal Builder
76+
const builder = factory.getTonWhalesWithdrawalBuilder();
77+
78+
// Set Header Info from Fixture
79+
builder.sender(fixture.sender);
80+
builder.publicKey(fixture.publicKey);
81+
builder.sequenceNumber(fixture.seqno);
82+
builder.expireTime(fixture.expireTime);
83+
builder.bounceable(fixture.bounceable);
84+
85+
// Set Destination and ATTACHED VALUE (The Fee)
86+
builder.send({
87+
address: fixture.recipient.address,
88+
amount: fixture.recipient.amount,
89+
});
90+
91+
// Set Payload Data (The Unstake Amount)
92+
// Note: This works for both partial (amount > 0) and full (amount = "0")
93+
builder.setWithdrawalMessage(fixture.withdrawAmount, fixture.queryId);
94+
95+
// Attach Signature from Fixture (Mocking the HSM signing process)
96+
if (fixture.signature) {
97+
builder.addSignature({ pub: fixture.publicKey }, Buffer.from(fixture.signature, 'hex'));
98+
}
99+
100+
// Build Signed Transaction
101+
const signedBuiltTx = await builder.build();
102+
103+
// Byte-for-byte equality with the Sandbox output
104+
should.equal(signedBuiltTx.toBroadcastFormat(), fixture.tx);
105+
should.equal(signedBuiltTx.type, TransactionType.TonWhalesWithdrawal);
106+
});
107+
});
108+
});
109+
});

modules/sdk-core/src/account-lib/baseCoin/enum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export enum TransactionType {
122122

123123
// ton whales
124124
TonWhalesDeposit,
125-
TonWhalesWithdraw,
125+
TonWhalesWithdrawal,
126126
}
127127

128128
/**

0 commit comments

Comments
 (0)