From 4fb5bd90ddbd92b69f5fc7485cfaf2eff17009a6 Mon Sep 17 00:00:00 2001
From: TomerHFB <158162596+TomerHFB@users.noreply.github.com>
Date: Mon, 16 Dec 2024 11:21:17 +0200
Subject: [PATCH 1/5] feat(@fireblocks/recovery-utility): :sparkles: add Jetton
support
Add the ability to derive Jetton wallets a withdraw the assets
---
.../WithdrawModal/CreateTransaction/index.tsx | 19 +-
.../components/WithdrawModal/index.tsx | 4 +-
apps/recovery-relay/context/Workspace.tsx | 10 +-
apps/recovery-relay/lib/defaultRPCs.ts | 6 +
.../lib/wallets/Jetton/index.ts | 186 ++++++++++++++++++
apps/recovery-relay/lib/wallets/TON/index.ts | 7 +-
apps/recovery-relay/lib/wallets/index.ts | 4 +
apps/recovery-relay/lib/wallets/types.ts | 1 +
.../WithdrawModal/SignTransaction/index.tsx | 4 +-
.../renderer/context/Workspace.tsx | 10 +-
.../renderer/lib/wallets/Jetton/index.ts | 72 +++++++
.../renderer/lib/wallets/TON/index.ts | 3 +-
.../renderer/lib/wallets/index.ts | 16 ++
packages/asset-config/config/patches.ts | 50 +++--
packages/asset-config/data/globalAssets.ts | 118 +++++++++++
packages/asset-config/util.ts | 8 +
packages/e2e-tests/tests.ts | 3 +
.../useBaseWorkspace/reduceDerivations.ts | 2 +-
packages/shared/lib/validateAddress.ts | 3 +
packages/wallet-derivation/index.ts | 1 +
.../wallets/chains/Jetton.ts | 8 +
.../wallet-derivation/wallets/chains/index.ts | 5 +
yarn.lock | 8 +-
23 files changed, 517 insertions(+), 31 deletions(-)
create mode 100644 apps/recovery-relay/lib/wallets/Jetton/index.ts
create mode 100644 apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
create mode 100644 packages/wallet-derivation/wallets/chains/Jetton.ts
diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index 13753b1d..b0f9cf86 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -22,6 +22,7 @@ import { useWorkspace } from '../../../context/Workspace';
import { Derivation, AccountData } from '../../../lib/wallets';
import { LateInitConnectedWallet } from '../../../lib/wallets/LateInitConnectedWallet';
import { useSettings } from '../../../context/Settings';
+import { Jetton } from '../../../lib/wallets/Jetton';
const logger = getLogger(LOGGER_NAME_RELAY);
@@ -149,7 +150,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
const fromAddress = values.fromAddress ?? defaultValues.fromAddress;
- const derivation = wallet?.derivations?.get(fromAddress);
+ const derivation = wallet?.derivations?.get(`${asset?.id}-${fromAddress}`); // fix for token support
// TODO: Show both original balance and adjusted balance in create tx UI
@@ -166,6 +167,11 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
throw new Error(`No RPC Url for: ${derivation?.assetId}`);
}
if (rpcUrl !== null) derivation!.setRPCUrl(rpcUrl);
+ if (asset.address && asset.protocol === 'TON') {
+ (derivation as Jetton).setTokenAddress(asset.address);
+ (derivation as Jetton).setDecimals(asset.decimals);
+ }
+
return await derivation!.prepare?.(toAddress, values.memo);
},
onSuccess: (prepare: AccountData) => {
@@ -421,6 +427,10 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
theme.palette.error.main}>
Insufficient balance for transaction
+ ) : prepareQuery.data?.insufficientFeeBalance === true && prepareQuery.data?.insufficientBalance !== true ? (
+ theme.palette.error.main}>
+ Insufficient fee asset balance for token transaction
+
) : (
''
)}
@@ -430,7 +440,12 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
type='submit'
disabled={
!prepareQuery.data?.balance ||
- (prepareQuery.data && prepareQuery.data?.insufficientBalance !== undefined && prepareQuery.data.insufficientBalance)
+ (prepareQuery.data &&
+ prepareQuery.data?.insufficientBalance !== undefined &&
+ prepareQuery.data.insufficientBalance) ||
+ (prepareQuery.data &&
+ prepareQuery.data?.insufficientFeeBalance !== undefined &&
+ prepareQuery.data.insufficientFeeBalance)
}
>
Prepare Transaction
diff --git a/apps/recovery-relay/components/WithdrawModal/index.tsx b/apps/recovery-relay/components/WithdrawModal/index.tsx
index bb4b1533..f42edb70 100644
--- a/apps/recovery-relay/components/WithdrawModal/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/index.tsx
@@ -127,7 +127,9 @@ export const WithdrawModal = () => {
.get(inboundRelayParams?.accountId)
?.wallets.get(inboundRelayParams?.signedTx.assetId);
- const derivation = wallet?.derivations?.get(inboundRelayParams?.signedTx.from);
+ const derivation = wallet?.derivations?.get(
+ `${inboundRelayParams?.signedTx.assetId}-${inboundRelayParams?.signedTx.from}`,
+ ); // fix for token support
const rpcUrl = getAssetURL(derivation?.assetId ?? '', RPCs);
if (rpcUrl === undefined) {
diff --git a/apps/recovery-relay/context/Workspace.tsx b/apps/recovery-relay/context/Workspace.tsx
index 88dab046..7a44abf6 100644
--- a/apps/recovery-relay/context/Workspace.tsx
+++ b/apps/recovery-relay/context/Workspace.tsx
@@ -14,6 +14,7 @@ import { getAssetConfig } from '@fireblocks/asset-config';
import packageJson from '../package.json';
import { WalletClasses, Derivation } from '../lib/wallets';
import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants';
+import { isTransferableToken } from '@fireblocks/asset-config/util';
type DistributiveOmit = T extends any ? Omit : never;
@@ -61,7 +62,6 @@ const getInboundRelayWalletIds = (inboundRelayParams?: RelayRequestParams) => {
return null;
}
- logger.info('Inbound Relay params', inboundRelayParams);
let ret;
switch (inboundRelayParams.action) {
case 'import':
@@ -116,6 +116,14 @@ export const WorkspaceProvider = ({ children }: Props) => {
app: 'relay',
relayBaseUrl: 'fireblocks-recovery:/',
deriveWallet: (input) => {
+ if (isTransferableToken(input.assetId)) {
+ if (input.assetId in WalletClasses) {
+ return new WalletClasses[input.assetId as keyof typeof WalletClasses](input, 0);
+ } else {
+ throw new Error(`Unsupported token: ${input.assetId}`);
+ }
+ }
+
const nativeAssetId = (getAssetConfig(input.assetId)?.nativeAsset ?? input.assetId) as keyof typeof WalletClasses;
if (nativeAssetId in WalletClasses) {
diff --git a/apps/recovery-relay/lib/defaultRPCs.ts b/apps/recovery-relay/lib/defaultRPCs.ts
index aa211042..cdacbf68 100644
--- a/apps/recovery-relay/lib/defaultRPCs.ts
+++ b/apps/recovery-relay/lib/defaultRPCs.ts
@@ -168,4 +168,10 @@ export const defaultRPCs: Record<
enabled: true,
allowedEmptyValue: false,
},
+ Jetton: {
+ url: 'https://toncenter.com/api/v2/jsonRPC',
+ name: 'The Open Network',
+ enabled: true,
+ allowedEmptyValue: false,
+ },
};
diff --git a/apps/recovery-relay/lib/wallets/Jetton/index.ts b/apps/recovery-relay/lib/wallets/Jetton/index.ts
new file mode 100644
index 00000000..1f20373b
--- /dev/null
+++ b/apps/recovery-relay/lib/wallets/Jetton/index.ts
@@ -0,0 +1,186 @@
+import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
+import { JettonMaster, TonClient, WalletContractV4 } from '@ton/ton';
+import { Address, beginCell, Cell, fromNano, toNano } from '@ton/core';
+import { AccountData } from '../types';
+import { defaultTonWalletV4R2code } from '../TON/tonParams';
+import axios from 'axios';
+import { LateInitConnectedWallet } from '../LateInitConnectedWallet';
+
+export class Jetton extends BaseTon implements LateInitConnectedWallet {
+ public memo: string | undefined;
+ public tokenAddress: string | undefined;
+ public decimals: number | undefined;
+
+ public setTokenAddress(address: string) {
+ this.tokenAddress = address;
+ }
+
+ public setDecimals(decimals: number) {
+ this.decimals = decimals;
+ }
+
+ 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 });
+
+ private async getContractAddress(): Promise {
+ const jettonMasterAddress = Address.parse(this.tokenAddress!);
+ const walletAddress = Address.parse(this.address);
+ const jettonMaster = this?.client?.open(JettonMaster.create(jettonMasterAddress));
+ return await jettonMaster?.getWalletAddress(walletAddress);
+ }
+
+ public async getBalance(): Promise {
+ if (this.client) {
+ if (!this.tokenAddress) {
+ this.relayLogger.error('TON Jettons: Jetton token address unavailable');
+ throw new Error('TON Jettons: Jetton token address unavailable');
+ }
+
+ const contractAddress = await this.getContractAddress();
+ if (!contractAddress) {
+ this.relayLogger.error(`TON Jettons: wallet's contract address unavailable`);
+ throw new Error(`TON Jettons: wallet's contract address unavailable`);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ const { stack } = await this.client.runMethod(contractAddress, 'get_wallet_data');
+ const normalizingFactor = 10 ** this.decimals!;
+
+ return stack.readNumber() / normalizingFactor;
+ } else {
+ this.relayLogger.error('TON Jettons: Client failed to initialize');
+ throw new Error('TON Jettons: Client failed to initialize');
+ }
+ }
+
+ public async broadcastTx(tx: string): Promise {
+ 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(Address.parse(this.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(await 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(`Jetton: Tx broadcasted: ${txHash}`);
+ return txHash;
+ } else {
+ throw new Error('Jetton: Client failed to initialize');
+ }
+ } catch (e) {
+ this.relayLogger.error(`Jetton: 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 {
+ // init the TonClient
+ this.init();
+
+ const jettonBalance = await this.getBalance();
+ const contract = this.client!.open(this.tonWallet);
+ const tonBalance = await contract.getBalance();
+
+ // fee for token tx is hardcoded to 0.1 TON
+ const feeRate = Number(toNano(0.1));
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ // get seqno of the wallet, set it as exrtaParams
+ const seqno = await this.getSeqno();
+
+ // get the contract address of the wallet
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ const contractAddress = await this.getContractAddress();
+
+ // set extraParams
+ const extraParams = new Map();
+ extraParams.set('seqno', seqno);
+ extraParams.set('contract-address', contractAddress?.toString({ bounceable: true, testOnly: false }));
+ extraParams.set('decimals', this.decimals);
+
+ const preperedData = {
+ balance: jettonBalance,
+ memo: this.memo,
+ feeRate,
+ extraParams,
+ insufficientBalance: jettonBalance <= 0,
+ insufficientFeeBalance: tonBalance < feeRate,
+ } as AccountData;
+
+ return preperedData;
+ }
+ private async getSeqno() {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ return await this.client!.open(this.tonWallet).getSeqno();
+ }
+
+ private async createStateInit(pubKey: Buffer): Promise {
+ // the initial data cell our contract will hold. Wallet V4 has an extra value for plugins in the end
+ const dataCell = beginCell()
+ .storeUint(await this.getSeqno(), 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)
+ .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;
+ }
+}
diff --git a/apps/recovery-relay/lib/wallets/TON/index.ts b/apps/recovery-relay/lib/wallets/TON/index.ts
index 4372429e..e403ce6f 100644
--- a/apps/recovery-relay/lib/wallets/TON/index.ts
+++ b/apps/recovery-relay/lib/wallets/TON/index.ts
@@ -1,6 +1,6 @@
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
import { TonClient, WalletContractV4 } from '@ton/ton';
-import { beginCell, Cell, fromNano } from '@ton/core';
+import { beginCell, Cell, fromNano, toNano } from '@ton/core';
import { AccountData } from '../types';
import { defaultTonWalletV4R2code } from './tonParams';
import axios from 'axios';
@@ -35,7 +35,6 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
public async getBalance(): Promise {
if (this.client) {
- await new Promise((resolve) => setTimeout(resolve, 2000));
const contract = this.client.open(this.tonWallet);
return Number(fromNano(await contract.getBalance()));
} else {
@@ -91,7 +90,7 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
const balance = await this.getBalance();
// fee for regular tx is hardcoded to 0.02 TON
- const feeRate = 0.02;
+ const feeRate = Number(toNano(0.02));
await new Promise((resolve) => setTimeout(resolve, 2000));
// get seqno of the wallet, set it as exrtaParams
@@ -104,7 +103,7 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
memo: this.memo,
feeRate,
extraParams,
- insufficientBalance: balance < 0.005,
+ insufficientBalance: Number(toNano(balance)) - feeRate < Number(toNano(0.005)), // 0.005 is minimum amount for transfer
} as AccountData;
return preperedData;
diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts
index 4c109e90..e29ad57e 100644
--- a/apps/recovery-relay/lib/wallets/index.ts
+++ b/apps/recovery-relay/lib/wallets/index.ts
@@ -37,6 +37,7 @@ import { Algorand } from './ALGO';
import { Celestia } from './CELESTIA';
import { CoreDAO } from './EVM/CORE_COREDAO';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
export { ConnectedWallet } from './ConnectedWallet';
export const WalletClasses = {
@@ -121,6 +122,9 @@ export const WalletClasses = {
CELESTIA_TEST: Celestia,
TON: Ton,
TON_TEST: Ton,
+ USDT_TON: Jetton,
+ NOTCOIN_TON: Jetton,
+ DOGS_TON: Jetton,
} as const;
type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];
diff --git a/apps/recovery-relay/lib/wallets/types.ts b/apps/recovery-relay/lib/wallets/types.ts
index 55ba2442..2afd0cef 100644
--- a/apps/recovery-relay/lib/wallets/types.ts
+++ b/apps/recovery-relay/lib/wallets/types.ts
@@ -28,6 +28,7 @@ export type AccountData = {
extraParams?: Map;
endpoint?: string;
insufficientBalance?: boolean;
+ insufficientFeeBalance?: boolean;
};
export type TxPayload = {
diff --git a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
index afd3db5a..cd2e1077 100644
--- a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
+++ b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
@@ -73,7 +73,9 @@ export const SignTransaction = ({ txId, account, asset, inboundRelayParams }: Pr
const { to, amount, misc } = unsignedTx;
- const derivation = account.wallets.get(asset.id)?.derivations.get(unsignedTx.from);
+ const derivation = account.wallets
+ .get(asset.id)
+ ?.derivations.get(`${inboundRelayParams?.unsignedTx.assetId}-${inboundRelayParams?.unsignedTx.from}`);
if (!derivation) {
throw new Error('Derivation not found');
diff --git a/apps/recovery-utility/renderer/context/Workspace.tsx b/apps/recovery-utility/renderer/context/Workspace.tsx
index 6b39ac4e..f901a940 100644
--- a/apps/recovery-utility/renderer/context/Workspace.tsx
+++ b/apps/recovery-utility/renderer/context/Workspace.tsx
@@ -17,6 +17,7 @@ import { handleRelayUrl } from '../lib/ipc/handleRelayUrl';
import { SigningWallet } from '../lib/wallets/SigningWallet';
import { useSettings } from './Settings';
import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants';
+import { isTransferableToken } from '@fireblocks/asset-config/util';
type DistributiveOmit = T extends any ? Omit : never;
@@ -65,7 +66,14 @@ export const WorkspaceProvider = ({ children }: Props) => {
app: 'utility',
relayBaseUrl,
deriveWallet: (input) => {
- const nativeAssetId = (getAssetConfig(input.assetId)?.nativeAsset ?? input.assetId) as keyof typeof WalletClasses;
+ let transferableToken = false;
+ const config = getAssetConfig(input.assetId);
+ if (config?.address && isTransferableToken(input.assetId)) {
+ transferableToken = true;
+ }
+ const nativeAssetId = (
+ transferableToken ? input.assetId : config?.nativeAsset ?? input.assetId
+ ) as keyof typeof WalletClasses;
logger.info('Deriving native asset', nativeAssetId);
diff --git a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
new file mode 100644
index 00000000..4328439d
--- /dev/null
+++ b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
@@ -0,0 +1,72 @@
+import { Ton as BaseTon, Input } from '@fireblocks/wallet-derivation';
+import { SigningWallet } from '../SigningWallet';
+import { Address, beginCell, Cell, SendMode } from '@ton/core';
+import { GenerateTxInput, TxPayload } from '../types';
+
+export class Jetton extends BaseTon implements SigningWallet {
+ constructor(input: Input) {
+ super(input);
+ }
+
+ public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise {
+ // check for jetton extra params (contract address, seqno and decimals)
+ if (!extraParams?.get('contract-address') || !extraParams?.get('decimals') || !extraParams?.get('seqno')) {
+ throw new Error('Jetton: Missing jetton parameters');
+ }
+
+ const JettonTransferOpcode = 0x0f8a7ea5;
+ const decimals = extraParams?.get('decimals');
+ const normalizingFactor = 10 ** decimals;
+ const amountToWithdraw = amount * normalizingFactor; // amount is the wallet balance
+
+ let internalMessageMemo = undefined;
+ // create the tx payload
+ const internalMessageBody = beginCell()
+ .storeUint(JettonTransferOpcode, 32) // opcode for jetton transfer
+ .storeUint(0, 64) // query id
+ .storeCoins(amountToWithdraw) // jetton balance
+ .storeAddress(Address.parse(to)) // tx destination
+ .storeAddress(Address.parse(this.address)) // excess fees sent back to native ton wallet
+ .storeBit(0); // no custom payload
+ if (memo) {
+ internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
+ internalMessageBody
+ .storeCoins(1) // forward amount - if >0, will send notification message
+ .storeBit(1) // we store forwardPayload as a reference
+ .storeRef(internalMessageMemo)
+ .endCell();
+ } else {
+ internalMessageBody
+ .storeCoins(0) // no memo added
+ .storeBit(0)
+ .endCell();
+ }
+ const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS;
+
+ const contractAddress = extraParams?.get('contract-address');
+ const internalMessage = beginCell()
+ .storeUint(0x18, 6) // bounceable tx
+ .storeAddress(Address.parse(contractAddress)) //wallet Jetton contract address
+ .storeCoins(BigInt(feeRate!))
+ .storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference
+ .storeRef(internalMessageBody)
+ .endCell();
+ const toSign = beginCell()
+ .storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids
+ .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(sendMode, 8)
+ .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') };
+ }
+}
diff --git a/apps/recovery-utility/renderer/lib/wallets/TON/index.ts b/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
index 3608fa2a..ddb6ae67 100644
--- a/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
@@ -10,8 +10,7 @@ export class Ton extends BaseTon implements SigningWallet {
public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise {
// 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
+ const amountToWithdraw = BigInt(toNano(amount)) - BigInt(feeRate!);
let internalMessageMemo = undefined;
if (memo) {
internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts
index 59a5a3e1..956cc6da 100644
--- a/apps/recovery-utility/renderer/lib/wallets/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/index.ts
@@ -21,6 +21,7 @@ import { Algorand } from './ALGO';
import { Bitcoin, BitcoinSV, LiteCoin, Dash, ZCash, Doge } from './BTC';
import { Celestia } from './CELESTIA';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
const fillEVMs = () => {
const evms = Object.keys(assets).reduce(
@@ -39,6 +40,18 @@ const fillEVMs = () => {
return evms;
};
+// const fillJettons = () => {
+// const jettons = Object.keys(assets).reduce(
+// (o, assetId) => ({
+// ...o,
+// [assets[assetId].id]: assets[assetId].protocol === 'TON' && assets[assetId].address ? Ton : undefined,
+// }),
+// {},
+// ) as any;
+// Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
+// return jettons;
+// };
+
export { SigningWallet as BaseWallet } from './SigningWallet';
export const WalletClasses = {
@@ -98,6 +111,9 @@ export const WalletClasses = {
HBAR_TEST: Hedera,
TON: Ton,
TON_TEST: Ton,
+ USDT_TON: Jetton,
+ NOTCOIN_TON: Jetton,
+ DOGS_TON: Jetton,
...fillEVMs(),
} as const;
diff --git a/packages/asset-config/config/patches.ts b/packages/asset-config/config/patches.ts
index 2a4c4252..e70c2348 100644
--- a/packages/asset-config/config/patches.ts
+++ b/packages/asset-config/config/patches.ts
@@ -24,6 +24,13 @@ const getSolanaExplorer: ExplorerUrlBuilder = (cluster?: string) => (type) => (v
return `${baseUrl}?cluster=${cluster}`;
};
+const getTonExplorerUrl: ExplorerUrlBuilder = (baseUrl) => (type) => (value) => {
+ if (type === 'tx') {
+ return `${baseUrl}/transaction/${value}`;
+ }
+
+ return `${baseUrl}/${value}`;
+};
const evm = (baseExplorerUrl: string, rpcUrl?: string, transfer = true): NativeAssetPatch => ({
derive: true,
@@ -344,13 +351,7 @@ export const nativeAssetPatches: NativeAssetPatches = {
segwit: false,
minBalance: true,
memo: true,
- getExplorerUrl: (type) => (value) => {
- if (type === 'tx') {
- return `https://tonviewer.com/transaction/${value}`;
- }
-
- return `https://tonviewer.com/${value}`;
- },
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
},
TON_TEST: {
derive: true,
@@ -359,12 +360,33 @@ export const nativeAssetPatches: NativeAssetPatches = {
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}`;
- },
+ getExplorerUrl: getTonExplorerUrl('https://testnet/tonviewer.com'),
+ },
+ DOGS_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
+ },
+ USDT_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
+ },
+ NOTCOIN_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
},
};
diff --git a/packages/asset-config/data/globalAssets.ts b/packages/asset-config/data/globalAssets.ts
index daf3e646..3e38c599 100644
--- a/packages/asset-config/data/globalAssets.ts
+++ b/packages/asset-config/data/globalAssets.ts
@@ -2022,6 +2022,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'BUIDL_ETH_X3N7',
+ symbol: 'BUIDL',
+ name: 'BlackRock USD Institutional Digital Liquidity Fund',
+ decimals: 6,
+ address: '0x7712c34205737192402172409a8F7ccef8aA2AEc',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'BURGER_BSC',
symbol: 'BURGER',
@@ -2198,6 +2208,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'CAT_B71VABWJ_BY1A',
+ symbol: 'CAT',
+ name: 'Simons Cat',
+ decimals: 18,
+ address: '0x6894CDe390a3f51155ea41Ed24a33A4827d3063D',
+ nativeAsset: 'BNB_BSC',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'CAVE_SOL',
symbol: 'CAVE',
@@ -3594,6 +3614,16 @@ export const globalAssets = [
protocol: 'BTC',
testnet: true,
},
+ {
+ id: 'DOGS_TON',
+ symbol: 'DOGS',
+ name: 'Dogs (Ton)',
+ decimals: 9,
+ address: 'EQCvxJy4eG8hyHBFsZ7eePxrRsUQSFE_jpptRAYBmcG_DOGS',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'DOPEX',
symbol: 'DOPEX',
@@ -4445,6 +4475,15 @@ export const globalAssets = [
protocol: 'SOL',
testnet: false,
},
+ {
+ id: 'FASTEX_BAHAMUT',
+ symbol: 'FTN',
+ name: 'Fastex Bahamut',
+ decimals: 18,
+ nativeAsset: 'FASTEX_BAHAMUT',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'FB_ATHENA_TEST',
symbol: 'FB_ATHENA_TEST',
@@ -7160,6 +7199,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'NOTCOIN_TON',
+ symbol: 'NOT',
+ name: 'Notcoin (Ton)',
+ decimals: 9,
+ address: 'EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'NOTE_ETH',
symbol: 'NOTE',
@@ -8343,6 +8392,15 @@ export const globalAssets = [
protocol: 'SOL',
testnet: false,
},
+ {
+ id: 'REDBELLY_TEST',
+ symbol: 'RBNT',
+ name: 'Redbelly Test',
+ decimals: 18,
+ nativeAsset: 'REDBELLY_TEST',
+ protocol: 'ETH',
+ testnet: true,
+ },
{
id: 'REEF',
symbol: 'REEF',
@@ -10175,6 +10233,26 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'TOKEN_BSC_RLDP',
+ symbol: 'TOKEN',
+ name: 'TokenFi',
+ decimals: 9,
+ address: '0x4507cEf57C46789eF8d1a19EA45f4216bae2B528',
+ nativeAsset: 'BNB_BSC',
+ protocol: 'ETH',
+ testnet: false,
+ },
+ {
+ id: 'TOKEN_ETH_JIKQ',
+ symbol: 'TOKEN',
+ name: 'TokenFi',
+ decimals: 9,
+ address: '0x4507cEf57C46789eF8d1a19EA45f4216bae2B528',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'TON',
symbol: 'TON',
@@ -10762,6 +10840,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: true,
},
+ {
+ id: 'USDC_BASECHAIN_ETH_5I5C',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
+ nativeAsset: 'BASECHAIN_ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'USDC_BSC',
symbol: 'USDC',
@@ -11015,6 +11103,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'USDT_TON',
+ symbol: 'USDT',
+ name: 'Tether USD (Ton)',
+ decimals: 6,
+ address: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'USDT2_AVAX',
symbol: 'USDT',
@@ -11330,6 +11428,26 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'WCT_B6QT1TZK_TZBJ',
+ symbol: 'WCT',
+ name: 'WalletConnect',
+ decimals: 18,
+ address: '0x61cc6aF18C351351148815c5F4813A16DEe7A7E4',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
+ {
+ id: 'WCT_B7K5S6PF_H7HU',
+ symbol: 'WCT',
+ name: 'WalletConnect',
+ decimals: 18,
+ address: '0xeF4461891DfB3AC8572cCf7C794664A8DD927945',
+ nativeAsset: 'ETH-OPT',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'WDGLD',
symbol: 'WDGLD',
diff --git a/packages/asset-config/util.ts b/packages/asset-config/util.ts
index 9c2f34c7..4f116367 100644
--- a/packages/asset-config/util.ts
+++ b/packages/asset-config/util.ts
@@ -42,6 +42,14 @@ export const isExplorerUrl = (url: string) => {
export const getAssetConfig = (assetId?: string) => (isAssetId(assetId) ? assets[assetId] : undefined);
+export const isTransferableToken = (assetId?: string) => {
+ if (!isAssetId) return false;
+ const assetConfig = assets[assetId!];
+
+ // Is this a non-native asset, has a contract / address that represents it (i.e token) and we enabled the transfer option
+ return assetConfig.nativeAsset !== assetConfig.id && assetConfig.address && assetConfig.transfer;
+};
+
export const getNetworkProtocol = (assetId: string) => getAssetConfig(assetId)?.protocol;
export const getNativeAssetConfig = (assetId?: ID) =>
diff --git a/packages/e2e-tests/tests.ts b/packages/e2e-tests/tests.ts
index 30afd326..35fa8939 100644
--- a/packages/e2e-tests/tests.ts
+++ b/packages/e2e-tests/tests.ts
@@ -100,6 +100,9 @@ const nativeMainnetAssets = [
// { assetId: 'XTZ' },
// { assetId: 'ZEC' },
// { assetId: 'TON' },
+ // { assetId: 'USDT_TON' },
+ // { assetId: 'NOTCOIN_TON' },
+ // { assetId: 'DOGS_TON' },
];
export const testAssets: AssetTestConfig[] = [...nativeTestnetAssets, ...nativeMainnetAssets];
diff --git a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
index 738014f0..d9e4b5ee 100644
--- a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
+++ b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
@@ -126,7 +126,7 @@ export const reduceDerivations = (input: Deri
wallet.derivations.delete(address);
}
- wallet.derivations.set(derivation.address, derivation);
+ wallet.derivations.set(`${derivation.assetId}-${derivation.address}`, derivation); // fix for token support
}
// Handle legacy + Segwit derivations
diff --git a/packages/shared/lib/validateAddress.ts b/packages/shared/lib/validateAddress.ts
index fb74d847..71a68e4a 100644
--- a/packages/shared/lib/validateAddress.ts
+++ b/packages/shared/lib/validateAddress.ts
@@ -104,6 +104,9 @@ export class AddressValidator {
return this.validateTERRA(address);
case 'TON':
case 'TON_TEST':
+ case 'USDT_TON':
+ case 'DOGS_TON':
+ case 'NOTCOIN_TON':
return this.validateTON(address);
default:
logger.error(`Unsupported networkProtocol for address validation ${validatorReference}`);
diff --git a/packages/wallet-derivation/index.ts b/packages/wallet-derivation/index.ts
index 39c51d65..3347587a 100644
--- a/packages/wallet-derivation/index.ts
+++ b/packages/wallet-derivation/index.ts
@@ -10,6 +10,7 @@ export const deriveWallet = (input: Input): BaseWallet => {
const WalletInstance = getWallet(nativeAssetId);
// TODO: defaultCoinType should be unnecessary here, fix types / class inheritance
+ //@ts-ignore
const wallet = new WalletInstance(input, 0);
return wallet;
diff --git a/packages/wallet-derivation/wallets/chains/Jetton.ts b/packages/wallet-derivation/wallets/chains/Jetton.ts
new file mode 100644
index 00000000..1eace884
--- /dev/null
+++ b/packages/wallet-derivation/wallets/chains/Jetton.ts
@@ -0,0 +1,8 @@
+import { Input } from '../../types';
+import { Ton } from './TON';
+
+export class Jetton extends Ton {
+ constructor(input: Input, protected tokenAddress: string) {
+ super(input);
+ }
+}
diff --git a/packages/wallet-derivation/wallets/chains/index.ts b/packages/wallet-derivation/wallets/chains/index.ts
index af267037..5976cf32 100644
--- a/packages/wallet-derivation/wallets/chains/index.ts
+++ b/packages/wallet-derivation/wallets/chains/index.ts
@@ -26,6 +26,7 @@ import { ZCash } from './ZEC';
import { Hedera } from './HBAR';
import { Celestia } from './TIA';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
export const getWallet = (assetId: string) => {
const asset = assets[assetId];
@@ -75,6 +76,10 @@ export const getWallet = (assetId: string) => {
case 'TON':
case 'TON_TEST':
return Ton;
+ case 'USDT_TON':
+ case 'NOTCOIN_TON':
+ case 'DOGS_TON':
+ return Jetton;
// ECDSA
case 'ATOM_COS':
diff --git a/yarn.lock b/yarn.lock
index 261e7a3c..58a7b791 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3254,7 +3254,7 @@
rxjs "^7.8.1"
tslib "^2.5.3"
-"@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
+"@polkadot/keyring@12.3.2", "@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.3.2.tgz#112a0c28816a1f47edad6260dc94222c29465a54"
integrity sha512-NTdtDeI0DP9l/45hXynNABeP5VB8piw5YR+CbUxK2e36xpJWVXwbcOepzslg5ghE9rs8UKJb30Z/HqTU4sBY0Q==
@@ -3440,7 +3440,7 @@
"@polkadot/wasm-util" "7.2.1"
tslib "^2.5.0"
-"@polkadot/wasm-crypto@^7.2.1":
+"@polkadot/wasm-crypto@7.2.1", "@polkadot/wasm-crypto@^7.2.1":
version "7.2.1"
resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.1.tgz#db671dcb73f1646dc13478b5ffc3be18c64babe1"
integrity sha512-SA2+33S9TAwGhniKgztVN6pxUKpGfN4Tre/eUZGUfpgRkT92wIUT2GpGWQE+fCCqGQgADrNiBcwt6XwdPqMQ4Q==
@@ -3467,7 +3467,7 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-fetch@^12.3.1":
+"@polkadot/x-fetch@12.3.2", "@polkadot/x-fetch@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.3.2.tgz#7e8d2113268e792dd5d1b259ef13839c6aa77996"
integrity sha512-3IEuZ5S+RI/t33NsdPLIIa5COfDCfpUW2sbaByEczn75aD1jLqJZSEDwiBniJ2osyNd4uUxBf6e5jw7LAZeZJg==
@@ -3507,7 +3507,7 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-ws@^12.3.1":
+"@polkadot/x-ws@12.3.2", "@polkadot/x-ws@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.3.2.tgz#422559dfbdaac4c965d5e1b406b6cc4529214f94"
integrity sha512-yM9Z64pLNlHpJE43+Xtr+iUXmYpFFY5u5hrke2PJt13O48H8f9Vb9cRaIh94appLyICoS0aekGhDkGH+MCspBA==
From 2151ed0dd20d536677d23b9fbc9e1e5abc63caa5 Mon Sep 17 00:00:00 2001
From: TomerHFB <158162596+TomerHFB@users.noreply.github.com>
Date: Mon, 16 Dec 2024 16:05:51 +0200
Subject: [PATCH 2/5] feat(@fireblocks/recovery-utility): :sparkles: add Jetton
support
---
.../WithdrawModal/CreateTransaction/index.tsx | 2 +-
.../components/WithdrawModal/index.tsx | 5 ++-
apps/recovery-relay/context/Workspace.tsx | 1 +
.../lib/wallets/Jetton/index.ts | 39 +++++++++----------
.../renderer/lib/wallets/index.ts | 29 +++++++-------
.../useBaseWorkspace/reduceDerivations.ts | 3 +-
packages/shared/index.ts | 2 +
packages/shared/lib/getDerivation.ts | 3 ++
packages/shared/lib/validateAddress.ts | 4 +-
.../wallet-derivation/wallets/chains/index.ts | 8 ++--
10 files changed, 51 insertions(+), 45 deletions(-)
create mode 100644 packages/shared/lib/getDerivation.ts
diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index b0f9cf86..bc33fb95 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -150,7 +150,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
const fromAddress = values.fromAddress ?? defaultValues.fromAddress;
- const derivation = wallet?.derivations?.get(`${asset?.id}-${fromAddress}`); // fix for token support
+ const derivation = wallet?.derivations?.get(`${asset?.id}-${fromAddress}`);
// TODO: Show both original balance and adjusted balance in create tx UI
diff --git a/apps/recovery-relay/components/WithdrawModal/index.tsx b/apps/recovery-relay/components/WithdrawModal/index.tsx
index f42edb70..fa430d30 100644
--- a/apps/recovery-relay/components/WithdrawModal/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/index.tsx
@@ -9,6 +9,7 @@ import {
RelaySignTxResponseParams,
getLogger,
useWrappedState,
+ getDerivationMapKey,
} from '@fireblocks/recovery-shared';
import { getDerivableAssetConfig } from '@fireblocks/asset-config';
import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants';
@@ -128,8 +129,8 @@ export const WithdrawModal = () => {
?.wallets.get(inboundRelayParams?.signedTx.assetId);
const derivation = wallet?.derivations?.get(
- `${inboundRelayParams?.signedTx.assetId}-${inboundRelayParams?.signedTx.from}`,
- ); // fix for token support
+ getDerivationMapKey(inboundRelayParams?.signedTx.assetId, inboundRelayParams?.signedTx.from),
+ );
const rpcUrl = getAssetURL(derivation?.assetId ?? '', RPCs);
if (rpcUrl === undefined) {
diff --git a/apps/recovery-relay/context/Workspace.tsx b/apps/recovery-relay/context/Workspace.tsx
index 7a44abf6..9a4821cb 100644
--- a/apps/recovery-relay/context/Workspace.tsx
+++ b/apps/recovery-relay/context/Workspace.tsx
@@ -118,6 +118,7 @@ export const WorkspaceProvider = ({ children }: Props) => {
deriveWallet: (input) => {
if (isTransferableToken(input.assetId)) {
if (input.assetId in WalletClasses) {
+ logger.debug(`Dervied wallet for a token: ${input.assetId}`);
return new WalletClasses[input.assetId as keyof typeof WalletClasses](input, 0);
} else {
throw new Error(`Unsupported token: ${input.assetId}`);
diff --git a/apps/recovery-relay/lib/wallets/Jetton/index.ts b/apps/recovery-relay/lib/wallets/Jetton/index.ts
index 1f20373b..24895532 100644
--- a/apps/recovery-relay/lib/wallets/Jetton/index.ts
+++ b/apps/recovery-relay/lib/wallets/Jetton/index.ts
@@ -10,6 +10,10 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
public memo: string | undefined;
public tokenAddress: string | undefined;
public decimals: number | undefined;
+ public rpcURL: string | undefined;
+ private client: TonClient | undefined;
+
+ private tonWallet = WalletContractV4.create({ publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'), workchain: 0 });
public setTokenAddress(address: string) {
this.tokenAddress = address;
@@ -24,32 +28,13 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
}
public getLateInitLabel(): string {
- throw new Error('Method not implemented.');
+ return 'Ton client wallet';
}
- 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 });
-
- private async getContractAddress(): Promise {
- const jettonMasterAddress = Address.parse(this.tokenAddress!);
- const walletAddress = Address.parse(this.address);
- const jettonMaster = this?.client?.open(JettonMaster.create(jettonMasterAddress));
- return await jettonMaster?.getWalletAddress(walletAddress);
- }
-
public async getBalance(): Promise {
if (this.client) {
if (!this.tokenAddress) {
@@ -156,6 +141,20 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
return preperedData;
}
+
+ private init() {
+ this.client = new TonClient({
+ endpoint: this.rpcURL!,
+ });
+ }
+
+ private async getContractAddress(): Promise {
+ const jettonMasterAddress = Address.parse(this.tokenAddress!);
+ const walletAddress = Address.parse(this.address);
+ const jettonMaster = this?.client?.open(JettonMaster.create(jettonMasterAddress));
+ return await jettonMaster?.getWalletAddress(walletAddress);
+ }
+
private async getSeqno() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return await this.client!.open(this.tonWallet).getSeqno();
diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts
index 956cc6da..9ee11d4f 100644
--- a/apps/recovery-utility/renderer/lib/wallets/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/index.ts
@@ -40,17 +40,17 @@ const fillEVMs = () => {
return evms;
};
-// const fillJettons = () => {
-// const jettons = Object.keys(assets).reduce(
-// (o, assetId) => ({
-// ...o,
-// [assets[assetId].id]: assets[assetId].protocol === 'TON' && assets[assetId].address ? Ton : undefined,
-// }),
-// {},
-// ) as any;
-// Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
-// return jettons;
-// };
+const fillJettons = () => {
+ const jettons = Object.keys(assets).reduce(
+ (o, assetId) => ({
+ ...o,
+ [assets[assetId].id]: assets[assetId].protocol === 'TON' && assets[assetId].address ? Jetton : undefined,
+ }),
+ {},
+ ) as any;
+ Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
+ return jettons;
+};
export { SigningWallet as BaseWallet } from './SigningWallet';
@@ -111,10 +111,11 @@ export const WalletClasses = {
HBAR_TEST: Hedera,
TON: Ton,
TON_TEST: Ton,
- USDT_TON: Jetton,
- NOTCOIN_TON: Jetton,
- DOGS_TON: Jetton,
+ // USDT_TON: Jetton,
+ // NOTCOIN_TON: Jetton,
+ // DOGS_TON: Jetton,
+ ...fillJettons(),
...fillEVMs(),
} as const;
diff --git a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
index d9e4b5ee..91591038 100644
--- a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
+++ b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
@@ -4,6 +4,7 @@ import { VaultAccount, Wallet } from '../../types';
import { LOGGER_NAME_SHARED } from '../../constants';
import { getLogger } from '../../lib/getLogger';
import { sanatize } from '../../lib/sanatize';
+import { getDerivationMapKey } from '../../lib/getDerivation';
const logger = getLogger(LOGGER_NAME_SHARED);
@@ -126,7 +127,7 @@ export const reduceDerivations = (input: Deri
wallet.derivations.delete(address);
}
- wallet.derivations.set(`${derivation.assetId}-${derivation.address}`, derivation); // fix for token support
+ wallet.derivations.set(getDerivationMapKey(derivation.assetId, derivation.address), derivation);
}
// Handle legacy + Segwit derivations
diff --git a/packages/shared/index.ts b/packages/shared/index.ts
index a8e6974c..ae0985e7 100644
--- a/packages/shared/index.ts
+++ b/packages/shared/index.ts
@@ -68,6 +68,8 @@ export { sanatize } from './lib/sanatize';
export { AddressValidator } from './lib/validateAddress';
+export { getDerivationMapKey } from './lib/getDerivation';
+
// Theme
export { monospaceFontFamily, theme } from './theme';
export { heebo } from './theme/fonts/heebo';
diff --git a/packages/shared/lib/getDerivation.ts b/packages/shared/lib/getDerivation.ts
new file mode 100644
index 00000000..19d7a321
--- /dev/null
+++ b/packages/shared/lib/getDerivation.ts
@@ -0,0 +1,3 @@
+export function getDerivationMapKey(assetId: string, fromAdd: string): string {
+ return `${assetId}-${fromAdd}`;
+}
diff --git a/packages/shared/lib/validateAddress.ts b/packages/shared/lib/validateAddress.ts
index 71a68e4a..7b48d5ca 100644
--- a/packages/shared/lib/validateAddress.ts
+++ b/packages/shared/lib/validateAddress.ts
@@ -104,9 +104,7 @@ export class AddressValidator {
return this.validateTERRA(address);
case 'TON':
case 'TON_TEST':
- case 'USDT_TON':
- case 'DOGS_TON':
- case 'NOTCOIN_TON':
+ case 'Jetton':
return this.validateTON(address);
default:
logger.error(`Unsupported networkProtocol for address validation ${validatorReference}`);
diff --git a/packages/wallet-derivation/wallets/chains/index.ts b/packages/wallet-derivation/wallets/chains/index.ts
index 5976cf32..e679be79 100644
--- a/packages/wallet-derivation/wallets/chains/index.ts
+++ b/packages/wallet-derivation/wallets/chains/index.ts
@@ -43,6 +43,10 @@ export const getWallet = (assetId: string) => {
return EVMWallet;
}
+ if (asset.protocol === 'TON' && asset.address) {
+ return Jetton;
+ }
+
switch (assetId) {
// EdDSA
case 'ADA':
@@ -76,10 +80,6 @@ export const getWallet = (assetId: string) => {
case 'TON':
case 'TON_TEST':
return Ton;
- case 'USDT_TON':
- case 'NOTCOIN_TON':
- case 'DOGS_TON':
- return Jetton;
// ECDSA
case 'ATOM_COS':
From 1d34bf92244e2be8f2dcec8dc502d6e3add7034c Mon Sep 17 00:00:00 2001
From: TomerHFB <158162596+TomerHFB@users.noreply.github.com>
Date: Mon, 16 Dec 2024 16:10:34 +0200
Subject: [PATCH 3/5] feat(@fireblocks/recovery-utility): :sparkles: add jetton
support
---
.../components/WithdrawModal/CreateTransaction/index.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index bc33fb95..08ab9852 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -15,6 +15,7 @@ import {
getLogger,
sanatize,
useOfflineQuery,
+ getDerivationMapKey,
} from '@fireblocks/recovery-shared';
import { AssetConfig, getAssetConfig, isNativeAssetId } from '@fireblocks/asset-config';
import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants';
@@ -150,7 +151,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
const fromAddress = values.fromAddress ?? defaultValues.fromAddress;
- const derivation = wallet?.derivations?.get(`${asset?.id}-${fromAddress}`);
+ const derivation = wallet?.derivations?.get(getDerivationMapKey(asset?.id, fromAddress));
// TODO: Show both original balance and adjusted balance in create tx UI
From 995d2026a85443f69af39cae9470ff583b7d8b53 Mon Sep 17 00:00:00 2001
From: TomerHFB <158162596+TomerHFB@users.noreply.github.com>
Date: Tue, 17 Dec 2024 16:08:31 +0200
Subject: [PATCH 4/5] feat(@fireblocks/recovery-utility): :sparkles: cr fixes
---
.../WithdrawModal/CreateTransaction/index.tsx | 6 +++---
.../recovery-relay/lib/wallets/Jetton/index.ts | 6 +++---
apps/recovery-relay/lib/wallets/index.ts | 18 +++++++++++++++---
apps/recovery-relay/lib/wallets/types.ts | 2 +-
.../WithdrawModal/SignTransaction/index.tsx | 3 ++-
.../renderer/context/Workspace.tsx | 3 ++-
.../renderer/lib/wallets/Jetton/index.ts | 4 ++--
.../renderer/lib/wallets/index.ts | 18 ++++++------------
packages/asset-config/README.md | 5 +++++
packages/asset-config/assets.ts | 10 ++++++++++
packages/asset-config/index.ts | 4 ++--
11 files changed, 51 insertions(+), 28 deletions(-)
diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index 08ab9852..ccf7990b 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -428,7 +428,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
theme.palette.error.main}>
Insufficient balance for transaction
- ) : prepareQuery.data?.insufficientFeeBalance === true && prepareQuery.data?.insufficientBalance !== true ? (
+ ) : prepareQuery.data?.insufficientBalanceForTokenTransfer === true && prepareQuery.data?.insufficientBalance !== true ? (
theme.palette.error.main}>
Insufficient fee asset balance for token transaction
@@ -445,8 +445,8 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
prepareQuery.data?.insufficientBalance !== undefined &&
prepareQuery.data.insufficientBalance) ||
(prepareQuery.data &&
- prepareQuery.data?.insufficientFeeBalance !== undefined &&
- prepareQuery.data.insufficientFeeBalance)
+ prepareQuery.data?.insufficientBalanceForTokenTransfer !== undefined &&
+ prepareQuery.data.insufficientBalanceForTokenTransfer)
}
>
Prepare Transaction
diff --git a/apps/recovery-relay/lib/wallets/Jetton/index.ts b/apps/recovery-relay/lib/wallets/Jetton/index.ts
index 24895532..63773500 100644
--- a/apps/recovery-relay/lib/wallets/Jetton/index.ts
+++ b/apps/recovery-relay/lib/wallets/Jetton/index.ts
@@ -1,6 +1,6 @@
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
import { JettonMaster, TonClient, WalletContractV4 } from '@ton/ton';
-import { Address, beginCell, Cell, fromNano, toNano } from '@ton/core';
+import { Address, beginCell, Cell, toNano } from '@ton/core';
import { AccountData } from '../types';
import { defaultTonWalletV4R2code } from '../TON/tonParams';
import axios from 'axios';
@@ -28,7 +28,7 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
}
public getLateInitLabel(): string {
- return 'Ton client wallet';
+ return 'Ton wallet client';
}
public setRPCUrl(url: string): void {
@@ -136,7 +136,7 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
feeRate,
extraParams,
insufficientBalance: jettonBalance <= 0,
- insufficientFeeBalance: tonBalance < feeRate,
+ insufficientBalanceForTokenTransfer: tonBalance < feeRate,
} as AccountData;
return preperedData;
diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts
index e29ad57e..4d13eedf 100644
--- a/apps/recovery-relay/lib/wallets/index.ts
+++ b/apps/recovery-relay/lib/wallets/index.ts
@@ -1,3 +1,4 @@
+import { getAllJettons } from '@fireblocks/asset-config';
import { Cardano } from './ADA';
import { Cosmos } from './ATOM';
import { Bitcoin, BitcoinCash, BitcoinSV, DASH, DogeCoin, LiteCoin, ZCash } from './BTCBased';
@@ -40,6 +41,19 @@ import { Ton } from './TON';
import { Jetton } from './Jetton';
export { ConnectedWallet } from './ConnectedWallet';
+const fillJettons = () => {
+ const jettonsList = getAllJettons();
+ const jettons = jettonsList.reduce(
+ (prev, curr) => ({
+ ...prev,
+ [curr]: Jetton,
+ }),
+ {},
+ ) as any;
+ Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
+ return jettons;
+};
+
export const WalletClasses = {
ALGO: Algorand,
ALGO_TEST: Algorand,
@@ -122,9 +136,7 @@ export const WalletClasses = {
CELESTIA_TEST: Celestia,
TON: Ton,
TON_TEST: Ton,
- USDT_TON: Jetton,
- NOTCOIN_TON: Jetton,
- DOGS_TON: Jetton,
+ ...fillJettons(),
} as const;
type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];
diff --git a/apps/recovery-relay/lib/wallets/types.ts b/apps/recovery-relay/lib/wallets/types.ts
index 2afd0cef..9ada6b11 100644
--- a/apps/recovery-relay/lib/wallets/types.ts
+++ b/apps/recovery-relay/lib/wallets/types.ts
@@ -28,7 +28,7 @@ export type AccountData = {
extraParams?: Map;
endpoint?: string;
insufficientBalance?: boolean;
- insufficientFeeBalance?: boolean;
+ insufficientBalanceForTokenTransfer?: boolean;
};
export type TxPayload = {
diff --git a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
index cd2e1077..0c70b7a0 100644
--- a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
+++ b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
@@ -12,6 +12,7 @@ import {
getLogger,
sanatize,
useWrappedState,
+ getDerivationMapKey,
} from '@fireblocks/recovery-shared';
import { AssetConfig } from '@fireblocks/asset-config';
import { CallMade, CallReceived, LeakAdd, Toll } from '@mui/icons-material';
@@ -75,7 +76,7 @@ export const SignTransaction = ({ txId, account, asset, inboundRelayParams }: Pr
const derivation = account.wallets
.get(asset.id)
- ?.derivations.get(`${inboundRelayParams?.unsignedTx.assetId}-${inboundRelayParams?.unsignedTx.from}`);
+ ?.derivations.get(getDerivationMapKey(inboundRelayParams?.unsignedTx.assetId, inboundRelayParams?.unsignedTx.from));
if (!derivation) {
throw new Error('Derivation not found');
diff --git a/apps/recovery-utility/renderer/context/Workspace.tsx b/apps/recovery-utility/renderer/context/Workspace.tsx
index f901a940..b33dbae8 100644
--- a/apps/recovery-utility/renderer/context/Workspace.tsx
+++ b/apps/recovery-utility/renderer/context/Workspace.tsx
@@ -68,8 +68,9 @@ export const WorkspaceProvider = ({ children }: Props) => {
deriveWallet: (input) => {
let transferableToken = false;
const config = getAssetConfig(input.assetId);
- if (config?.address && isTransferableToken(input.assetId)) {
+ if (isTransferableToken(input.assetId)) {
transferableToken = true;
+ logger.info('Found a transferable token:', input.assetId);
}
const nativeAssetId = (
transferableToken ? input.assetId : config?.nativeAsset ?? input.assetId
diff --git a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
index 4328439d..71b22c9e 100644
--- a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
@@ -14,7 +14,7 @@ export class Jetton extends BaseTon implements SigningWallet {
throw new Error('Jetton: Missing jetton parameters');
}
- const JettonTransferOpcode = 0x0f8a7ea5;
+ const jettonTransferOpcode = 0x0f8a7ea5;
const decimals = extraParams?.get('decimals');
const normalizingFactor = 10 ** decimals;
const amountToWithdraw = amount * normalizingFactor; // amount is the wallet balance
@@ -22,7 +22,7 @@ export class Jetton extends BaseTon implements SigningWallet {
let internalMessageMemo = undefined;
// create the tx payload
const internalMessageBody = beginCell()
- .storeUint(JettonTransferOpcode, 32) // opcode for jetton transfer
+ .storeUint(jettonTransferOpcode, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(amountToWithdraw) // jetton balance
.storeAddress(Address.parse(to)) // tx destination
diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts
index 9ee11d4f..c33e0125 100644
--- a/apps/recovery-utility/renderer/lib/wallets/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/index.ts
@@ -1,5 +1,5 @@
// import { Bitcoin } from './BTC';
-import { assets } from '@fireblocks/asset-config';
+import { assets, getAllJettons } from '@fireblocks/asset-config';
import { ERC20, ETC } from '@fireblocks/wallet-derivation';
import { Ripple } from './XRP';
import { Cosmos } from './ATOM';
@@ -41,14 +41,11 @@ const fillEVMs = () => {
};
const fillJettons = () => {
- const jettons = Object.keys(assets).reduce(
- (o, assetId) => ({
- ...o,
- [assets[assetId].id]: assets[assetId].protocol === 'TON' && assets[assetId].address ? Jetton : undefined,
- }),
- {},
- ) as any;
- Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
+ const jettonsList = getAllJettons();
+ const jettons: { [key: string]: any } = {};
+ for (const jetton of jettonsList) {
+ jettons[jetton] = Jetton;
+ }
return jettons;
};
@@ -111,9 +108,6 @@ export const WalletClasses = {
HBAR_TEST: Hedera,
TON: Ton,
TON_TEST: Ton,
- // USDT_TON: Jetton,
- // NOTCOIN_TON: Jetton,
- // DOGS_TON: Jetton,
...fillJettons(),
...fillEVMs(),
diff --git a/packages/asset-config/README.md b/packages/asset-config/README.md
index af6ddfbe..883aee2e 100644
--- a/packages/asset-config/README.md
+++ b/packages/asset-config/README.md
@@ -50,6 +50,11 @@ For your convinience we have provided base methods for common types of chains:
- `evm(baseExplorerUrl: string, rpcUrl?: string)` to create a basic EVM chain, simply provide the `baseExplorerUrl` (the URL of an explorer) and optionally `rpcUrl` as the URL of the RPC to communicate with
- `btc(baseExplorerUrl: string, segwit: boolean)` to create a basic BTC chain (ZCash, LTC, etc are all considered such) simply provide the `baseExplorerUrl` (the URL of an explorer) and optionally `segwit` should be false, as only BTC is relevant for this field
+### Add a new Jetton token
+
+To add support for withdrawals of a listed Jetton, make sure the token is listed in [globalAssets](/Users/tomerhorviz/Documents/recovery/packages/asset-config/data/globalAssets.ts) and in [patches](packages/asset-config/config/patches.ts).
+The Jetton master contract address must be present in the 'globalAssets' list as the 'address' parameter.
+
### Token or new Base Asset Support
In case a token has bad data, alternatively a token is missing or you want to add a new base asset, it can be added by performing the following steps:
diff --git a/packages/asset-config/assets.ts b/packages/asset-config/assets.ts
index 584c44b1..9f377548 100644
--- a/packages/asset-config/assets.ts
+++ b/packages/asset-config/assets.ts
@@ -12,3 +12,13 @@ export const assets = globalAssets.reduce(
}),
{},
);
+
+export function getAllJettons(): string[] {
+ const jettons = [];
+ for (const asset of globalAssets) {
+ if (asset.protocol === 'TON' && asset.address) {
+ jettons.push(asset.id);
+ }
+ }
+ return jettons;
+}
diff --git a/packages/asset-config/index.ts b/packages/asset-config/index.ts
index 7c0e0a5a..06c48055 100644
--- a/packages/asset-config/index.ts
+++ b/packages/asset-config/index.ts
@@ -1,10 +1,10 @@
import { orderId, orderAssetById } from './config/sort';
import { isNativeAssetId, isDerivableAssetId, isTestnetAsset } from './util';
-import { assets } from './assets';
+import { assets, getAllJettons } from './assets';
export { getAssetConfig, getNativeAssetConfig, getDerivableAssetConfig, isExplorerUrl } from './util';
-export { assets };
+export { assets, getAllJettons };
export type * from './types';
From 74a7956fa3fda468b58c0e8ea56f29503d2a0930 Mon Sep 17 00:00:00 2001
From: T0m3r <158162596+TomerHFB@users.noreply.github.com>
Date: Tue, 17 Dec 2024 16:10:49 +0200
Subject: [PATCH 5/5] Update README.md
---
packages/asset-config/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/asset-config/README.md b/packages/asset-config/README.md
index 883aee2e..f3f3bea6 100644
--- a/packages/asset-config/README.md
+++ b/packages/asset-config/README.md
@@ -52,7 +52,7 @@ For your convinience we have provided base methods for common types of chains:
### Add a new Jetton token
-To add support for withdrawals of a listed Jetton, make sure the token is listed in [globalAssets](/Users/tomerhorviz/Documents/recovery/packages/asset-config/data/globalAssets.ts) and in [patches](packages/asset-config/config/patches.ts).
+To add support for withdrawals of a listed Jetton, make sure the token is listed in `globalAssets.ts` and in `patches.ts`.
The Jetton master contract address must be present in the 'globalAssets' list as the 'address' parameter.
### Token or new Base Asset Support
|