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..74ddd64e4 --- /dev/null +++ b/__tests__/integration/fullnode-specific/get-balance.test.ts @@ -0,0 +1,66 @@ +/** + * 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 behavior (e.g. no-arg getBalance() + * rejection, nonexistent token returning a zero-balance entry). + * + * 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 } from '../helpers/wallet.helper'; + +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 return zero balance for a nonexistent token', async () => { + const hWallet = await generateWalletHelper(); + + const emptyBalance = await hWallet.getBalance(fakeTokenUid); + expect(emptyBalance).toHaveLength(1); + expect(emptyBalance[0]).toMatchObject({ + token: { id: fakeTokenUid }, + balance: { unlocked: 0n, locked: 0n }, + transactions: 0, + }); + }); + + it('should not show custom token balance on a different wallet', async () => { + const hWallet = await generateWalletHelper(); + + 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 { 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..37abbd3a6 --- /dev/null +++ b/__tests__/integration/service-specific/get-balance.test.ts @@ -0,0 +1,83 @@ +/** + * 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); + expect(addr).toBeDefined(); + await adapter.injectFunds(w, addr as string, 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(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 }); + + 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); + }); + + 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..3fe1fe6ba --- /dev/null +++ b/__tests__/integration/shared/get-balance.test.ts @@ -0,0 +1,151 @@ +/** + * 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 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(); + + 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); + } + }); + + 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); + } + }); +}); diff --git a/__tests__/integration/walletservice_facade.test.ts b/__tests__/integration/walletservice_facade.test.ts index efbb6bbf7..bdf978e33 100644 --- a/__tests__/integration/walletservice_facade.test.ts +++ b/__tests__/integration/walletservice_facade.test.ts @@ -1064,94 +1064,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;