From 54102cf192a3ab59987c63ab6c955f91285e7d5f Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:43:14 +0200 Subject: [PATCH 1/6] feat(@fireblocks/recovery-utility): :sparkles: add getAllERC20s functionality to assets --- apps/recovery-relay/lib/wallets/index.ts | 2 +- apps/recovery-utility/renderer/lib/wallets/index.ts | 2 +- packages/asset-config/assets.ts | 10 ++++++++++ packages/asset-config/index.ts | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts index 4d13eed..85c1cc4 100644 --- a/apps/recovery-relay/lib/wallets/index.ts +++ b/apps/recovery-relay/lib/wallets/index.ts @@ -1,4 +1,4 @@ -import { getAllJettons } from '@fireblocks/asset-config'; +import { getAllJettons, getAllERC20s } from '@fireblocks/asset-config'; import { Cardano } from './ADA'; import { Cosmos } from './ATOM'; import { Bitcoin, BitcoinCash, BitcoinSV, DASH, DogeCoin, LiteCoin, ZCash } from './BTCBased'; diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts index c33e012..89d90f7 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, getAllJettons } from '@fireblocks/asset-config'; +import { assets, getAllJettons, getAllERC20s } from '@fireblocks/asset-config'; import { ERC20, ETC } from '@fireblocks/wallet-derivation'; import { Ripple } from './XRP'; import { Cosmos } from './ATOM'; diff --git a/packages/asset-config/assets.ts b/packages/asset-config/assets.ts index 9f37754..21bd7b8 100644 --- a/packages/asset-config/assets.ts +++ b/packages/asset-config/assets.ts @@ -22,3 +22,13 @@ export function getAllJettons(): string[] { } return jettons; } + +export function getAllERC20s(): string[] { + const erc20Tokens = []; + for (const asset of globalAssets) { + if (asset.protocol === 'ETH' && asset.address) { + erc20Tokens.push(asset.id); + } + } + return erc20Tokens; +} diff --git a/packages/asset-config/index.ts b/packages/asset-config/index.ts index 06c4805..d2ff856 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, getAllJettons } from './assets'; +import { assets, getAllJettons, getAllERC20s } from './assets'; export { getAssetConfig, getNativeAssetConfig, getDerivableAssetConfig, isExplorerUrl } from './util'; -export { assets, getAllJettons }; +export { assets, getAllJettons, getAllERC20s }; export type * from './types'; From 2000191b7d58287be3cd8d17e3073b2990c53b42 Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:46:12 +0200 Subject: [PATCH 2/6] refactor: update withdraw modal --- .../components/WithdrawModal/CreateTransaction/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx index ccf7990..26e4f6e 100644 --- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx +++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx @@ -444,9 +444,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse (prepareQuery.data && prepareQuery.data?.insufficientBalance !== undefined && prepareQuery.data.insufficientBalance) || - (prepareQuery.data && - prepareQuery.data?.insufficientBalanceForTokenTransfer !== undefined && - prepareQuery.data.insufficientBalanceForTokenTransfer) + (prepareQuery.data && prepareQuery.data.insufficientBalanceForTokenTransfer) } > Prepare Transaction From 5031cc4858ed9a8e1a51e15bef0fa953dd167e55 Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:10:22 +0200 Subject: [PATCH 3/6] feat(@fireblocks/recovery-utility): :sparkles: add erc20 support -> fix patches and readme --- apps/recovery-relay/lib/wallets/index.ts | 15 +++++++++++ .../renderer/lib/wallets/index.ts | 2 +- packages/asset-config/README.md | 4 +-- packages/asset-config/config/patches.ts | 27 ------------------- 4 files changed, 18 insertions(+), 30 deletions(-) diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts index 85c1cc4..209d1df 100644 --- a/apps/recovery-relay/lib/wallets/index.ts +++ b/apps/recovery-relay/lib/wallets/index.ts @@ -39,6 +39,7 @@ import { Celestia } from './CELESTIA'; import { CoreDAO } from './EVM/CORE_COREDAO'; import { Ton } from './TON'; import { Jetton } from './Jetton'; +import { ERC20 } from './ERC20'; export { ConnectedWallet } from './ConnectedWallet'; const fillJettons = () => { @@ -54,6 +55,19 @@ const fillJettons = () => { return jettons; }; +const fillERC20s = () => { + const jerc20List = getAllERC20s(); + const erc20Tokens = jerc20List.reduce( + (prev, curr) => ({ + ...prev, + [curr]: ERC20, + }), + {}, + ) as any; + Object.keys(erc20Tokens).forEach((key) => (erc20Tokens[key] === undefined ? delete erc20Tokens[key] : {})); + return erc20Tokens; +}; + export const WalletClasses = { ALGO: Algorand, ALGO_TEST: Algorand, @@ -137,6 +151,7 @@ export const WalletClasses = { TON: Ton, TON_TEST: Ton, ...fillJettons(), + ...fillERC20s(), } as const; type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses]; diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts index 89d90f7..c33e012 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, getAllJettons, getAllERC20s } 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'; diff --git a/packages/asset-config/README.md b/packages/asset-config/README.md index f3f3bea..e175093 100644 --- a/packages/asset-config/README.md +++ b/packages/asset-config/README.md @@ -52,8 +52,8 @@ 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.ts` and in `patches.ts`. -The Jetton master contract address must be present in the 'globalAssets' list as the 'address' parameter. +To add support for withdrawals of a listed Jetton, make sure the token is listed in `globalAssets.ts`. +The Jetton master contract address must be present in the `globalAssets` list as the `address` parameter. ### Token or new Base Asset Support diff --git a/packages/asset-config/config/patches.ts b/packages/asset-config/config/patches.ts index e70c234..934e58b 100644 --- a/packages/asset-config/config/patches.ts +++ b/packages/asset-config/config/patches.ts @@ -362,31 +362,4 @@ export const nativeAssetPatches: NativeAssetPatches = { memo: true, 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'), - }, }; From cf658f5bb2c11851cb5662aa2135ec129e45d77a Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:47:21 +0200 Subject: [PATCH 4/6] feat(@fireblocks/recovery-relay): :sparkles: add erc20 withdraw support --- .../WithdrawModal/CreateTransaction/index.tsx | 6 + .../recovery-relay/lib/wallets/ERC20/index.ts | 139 ++++++++++++------ 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx index 26e4f6e..48f5973 100644 --- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx +++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx @@ -24,6 +24,7 @@ import { Derivation, AccountData } from '../../../lib/wallets'; import { LateInitConnectedWallet } from '../../../lib/wallets/LateInitConnectedWallet'; import { useSettings } from '../../../context/Settings'; import { Jetton } from '../../../lib/wallets/Jetton'; +import { ERC20 } from '../../../lib/wallets/ERC20'; const logger = getLogger(LOGGER_NAME_RELAY); @@ -172,6 +173,11 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse (derivation as Jetton).setTokenAddress(asset.address); (derivation as Jetton).setDecimals(asset.decimals); } + if (asset.address && asset.protocol === 'ETH') { + (derivation as ERC20).setTokenAddress(asset.address); + (derivation as ERC20).setDecimals(asset.decimals); + (derivation as ERC20).setToAddress(toAddress); + } return await derivation!.prepare?.(toAddress, values.memo); }, diff --git a/apps/recovery-relay/lib/wallets/ERC20/index.ts b/apps/recovery-relay/lib/wallets/ERC20/index.ts index a5d6405..cdbbd10 100644 --- a/apps/recovery-relay/lib/wallets/ERC20/index.ts +++ b/apps/recovery-relay/lib/wallets/ERC20/index.ts @@ -1,73 +1,130 @@ /* eslint-disable prefer-destructuring */ -import { Input } from '@fireblocks/wallet-derivation'; -import { Contract, Interface, Transaction, ethers } from 'ethers'; -import { AccountData, TxPayload, RawSignature } from '../types'; +import { Contract, ethers, formatEther, JsonRpcProvider } from 'ethers'; +import { AccountData } from '../types'; import { ConnectedWallet } from '../ConnectedWallet'; -import { Ethereum } from '../EVM/ETH'; +import { EVM } from '../EVM'; import { erc20Abi } from './erc20.abi'; -import { transferAbi } from './transfer.abi'; -import BigNumber from 'bignumber.js'; -export class ERC20 extends Ethereum implements ConnectedWallet { - private contract: Contract; +export class ERC20 extends EVM implements ConnectedWallet { + protected provider: JsonRpcProvider | undefined; + public rpcURL: string | undefined; + public contract!: Contract; + public tokenAddress: string | undefined; + public decimals: number | undefined; + public toAddress: string | undefined; - constructor(input: Input, tokenAddress: string) { - super(input); + public setRPCUrl(url: string): void { + this.rpcURL = url; + this.provider = new JsonRpcProvider(this.rpcURL); + } + + public setTokenAddress(address: string) { + this.tokenAddress = address; + } + + public init() { + if (!this.tokenAddress) { + this.relayLogger.error(`ERC20 Token address unavailable: ${this.assetId}`); + throw new Error(`ERC20 Token address unavailable: ${this.assetId}`); + } + this.contract = new ethers.Contract(this.tokenAddress, erc20Abi, this.provider); + } + + public setDecimals(decimals: number) { + this.decimals = decimals; + } - this.contract = new ethers.Contract(tokenAddress, erc20Abi); + public setToAddress(toAddress: string) { + this.toAddress = toAddress; } public async getBalance(): Promise { - this.weiBalance = await this.contract.balanceOf(this.address); - return parseFloat(parseFloat(ethers.formatEther(this.weiBalance)).toFixed(2)); + const weiBalance = await this.contract.balanceOf(this.address); + return parseFloat(parseFloat(ethers.formatEther(weiBalance)).toFixed(2)); } public async prepare(): Promise { + this.init(); + const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); + const chainId = (await this.provider!.getNetwork()).chainId; + const displayBalance = await this.getBalance(); + const ethBalance = await this.getEthBalance(); + + let { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await this.provider!.getFeeData(); + + const iface = new ethers.Interface(erc20Abi); + const data = iface.encodeFunctionData('transfer', [this.toAddress, ethers.parseUnits(displayBalance.toFixed(2), 'ether')]); + + const tx = { + to: this.tokenAddress, + from: this.address, + data: data, + }; + const gasLimit = await this.provider?.estimateGas(tx); + const extraParams = new Map(); - extraParams.set(this.KEY_EVM_WEI_BALANCE, new BigNumber(this.weiBalance.toString()).toString(16)); - const preparedData = { + extraParams.set('gasLimit', gasLimit); + extraParams.set('maxFee', maxFeePerGas); + extraParams.set('priorityFee', maxPriorityFeePerGas); + + const preparedData: AccountData = { balance: displayBalance, extraParams, + gasPrice, + nonce, + chainId: Number(chainId), + insufficientBalance: displayBalance <= 0, + insufficientBalanceForTokenTransfer: ethBalance <= gasPrice! * gasLimit!, }; this.relayLogger.logPreparedData('ERC20', preparedData); return preparedData; } - public async generateTx(to: string, amount: number): Promise { - const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); + // public async generateTx(to: string, amount: number): Promise { + // const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); - // Should we use maxGasPrice? i.e. EIP1559. - const { gasPrice } = await this.provider!.getFeeData(); + // // Should we use maxGasPrice? i.e. EIP1559. + // const { gasPrice } = await this.provider!.getFeeData(); - const tx = { - from: this.address, - to, - nonce, - gasLimit: 21000, - gasPrice, - value: 0, - chainId: this.path.coinType === 1 ? 5 : 1, - data: new Interface(transferAbi).encodeFunctionData('transfer', [ - to, - BigInt(amount) * BigInt(await this.contract.decimals()), - ]), - }; + // const tx = { + // from: this.address, + // to, + // nonce, + // gasLimit: 21000, + // gasPrice, + // value: 0, + // chainId: this.path.coinType === 1 ? 5 : 1, + // data: new Interface(transferAbi).encodeFunctionData('transfer', [ + // to, + // BigInt(amount) * BigInt(await this.contract.decimals()), + // ]), + // }; - this.relayLogger.debug(`ERC20: Generated tx: ${JSON.stringify(tx, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2)}`); + // this.relayLogger.debug(`ERC20: Generated tx: ${JSON.stringify(tx, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2)}`); - const unsignedTx = Transaction.from(tx).serialized; + // const unsignedTx = Transaction.from(tx).serialized; - const preparedData = { - derivationPath: this.pathParts, - tx: unsignedTx, - }; + // const preparedData = { + // derivationPath: this.pathParts, + // tx: unsignedTx, + // }; - this.relayLogger.debug(`ERC20: Prepared data: ${JSON.stringify(preparedData, null, 2)}`); - return preparedData; - } + // this.relayLogger.debug(`ERC20: Prepared data: ${JSON.stringify(preparedData, null, 2)}`); + // return preparedData; + // } public async broadcastTx(txHex: string): Promise { return super.broadcastTx(txHex); } + + private async getEthBalance() { + const weiBalance = await this.provider?.getBalance(this.address); + const balance = formatEther(weiBalance!); + const ethBalance = Number(balance); + + console.info('Eth balance info', { ethBalance }); + + return ethBalance; + } } From 17d46e877767ab2a49caf480966917261f50da6a Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:41:55 +0200 Subject: [PATCH 5/6] feat(@fireblocks/recovery-utility): :sparkles: add erc20 withdrawal support --- .../recovery-relay/lib/wallets/ERC20/index.ts | 1 + .../renderer/lib/wallets/ERC20/erc20.abi.ts | 224 ++++++++++++++++++ .../renderer/lib/wallets/ERC20/index.ts | 43 ++++ 3 files changed, 268 insertions(+) create mode 100644 apps/recovery-utility/renderer/lib/wallets/ERC20/erc20.abi.ts create mode 100644 apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts diff --git a/apps/recovery-relay/lib/wallets/ERC20/index.ts b/apps/recovery-relay/lib/wallets/ERC20/index.ts index cdbbd10..d6a8e41 100644 --- a/apps/recovery-relay/lib/wallets/ERC20/index.ts +++ b/apps/recovery-relay/lib/wallets/ERC20/index.ts @@ -64,6 +64,7 @@ export class ERC20 extends EVM implements ConnectedWallet { const gasLimit = await this.provider?.estimateGas(tx); const extraParams = new Map(); + extraParams.set('tokenAddress', this.tokenAddress); extraParams.set('gasLimit', gasLimit); extraParams.set('maxFee', maxFeePerGas); extraParams.set('priorityFee', maxPriorityFeePerGas); diff --git a/apps/recovery-utility/renderer/lib/wallets/ERC20/erc20.abi.ts b/apps/recovery-utility/renderer/lib/wallets/ERC20/erc20.abi.ts new file mode 100644 index 0000000..607c358 --- /dev/null +++ b/apps/recovery-utility/renderer/lib/wallets/ERC20/erc20.abi.ts @@ -0,0 +1,224 @@ +import { InterfaceAbi } from 'ethers'; + +export const erc20Abi: InterfaceAbi = [ + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_from', + type: 'address', + }, + { + name: '_to', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'transferFrom', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_to', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + { + name: '_spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + payable: true, + stateMutability: 'payable', + type: 'fallback', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address', + }, + { + indexed: true, + name: 'spender', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, +]; diff --git a/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts new file mode 100644 index 0000000..c0a1c5f --- /dev/null +++ b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts @@ -0,0 +1,43 @@ +import { ethers, Wallet } from 'ethers'; +import { EVMWallet as EVMBase, Input } from '@fireblocks/wallet-derivation'; +import { TxPayload, GenerateTxInput } from '../types'; +import { SigningWallet } from '../SigningWallet'; +import { erc20Abi } from './erc20.abi'; + +export class ERC20 extends EVMBase implements SigningWallet { + constructor(input: Input, chainId?: number) { + super(input); + } + + public async generateTx({ to, amount, extraParams, gasPrice, nonce, chainId }: GenerateTxInput): Promise { + if (!this.privateKey) { + throw new Error('No private key found'); + } + + const balanceWei = ethers.parseUnits(amount.toFixed(2), 'ether'); + const tokenAddress = extraParams?.get('tokenAddress'); + const maxPriorityFeePerGas = extraParams?.get('priorityFee'); + const maxFeePerGas = extraParams?.get('maxFee'); + const gasLimit = extraParams?.get('gasLimit'); + + const iface = new ethers.Interface(erc20Abi); + const data = iface.encodeFunctionData('transfer', [to, balanceWei]); + + const txObject = { + to: tokenAddress, + data, + nonce, + gasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + }; + + this.utilityLogger.logSigningTx('EVM', txObject); + + const serialized = await new Wallet(this.privateKey).signTransaction(txObject); + + return { + tx: serialized, + }; + } +} From 1cfce325600f1cc4efd03cbce432a5176e60a0ab Mon Sep 17 00:00:00 2001 From: TomerHFB <158162596+TomerHFB@users.noreply.github.com> Date: Sun, 22 Dec 2024 21:45:19 +0200 Subject: [PATCH 6/6] feat(@fireblocks/recovery-utility): :sparkles: add erc20 withdrawals supoprt for evms --- .../WithdrawModal/CreateTransaction/index.tsx | 25 ++--- .../components/WithdrawModal/index.tsx | 45 +++++---- .../lib/wallets/ERC20/chains.ts | 34 +++++++ .../recovery-relay/lib/wallets/ERC20/index.ts | 99 ++++++++----------- .../renderer/lib/wallets/ERC20/index.ts | 36 ++++--- .../renderer/lib/wallets/index.ts | 5 +- packages/asset-config/config/patches.ts | 2 +- yarn.lock | 13 ++- 8 files changed, 144 insertions(+), 115 deletions(-) create mode 100644 apps/recovery-relay/lib/wallets/ERC20/chains.ts diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx index 48f5973..c1f7335 100644 --- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx +++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx @@ -177,6 +177,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse (derivation as ERC20).setTokenAddress(asset.address); (derivation as ERC20).setDecimals(asset.decimals); (derivation as ERC20).setToAddress(toAddress); + (derivation as ERC20).getNativeAsset(asset.nativeAsset); } return await derivation!.prepare?.(toAddress, values.memo); @@ -243,18 +244,18 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse const balanceId = useId(); const addressExplorerId = useId(); - logger.info('Parameters for CreateTransaction ', { - txId, - accountId, - values, - asset, - derivation: sanatize(derivation), - prepare: JSON.stringify( - prepareQuery.data, - (_, v) => (typeof v === 'bigint' ? v.toString() : typeof v === 'function' ? 'function' : v), - 2, - ), - }); + // logger.info('Parameters for CreateTransaction ', { + // txId, + // accountId, + // values, + // asset, + // derivation: sanatize(derivation), + // prepare: JSON.stringify( + // prepareQuery.data, + // (_, v) => (typeof v === 'bigint' ? v.toString() : typeof v === 'function' ? 'function' : v), + // 2, + // ), + // }); return ( { )} {!!txHash && ( - *': { - marginRight: '0.5rem', - }, - }} - > - Transaction hash: - {asset.getExplorerUrl ? ( - - {txHash} - - ) : ( - txHash - )} - + + *': { + marginRight: '0.5rem', + }, + }} + > + Transaction Hash: + {asset.getExplorerUrl ? ( + + {txHash} + + ) : ( + txHash + )} + + + The transaction might take a few seconds to appear on the block explorer + + )} {!!txBroadcastError && ( theme.palette.error.main}> diff --git a/apps/recovery-relay/lib/wallets/ERC20/chains.ts b/apps/recovery-relay/lib/wallets/ERC20/chains.ts new file mode 100644 index 0000000..0508b59 --- /dev/null +++ b/apps/recovery-relay/lib/wallets/ERC20/chains.ts @@ -0,0 +1,34 @@ +export function getChainId(nativeAsset: string): number | undefined { + switch (nativeAsset) { + case 'ETH': + return 1; + case 'BNB_BSC': + return 56; + case 'CHZ_$CHZ': + return 88888; + case 'CELO': + return 42220; + case 'RBTC': + return 30; + case 'AVAX': + return 43114; + case 'MATIC_POLYGON': + return 137; + case 'RON': + return 2020; + case 'ETH_TEST5': + return 11155111; + case 'ETH_TEST6': + return 17000; + case 'SMARTBCH': + return 10000; + case 'ETH-AETH': + return 42161; + case 'BNB_TEST': + return 97; + case 'FTM_FANTOM': + return 250; + default: + return undefined; + } +} diff --git a/apps/recovery-relay/lib/wallets/ERC20/index.ts b/apps/recovery-relay/lib/wallets/ERC20/index.ts index d6a8e41..11b33d6 100644 --- a/apps/recovery-relay/lib/wallets/ERC20/index.ts +++ b/apps/recovery-relay/lib/wallets/ERC20/index.ts @@ -1,21 +1,31 @@ /* eslint-disable prefer-destructuring */ -import { Contract, ethers, formatEther, JsonRpcProvider } from 'ethers'; +import { Contract, ethers, JsonRpcProvider } from 'ethers'; import { AccountData } from '../types'; import { ConnectedWallet } from '../ConnectedWallet'; -import { EVM } from '../EVM'; +import { EVMWallet as EVMBase } from '@fireblocks/wallet-derivation'; import { erc20Abi } from './erc20.abi'; +import { getChainId } from './chains'; -export class ERC20 extends EVM implements ConnectedWallet { +export class ERC20 extends EVMBase implements ConnectedWallet { protected provider: JsonRpcProvider | undefined; public rpcURL: string | undefined; public contract!: Contract; public tokenAddress: string | undefined; public decimals: number | undefined; public toAddress: string | undefined; + private normalizingFactor: bigint | undefined; + private chainId: number | undefined; + + public getNativeAsset(nativeAsset: string) { + this.chainId = getChainId(nativeAsset); + if (!this.chainId) { + throw new Error('Unrecognaized native asset for ERC20 token withdrawal'); + } + } public setRPCUrl(url: string): void { this.rpcURL = url; - this.provider = new JsonRpcProvider(this.rpcURL); + this.provider = new JsonRpcProvider(this.rpcURL, this.chainId, { cacheTimeout: -1 }); } public setTokenAddress(address: string) { @@ -32,6 +42,7 @@ export class ERC20 extends EVM implements ConnectedWallet { public setDecimals(decimals: number) { this.decimals = decimals; + this.normalizingFactor = BigInt(10 ** decimals); } public setToAddress(toAddress: string) { @@ -39,22 +50,21 @@ export class ERC20 extends EVM implements ConnectedWallet { } public async getBalance(): Promise { - const weiBalance = await this.contract.balanceOf(this.address); - return parseFloat(parseFloat(ethers.formatEther(weiBalance)).toFixed(2)); + const weiBalance: bigint = await this.contract.balanceOf(this.address); + return Number(weiBalance / this.normalizingFactor!); } public async prepare(): Promise { this.init(); const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); - const chainId = (await this.provider!.getNetwork()).chainId; const displayBalance = await this.getBalance(); const ethBalance = await this.getEthBalance(); - let { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await this.provider!.getFeeData(); + const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await this.provider!.getFeeData(); const iface = new ethers.Interface(erc20Abi); - const data = iface.encodeFunctionData('transfer', [this.toAddress, ethers.parseUnits(displayBalance.toFixed(2), 'ether')]); + const data = iface.encodeFunctionData('transfer', [this.toAddress, BigInt(displayBalance) * this.normalizingFactor!]); const tx = { to: this.tokenAddress, @@ -63,69 +73,44 @@ export class ERC20 extends EVM implements ConnectedWallet { }; const gasLimit = await this.provider?.estimateGas(tx); - const extraParams = new Map(); + const extraParams = new Map(); extraParams.set('tokenAddress', this.tokenAddress); - extraParams.set('gasLimit', gasLimit); - extraParams.set('maxFee', maxFeePerGas); - extraParams.set('priorityFee', maxPriorityFeePerGas); + extraParams.set('gasLimit', gasLimit?.toString()); + extraParams.set('maxFee', maxFeePerGas?.toString()); + extraParams.set('priorityFee', maxPriorityFeePerGas?.toString()); + extraParams.set('weiBalance', (BigInt(displayBalance) * this.normalizingFactor!).toString()); const preparedData: AccountData = { balance: displayBalance, extraParams, gasPrice, nonce, - chainId: Number(chainId), + chainId: this.chainId, insufficientBalance: displayBalance <= 0, - insufficientBalanceForTokenTransfer: ethBalance <= gasPrice! * gasLimit!, + insufficientBalanceForTokenTransfer: Number(ethBalance!) <= Number(gasPrice! * gasLimit!), }; - this.relayLogger.logPreparedData('ERC20', preparedData); return preparedData; } - // public async generateTx(to: string, amount: number): Promise { - // const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); - - // // Should we use maxGasPrice? i.e. EIP1559. - // const { gasPrice } = await this.provider!.getFeeData(); - - // const tx = { - // from: this.address, - // to, - // nonce, - // gasLimit: 21000, - // gasPrice, - // value: 0, - // chainId: this.path.coinType === 1 ? 5 : 1, - // data: new Interface(transferAbi).encodeFunctionData('transfer', [ - // to, - // BigInt(amount) * BigInt(await this.contract.decimals()), - // ]), - // }; - - // this.relayLogger.debug(`ERC20: Generated tx: ${JSON.stringify(tx, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2)}`); - - // const unsignedTx = Transaction.from(tx).serialized; - - // const preparedData = { - // derivationPath: this.pathParts, - // tx: unsignedTx, - // }; - - // this.relayLogger.debug(`ERC20: Prepared data: ${JSON.stringify(preparedData, null, 2)}`); - // return preparedData; - // } - public async broadcastTx(txHex: string): Promise { - return super.broadcastTx(txHex); + try { + const txRes = await this.provider!.broadcastTransaction(txHex); + this.relayLogger.debug(`EVM: Tx broadcasted: ${JSON.stringify(txRes, null, 2)}`); + return txRes.hash; + } catch (e) { + this.relayLogger.error('EVM: Error broadcasting tx:', e); + if ((e as Error).message.includes('insufficient funds for intrinsic transaction cost')) { + throw new Error( + 'Insufficient funds for transfer, this might be due to a spike in network fees, please wait and try again', + ); + } + throw e; + } } private async getEthBalance() { - const weiBalance = await this.provider?.getBalance(this.address); - const balance = formatEther(weiBalance!); - const ethBalance = Number(balance); - - console.info('Eth balance info', { ethBalance }); - - return ethBalance; + const weiBalanceBN = await this.provider?.getBalance(this.address); + console.info('Eth balance info', { weiBalanceBN }); + return weiBalanceBN; } } diff --git a/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts index c0a1c5f..e9b0476 100644 --- a/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts @@ -1,38 +1,36 @@ import { ethers, Wallet } from 'ethers'; -import { EVMWallet as EVMBase, Input } from '@fireblocks/wallet-derivation'; +import { EVMWallet as EVMBase } from '@fireblocks/wallet-derivation'; import { TxPayload, GenerateTxInput } from '../types'; import { SigningWallet } from '../SigningWallet'; import { erc20Abi } from './erc20.abi'; export class ERC20 extends EVMBase implements SigningWallet { - constructor(input: Input, chainId?: number) { - super(input); - } - - public async generateTx({ to, amount, extraParams, gasPrice, nonce, chainId }: GenerateTxInput): Promise { + public async generateTx({ to, extraParams, nonce, chainId, gasPrice }: GenerateTxInput): Promise { if (!this.privateKey) { throw new Error('No private key found'); } - const balanceWei = ethers.parseUnits(amount.toFixed(2), 'ether'); + const balanceWei = BigInt(extraParams?.get('weiBalance')); + const tokenAddress = extraParams?.get('tokenAddress'); - const maxPriorityFeePerGas = extraParams?.get('priorityFee'); - const maxFeePerGas = extraParams?.get('maxFee'); - const gasLimit = extraParams?.get('gasLimit'); + + const maxPriorityFeePerGas = (BigInt(extraParams?.get('priorityFee')) * 115n) / 100n; //increase priority fee by 15% to increase chance of tx to be included in next block + const maxFeePerGas = BigInt(extraParams?.get('maxFee')); + const gasLimit = BigInt(extraParams?.get('gasLimit')); const iface = new ethers.Interface(erc20Abi); const data = iface.encodeFunctionData('transfer', [to, balanceWei]); - const txObject = { - to: tokenAddress, - data, - nonce, - gasLimit, - maxFeePerGas, - maxPriorityFeePerGas, - }; + let txObject = {}; + // EIP-1559 chain + if (maxFeePerGas && maxPriorityFeePerGas) { + txObject = { to: tokenAddress, data, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, chainId }; + // non EIP-1559 chain + } else { + txObject = { to: tokenAddress, data, nonce, gasLimit, gasPrice, chainId }; + } - this.utilityLogger.logSigningTx('EVM', txObject); + this.utilityLogger.logSigningTx('ERC20', txObject); const serialized = await new Wallet(this.privateKey).signTransaction(txObject); diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts index c33e012..01fd0f0 100644 --- a/apps/recovery-utility/renderer/lib/wallets/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/index.ts @@ -1,6 +1,6 @@ // import { Bitcoin } from './BTC'; import { assets, getAllJettons } from '@fireblocks/asset-config'; -import { ERC20, ETC } from '@fireblocks/wallet-derivation'; +import { ETC } from '@fireblocks/wallet-derivation'; import { Ripple } from './XRP'; import { Cosmos } from './ATOM'; import { EOS } from './EOS'; @@ -22,6 +22,7 @@ import { Bitcoin, BitcoinSV, LiteCoin, Dash, ZCash, Doge } from './BTC'; import { Celestia } from './CELESTIA'; import { Ton } from './TON'; import { Jetton } from './Jetton'; +import { ERC20 } from './ERC20'; const fillEVMs = () => { const evms = Object.keys(assets).reduce( @@ -89,6 +90,7 @@ export const WalletClasses = { LUNA2_TEST: Luna, CELESTIA: Celestia, CELESTIA_TEST: Celestia, + ...fillEVMs(), // EDDSA SOL: Solana, @@ -110,7 +112,6 @@ export const WalletClasses = { TON_TEST: Ton, ...fillJettons(), - ...fillEVMs(), } as const; type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses]; diff --git a/packages/asset-config/config/patches.ts b/packages/asset-config/config/patches.ts index 934e58b..0de3827 100644 --- a/packages/asset-config/config/patches.ts +++ b/packages/asset-config/config/patches.ts @@ -140,7 +140,7 @@ export const nativeAssetPatches: NativeAssetPatches = { }, ETC: evm('blockscout.com/etc/mainnet', 'https://geth-de.etc-network.info'), ETC_TEST: evm('blockscout.com/etc/kotti', 'https://geth-mordor.etc-network.info'), - ETH: evm('etherscan.io', 'https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'), + ETH: evm('etherscan.io', 'https://eth-mainnet.public.blastapi.io'), ETH_TEST3: evm('goerli.etherscan.io', 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'), ETH_TEST5: evm('sepolia.etherscan.io', 'https://sepolia.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'), ETH_TEST6: evm('holesky.etherscan.io', 'https://ethereum-holesky-rpc.publicnode.com'), diff --git a/yarn.lock b/yarn.lock index 58a7b79..fc1a81e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3254,7 +3254,7 @@ rxjs "^7.8.1" tslib "^2.5.3" -"@polkadot/keyring@12.3.2", "@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1": +"@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.2", "@polkadot/x-fetch@^12.3.1": +"@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.2", "@polkadot/x-ws@^12.3.1": +"@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== @@ -7961,6 +7961,11 @@ eventsource@^1.1.1: resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2" integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== +evm-chains@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/evm-chains/-/evm-chains-0.2.0.tgz#401354aa2b474bb9b34e796d328f281e67d07349" + integrity sha512-iMpFpmKT9VydfIUpRFoQHNzLdZ3WSNagiq6PABqWDSRwAA9OW+dpKYczkvJ/WSUesGWgcfzYoKRvfusn/9bBYA== + execa@5.1.1, execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"