From 0640ccbb9b1b061ef201ae5e759c3126bca560ef Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:06:35 +0200 Subject: [PATCH] feat: :sparkles: add jetton withdrawal support --- .../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==