From 7e30c701ea22754cafc577733fed13954206dee5 Mon Sep 17 00:00:00 2001 From: tomer-shoham Date: Thu, 1 Oct 2026 15:59:12 +0300 Subject: [PATCH 1/2] fix: support quoted CSV import and handle tiny fee balance for ERC20 --- .../recovery-relay/lib/wallets/ERC20/index.ts | 31 +++++++++---------- packages/shared/lib/csv.ts | 1 - packages/shared/tests/index.test.ts | 18 +++++++++++ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/apps/recovery-relay/lib/wallets/ERC20/index.ts b/apps/recovery-relay/lib/wallets/ERC20/index.ts index e0a25785..fc67ae27 100644 --- a/apps/recovery-relay/lib/wallets/ERC20/index.ts +++ b/apps/recovery-relay/lib/wallets/ERC20/index.ts @@ -1,4 +1,4 @@ -import { Contract, ethers, JsonRpcProvider, parseUnits } from 'ethers'; +import { Contract, ethers, formatUnits, JsonRpcProvider } from 'ethers'; import { AccountData } from '../types'; import { ConnectedWallet } from '../ConnectedWallet'; import { Input, EVMWallet as EVMBase } from '@fireblocks/wallet-derivation'; @@ -14,7 +14,6 @@ export class ERC20 extends EVMBase implements ConnectedWallet { public tokenAddress: string | undefined; public decimals: number | undefined; public toAddress: string | undefined; - private normalizingFactor: string | bigint | undefined; constructor(private input: Input) { super(input); @@ -54,38 +53,36 @@ export class ERC20 extends EVMBase implements ConnectedWallet { this.relayLogger.error(`ERC20 Token decimals are unavailable: ${this.assetId}`); throw new Error(`ERC20 Token decimals are unavailable: ${this.assetId}`); } - - this.normalizingFactor = (10 ** decimals).toString(); } public setToAddress(toAddress: string) { this.toAddress = toAddress; } + private async getTokenBalanceInBaseUnits(): Promise { + return BigInt(await this.contract!.balanceOf(this.address)); + } + public async getBalance(): Promise { - const weiBalance: bigint = await this.contract?.balanceOf(this.address); - return Number(weiBalance / BigInt(this.normalizingFactor!)); + return Number(formatUnits(await this.getTokenBalanceInBaseUnits(), this.decimals!)); } public async prepare(): Promise { this.init(); const nonce = await this.provider?.getTransactionCount(this.address, 'latest'); - const displayBalance = await this.getBalance(); - const ethBalance = await this.backendWallet?.getBalance(); - if (!ethBalance) { + const tokenBalanceInBaseUnits = await this.getTokenBalanceInBaseUnits(); + const displayBalance = Number(formatUnits(tokenBalanceInBaseUnits, this.decimals!)); + const ethBalanceInWei = await this.provider!.getBalance(this.address); + if (!ethBalanceInWei) { this.relayLogger.error(`Fee asset balance not available`); throw new Error(`Fee asset balance not available`); } - const ethBalanceInWei = parseUnits(ethBalance.toString(), 'ether'); const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await this.provider!.getFeeData(); const abiInterface = new ethers.Interface(erc20Abi); - const data = abiInterface.encodeFunctionData('transfer', [ - this.toAddress, - BigInt(displayBalance) * BigInt(this.normalizingFactor!), - ]); + const data = abiInterface.encodeFunctionData('transfer', [this.toAddress, tokenBalanceInBaseUnits]); const tx = { to: this.tokenAddress, @@ -100,7 +97,7 @@ export class ERC20 extends EVMBase implements ConnectedWallet { extraParams.set('gasPrice', gasPrice?.toString()); extraParams.set('maxFee', maxFeePerGas?.toString()); extraParams.set('priorityFee', maxPriorityFeePerGas?.toString()); - extraParams.set('weiBalance', (BigInt(displayBalance) * BigInt(this.normalizingFactor!)).toString()); + extraParams.set('weiBalance', tokenBalanceInBaseUnits.toString()); const preparedData: AccountData = { balance: displayBalance, @@ -108,8 +105,8 @@ export class ERC20 extends EVMBase implements ConnectedWallet { nonce, //@ts-ignore chainId: this.backendWallet?.chainId, - insufficientBalance: displayBalance <= 0, - insufficientBalanceForTokenTransfer: Number(ethBalanceInWei!) <= Number(gasPrice! * gasLimit!), + insufficientBalance: tokenBalanceInBaseUnits <= 0n, + insufficientBalanceForTokenTransfer: ethBalanceInWei <= gasPrice! * gasLimit!, }; return preparedData; } diff --git a/packages/shared/lib/csv.ts b/packages/shared/lib/csv.ts index 7618b3cd..8842f373 100644 --- a/packages/shared/lib/csv.ts +++ b/packages/shared/lib/csv.ts @@ -177,7 +177,6 @@ export const csvImport = async ( header: true, dynamicTyping: true, skipEmptyLines: 'greedy', - fastMode: true, step: ({ data, errors }) => { const sanatizedData = sanatize(data as unknown); logger.debug('csvImport', { sanatizedData, errors }); diff --git a/packages/shared/tests/index.test.ts b/packages/shared/tests/index.test.ts index 53530786..814cc10d 100644 --- a/packages/shared/tests/index.test.ts +++ b/packages/shared/tests/index.test.ts @@ -210,6 +210,24 @@ Test,1523,BTC,Bitcoin,abcd,Deposit,1111111111,,`; }); }); + it('Should import CSV with quoted fields (console export format)', async () => { + const quotedCsv = `"Account Name","Account ID","Asset","Asset Name","Address","Address Type","Address Description","Tag","HD Path" +"Default","0","BASECHAIN_ETH","Ethereum (Base)","abcd","Permanent","","","m / 44 / 60 / 0 / 0 / 0"`; + const rows: string[] = []; + await csvImport(Readable.from(quotedCsv), 'addresses', (row) => { + rows.push(row.address); + }); + expect(rows).toEqual(['abcd']); + }); + + it('Should fail importing quoted CSV with macros', async () => { + const quotedMacroCsv = `"Account Name","Account ID","Asset","Asset Name","Address","Address Type","Address Description","Tag","HD Path" +"Default","0","ETH","Ethereum","=6+2","Permanent","","","m / 44 / 60 / 0 / 0 / 0"`; + await expect(csvImport(Readable.from(quotedMacroCsv), 'addresses', (_) => {})).rejects.toThrow( + 'Row contains prohibited characters - please reset workspace and check your importing CSV', + ); + }); + it('Should fail importing CSV with too many fields error', async () => { expect(async () => { await csvImport(Readable.from(invalidCsvTooManyFields), 'addresses', (_) => {}); From 4fa15b48646df4d656dd1603725f2ef1bf0e0f30 Mon Sep 17 00:00:00 2001 From: tomer-shoham Date: Tue, 13 Oct 2026 15:29:18 +0300 Subject: [PATCH 2/2] chore: bump version to 1.8.8 --- apps/recovery-relay/package.json | 2 +- apps/recovery-utility/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/recovery-relay/package.json b/apps/recovery-relay/package.json index 11360c0b..adcaeb45 100644 --- a/apps/recovery-relay/package.json +++ b/apps/recovery-relay/package.json @@ -4,7 +4,7 @@ "author": "Fireblocks (https://www.fireblocks.com)", "repository": "https://github.com/fireblocks/recovery/tree/main/apps/recovery-relay", "license": "GPL-3.0-or-later", - "version": "1.8.6", + "version": "1.8.8", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/recovery-utility/package.json b/apps/recovery-utility/package.json index 233a6fc5..a6982465 100644 --- a/apps/recovery-utility/package.json +++ b/apps/recovery-utility/package.json @@ -4,7 +4,7 @@ "author": "Fireblocks (https://www.fireblocks.com)", "repository": "https://github.com/fireblocks/recovery/tree/main/apps/recovery-utility", "homepage": "https://www.fireblocks.com", - "version": "1.8.7", + "version": "1.8.8", "license": "GPL-3.0-or-later", "private": true, "main": "app/background.js",