Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add TON/TON_TEST support #95

Merged
merged 2 commits into from
Dec 1, 2024
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
12 changes: 12 additions & 0 deletions apps/recovery-relay/lib/defaultRPCs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,16 @@ export const defaultRPCs: Record<
enabled: true,
allowedEmptyValue: false,
},
TON_TEST: {
url: 'https://testnet.toncenter.com/api/v2/jsonRPC',
name: 'The Open Network - Testnet',
enabled: true,
allowedEmptyValue: false,
},
TON: {
url: 'https://toncenter.com/api/v2/jsonRPC',
name: 'The Open Network',
enabled: true,
allowedEmptyValue: false,
},
};
139 changes: 139 additions & 0 deletions apps/recovery-relay/lib/wallets/TON/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
import { TonClient, WalletContractV4 } from '@ton/ton';
import { beginCell, Cell, fromNano } from '@ton/core';
import { AccountData } from '../types';
import { defaultTonWalletV4R2code } from './tonParams';
import axios from 'axios';
import { LateInitConnectedWallet } from '../LateInitConnectedWallet';

export class Ton extends BaseTon implements LateInitConnectedWallet {
public memo: string | undefined;

public updateDataEndpoint(memo?: string): void {
this.memo = memo;
}

public getLateInitLabel(): string {
throw new Error('Method not implemented.');
}

public rpcURL: string | undefined;

public setRPCUrl(url: string): void {
this.rpcURL = url;
}

private client: TonClient | undefined;

private init() {
this.client = new TonClient({
endpoint: this.rpcURL!,
});
}

private tonWallet = WalletContractV4.create({ publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'), workchain: 0 });

public async getBalance(): Promise<number> {
if (this.client) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const contract = this.client.open(this.tonWallet);
return Number(fromNano(await contract.getBalance()));
} else {
this.relayLogger.error('TON: Client failed to initialize');
throw new Error('TON: Client failed to initialize');
}
}

public async broadcastTx(tx: string): Promise<string> {
try {
// init the TonClient
this.init();

// parse the tx back to Ton Cell
const body = Cell.fromBoc(Buffer.from(tx, 'base64'))[0];
const pubKey = Buffer.from(this.publicKey.replace('0x', ''), 'hex');
const externalMessage = beginCell().storeUint(0b10, 2).storeUint(0, 2).storeAddress(this.tonWallet.address).storeCoins(0);
const seqno = await this.getSeqno();
if (seqno === 0) {
// for the fist transaction we initialize a state init struct which consists of init struct and code
externalMessage
.storeBit(1) // We have State Init
.storeBit(1) // We store State Init as a reference
.storeRef(this.createStateInit(pubKey)); // Store State Init as a reference
} else {
externalMessage.storeBit(0); // We don't have state init
}
const finalExternalMessage = externalMessage.storeBit(1).storeRef(body).endCell();

if (this.client) {
// broadcast Tx and calc TxHash
await new Promise((resolve) => setTimeout(resolve, 2000));
await this.client.sendFile(finalExternalMessage.toBoc());
const txHash = finalExternalMessage.hash().toString('hex');
this.relayLogger.debug(`TON: Tx broadcasted: ${txHash}`);
return txHash;
} else {
throw new Error('TON: Client failed to initialize');
}
} catch (e) {
this.relayLogger.error(`TON: Error broadcasting tx: ${e}`);
if (axios.isAxiosError(e)) {
this.relayLogger.error(`Axios error: ${e.message}\n${e.response?.data}`);
}
throw e;
}
}

public async prepare(): Promise<AccountData> {
// init the TonClient
this.init();
// get the balance, returned in nanoTon
const balance = await this.getBalance();

// fee for regular tx is hardcoded to 0.02 TON
const feeRate = 0.02;
await new Promise((resolve) => setTimeout(resolve, 2000));

// get seqno of the wallet, set it as exrtaParams
const seqno = await this.getSeqno();
const extraParams = new Map<string, any>();
extraParams.set('seqno', seqno);

const preperedData = {
balance,
memo: this.memo,
feeRate,
extraParams,
insufficientBalance: balance < 0.005,
} as AccountData;

return preperedData;
}
private async getSeqno() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return await this.client!.open(this.tonWallet).getSeqno();
}

private createStateInit(pubKey: Buffer) {
// the initial data cell our contract will hold. Wallet V4 has an extra value for plugins in the end
const dataCell = beginCell()
.storeUint(0, 32) // Seqno 0 for the first tx
.storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids
.storeBuffer(pubKey) // Public Key
.storeBit(0) // only for Wallet V4
.endCell();

// we take a boiler place already made WalletV4R2 code
const codeCell = Cell.fromBoc(Buffer.from(defaultTonWalletV4R2code, 'base64'))[0];
const stateInit = beginCell()
.storeBit(0) // No split_depth
.storeBit(0) // No special
.storeBit(1) // We have code
.storeRef(codeCell)
.storeBit(1) // We have data
.storeRef(dataCell)
.storeBit(0) // No library
.endCell();
return stateInit;
}
}
2 changes: 2 additions & 0 deletions apps/recovery-relay/lib/wallets/TON/tonParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const defaultTonWalletV4R2code: string =
'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==';
3 changes: 3 additions & 0 deletions apps/recovery-relay/lib/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { Tezos } from './XTZ';
import { Algorand } from './ALGO';
import { Celestia } from './CELESTIA';
import { CoreDAO } from './EVM/CORE_COREDAO';
import { Ton } from './TON';
export { ConnectedWallet } from './ConnectedWallet';

