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 1 commit
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,
},
};
106 changes: 106 additions & 0 deletions apps/recovery-relay/lib/wallets/TON/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
import { ConnectedWallet } from '../ConnectedWallet';
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';

export class Ton extends BaseTon implements ConnectedWallet {
public rpcURL: string | undefined;
public setRPCUrl(url: string): void {
this.rpcURL = url;
}
private client = new TonClient({
endpoint: this.isTestnet ? 'https://testnet.toncenter.com/api/v2/jsonRPC' : 'https://toncenter.com/api/v2/jsonRPC',
});
private tonWallet = WalletContractV4.create({ publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'), workchain: 0 });

public async getBalance(): Promise<number> {
await new Promise((resolve) => setTimeout(resolve, 2000));
const contract = this.client.open(this.tonWallet);
return Number(fromNano(await contract.getBalance()));
}
public async broadcastTx(tx: string): Promise<string> {
try {
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();

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;
} 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> {
// get the balance
const balance = await this.getBalance(); // returned in nanoTon

// fee for regular tx is hardcoded to 0.02 TON
const feeRate = 0.02;
await new Promise((resolve) => setTimeout(resolve, 1000));
// 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,
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
.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,
});
}
}
5 changes: 5 additions & 0 deletions packages/wallet-derivation/wallets/chains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Tezos } from './XTZ';
import { ZCash } from './ZEC';
import { Hedera } from './HBAR';
import { Celestia } from './TIA';
import { Ton } from './TON';

export const getWallet = (assetId: string) => {
const asset = assets[assetId];
Expand Down Expand Up @@ -71,6 +72,9 @@ export const getWallet = (assetId: string) => {
case 'HBAR':
case 'HBAR_TEST':
return Hedera;
case 'TON':
case 'TON_TEST':
return Ton;

// ECDSA
case 'ATOM_COS':
Expand Down Expand Up @@ -145,4 +149,5 @@ export {
NEM,
Hedera,
Celestia,
Ton,
};
Loading