Skip to content

Commit 7f3e56f

Browse files
TomerHFBa0ngo
authored andcommitted
feat: add TON/TON_TEST support
1 parent 01c4c1f commit 7f3e56f

File tree

15 files changed

+332
-8
lines changed

15 files changed

+332
-8
lines changed

apps/recovery-relay/lib/defaultRPCs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,16 @@ export const defaultRPCs: Record<
156156
enabled: true,
157157
allowedEmptyValue: false,
158158
},
159+
TON_TEST: {
160+
url: 'https://testnet.toncenter.com/api/v2/jsonRPC',
161+
name: 'The Open Network - Testnet',
162+
enabled: true,
163+
allowedEmptyValue: false,
164+
},
165+
TON: {
166+
url: 'https://toncenter.com/api/v2/jsonRPC',
167+
name: 'The Open Network',
168+
enabled: true,
169+
allowedEmptyValue: false,
170+
},
159171
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
2+
import { ConnectedWallet } from '../ConnectedWallet';
3+
import { TonClient, WalletContractV4 } from '@ton/ton';
4+
import { beginCell, Cell, fromNano } from '@ton/core';
5+
import { AccountData } from '../types';
6+
import { defaultTonWalletV4R2code } from './tonParams';
7+
import axios from 'axios';
8+
9+
export class Ton extends BaseTon implements ConnectedWallet {
10+
public rpcURL: string | undefined;
11+
public setRPCUrl(url: string): void {
12+
this.rpcURL = url;
13+
}
14+
private client = new TonClient({
15+
endpoint: this.isTestnet ? 'https://testnet.toncenter.com/api/v2/jsonRPC' : 'https://toncenter.com/api/v2/jsonRPC',
16+
});
17+
private tonWallet = WalletContractV4.create({ publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'), workchain: 0 });
18+
19+
public async getBalance(): Promise<number> {
20+
await new Promise((resolve) => setTimeout(resolve, 2000));
21+
const contract = this.client.open(this.tonWallet);
22+
return Number(fromNano(await contract.getBalance()));
23+
}
24+
public async broadcastTx(tx: string): Promise<string> {
25+
try {
26+
const body = Cell.fromBoc(Buffer.from(tx, 'base64'))[0];
27+
const pubKey = Buffer.from(this.publicKey.replace('0x', ''), 'hex');
28+
29+
const externalMessage = beginCell().storeUint(0b10, 2).storeUint(0, 2).storeAddress(this.tonWallet.address).storeCoins(0);
30+
31+
const seqno = await this.getSeqno();
32+
if (seqno === 0) {
33+
// for the fist transaction we initialize a state init struct which consists of init struct and code
34+
externalMessage
35+
.storeBit(1) // We have State Init
36+
.storeBit(1) // We store State Init as a reference
37+
.storeRef(this.createStateInit(pubKey)); // Store State Init as a reference
38+
} else {
39+
externalMessage.storeBit(0); // We don't have state init
40+
}
41+
const finalExternalMessage = externalMessage.storeBit(1).storeRef(body).endCell();
42+
43+
await new Promise((resolve) => setTimeout(resolve, 2000));
44+
await this.client.sendFile(finalExternalMessage.toBoc());
45+
const txHash = finalExternalMessage.hash().toString('hex');
46+
this.relayLogger.debug(`TON: Tx broadcasted: ${txHash}`);
47+
return txHash;
48+
} catch (e) {
49+
this.relayLogger.error(`TON: Error broadcasting tx: ${e}`);
50+
if (axios.isAxiosError(e)) {
51+
this.relayLogger.error(`Axios error: ${e.message}\n${e.response?.data}`);
52+
}
53+
throw e;
54+
}
55+
}
56+
public async prepare(): Promise<AccountData> {
57+
// get the balance
58+
const balance = await this.getBalance(); // returned in nanoTon
59+
60+
// fee for regular tx is hardcoded to 0.02 TON
61+
const feeRate = 0.02;
62+
await new Promise((resolve) => setTimeout(resolve, 1000));
63+
// get seqno of the wallet, set it as exrtaParams
64+
const seqno = await this.getSeqno();
65+
const extraParams = new Map<string, any>();
66+
extraParams.set('seqno', seqno);
67+
68+
const preperedData = {
69+
balance,
70+
feeRate,
71+
extraParams,
72+
insufficientBalance: balance < 0.005,
73+
} as AccountData;
74+
75+
return preperedData;
76+
}
77+
private async getSeqno() {
78+
await new Promise((resolve) => setTimeout(resolve, 2000));
79+
return await this.client.open(this.tonWallet).getSeqno();
80+
}
81+
82+
private createStateInit(pubKey: Buffer) {
83+
// the initial data cell our contract will hold. Wallet V4 has an extra value for plugins in the end
84+
const dataCell = beginCell()
85+
.storeUint(0, 32) // Seqno 0 for the first tx
86+
.storeUint(698983191, 32) // Subwallet ID
87+
.storeBuffer(pubKey) // Public Key
88+
.storeBit(0) // only for Wallet V4
89+
.endCell();
90+
91+
// we take a boiler place already made WalletV4R2 code
92+
const codeCell = Cell.fromBoc(Buffer.from(defaultTonWalletV4R2code, 'base64'))[0];
93+
94+
const stateInit = beginCell()
95+
.storeBit(0) // No split_depth
96+
.storeBit(0) // No special
97+
.storeBit(1) // We have code
98+
.storeRef(codeCell)
99+
.storeBit(1) // We have data
100+
.storeRef(dataCell)
101+
.storeBit(0) // No library
102+
.endCell();
103+
104+
return stateInit;
105+
}
106+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const defaultTonWalletV4R2code: string =
2+
'te6ccgECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVA==';

apps/recovery-relay/lib/wallets/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Tezos } from './XTZ';
3636
import { Algorand } from './ALGO';
3737
import { Celestia } from './CELESTIA';
3838
import { CoreDAO } from './EVM/CORE_COREDAO';
39+
import { Ton } from './TON';
3940
export { ConnectedWallet } from './ConnectedWallet';
4041

4142
export const WalletClasses = {
@@ -118,6 +119,8 @@ export const WalletClasses = {
118119
HBAR_TEST: Hedera,
119120
CELESTIA: Celestia,
120121
CELESTIA_TEST: Celestia,
122+
TON: Ton,
123+
TON_TEST: Ton,
121124
} as const;
122125

123126
type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];

apps/recovery-relay/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"@taquito/signer": "^16.2.0",
3737
"@taquito/taquito": "^16.2.0",
3838
"@terra-money/terra.js": "^3.1.10",
39+
"@ton/core": "^0.59.0",
40+
"@ton/crypto": "^3.3.0",
41+
"@ton/ton": "^15.1.0",
3942
"algosdk": "2.5.0",
4043
"axios": "^1.4.0",
4144
"base58": "^2.0.1",
@@ -82,4 +85,4 @@
8285
"eslint-config-next": "^13.4.9",
8386
"typescript": "^5.1.6"
8487
}
85-
}
88+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Ton as BaseTon, Input } from '@fireblocks/wallet-derivation';
2+
import { SigningWallet } from '../SigningWallet';
3+
import { Address, beginCell, Cell, toNano } from '@ton/core';
4+
import { GenerateTxInput, TxPayload } from '../types';
5+
6+
export class Ton extends BaseTon implements SigningWallet {
7+
constructor(input: Input) {
8+
super(input);
9+
}
10+
11+
public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise<TxPayload> {
12+
// calculate the amount to withdraw
13+
const fee = BigInt(toNano(feeRate!)); // feeRate as BigInt in nano
14+
const amountToWithdraw = BigInt(toNano(amount)) - fee; // amount is the wallet balance
15+
let internalMessageMemo = undefined;
16+
if (memo) {
17+
internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
18+
}
19+
// create the tx payload
20+
let internalMessage = beginCell()
21+
.storeUint(0x10, 6) // 0x10 is no bounce
22+
.storeAddress(Address.parse(to)) // Store the recipient address
23+
.storeCoins(amountToWithdraw); // Store the amount within the payload
24+
25+
if (internalMessageMemo) {
26+
internalMessage
27+
.storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // store memo as reference
28+
.storeRef(internalMessageMemo)
29+
.endCell();
30+
} else {
31+
internalMessage.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1).endCell(); // no memo added
32+
}
33+
34+
let toSign = beginCell()
35+
.storeUint(698983191, 32) // sub wallet_id
36+
.storeUint(Math.floor(Date.now() / 1e3) + 600, 32) // Transaction expiration time, +600 = 10 minute
37+
.storeUint(extraParams?.get('seqno'), 32) // store seqno
38+
.storeUint(0, 8)
39+
.storeUint(128, 8) // store SendMode as CARRY_ALL_REMAINING_BALANCE
40+
.storeRef(internalMessage) // store our internalMessage as a reference
41+
.endCell();
42+
43+
const signMessage = toSign.toBoc().toString('base64');
44+
const signData = toSign.hash();
45+
46+
const signature = Buffer.from(await this.sign(Uint8Array.from(signData))).toString('base64');
47+
const unsignedTx = Cell.fromBase64(signMessage).asBuilder();
48+
49+
const body = beginCell().storeBuffer(Buffer.from(signature, 'base64')).storeBuilder(unsignedTx).endCell();
50+
return { tx: body.toBoc().toString('base64') };
51+
}
52+
}