export const WalletClasses = {
Expand Down Expand Up @@ -118,6 +119,8 @@ export const WalletClasses = {
HBAR_TEST: Hedera,
CELESTIA: Celestia,
CELESTIA_TEST: Celestia,
TON: Ton,
TON_TEST: Ton,
} as const;

type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];
Expand Down
5 changes: 4 additions & 1 deletion apps/recovery-relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"@taquito/signer": "^16.2.0",
"@taquito/taquito": "^16.2.0",
"@terra-money/terra.js": "^3.1.10",
"@ton/core": "^0.59.0",
"@ton/crypto": "^3.3.0",
"@ton/ton": "^15.1.0",
"algosdk": "2.5.0",
"axios": "^1.4.0",
"base58": "^2.0.1",
Expand Down Expand Up @@ -82,4 +85,4 @@
"eslint-config-next": "^13.4.9",
"typescript": "^5.1.6"
}
}
}
52 changes: 52 additions & 0 deletions apps/recovery-utility/renderer/lib/wallets/TON/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Ton as BaseTon, Input } from '@fireblocks/wallet-derivation';
import { SigningWallet } from '../SigningWallet';
import { Address, beginCell, Cell, toNano } from '@ton/core';
import { GenerateTxInput, TxPayload } from '../types';

export class Ton extends BaseTon implements SigningWallet {
constructor(input: Input) {
super(input);
}

public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise<TxPayload> {
// calculate the amount to withdraw
const fee = BigInt(toNano(feeRate!)); // feeRate as BigInt in nano
const amountToWithdraw = BigInt(toNano(amount)) - fee; // amount is the wallet balance
let internalMessageMemo = undefined;
if (memo) {
internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
}
// create the tx payload
let internalMessage = beginCell()
.storeUint(0x10, 6) // 0x10 is no bounce
.storeAddress(Address.parse(to)) // Store the recipient address
.storeCoins(amountToWithdraw); // Store the amount within the payload

if (internalMessageMemo) {
internalMessage
.storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // store memo as reference
.storeRef(internalMessageMemo)
.endCell();
} else {
internalMessage.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1).endCell(); // no memo added
}

let toSign = beginCell()
.storeUint(698983191, 32) // sub wallet_id
.storeUint(Math.floor(Date.now() / 1e3) + 600, 32) // Transaction expiration time, +600 = 10 minute
.storeUint(extraParams?.get('seqno'), 32) // store seqno
.storeUint(0, 8)
.storeUint(128, 8) // store SendMode as CARRY_ALL_REMAINING_BALANCE
.storeRef(internalMessage) // store our internalMessage as a reference
.endCell();

const signMessage = toSign.toBoc().toString('base64');
const signData = toSign.hash();

const signature = Buffer.from(await this.sign(Uint8Array.from(signData))).toString('base64');
const unsignedTx = Cell.fromBase64(signMessage).asBuilder();

const body = beginCell().storeBuffer(Buffer.from(signature, 'base64')).storeBuilder(unsignedTx).endCell();
return { tx: body.toBoc().toString('base64') };
}
}
3 changes: 3 additions & 0 deletions apps/recovery-utility/renderer/lib/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Hedera } from './HBAR';
import { Algorand } from './ALGO';
import { Bitcoin, BitcoinSV, LiteCoin, Dash, ZCash, Doge } from './BTC';
import { Celestia } from './CELESTIA';
import { Ton } from './TON';

