From 4b52a9c1f852422d6f8f4b67a9b348c5c375b3a6 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 31 Mar 2026 14:35:07 -0300 Subject: [PATCH 1/6] test: shared getBalance tests for both facades Extract getBalance coverage from monolithic facade test files into the shared test framework. Shared tests run against both fullnode and wallet-service adapters via describe.each. - shared/get-balance.test.ts: zero balance, funded balance - fullnode-specific/get-balance.test.ts: custom token, mandatory tokenUid, internal transfer - service-specific/get-balance.test.ts: no-arg getBalance, not-ready error, skipped empty-wallet bugs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fullnode-specific/get-balance.test.ts | 96 +++++++++++++++ .../integration/hathorwallet_facade.test.ts | 85 +------------ .../service-specific/get-balance.test.ts | 116 ++++++++++++++++++ .../integration/shared/get-balance.test.ts | 73 +++++++++++ .../integration/walletservice_facade.test.ts | 89 +------------- 5 files changed, 287 insertions(+), 172 deletions(-) create mode 100644 __tests__/integration/fullnode-specific/get-balance.test.ts create mode 100644 __tests__/integration/service-specific/get-balance.test.ts create mode 100644 __tests__/integration/shared/get-balance.test.ts diff --git a/__tests__/integration/fullnode-specific/get-balance.test.ts b/__tests__/integration/fullnode-specific/get-balance.test.ts new file mode 100644 index 000000000..b212623b8 --- /dev/null +++ b/__tests__/integration/fullnode-specific/get-balance.test.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Fullnode-facade getBalance() tests. + * + * Tests that rely on fullnode-only APIs or helpers (e.g. {@link createTokenHelper}, + * direct sendTransaction without pinCode). + * + * Shared getBalance() tests live in `shared/get-balance.test.ts`. + */ + +import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper'; +import { getRandomInt } from '../utils/core.util'; +import { + createTokenHelper, + generateWalletHelper, + stopAllWallets, + waitForTxReceived, +} from '../helpers/wallet.helper'; +import { NATIVE_TOKEN_UID } from '../../../src/constants'; + +const fakeTokenUid = '008a19f84f2ae284f19bf3d03386c878ddd15b8b0b604a3a3539aa9d714686e1'; + +describe('[Fullnode] getBalance', () => { + afterEach(async () => { + await stopAllWallets(); + }); + + it('should reject when tokenUid is not provided', async () => { + const hWallet = await generateWalletHelper(); + await expect(hWallet.getBalance()).rejects.toThrow(); + }); + + it('should not change balance after internal transfer', async () => { + const hWallet = await generateWalletHelper(); + + const injectedValue = BigInt(getRandomInt(10, 2)); + await GenesisWalletHelper.injectFunds( + hWallet, + await hWallet.getAddressAtIndex(0), + injectedValue + ); + + const balanceBefore = await hWallet.getBalance(NATIVE_TOKEN_UID); + + const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(1), 2n); + await waitForTxReceived(hWallet, tx1.hash); + const balanceAfter = await hWallet.getBalance(NATIVE_TOKEN_UID); + expect(balanceAfter[0].balance).toEqual(balanceBefore[0].balance); + }); + + it('should get the balance for a custom token', async () => { + const hWallet = await generateWalletHelper(); + + // Validating results for a nonexistent token + const emptyBalance = await hWallet.getBalance(fakeTokenUid); + expect(emptyBalance).toHaveLength(1); + expect(emptyBalance[0]).toMatchObject({ + token: { id: fakeTokenUid }, + balance: { unlocked: 0n, locked: 0n }, + transactions: 0, + }); + + // Creating a new custom token + await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n); + const newTokenAmount = BigInt(getRandomInt(1000, 10)); + const { hash: tokenUid } = await createTokenHelper( + hWallet, + 'BalanceToken', + 'BAT', + newTokenAmount + ); + + const tknBalance = await hWallet.getBalance(tokenUid); + expect(tknBalance[0]).toMatchObject({ + balance: { unlocked: newTokenAmount, locked: 0n }, + transactions: expect.any(Number), + // transactions: 1, // TODO: The amount of transactions is often 8 but should be 1. Ref #397 + }); + + // Validating that a different wallet (genesis) has no access to this token + const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton(); + const genesisTknBalance = await gWallet.getBalance(tokenUid); + expect(genesisTknBalance).toHaveLength(1); + expect(genesisTknBalance[0]).toMatchObject({ + token: { id: tokenUid }, + balance: { unlocked: 0n, locked: 0n }, + transactions: 0, + }); + }); +}); diff --git a/__tests__/integration/hathorwallet_facade.test.ts b/__tests__/integration/hathorwallet_facade.test.ts index 3013a788f..79f8a8f5e 100644 --- a/__tests__/integration/hathorwallet_facade.test.ts +++ b/__tests__/integration/hathorwallet_facade.test.ts @@ -457,90 +457,7 @@ describe('addresses methods', () => { }); }); -describe('getBalance', () => { - afterEach(async () => { - await stopAllWallets(); - await GenesisWalletHelper.clearListeners(); - }); - - it('should get the balance for the HTR token', async () => { - const hWallet = await generateWalletHelper(); - - // Validating that the token uid parameter is mandatory. - await expect(hWallet.getBalance()).rejects.toThrow(); - - // Validating the return array has one entry on an empty wallet - const balance = await hWallet.getBalance(NATIVE_TOKEN_UID); - expect(balance).toHaveLength(1); - expect(balance[0]).toMatchObject({ - token: { id: NATIVE_TOKEN_UID }, - balance: { unlocked: 0n, locked: 0n }, - transactions: 0, - }); - - // Generating one transaction to validate its effects - const injectedValue = BigInt(getRandomInt(10, 2)); - await GenesisWalletHelper.injectFunds( - hWallet, - await hWallet.getAddressAtIndex(0), - injectedValue - ); - - // Validating the transaction effects - const balance1 = await hWallet.getBalance(NATIVE_TOKEN_UID); - expect(balance1[0]).toMatchObject({ - balance: { unlocked: injectedValue, locked: 0n }, - transactions: expect.any(Number), - // transactions: 1, // TODO: The amount of transactions is often 2 but should be 1. Ref #397 - }); - - // Transferring tokens inside the wallet should not change the balance - const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(1), 2n); - await waitForTxReceived(hWallet, tx1.hash); - const balance2 = await hWallet.getBalance(NATIVE_TOKEN_UID); - expect(balance2[0].balance).toEqual(balance1[0].balance); - }); - - it('should get the balance for a custom token', async () => { - const hWallet = await generateWalletHelper(); - - // Validating results for a nonexistant token - const emptyBalance = await hWallet.getBalance(fakeTokenUid); - expect(emptyBalance).toHaveLength(1); - expect(emptyBalance[0]).toMatchObject({ - token: { id: fakeTokenUid }, - balance: { unlocked: 0n, locked: 0n }, - transactions: 0, - }); - - // Creating a new custom token - await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n); - const newTokenAmount = BigInt(getRandomInt(1000, 10)); - const { hash: tokenUid } = await createTokenHelper( - hWallet, - 'BalanceToken', - 'BAT', - newTokenAmount - ); - - const tknBalance = await hWallet.getBalance(tokenUid); - expect(tknBalance[0]).toMatchObject({ - balance: { unlocked: newTokenAmount, locked: 0n }, - transactions: expect.any(Number), - // transactions: 1, // TODO: The amount of transactions is often 8 but should be 1. Ref #397 - }); - - // Validating that a different wallet (genesis) has no access to this token - const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton(); - const genesisTknBalance = await gWallet.getBalance(tokenUid); - expect(genesisTknBalance).toHaveLength(1); - expect(genesisTknBalance[0]).toMatchObject({ - token: { id: tokenUid }, - balance: { unlocked: 0n, locked: 0n }, - transactions: 0, - }); - }); -}); +// getBalance tests moved to shared/get-balance.test.ts and fullnode-specific/get-balance.test.ts describe('getFullHistory', () => { afterEach(async () => { diff --git a/__tests__/integration/service-specific/get-balance.test.ts b/__tests__/integration/service-specific/get-balance.test.ts new file mode 100644 index 000000000..242c66867 --- /dev/null +++ b/__tests__/integration/service-specific/get-balance.test.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Service-facade getBalance() tests. + * + * Tests for service-only behavior: no-arg getBalance() (returns all tokens), + * not-ready wallet rejection, and skipped empty-wallet bugs. + * + * Shared getBalance() tests live in `shared/get-balance.test.ts`. + */ + +import type { HathorWalletServiceWallet } from '../../../src'; +import { NATIVE_TOKEN_UID } from '../../../src/constants'; +import { buildWalletInstance, emptyWallet } from '../helpers/service-facade.helper'; +import { ServiceWalletTestAdapter } from '../adapters/service.adapter'; + +const adapter = new ServiceWalletTestAdapter(); + +beforeAll(async () => { + await adapter.suiteSetup(); +}); + +afterAll(async () => { + await adapter.suiteTeardown(); +}); + +describe('[Service] getBalance', () => { + let wallet: HathorWalletServiceWallet; + + afterEach(async () => { + if (wallet) { + try { + await wallet.stop({ cleanStorage: true }); + } catch { + // Wallet may already be stopped + } + } + }); + + it('should return balance for a funded wallet using no-arg getBalance()', async () => { + const { wallet: w } = await adapter.createWallet(); + wallet = w as unknown as HathorWalletServiceWallet; + + const addr = await w.getAddressAtIndex(0); + await adapter.injectFunds(w, addr!, 1n); + + const balances = await wallet.getBalance(); + expect(Array.isArray(balances)).toBe(true); + expect(balances.length).toBeGreaterThanOrEqual(1); + + const htrBalance = balances.find(b => b.token.id === NATIVE_TOKEN_UID); + expect(htrBalance).toBeDefined(); + expect(typeof htrBalance?.balance).toBe('object'); + }); + + // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token + it.skip('should return balance array for empty wallet', async () => { + ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); + await wallet.start({ pinCode: adapter.defaultPinCode, password: adapter.defaultPassword }); + + const balances = await wallet.getBalance(); + + expect(Array.isArray(balances)).toBe(true); + expect(balances.length).toStrictEqual(1); + + const htrBalance = balances.find(b => b.token.id === NATIVE_TOKEN_UID); + expect(htrBalance).toBeDefined(); + expect(htrBalance?.balance).toBe(0n); + }); + + // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token + it.skip('should return balance for specific token when token parameter is provided', async () => { + ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); + await wallet.start({ pinCode: adapter.defaultPinCode, password: adapter.defaultPassword }); + + const balances = await wallet.getBalance(NATIVE_TOKEN_UID); + + expect(Array.isArray(balances)).toBe(true); + expect(balances.length).toStrictEqual(1); + expect(balances[0]).toEqual( + expect.objectContaining({ + token: expect.objectContaining({ + id: NATIVE_TOKEN_UID, + name: expect.any(String), + symbol: expect.any(String), + }), + balance: expect.objectContaining({ + unlocked: 0n, + locked: 0n, + }), + tokenAuthorities: expect.objectContaining({ + unlocked: expect.objectContaining({ + mint: false, + melt: false, + }), + locked: expect.objectContaining({ + mint: false, + melt: false, + }), + }), + transactions: 0, + lockExpires: expect.anything(), + }) + ); + }); + + it('should throw error when wallet is not ready', async () => { + const { wallet: notReadyWallet } = buildWalletInstance({ words: emptyWallet.words }); + await expect(notReadyWallet.getBalance()).rejects.toThrow('Wallet not ready'); + }); +}); diff --git a/__tests__/integration/shared/get-balance.test.ts b/__tests__/integration/shared/get-balance.test.ts new file mode 100644 index 000000000..a3850f690 --- /dev/null +++ b/__tests__/integration/shared/get-balance.test.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Shared getBalance() tests. + * + * Validates balance query behavior that is common to both the fullnode + * ({@link HathorWallet}) and wallet-service ({@link HathorWalletServiceWallet}) + * facades. + * + * Facade-specific tests live in: + * - `fullnode-specific/get-balance.test.ts` + * - `service-specific/get-balance.test.ts` + */ + +import type { IWalletTestAdapter } from '../adapters/types'; +import { NATIVE_TOKEN_UID } from '../../../src/constants'; +import { getRandomInt } from '../utils/core.util'; +import { FullnodeWalletTestAdapter } from '../adapters/fullnode.adapter'; +import { ServiceWalletTestAdapter } from '../adapters/service.adapter'; + +const adapters: IWalletTestAdapter[] = [ + new FullnodeWalletTestAdapter(), + new ServiceWalletTestAdapter(), +]; + +describe.each(adapters)('[Shared] getBalance — $name', adapter => { + beforeAll(async () => { + await adapter.suiteSetup(); + }); + + afterAll(async () => { + await adapter.suiteTeardown(); + }); + + it('should return zero balance on an empty wallet', async () => { + const { wallet } = await adapter.createWallet(); + + try { + const balance = await wallet.getBalance(NATIVE_TOKEN_UID); + expect(balance).toHaveLength(1); + expect(balance[0]).toMatchObject({ + token: { id: NATIVE_TOKEN_UID }, + balance: { unlocked: 0n, locked: 0n }, + transactions: 0, + }); + } finally { + await adapter.stopWallet(wallet); + } + }); + + it('should reflect injected funds in balance', async () => { + const { wallet } = await adapter.createWallet(); + + try { + const injectedValue = BigInt(getRandomInt(10, 2)); + const addr = await wallet.getAddressAtIndex(0); + expect(addr).toBeDefined(); + await adapter.injectFunds(wallet, addr!, injectedValue); + + const balance = await wallet.getBalance(NATIVE_TOKEN_UID); + expect(balance[0]).toMatchObject({ + balance: { unlocked: injectedValue, locked: 0n }, + }); + } finally { + await adapter.stopWallet(wallet); + } + }); +}); diff --git a/__tests__/integration/walletservice_facade.test.ts b/__tests__/integration/walletservice_facade.test.ts index 95c9a827a..2ef71fb54 100644 --- a/__tests__/integration/walletservice_facade.test.ts +++ b/__tests__/integration/walletservice_facade.test.ts @@ -1083,94 +1083,7 @@ describe('basic transaction methods', () => { describe.skip('websocket events', () => {}); -describe('balances', () => { - beforeEach(async () => { - ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); - await wallet.start({ pinCode, password }); - }); - - afterEach(async () => { - if (wallet) { - await wallet.stop({ cleanStorage: true }); - } - }); - - describe('getBalance', () => { - // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token - it.skip('should return balance array for empty wallet', async () => { - const balances = await wallet.getBalance(); - - expect(Array.isArray(balances)).toBe(true); - expect(balances.length).toStrictEqual(1); - - // Should have HTR (native token) with zero balance for empty wallet - const htrBalance = balances.find(b => b.token.id === NATIVE_TOKEN_UID); - expect(htrBalance).toBeDefined(); - expect(htrBalance?.balance).toBe(0n); - }); - - it('should return balance array for wallet with transactions', async () => { - // Use walletWithTxs which has transaction history - const { wallet: walletTxs } = buildWalletInstance({ words: walletWithTxs.words }); - await walletTxs.start({ pinCode, password }); - - const balances = await walletTxs.getBalance(); - - expect(Array.isArray(balances)).toBe(true); - expect(balances.length).toBeGreaterThanOrEqual(1); - - // Should have HTR balance - const htrBalance = balances.find(b => b.token.id === NATIVE_TOKEN_UID); - expect(htrBalance).toBeDefined(); - expect(typeof htrBalance?.balance).toBe('object'); - - await walletTxs.stop({ cleanStorage: true }); - }); - - // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token - it.skip('should return balance for specific token when token parameter is provided', async () => { - const balances = await wallet.getBalance(NATIVE_TOKEN_UID); // HTR token - - expect(Array.isArray(balances)).toBe(true); - // When requesting specific token, should return that token's balance - expect(balances.length).toStrictEqual(1); - expect(balances[0]).toEqual( - expect.objectContaining({ - token: expect.objectContaining({ - id: NATIVE_TOKEN_UID, - name: expect.any(String), - symbol: expect.any(String), - }), - balance: expect.objectContaining({ - unlocked: 0n, - locked: 0n, - }), - tokenAuthorities: expect.objectContaining({ - unlocked: expect.objectContaining({ - mint: false, - melt: false, - }), - locked: expect.objectContaining({ - mint: false, - melt: false, - }), - }), - transactions: 0, - lockExpires: expect.anything(), - }) - ); - }); - - it('should throw error when wallet is not ready', async () => { - const { wallet: notReadyWallet } = buildWalletInstance({ words: emptyWallet.words }); - // Don't start the wallet, so it's not ready - - await expect(notReadyWallet.getBalance()).rejects.toThrow('Wallet not ready'); - }); - }); - - describe.skip('getTxBalance', () => {}); -}); +// balances tests moved to shared/get-balance.test.ts and service-specific/get-balance.test.ts describe('address management methods', () => { const knownAddresses = addressesWallet.addresses; From 310958b2e95edcb1720d03502e4e1e30740bebe6 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 31 Mar 2026 19:30:56 -0300 Subject: [PATCH 2/6] fix: address PR review comments - Add issue #397 reference to FIXME comments on skipped empty-wallet balance tests in service-specific - Add non-null assertion to tx1.hash in fullnode-specific waitForTxReceived call Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integration/fullnode-specific/get-balance.test.ts | 2 +- .../integration/service-specific/get-balance.test.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/__tests__/integration/fullnode-specific/get-balance.test.ts b/__tests__/integration/fullnode-specific/get-balance.test.ts index b212623b8..b90b63f49 100644 --- a/__tests__/integration/fullnode-specific/get-balance.test.ts +++ b/__tests__/integration/fullnode-specific/get-balance.test.ts @@ -49,7 +49,7 @@ describe('[Fullnode] getBalance', () => { const balanceBefore = await hWallet.getBalance(NATIVE_TOKEN_UID); const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(1), 2n); - await waitForTxReceived(hWallet, tx1.hash); + await waitForTxReceived(hWallet, tx1.hash!); const balanceAfter = await hWallet.getBalance(NATIVE_TOKEN_UID); expect(balanceAfter[0].balance).toEqual(balanceBefore[0].balance); }); diff --git a/__tests__/integration/service-specific/get-balance.test.ts b/__tests__/integration/service-specific/get-balance.test.ts index 242c66867..48c1b0ba9 100644 --- a/__tests__/integration/service-specific/get-balance.test.ts +++ b/__tests__/integration/service-specific/get-balance.test.ts @@ -58,7 +58,9 @@ describe('[Service] getBalance', () => { expect(typeof htrBalance?.balance).toBe('object'); }); - // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token + // FIXME(wallet-service): getBalance() on an empty wallet should return a single + // entry with 0 balance for the native token, but currently returns an empty array. + // Ref: https://github.com/HathorNetwork/hathor-wallet-lib/issues/397 it.skip('should return balance array for empty wallet', async () => { ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); await wallet.start({ pinCode: adapter.defaultPinCode, password: adapter.defaultPassword }); @@ -73,7 +75,9 @@ describe('[Service] getBalance', () => { expect(htrBalance?.balance).toBe(0n); }); - // FIXME: The test does not return balance for empty wallet. It should return 0 for the native token + // FIXME(wallet-service): getBalance(tokenUid) on an empty wallet should return + // a single entry with 0 balance, but currently returns an empty array. + // Ref: https://github.com/HathorNetwork/hathor-wallet-lib/issues/397 it.skip('should return balance for specific token when token parameter is provided', async () => { ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); await wallet.start({ pinCode: adapter.defaultPinCode, password: adapter.defaultPassword }); From 1f684464e634ea8c3e8fb69e1cd5c3810aec06f0 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 31 Mar 2026 20:05:19 -0300 Subject: [PATCH 3/6] fix: guard getAddressAtIndex with toBeDefined Replace non-null assertion with explicit expect guard before passing address to injectFunds. Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/integration/service-specific/get-balance.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__tests__/integration/service-specific/get-balance.test.ts b/__tests__/integration/service-specific/get-balance.test.ts index 48c1b0ba9..92ac8b3e2 100644 --- a/__tests__/integration/service-specific/get-balance.test.ts +++ b/__tests__/integration/service-specific/get-balance.test.ts @@ -47,7 +47,8 @@ describe('[Service] getBalance', () => { wallet = w as unknown as HathorWalletServiceWallet; const addr = await w.getAddressAtIndex(0); - await adapter.injectFunds(w, addr!, 1n); + expect(addr).toBeDefined(); + await adapter.injectFunds(w, addr as string, 1n); const balances = await wallet.getBalance(); expect(Array.isArray(balances)).toBe(true); From 80fa9d14d26af95dc2d2aa78247ab01d781d69e2 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Wed, 1 Apr 2026 17:30:48 -0300 Subject: [PATCH 4/6] test: move balance tests to shared suite Move 'internal transfer' and 'custom token' tests from fullnode-specific to shared get-balance suite. Extract fullnode-only assertions (nonexistent token zero-balance, cross-wallet isolation) into dedicated fullnode-specific tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fullnode-specific/get-balance.test.ts | 40 +++------------- .../integration/shared/get-balance.test.ts | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/__tests__/integration/fullnode-specific/get-balance.test.ts b/__tests__/integration/fullnode-specific/get-balance.test.ts index b90b63f49..f81eddf89 100644 --- a/__tests__/integration/fullnode-specific/get-balance.test.ts +++ b/__tests__/integration/fullnode-specific/get-balance.test.ts @@ -8,8 +8,8 @@ /** * Fullnode-facade getBalance() tests. * - * Tests that rely on fullnode-only APIs or helpers (e.g. {@link createTokenHelper}, - * direct sendTransaction without pinCode). + * Tests that rely on fullnode-only APIs or behavior (e.g. no-arg getBalance() + * rejection, nonexistent token returning a zero-balance entry). * * Shared getBalance() tests live in `shared/get-balance.test.ts`. */ @@ -20,9 +20,7 @@ import { createTokenHelper, generateWalletHelper, stopAllWallets, - waitForTxReceived, } from '../helpers/wallet.helper'; -import { NATIVE_TOKEN_UID } from '../../../src/constants'; const fakeTokenUid = '008a19f84f2ae284f19bf3d03386c878ddd15b8b0b604a3a3539aa9d714686e1'; @@ -36,28 +34,9 @@ describe('[Fullnode] getBalance', () => { await expect(hWallet.getBalance()).rejects.toThrow(); }); - it('should not change balance after internal transfer', async () => { + it('should return zero balance for a nonexistent token', async () => { const hWallet = await generateWalletHelper(); - const injectedValue = BigInt(getRandomInt(10, 2)); - await GenesisWalletHelper.injectFunds( - hWallet, - await hWallet.getAddressAtIndex(0), - injectedValue - ); - - const balanceBefore = await hWallet.getBalance(NATIVE_TOKEN_UID); - - const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(1), 2n); - await waitForTxReceived(hWallet, tx1.hash!); - const balanceAfter = await hWallet.getBalance(NATIVE_TOKEN_UID); - expect(balanceAfter[0].balance).toEqual(balanceBefore[0].balance); - }); - - it('should get the balance for a custom token', async () => { - const hWallet = await generateWalletHelper(); - - // Validating results for a nonexistent token const emptyBalance = await hWallet.getBalance(fakeTokenUid); expect(emptyBalance).toHaveLength(1); expect(emptyBalance[0]).toMatchObject({ @@ -65,8 +44,11 @@ describe('[Fullnode] getBalance', () => { balance: { unlocked: 0n, locked: 0n }, transactions: 0, }); + }); + + it('should not show custom token balance on a different wallet', async () => { + const hWallet = await generateWalletHelper(); - // Creating a new custom token await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n); const newTokenAmount = BigInt(getRandomInt(1000, 10)); const { hash: tokenUid } = await createTokenHelper( @@ -76,14 +58,6 @@ describe('[Fullnode] getBalance', () => { newTokenAmount ); - const tknBalance = await hWallet.getBalance(tokenUid); - expect(tknBalance[0]).toMatchObject({ - balance: { unlocked: newTokenAmount, locked: 0n }, - transactions: expect.any(Number), - // transactions: 1, // TODO: The amount of transactions is often 8 but should be 1. Ref #397 - }); - - // Validating that a different wallet (genesis) has no access to this token const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton(); const genesisTknBalance = await gWallet.getBalance(tokenUid); expect(genesisTknBalance).toHaveLength(1); diff --git a/__tests__/integration/shared/get-balance.test.ts b/__tests__/integration/shared/get-balance.test.ts index a3850f690..501a1b931 100644 --- a/__tests__/integration/shared/get-balance.test.ts +++ b/__tests__/integration/shared/get-balance.test.ts @@ -70,4 +70,52 @@ describe.each(adapters)('[Shared] getBalance — $name', adapter => { await adapter.stopWallet(wallet); } }); + + it('should not change balance after internal transfer', async () => { + const { wallet } = await adapter.createWallet(); + + try { + const injectedValue = BigInt(getRandomInt(10, 2)); + const addr = await wallet.getAddressAtIndex(0); + expect(addr).toBeDefined(); + await adapter.injectFunds(wallet, addr!, injectedValue); + + const balanceBefore = await wallet.getBalance(NATIVE_TOKEN_UID); + + const tx = await wallet.sendTransaction(await wallet.getAddressAtIndex(1), 2n, { + pinCode: adapter.defaultPinCode, + }); + await adapter.waitForTx(wallet, tx.hash!); + + const balanceAfter = await wallet.getBalance(NATIVE_TOKEN_UID); + expect(balanceAfter[0].balance).toEqual(balanceBefore[0].balance); + } finally { + await adapter.stopWallet(wallet); + } + }); + + it('should get the balance for a custom token', async () => { + const { wallet } = await adapter.createWallet(); + + try { + // Creating a new custom token + const addr = await wallet.getAddressAtIndex(0); + expect(addr).toBeDefined(); + await adapter.injectFunds(wallet, addr!, 10n); + + const newTokenAmount = BigInt(getRandomInt(1000, 10)); + const newToken = await wallet.createNewToken('BalanceToken', 'BAT', newTokenAmount, { + pinCode: adapter.defaultPinCode, + }); + await adapter.waitForTx(wallet, newToken.hash!); + + const tknBalance = await wallet.getBalance(newToken.hash!); + expect(tknBalance[0]).toMatchObject({ + balance: { unlocked: newTokenAmount, locked: 0n }, + transactions: expect.any(Number), + }); + } finally { + await adapter.stopWallet(wallet); + } + }); }); From a20899af7779532af44e46802f40c10e70f99eab Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Thu, 2 Apr 2026 11:15:55 -0300 Subject: [PATCH 5/6] style: format fullnode get-balance imports Co-Authored-By: Claude Opus 4.6 (1M context) --- __tests__/integration/fullnode-specific/get-balance.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/__tests__/integration/fullnode-specific/get-balance.test.ts b/__tests__/integration/fullnode-specific/get-balance.test.ts index f81eddf89..74ddd64e4 100644 --- a/__tests__/integration/fullnode-specific/get-balance.test.ts +++ b/__tests__/integration/fullnode-specific/get-balance.test.ts @@ -16,11 +16,7 @@ import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper'; import { getRandomInt } from '../utils/core.util'; -import { - createTokenHelper, - generateWalletHelper, - stopAllWallets, -} from '../helpers/wallet.helper'; +import { createTokenHelper, generateWalletHelper, stopAllWallets } from '../helpers/wallet.helper'; const fakeTokenUid = '008a19f84f2ae284f19bf3d03386c878ddd15b8b0b604a3a3539aa9d714686e1'; From 08f37efb77dcf07a3b5572f187ea5f50f544cfff Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 7 Apr 2026 12:04:54 -0300 Subject: [PATCH 6/6] test: move balance shape test to shared suite The getBalance(tokenUid) test for empty wallets works on both facades. Moved from service-specific (where it was skipped) to the shared suite with updated assertions for the `version` field and facade-agnostic authority checks. The no-arg getBalance() empty wallet test remains skipped per #397. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../service-specific/get-balance.test.ts | 38 ------------------- .../integration/shared/get-balance.test.ts | 30 +++++++++++++++ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/__tests__/integration/service-specific/get-balance.test.ts b/__tests__/integration/service-specific/get-balance.test.ts index 92ac8b3e2..37abbd3a6 100644 --- a/__tests__/integration/service-specific/get-balance.test.ts +++ b/__tests__/integration/service-specific/get-balance.test.ts @@ -76,44 +76,6 @@ describe('[Service] getBalance', () => { expect(htrBalance?.balance).toBe(0n); }); - // FIXME(wallet-service): getBalance(tokenUid) on an empty wallet should return - // a single entry with 0 balance, but currently returns an empty array. - // Ref: https://github.com/HathorNetwork/hathor-wallet-lib/issues/397 - it.skip('should return balance for specific token when token parameter is provided', async () => { - ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); - await wallet.start({ pinCode: adapter.defaultPinCode, password: adapter.defaultPassword }); - - const balances = await wallet.getBalance(NATIVE_TOKEN_UID); - - expect(Array.isArray(balances)).toBe(true); - expect(balances.length).toStrictEqual(1); - expect(balances[0]).toEqual( - expect.objectContaining({ - token: expect.objectContaining({ - id: NATIVE_TOKEN_UID, - name: expect.any(String), - symbol: expect.any(String), - }), - balance: expect.objectContaining({ - unlocked: 0n, - locked: 0n, - }), - tokenAuthorities: expect.objectContaining({ - unlocked: expect.objectContaining({ - mint: false, - melt: false, - }), - locked: expect.objectContaining({ - mint: false, - melt: false, - }), - }), - transactions: 0, - lockExpires: expect.anything(), - }) - ); - }); - it('should throw error when wallet is not ready', async () => { const { wallet: notReadyWallet } = buildWalletInstance({ words: emptyWallet.words }); await expect(notReadyWallet.getBalance()).rejects.toThrow('Wallet not ready'); diff --git a/__tests__/integration/shared/get-balance.test.ts b/__tests__/integration/shared/get-balance.test.ts index 501a1b931..3fe1fe6ba 100644 --- a/__tests__/integration/shared/get-balance.test.ts +++ b/__tests__/integration/shared/get-balance.test.ts @@ -53,6 +53,36 @@ describe.each(adapters)('[Shared] getBalance — $name', adapter => { } }); + it('should return full balance shape for a specific token on an empty wallet', async () => { + const { wallet } = await adapter.createWallet(); + + try { + const balance = await wallet.getBalance(NATIVE_TOKEN_UID); + expect(balance).toHaveLength(1); + expect(balance[0]).toMatchObject({ + token: { + id: NATIVE_TOKEN_UID, + name: expect.any(String), + symbol: expect.any(String), + version: expect.any(Number), + }, + balance: { unlocked: 0n, locked: 0n }, + transactions: 0, + }); + + // Authorities shape is present but the value type differs between facades + // (fullnode returns 0n, wallet-service returns false), so we check structure only + const { tokenAuthorities } = balance[0]; + expect(tokenAuthorities).toBeDefined(); + expect(tokenAuthorities).toHaveProperty('unlocked.mint'); + expect(tokenAuthorities).toHaveProperty('unlocked.melt'); + expect(tokenAuthorities).toHaveProperty('locked.mint'); + expect(tokenAuthorities).toHaveProperty('locked.melt'); + } finally { + await adapter.stopWallet(wallet); + } + }); + it('should reflect injected funds in balance', async () => { const { wallet } = await adapter.createWallet();