apps/recovery-utility/renderer/lib/wallets/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Hedera } from './HBAR';
2020
import { Algorand } from './ALGO';
2121
import { Bitcoin, BitcoinSV, LiteCoin, Dash, ZCash, Doge } from './BTC';
2222
import { Celestia } from './CELESTIA';
23+
import { Ton } from './TON';
2324

2425
const fillEVMs = () => {
2526
const evms = Object.keys(assets).reduce(
@@ -95,6 +96,8 @@ export const WalletClasses = {
9596
XEM_TEST: NEM,
9697
HBAR: Hedera,
9798
HBAR_TEST: Hedera,
99+
TON: Ton,
100+
TON_TEST: Ton,
98101

99102
...fillEVMs(),
100103
} as const;

packages/asset-config/config/patches.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,34 @@ export const nativeAssetPatches: NativeAssetPatches = {
337337
memo: false,
338338
getExplorerUrl: getStandardExplorer('explorer.testnet.z.cash'),
339339
},
340+
TON: {
341+
derive: true,
342+
transfer: true,
343+
utxo: false,
344+
segwit: false,
345+
minBalance: true,
346+
memo: true,
347+
getExplorerUrl: (type) => (value) => {
348+
if (type === 'tx') {
349+
return `https://tonviewer.com/transaction/${value}`;
350+
}
351+
352+
return `https://tonviewer.com/${value}`;
353+
},
354+
},
355+
TON_TEST: {
356+
derive: true,
357+
transfer: true,
358+
utxo: false,
359+
segwit: false,
360+
minBalance: true,
361+
memo: true,
362+
getExplorerUrl: (type) => (value) => {
363+
if (type === 'tx') {
364+
return `https://testnet.tonviewer.com/transaction/${value}`;
365+
}
366+
367+
return `https://testnet.tonviewer.com/${value}`;
368+
},
369+
},
340370
};

packages/e2e-tests/tests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const nativeTestnetAssets: AssetTestConfig[] = [
4141
// { assetId: 'XRP_TEST' },
4242
// { assetId: 'XTZ_TEST' },
4343
// { assetId: 'ZEC_TEST' },
44+
// { assetId: 'TON_TEST' },
4445
];
4546

4647
const nativeMainnetAssets = [
@@ -98,6 +99,7 @@ const nativeMainnetAssets = [
9899
// { assetId: 'XRP' },
99100
// { assetId: 'XTZ' },
100101
// { assetId: 'ZEC' },
102+
// { assetId: 'TON' },
101103
];
102104

103105
export const testAssets: AssetTestConfig[] = [...nativeTestnetAssets, ...nativeMainnetAssets];

packages/shared/lib/validateAddress.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { bech32 } from 'bech32';
33
import { isTestnetAsset } from '@fireblocks/asset-config';
44
import { getLogger } from './getLogger';
55
import { LOGGER_NAME_SHARED } from '../constants';
6+
import { Address } from '@ton/ton';
67

78
const logger = getLogger(LOGGER_NAME_SHARED);
89

@@ -101,6 +102,9 @@ export class AddressValidator {
101102
return this.validateCOSMOS(address);
102103
case 'TERRA':
103104
return this.validateTERRA(address);
105+
case 'TON':
106+
case 'TON_TEST':
107+
return this.validateTON(address);
104108
default:
105109
logger.error(`Unsupported networkProtocol for address validation ${validatorReference}`);
106110
throw new Error(`Unsupported networkProtocol for address validation ${validatorReference}`);
@@ -150,4 +154,13 @@ export class AddressValidator {
150154
private validateTERRA(address: string): boolean {
151155
return this.validateCosmosBased('terra')(address);
152156
}
157+
158+
private validateTON(address: string): boolean {
159+
try {
160+
Address.parse(address);
161+
return true;
162+
} catch (error) {
163+
return false;
164+
}
165+
}
153166
}

0 commit comments

Comments
 (0)