const fillEVMs = () => {
const evms = Object.keys(assets).reduce(
Expand Down Expand Up @@ -95,6 +96,8 @@ export const WalletClasses = {
XEM_TEST: NEM,
HBAR: Hedera,
HBAR_TEST: Hedera,
TON: Ton,
TON_TEST: Ton,

...fillEVMs(),
} as const;
Expand Down
30 changes: 30 additions & 0 deletions packages/asset-config/config/patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,34 @@ export const nativeAssetPatches: NativeAssetPatches = {
memo: false,
getExplorerUrl: getStandardExplorer('explorer.testnet.z.cash'),
},
TON: {
derive: true,
transfer: true,
utxo: false,
segwit: false,
minBalance: true,
memo: true,
getExplorerUrl: (type) => (value) => {
if (type === 'tx') {
return `https://tonviewer.com/transaction/${value}`;
}

return `https://tonviewer.com/${value}`;
},
},
TON_TEST: {
derive: true,
transfer: true,
utxo: false,
segwit: false,
minBalance: true,
memo: true,
getExplorerUrl: (type) => (value) => {
if (type === 'tx') {
return `https://testnet.tonviewer.com/transaction/${value}`;
}

return `https://testnet.tonviewer.com/${value}`;
},
},
};
2 changes: 2 additions & 0 deletions packages/e2e-tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const nativeTestnetAssets: AssetTestConfig[] = [
// { assetId: 'XRP_TEST' },
// { assetId: 'XTZ_TEST' },
// { assetId: 'ZEC_TEST' },
// { assetId: 'TON_TEST' },
];

const nativeMainnetAssets = [
Expand Down Expand Up @@ -98,6 +99,7 @@ const nativeMainnetAssets = [
// { assetId: 'XRP' },
// { assetId: 'XTZ' },
// { assetId: 'ZEC' },
// { assetId: 'TON' },
];

export const testAssets: AssetTestConfig[] = [...nativeTestnetAssets, ...nativeMainnetAssets];
13 changes: 13 additions & 0 deletions packages/shared/lib/validateAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { bech32 } from 'bech32';
import { isTestnetAsset } from '@fireblocks/asset-config';
import { getLogger } from './getLogger';
import { LOGGER_NAME_SHARED } from '../constants';
import { Address } from '@ton/ton';

const logger = getLogger(LOGGER_NAME_SHARED);

Expand Down Expand Up @@ -101,6 +102,9 @@ export class AddressValidator {
return this.validateCOSMOS(address);
case 'TERRA':
return this.validateTERRA(address);
case 'TON':
case 'TON_TEST':
return this.validateTON(address);
default:
logger.error(`Unsupported networkProtocol for address validation ${validatorReference}`);
throw new Error(`Unsupported networkProtocol for address validation ${validatorReference}`);
Expand Down Expand Up @@ -150,4 +154,13 @@ export class AddressValidator {
private validateTERRA(address: string): boolean {
return this.validateCosmosBased('terra')(address);
}

private validateTON(address: string): boolean {
try {
Address.parse(address);
return true;
} catch (error) {
return false;
}
}
}
3 changes: 2 additions & 1 deletion packages/wallet-derivation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@solana/web3.js": "^1.78.0",
"@substrate/txwrapper-polkadot": "6.0.1",
"@terra-money/terra.js": "^3.1.10",
"@ton/ton": "^15.1.0",
"@types/bech32": "^1.1.4",
"@types/bitcore-lib-cash": "^8.23.5",
"@types/blake2b": "^2.1.0",
Expand Down Expand Up @@ -53,4 +54,4 @@
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1"
}
}
}
2 changes: 1 addition & 1 deletion packages/wallet-derivation/wallets/EdDSAWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export abstract class EdDSAWallet extends BaseWallet {

const privateKeyInt = hexToNumber(this.privateKey.slice(2));
const privateKeyBytes = numberToBytesLE(privateKeyInt, 32);
const messagesBytes = typeof message === 'string' ? Buffer.from(message) : message;
const messagesBytes = typeof message === 'string' ? Buffer.from(message, 'hex') : message;
const messageBytes = concatBytes(messagesBytes);

const seed = randomBytes();
Expand Down
19 changes: 19 additions & 0 deletions packages/wallet-derivation/wallets/chains/TON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Input } from '../../types';
import { EdDSAWallet } from '../EdDSAWallet';
import { WalletContractV4 } from '@ton/ton';

export class Ton extends EdDSAWallet {
constructor(input: Input) {
super(input, 607);
}

protected getAddress() {
return WalletContractV4.create({
publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'),
workchain: 0,
}).address.toString({
bounceable: false,
testOnly: this.isTestnet,
});
}
}
Loading