Skip to content

Commit ee1b696

Browse files
feat(sdk-coin-ton): add jetton transaction support
TICKET: COIN-5626
1 parent b01a9c1 commit ee1b696

13 files changed

+843
-36
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const WALLET_ID = 698983191;
2+
export const JETTON_TRANSFER_OPCODE = 0x0f8a7ea5;
3+
export const WITHDRAW_OPCODE = '00001000';

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { TransactionBuilder } from './transactionBuilder';
21
import { BaseCoin as CoinConfig } from '@bitgo/statics';
32
import { Recipient, TransactionType } from '@bitgo/sdk-core';
3+
import { TransactionBuilder } from './transactionBuilder';
4+
import { Transaction } from './transaction';
45

56
export class SingleNominatorWithdrawBuilder extends TransactionBuilder {
67
constructor(_coinConfig: Readonly<CoinConfig>) {
78
super(_coinConfig);
9+
this._transaction = new Transaction(_coinConfig);
810
}
911

1012
protected get transactionType(): TransactionType {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import TonWeb from 'tonweb';
2+
import { BN } from 'bn.js';
3+
import { Cell } from 'tonweb/dist/types/boc/cell';
4+
import { TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
5+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
6+
import { TransactionExplanation, TxData } from './iface';
7+
import { Transaction } from './transaction';
8+
import { JETTON_TRANSFER_OPCODE, WALLET_ID } from './constants';
9+
10+
export class TokenTransaction extends Transaction {
11+
public forwardTonAmount: string; // for covering forward fees for notify transfer
12+
public senderJettonWalletAddress: string; // the sender's Jetton wallet address
13+
public tonAmount: string; // amount of TON sent to the sender's Jetton wallet
14+
15+
constructor(coinConfig: Readonly<CoinConfig>) {
16+
super(coinConfig);
17+
}
18+
19+
toJson(): TxData {
20+
const json = super.toJson();
21+
return {
22+
...json,
23+
forwardTonAmount: this.forwardTonAmount,
24+
senderJettonWalletAddress: this.senderJettonWalletAddress,
25+
tonAmount: this.tonAmount,
26+
} as TxData;
27+
}
28+
29+
private createJettonTransferPayload(
30+
jettonAmount: string,
31+
toAddress: string,
32+
forwardTonAmount: string,
33+
comment?: string
34+
): Cell {
35+
const forwardPayload = new TonWeb.boc.Cell();
36+
if (comment) {
37+
forwardPayload.bits.writeUint(0, 32);
38+
forwardPayload.bits.writeString(comment);
39+
}
40+
41+
const payload = new TonWeb.boc.Cell();
42+
payload.bits.writeUint(JETTON_TRANSFER_OPCODE, 32); // Store Jetton transfer op code
43+
payload.bits.writeUint(0, 64); // query_id
44+
payload.bits.writeCoins(new BN(jettonAmount)); // Jetton amount to transfer
45+
payload.bits.writeAddress(new TonWeb.Address(toAddress)); // recipient's TON wallet address
46+
payload.bits.writeAddress(new TonWeb.Address(this.sender)); // response_destination for sending excess TON (excess is returned back to the sender)
47+
payload.bits.writeBit(false); // No custom payload
48+
payload.bits.writeCoins(new BN(forwardTonAmount)); // forward_ton_amount to pay fees
49+
payload.bits.writeBit(true); // Forward payload exists as a reference
50+
payload.refs.push(forwardPayload); // Add forward payload as a reference
51+
52+
return payload;
53+
}
54+
55+
async build(): Promise<void> {
56+
const jettonTransferPayload = this.createJettonTransferPayload(
57+
this.recipient.amount,
58+
this.recipient.address,
59+
this.forwardTonAmount,
60+
this.message
61+
);
62+
63+
const signingMessage = this.createSigningMessage(WALLET_ID, this.seqno, this.expireTime);
64+
const sendMode = 3;
65+
signingMessage.bits.writeUint8(sendMode);
66+
const outMsg = this.createOutMsg(this.senderJettonWalletAddress, this.tonAmount, jettonTransferPayload);
67+
68+
signingMessage.refs.push(outMsg);
69+
this.unsignedMessage = Buffer.from(await signingMessage.hash()).toString('hex');
70+
71+
const signature =
72+
this._signatures.length > 0 ? this._signatures[0] : Buffer.from(new Uint8Array(64)).toString('hex');
73+
const finalMessage = await this.createExternalMessage(signingMessage, this.seqno, signature);
74+
75+
this.finalMessage = TonWeb.utils.bytesToBase64(await finalMessage.toBoc(false));
76+
77+
const originalTxId = TonWeb.utils.bytesToBase64(await finalMessage.hash());
78+
this._id = originalTxId.replace(/\//g, '_').replace(/\+/g, '-');
79+
}
80+
81+
fromRawTransaction(rawTransaction: string): void {
82+
try {
83+
const cell = TonWeb.boc.Cell.oneFromBoc(TonWeb.utils.base64ToBytes(rawTransaction));
84+
const parsed = this.parseTransaction(cell);
85+
86+
this.transactionType = TransactionType.SendToken;
87+
this.sender = parsed.fromAddress;
88+
this.recipient = { address: parsed.payload.jettonRecipient, amount: parsed.payload.jettonAmount };
89+
this.tonAmount = parsed.value;
90+
this.forwardTonAmount = parsed.payload.forwardTonAmount;
91+
this.senderJettonWalletAddress = parsed.toAddress;
92+
this.seqno = parsed.seqno;
93+
this.publicKey = parsed.publicKey as string;
94+
this.expireTime = parsed.expireAt;
95+
this.message = parsed.payload.message;
96+
this._signatures.push(parsed.signature);
97+
this.bounceable = parsed.bounce;
98+
} catch (e) {
99+
throw new Error('invalid raw transaction');
100+
}
101+
}
102+
103+
/** @inheritDoc */
104+
explainTransaction(): TransactionExplanation {
105+
const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'];
106+
107+
const outputs: TransactionRecipient[] = [this.recipient];
108+
const outputAmount = this.recipient.amount;
109+
const withdrawAmount = this.withdrawAmount;
110+
return {
111+
displayOrder,
112+
id: this.id,
113+
outputs,
114+
outputAmount,
115+
changeOutputs: [],
116+
changeAmount: '0',
117+
fee: { fee: 'UNKNOWN' },
118+
withdrawAmount,
119+
};
120+
}
121+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { TransactionType, Recipient } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionBuilder } from './transactionBuilder';
4+
import { TokenTransaction } from './tokenTransaction';
5+
import { Transaction } from './transaction';
6+
7+
export class TokenTransferBuilder extends TransactionBuilder {
8+
protected _transaction: TokenTransaction;
9+
10+
constructor(coinConfig: Readonly<CoinConfig>) {
11+
super(coinConfig);
12+
this._transaction = new TokenTransaction(coinConfig);
13+
}
14+
15+
protected get transactionType(): TransactionType {
16+
return TransactionType.SendToken;
17+
}
18+
19+
/** @inheritdoc */
20+
protected fromImplementation(rawTransaction: string): Transaction {
21+
this.transaction.transactionType = TransactionType.SendToken;
22+
this.transaction.fromRawTransaction(rawTransaction);
23+
return this.transaction;
24+
}
25+
26+
/** @inheritdoc */
27+
protected async buildImplementation(): Promise<Transaction> {
28+
this.transaction.transactionType = TransactionType.SendToken;
29+
await this.transaction.build();
30+
this.transaction.loadInputsAndOutputs();
31+
return this.transaction;
32+
}
33+
34+
setForwardTonAmount(amount: string): TokenTransferBuilder {
35+
(this.transaction as TokenTransaction).forwardTonAmount = amount;
36+
return this;
37+
}
38+
39+
setSenderJettonWalletAddress(address: string): TokenTransferBuilder {
40+
(this.transaction as TokenTransaction).senderJettonWalletAddress = address;
41+
return this;
42+
}
43+
44+
setTonAmount(amount: string): TokenTransferBuilder {
45+
(this.transaction as TokenTransaction).tonAmount = amount;
46+
return this;
47+
}
48+
49+
// recipient method to handle both TON and token amounts
50+
recipient(
51+
address: string,
52+
senderJettonWalletAddress: string,
53+
tonAmount: string,
54+
jettonAmount: string,
55+
forwardTonAmount: string
56+
): TokenTransferBuilder {
57+
this._transaction.recipient = {
58+
address: address,
59+
amount: jettonAmount, // Jetton amount to be transferred
60+
};
61+
(this._transaction as TokenTransaction).senderJettonWalletAddress = senderJettonWalletAddress; // The sender's Jetton wallet address
62+
(this._transaction as TokenTransaction).tonAmount = tonAmount; // TON amount sent to the sender's Jetton wallet
63+
(this._transaction as TokenTransaction).forwardTonAmount = forwardTonAmount; // TON amount to cover forward fees in case of notify transfer
64+
return this;
65+
}
66+
67+
send(recipient: Recipient): TokenTransferBuilder {
68+
this.transaction.recipient = recipient;
69+
return this;
70+
}
71+
}

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { BaseKey, BaseTransaction, Entry, Recipient, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
2-
import { TxData, TransactionExplanation } from './iface';
3-
import { BaseCoin as CoinConfig } from '@bitgo/statics';
41
import TonWeb from 'tonweb';
52
import { BN } from 'bn.js';
63
import { Cell } from 'tonweb/dist/types/boc/cell';
7-
import { WITHDRAW_OPCODE } from './transactionBuilder';
84

9-
const WALLET_ID = 698983191;
5+
import { BaseKey, BaseTransaction, Entry, Recipient, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
6+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
7+
import { TransactionExplanation, TxData } from './iface';
8+
import { WITHDRAW_OPCODE, WALLET_ID, JETTON_TRANSFER_OPCODE } from './constants';
109

1110
export class Transaction extends BaseTransaction {
1211
public recipient: Recipient;
@@ -19,8 +18,8 @@ export class Transaction extends BaseTransaction {
1918
expireTime: number;
2019
sender: string;
2120
publicKey: string;
22-
private unsignedMessage: string;
23-
private finalMessage: string;
21+
protected unsignedMessage: string;
22+
protected finalMessage: string;
2423

2524
constructor(coinConfig: Readonly<CoinConfig>) {
2625
super(coinConfig);
@@ -88,7 +87,7 @@ export class Transaction extends BaseTransaction {
8887
this._id = originalTxId.replace(/\//g, '_').replace(/\+/g, '-');
8988
}
9089

91-
private createSigningMessage(walletId, seqno, expireAt) {
90+
protected createSigningMessage(walletId, seqno, expireAt) {
9291
const message = new TonWeb.boc.Cell();
9392
message.bits.writeUint(walletId, 32);
9493
// expireAt should be set as per the provided arg value, regardless of the seqno
@@ -98,7 +97,7 @@ export class Transaction extends BaseTransaction {
9897
return message;
9998
}
10099

101-
private createOutMsg(address, amount, payload) {
100+
protected createOutMsg(address, amount, payload) {
102101
let payloadCell = new TonWeb.boc.Cell();
103102
if (payload) {
104103
if (payload.refs) {
@@ -152,8 +151,7 @@ export class Transaction extends BaseTransaction {
152151
}
153152

154153
const header = TonWeb.Contract.createExternalMessageHeader(this.sender);
155-
const resultMessage = TonWeb.Contract.createCommonMsgInfo(header, stateInit, body);
156-
return resultMessage;
154+
return TonWeb.Contract.createCommonMsgInfo(header, stateInit, body);
157155
}
158156

159157
loadInputsAndOutputs(): void {
@@ -176,11 +174,8 @@ export class Transaction extends BaseTransaction {
176174
fromRawTransaction(rawTransaction: string): void {
177175
try {
178176
const cell = TonWeb.boc.Cell.oneFromBoc(TonWeb.utils.base64ToBytes(rawTransaction));
179-
180177
const parsed = this.parseTransaction(cell);
181-
parsed.value = parsed.value.toString();
182-
parsed.fromAddress = parsed.fromAddress.toString(true, true, this.fromAddressBounceable);
183-
parsed.toAddress = parsed.toAddress.toString(true, true, this.toAddressBounceable);
178+
184179
this.sender = parsed.fromAddress;
185180
this.recipient = { address: parsed.toAddress, amount: parsed.value };
186181
this.withdrawAmount = parsed.withdrawAmount;
@@ -214,7 +209,7 @@ export class Transaction extends BaseTransaction {
214209
};
215210
}
216211

217-
private parseTransaction(cell: Cell): any {
212+
protected parseTransaction(cell: Cell): any {
218213
const slice = (cell as any).beginParse();
219214

220215
// header
@@ -253,7 +248,7 @@ export class Transaction extends BaseTransaction {
253248
const bodySlice = slice.loadBit() ? slice.loadRef() : slice;
254249

255250
return {
256-
fromAddress: externalDestAddress,
251+
fromAddress: externalDestAddress.toString(true, true, this.fromAddressBounceable),
257252
publicKey,
258253
...this.parseTransactionBody(bodySlice),
259254
};
@@ -285,7 +280,7 @@ export class Transaction extends BaseTransaction {
285280
const sourceAddress = order.loadAddress();
286281
if (sourceAddress !== null) throw Error('invalid externalSourceAddress');
287282
const destAddress = order.loadAddress();
288-
const value = order.loadCoins();
283+
const value = order.loadCoins().toString();
289284

290285
if (order.loadBit()) throw Error('invalid currencyCollection');
291286
const ihrFees = order.loadCoins();
@@ -321,13 +316,47 @@ export class Transaction extends BaseTransaction {
321316
withdrawAmount = order.loadCoins().toNumber().toString();
322317
payload = WITHDRAW_OPCODE + queryId.toString(16).padStart(16, '0') + withdrawAmount;
323318
this.transactionType = TransactionType.SingleNominatorWithdraw;
319+
} else if (opcode === JETTON_TRANSFER_OPCODE) {
320+
const queryId = order.loadUint(64).toNumber();
321+
if (queryId !== 0) throw new Error('invalid queryId for jetton transfer');
322+
323+
const jettonAmount = order.loadCoins();
324+
if (!jettonAmount.gt(new BN(0))) throw new Error('invalid jettonAmount');
325+
326+
const jettonRecipient = order.loadAddress();
327+
if (!jettonRecipient) throw new Error('invalid jettonRecipient');
328+
329+
const forwarderAddress = order.loadAddress();
330+
if (!forwarderAddress) throw new Error('invalid forwarderAddress');
331+
332+
order.loadBit(); // skip bit
333+
334+
const forwardTonAmount = order.loadCoins();
335+
if (!forwardTonAmount.gt(new BN(0))) throw new Error('invalid forwardTonAmount');
336+
337+
let message = '';
338+
if (order.loadBit()) {
339+
order = order.loadRef();
340+
const messageOpcode = order.loadUint(32).toNumber();
341+
if (messageOpcode !== 0) throw new Error('invalid message opcode');
342+
const messageBytes = order.loadBits(order.getFreeBits());
343+
message = new TextDecoder().decode(messageBytes);
344+
}
345+
346+
payload = {
347+
jettonAmount: jettonAmount.toString(),
348+
jettonRecipient: jettonRecipient.toString(true, true, this.toAddressBounceable),
349+
forwarderAddress: forwarderAddress.toString(true, true, this.fromAddressBounceable),
350+
forwardTonAmount: forwardTonAmount.toString(),
351+
message: message,
352+
};
324353
} else {
325354
payload = '';
326355
}
327356
}
328357
}
329358
return {
330-
toAddress: destAddress,
359+
toAddress: destAddress.toString(true, true, this.fromAddressBounceable),
331360
value,
332361
bounce,
333362
seqno,

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import BigNumber from 'bignumber.js';
2+
import TonWeb from 'tonweb';
3+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
14
import {
25
BaseAddress,
36
BaseKey,
@@ -10,9 +13,6 @@ import {
1013
} from '@bitgo/sdk-core';
1114
import { Transaction } from './transaction';
1215
import utils from './utils';
13-
import BigNumber from 'bignumber.js';
14-
import { BaseCoin as CoinConfig } from '@bitgo/statics';
15-
import TonWeb from 'tonweb';
1616

1717
export const WITHDRAW_OPCODE = '00001000';
1818

@@ -22,7 +22,6 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
2222

2323
constructor(coinConfig: Readonly<CoinConfig>) {
2424
super(coinConfig);
25-
this._transaction = new Transaction(coinConfig);
2625
}
2726

2827
// get and set region

0 commit comments

Comments
 (0)