From c800e168a9ffaa03b574a2376b2a92e6826b1eef Mon Sep 17 00:00:00 2001 From: Support Bot Date: Wed, 1 Jul 2026 05:52:55 +0000 Subject: [PATCH] fix(sdk-coin-etc): fix hex validation in queryAddressBalance and getAddressNonce Replace isNaN() checks with a strict hex regex validator. isNaN() returns true for bare '0x' (valid zero-balance response from some RPC nodes), causing the recovery flow to throw instead of returning zero. Also guard against BN parsing empty string by falling back to '0' when the hex suffix is absent. Add unit tests covering standard hex, '0x0', bare '0x', missing result, RPC error, and non-hex result cases. Ticket: SPT-78 Session-Id: f9f4d3e1-2080-4145-88f2-ff2c4dbb6718 Task-Id: 569e8750-ba8b-4ce0-95af-935c9fd486d6 --- modules/sdk-coin-etc/src/etc.ts | 10 ++--- modules/sdk-coin-etc/test/unit/etc.ts | 55 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/modules/sdk-coin-etc/src/etc.ts b/modules/sdk-coin-etc/src/etc.ts index 078416c7ad..fb9ecc5664 100644 --- a/modules/sdk-coin-etc/src/etc.ts +++ b/modules/sdk-coin-etc/src/etc.ts @@ -296,11 +296,11 @@ export class Etc extends AbstractEthLikeCoin { params: [address, 'latest'], id: 1, }); - if (!result || isNaN(result.result)) { + if (!result || !result.result || typeof result.result !== 'string' || !/^0x[0-9a-fA-F]*$/.test(result.result)) { throw new Error('Unable to find next nonce from etc.network, got: ' + JSON.stringify(result)); } const nonceHex = result.result; - return new optionalDeps.ethUtil.BN(nonceHex.slice(2), 16).toNumber(); + return new optionalDeps.ethUtil.BN(nonceHex.slice(2) || '0', 16).toNumber(); } /** @@ -322,13 +322,13 @@ export class Etc extends AbstractEthLikeCoin { } else if (result.error) { // throw if result.error exists throw new Error(`Could not obtain address balance for ${address} from etc.network, got: ${result.error}`); - } else if (!result.result || isNaN(result.result)) { - // throw if the result.result is not a number + } else if (!result.result || typeof result.result !== 'string' || !/^0x[0-9a-fA-F]*$/.test(result.result)) { throw new Error(`Could not obtain address balance for ${address} from etc.network, got: Incorrect Balance Hex`); } const nativeBalanceHex = result.result; - return new optionalDeps.ethUtil.BN(nativeBalanceHex.slice(2), 16); + // slice(2) strips '0x'; fall back to '0' if bare '0x' is returned for zero balance + return new optionalDeps.ethUtil.BN(nativeBalanceHex.slice(2) || '0', 16); } /** * Queries the contract (via explorer API) for the next sequence ID diff --git a/modules/sdk-coin-etc/test/unit/etc.ts b/modules/sdk-coin-etc/test/unit/etc.ts index b4a424fb11..8c31bb3b79 100644 --- a/modules/sdk-coin-etc/test/unit/etc.ts +++ b/modules/sdk-coin-etc/test/unit/etc.ts @@ -9,6 +9,61 @@ import { getBuilder } from './getBuilder'; import { FullySignedTransaction } from '@bitgo/sdk-core'; import * as should from 'should'; +describe('queryAddressBalance', function () { + let bitgo: TestBitGoAPI; + let etcCoin: Etc; + let sandbox: sinon.SinonSandbox; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.initializeTestVars(); + bitgo.safeRegister('etc', Etc.createInstance); + bitgo.safeRegister('tetc', Tetc.createInstance); + }); + + beforeEach(function () { + sandbox = sinon.createSandbox(); + etcCoin = bitgo.coin('etc') as Etc; + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return the correct balance for a standard hex response', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves({ result: '0x1bc16d674ec80000' }); + const balance = await etcCoin.queryAddressBalance('0x1234'); + balance.toString().should.equal('2000000000000000000'); + }); + + it('should return zero for a "0x0" response', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves({ result: '0x0' }); + const balance = await etcCoin.queryAddressBalance('0x1234'); + balance.toString().should.equal('0'); + }); + + it('should return zero for a bare "0x" response (zero balance edge case)', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves({ result: '0x' }); + const balance = await etcCoin.queryAddressBalance('0x1234'); + balance.toString().should.equal('0'); + }); + + it('should throw for a missing result', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves(null); + await etcCoin.queryAddressBalance('0x1234').should.be.rejectedWith(/Empty object response/); + }); + + it('should throw for an error result', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves({ error: 'node error' }); + await etcCoin.queryAddressBalance('0x1234').should.be.rejectedWith(/node error/); + }); + + it('should throw for a non-hex result', async function () { + sandbox.stub(etcCoin, 'recoveryBlockchainExplorerQuery').resolves({ result: 'not-a-hex' }); + await etcCoin.queryAddressBalance('0x1234').should.be.rejectedWith(/Incorrect Balance Hex/); + }); +}); + describe('Ethereum Classic', function () { let bitgo: TestBitGoAPI;