-
Notifications
You must be signed in to change notification settings - Fork 15
Test: Shared getBalance tests for both facades #1051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4b52a9c
310958b
1f68446
9b2989d
dfae5b7
80fa9d1
a20899a
12ba34b
5ba726e
08f37ef
a8a5b83
d121224
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
|
pedroferreira1 marked this conversation as resolved.
|
||
|
|
||
| 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, | ||
| }); | ||
| }); | ||
| }); | ||
|
Comment on lines
+45
to
+66
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't it in the shared tests?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TL;DR: It's blocked by a wallet-service inconsistency (issue #397). Once the wallet-service returns a zero-balance entry instead of an empty array for unknown tokens, this test can be promoted to the shared suite. -- The fullnode facade returns a zero-balance entry (line 60-64 of the test) when you ask for the balance of a token the wallet has never interacted with — the fullnode always synthesizes a response. The wallet-service facade, on the other hand, has a known bug where it returns an empty array for tokens the wallet doesn't hold (see the two Since the two facades don't agree on the return shape for "token not on this wallet," the test can't be written with a single shared assertion. Moving it to the shared suite would either require:
The test was intentionally kept fullnode-specific until the wallet-service behavior converges.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's good to see if the error that is being raised is related to the wallet stopping. Otherwise it could hide something. |
||
| } | ||
| } | ||
| }); | ||
|
|
||
| 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 () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had fixed that and the next one, not sure why it's not working
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried removing all the --
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of this investigation above I ended up identifying another specific test that could to go Shared. It's not perfect since it cannot validate the value, but it's better than a skipped specific test anyway. Done on 08f37ef
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tuliomir did you run with a updated image of wallet-sevice? This is a known issue of fixes not being applied to the integration tests due to the usage of stale container images.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just re-created the images locally with the HEAD at HathorNetwork/hathor-wallet-service@947e90f . This has still failed. The wallet service still does not synthesize a "zero balance" record for HTR when there are no transactions. Could we solve this in a future PR so that we can advance with the Shared Tests suite construction? Future improvements will be easier to implement when we have that in place.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Open an issue for it pls |
||
| ({ 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'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| }); | ||
| }); |

Uh oh!
There was an error while loading. Please reload this page.