Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/sdk-coin-ton/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ 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';
export const TON_WHALES_WITHDRAW_OPCODE = '3665837821';
62 changes: 62 additions & 0 deletions modules/sdk-coin-ton/src/lib/tonWhalesWithdrawalBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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_WITHDRAW_OPCODE } from './constants';

export class TonWhalesWithdrawalBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new Transaction(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.TonWhalesWithdrawal;
}

/**
* Sets the payload for the withdrawal request.
* Structure: OpCode (32) + QueryId (64) + GasLimit (Coins) + UnstakeAmount (Coins)
* * @param unstakeAmount The amount of NanoTON to unstake (inside payload)
* @param unstakeAmount The amount to unstake
* @param queryId Optional custom query ID
*/
setWithdrawalMessage(unstakeAmount: string, queryId?: string): TonWhalesWithdrawalBuilder {
const qId = queryId || '0000000000000000';

this.transaction.message = TON_WHALES_WITHDRAW_OPCODE + qId + unstakeAmount;
return this;
}

/**
* Sets the message to withdraw EVERYTHING from the pool.
* This sets the unstakeAmount to "0", which is the specific signal for full withdrawal.
*/
setFullWithdrawalMessage(queryId?: string): TonWhalesWithdrawalBuilder {
return this.setWithdrawalMessage('0', queryId);
}

/**
* Sets the value attached to the transaction (The Fees).
* NOTE: This is NOT the unstake amount. This is the fee paid to the pool
* to process the request (e.g. withdrawFee + receiptPrice).
* * @param amount NanoTON amount to attach to the message
*/
setForwardAmount(amount: string): TonWhalesWithdrawalBuilder {
if (!this.transaction.recipient) {
this.transaction.recipient = { address: '', amount: amount };
} else {
this.transaction.recipient.amount = amount;
}
return this;
}

send(recipient: Recipient): TonWhalesWithdrawalBuilder {
this.transaction.recipient = recipient;
return this;
}

setMessage(msg: string): TonWhalesWithdrawalBuilder {
throw new Error('Use setWithdrawalMessage for specific payload construction');
}
}
27 changes: 27 additions & 0 deletions modules/sdk-coin-ton/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
JETTON_TRANSFER_OPCODE,
VESTING_CONTRACT_WALLET_ID,
TON_WHALES_DEPOSIT_OPCODE,
TON_WHALES_WITHDRAW_OPCODE,
} from './constants';

export class Transaction extends BaseTransaction {
Expand Down Expand Up @@ -137,7 +138,21 @@ export class Transaction extends BaseTransaction {
const queryId = payload.substring(10, 26);
payloadCell.bits.writeUint(parseInt(TON_WHALES_DEPOSIT_OPCODE, 10), 32);
payloadCell.bits.writeUint(parseInt(queryId, 16), 64);
// The Ton Whales protocol requires a specific 'gas limit' field in the payload
// structure (OpCode -> QueryId -> GasLimit -> Amount).
payloadCell.bits.writeCoins(TonWeb.utils.toNano('1'));
} else if (payload.length >= 26 && payload.substring(0, 10) === TON_WHALES_WITHDRAW_OPCODE) {
const queryId = payload.substring(10, 26);
const amountStr = payload.substring(26);

payloadCell.bits.writeUint(parseInt(TON_WHALES_WITHDRAW_OPCODE, 10), 32);
payloadCell.bits.writeUint(new BN(queryId, 16), 64);
// The Ton Whales protocol requires a specific 'gas limit' field in the payload
// structure (OpCode -> QueryId -> GasLimit -> Amount).
// We hardcode 1 TON here to match the Deposit implementation and ensure
// sufficient gas for the pool to process the request.
payloadCell.bits.writeCoins(TonWeb.utils.toNano('1'));
payloadCell.bits.writeCoins(new BN(amountStr));
} else {
payloadCell.bits.writeUint(0, 32);
payloadCell.bits.writeString(payload);
Expand Down Expand Up @@ -387,6 +402,18 @@ export class Transaction extends BaseTransaction {
// We do not need to store it
order.loadCoins();
payload = TON_WHALES_DEPOSIT_OPCODE + queryId.toString(16).padStart(16, '0');
} else if (opcode === parseInt(TON_WHALES_WITHDRAW_OPCODE, 10)) {
this.transactionType = TransactionType.TonWhalesWithdrawal;

const queryId = order.loadUint(64).toNumber();
order.loadCoins(); // Skip Gas (Hardcoded in builder)
const amount = order.loadCoins();

withdrawAmount = amount.toString(); // Decimal String

// Reconstruct Payload: Decimal Op + Hex Query + Decimal Amount
const queryHex = new BN(queryId).toString(16).padStart(16, '0');
payload = TON_WHALES_WITHDRAW_OPCODE + queryHex + withdrawAmount;
} else {
payload = '';
}
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Transaction } from './transaction';
import { TokenTransferBuilder } from './tokenTransferBuilder';
import { TokenTransaction } from './tokenTransaction';
import { TonWhalesDepositBuilder } from './tonWhalesDepositBuilder';
import { TonWhalesWithdrawalBuilder } from './tonWhalesWithdrawalBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -41,6 +42,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
case TransactionType.TonWhalesDeposit:
builder = this.getTonWhalesDepositBuilder();
break;
case TransactionType.TonWhalesWithdrawal:
builder = this.getTonWhalesWithdrawalBuilder();
break;
default:
throw new InvalidTransactionError('unsupported transaction');
}
Expand Down Expand Up @@ -78,4 +82,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
getTonWhalesDepositBuilder(): TonWhalesDepositBuilder {
return new TonWhalesDepositBuilder(this._coinConfig);
}

