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