getTonWhalesWithdrawalBuilder(): TonWhalesWithdrawalBuilder {
return new TonWhalesWithdrawalBuilder(this._coinConfig);
}
}
36 changes: 36 additions & 0 deletions modules/sdk-coin-ton/test/resources/ton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,39 @@ export const signedTonWhalesDepositTransaction = {
'aff471790c6587d07ae69e5a9519428ca6456eddb4cd1a6a8573b55f2cd6809309b57af7f50ebb160bee2a729b0d9d6336ea202312fea35325d33b02f1e9ff01',
bounceable: true,
};

export const signedTonWhalesWithdrawalTransaction = {
recipient: {
//https://testnet.tonscan.org/address/kQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_l7mg
address: 'EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq',
amount: '200000000', // 10 TON
},
withdrawAmount: '10000000000',
// This is the raw TX from sandboxing a withdrawal request to Ton Whales
tx: 'te6cckEBAgEAwAAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwGzbdqzqRjzzou/GIUqqqdZn7Tevr+oSawF529ibEgSoxfcezGF5GW4oF6/Ws+4OanMgBwMVCe0GIEK3GSTzCIaU1NGLtKVSvAAAAC6AAcAQCUYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6BfXhAAAAAAAAAAAAAAAAAAANqAPv0AAAAAaUqlPEO5rKAFAlQL5ACKp3CI',
seqno: 93,
queryId: '00000000694aa53c',
expireTime: 1766499704,
sender: 'EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ',
publicKey: '9d6d3714aeb1f007f6e6aa728f79fdd005ea2c7ad459b2f54d73f9e672426230',
signature:
'd9b6ed59d48c79e745df8c42955553accfda6f5f5fd424d602f3b7b1362409518bee3d98c2f232dc502f5fad67dc1cd4e6400e062a13da0c40856e3249e6110d',
bounceable: true,
};

export const signedTonWhalesFullWithdrawalTransaction = {
recipient: {
address: 'EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq',
amount: '200000000', // 0.2 TON (Fee)
},
withdrawAmount: '0', // 0 means Full Withdraw
tx: 'te6cckEBAgEAuwAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwHSrLxEIwA9nyfxKqom8MsGbPCL5SfwqGDzHyYnKzJwU8ecNqb6xkB7u9gBwBrZdO3NvecF44nXe2Lm/+OL8Z4aU1NGLtKVgg4AAAC8AAcAQCKYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6BfXhAAAAAAAAAAAAAAAAAAANqAPv0AAAAAaUrAy0O5rKAAudrTIw==',
seqno: 94,
queryId: '00000000694ac0cb',
expireTime: 1766506759,
sender: 'EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ',
publicKey: '9d6d3714aeb1f007f6e6aa728f79fdd005ea2c7ad459b2f54d73f9e672426230',
signature:
'e9565e2211801ecf93f8955513786583367845f293f85430798f931395993829e3ce1b537d63203dddec00e00d6cba76e6def382f1c4ebbdb1737ff1c5f8cf0d',
bounceable: true,
};
109 changes: 109 additions & 0 deletions modules/sdk-coin-ton/test/unit/tonWhalesWithdrawalBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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_WITHDRAW_OPCODE } from '../../src/lib/constants';

describe('Ton Whales Withdrawal Builder', () => {
const factory = new TransactionBuilderFactory(coins.get('tton'));

// Define the scenarios we want to test
const scenarios = [
{
name: 'Partial Withdrawal (10 TON)',
fixture: testData.signedTonWhalesWithdrawalTransaction,
},
{
name: 'Full Withdrawal (Amount 0)',
fixture: testData.signedTonWhalesFullWithdrawalTransaction,
},
];

scenarios.forEach((scenario) => {
describe(scenario.name, () => {
const fixture = scenario.fixture;

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.TonWhalesWithdrawal);

// NOTE: In withdrawals, recipient.amount is the FEE, withdrawAmount is the STAKE
should.equal(jsonTx.amount, fixture.recipient.amount);
should.equal(jsonTx.withdrawAmount, fixture.withdrawAmount);

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
// Logic: DecimalOpCode + HexQueryId + DecimalAmount
const msg = builtTx['message'] || '';
should.equal(msg.startsWith(TON_WHALES_WITHDRAW_OPCODE), true);

// Ensure the payload ENDS with the decimal amount (either "1000..." or "0")
should.equal(msg.endsWith(fixture.withdrawAmount), 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.TonWhalesWithdrawal);
});

it('should build a transaction from scratch that byte-for-byte matches the raw fixture', async function () {
// Get the specific Withdrawal Builder
const builder = factory.getTonWhalesWithdrawalBuilder();

// 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 Destination and ATTACHED VALUE (The Fee)
builder.send({
address: fixture.recipient.address,
amount: fixture.recipient.amount,
});

// Set Payload Data (The Unstake Amount)
// Note: This works for both partial (amount > 0) and full (amount = "0")
builder.setWithdrawalMessage(fixture.withdrawAmount, 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();

// Byte-for-byte equality with the Sandbox output
should.equal(signedBuiltTx.toBroadcastFormat(), fixture.tx);
should.equal(signedBuiltTx.type, TransactionType.TonWhalesWithdrawal);
});
});
});
});
2 changes: 1 addition & 1 deletion modules/sdk-core/src/account-lib/baseCoin/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export enum TransactionType {

// ton whales
TonWhalesDeposit,
TonWhalesWithdraw,
TonWhalesWithdrawal,
}

/**
Expand Down