diff --git a/__tests__/integration/adapters/fullnode.adapter.ts b/__tests__/integration/adapters/fullnode.adapter.ts index 65233fc83..ca7dba5e7 100644 --- a/__tests__/integration/adapters/fullnode.adapter.ts +++ b/__tests__/integration/adapters/fullnode.adapter.ts @@ -21,7 +21,7 @@ import { import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper'; import { precalculationHelpers } from '../helpers/wallet-precalculation.helper'; import type { WalletStopOptions } from '../../../src/new/types'; -import { NETWORK_NAME } from '../configuration/test-constants'; +import { FULLNODE_URL, NETWORK_NAME } from '../configuration/test-constants'; import type { FuzzyWalletType, IWalletTestAdapter, @@ -51,6 +51,10 @@ export class FullnodeWalletTestAdapter implements IWalletTestAdapter { defaultPassword = DEFAULT_PASSWORD; + originalServerUrl = FULLNODE_URL; + + testnetServerUrl = 'https://node1.testnet.hathor.network/v1a/'; + capabilities: WalletCapabilities = { supportsMultisig: true, supportsTokenScope: true, diff --git a/__tests__/integration/adapters/service.adapter.ts b/__tests__/integration/adapters/service.adapter.ts index fe83e9243..05cc7941a 100644 --- a/__tests__/integration/adapters/service.adapter.ts +++ b/__tests__/integration/adapters/service.adapter.ts @@ -7,6 +7,7 @@ */ import { HathorWalletServiceWallet } from '../../../src'; +import config from '../../../src/config'; import { WalletTracker } from '../utils/wallet-tracker.util'; import type Transaction from '../../../src/models/transaction'; import { @@ -50,6 +51,23 @@ export class ServiceWalletTestAdapter implements IWalletTestAdapter { defaultPassword = SERVICE_PASSWORD; + private _originalServerUrl?: string; + + testnetServerUrl = 'https://wallet-service.testnet.hathor.network/'; + + get originalServerUrl(): string { + if (!this._originalServerUrl) { + throw new Error('originalServerUrl not initialized. Call suiteSetup() first.'); + } + return this._originalServerUrl; + } + + async suiteSetup(): Promise { + initializeServiceGlobalConfigs(); + this._originalServerUrl = config.getWalletServiceBaseUrl(); + await GenesisWalletServiceHelper.start(); + } + capabilities: WalletCapabilities = { supportsMultisig: false, supportsTokenScope: false, @@ -80,11 +98,6 @@ export class ServiceWalletTestAdapter implements IWalletTestAdapter { return wallet as unknown as HathorWalletServiceWallet; } - async suiteSetup(): Promise { - initializeServiceGlobalConfigs(); - await GenesisWalletServiceHelper.start(); - } - async suiteTeardown(): Promise { await this.stopAllWallets(); await GenesisWalletServiceHelper.stop(); diff --git a/__tests__/integration/adapters/types.ts b/__tests__/integration/adapters/types.ts index 79df5a702..ae0a9137b 100644 --- a/__tests__/integration/adapters/types.ts +++ b/__tests__/integration/adapters/types.ts @@ -95,6 +95,20 @@ export interface IWalletTestAdapter { defaultPinCode: string; defaultPassword: string; + /** + * The server URL that changeServer should revert to after tests. + * Fullnode: the fullnode connection URL (e.g. FULLNODE_URL) + * Wallet Service: the wallet-service base URL (e.g. 'http://localhost:3000/dev/') + */ + originalServerUrl: string; + + /** + * A real testnet server URL for validating changeServer via getVersionData. + * Fullnode: a testnet fullnode URL + * Wallet Service: a testnet wallet-service URL + */ + testnetServerUrl: string; + // --- Lifecycle --- /** One-time setup for the test suite (e.g. start genesis wallet, init configs) */ diff --git a/__tests__/integration/fullnode-specific/internal.test.ts b/__tests__/integration/fullnode-specific/internal.test.ts new file mode 100644 index 000000000..280a1f4b3 --- /dev/null +++ b/__tests__/integration/fullnode-specific/internal.test.ts @@ -0,0 +1,55 @@ +/** + * 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 internal method tests. + * + * Tests HathorWallet-only features: debug mode toggling and storage reload + * via onConnectionChangedState. These are side-effect tests that mutate + * wallet state. + * + * Shared internal tests live in `shared/internal.test.ts`. + * Shared server change tests live in `shared/server_changes.test.ts`. + */ + +import HathorWallet from '../../../src/new/wallet'; +import { ConnectionState } from '../../../src/wallet/types'; +import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper'; +import { generateWalletHelper } from '../helpers/wallet.helper'; + +describe('[Fullnode] internal methods', () => { + let gWallet: HathorWallet; + let hWallet: HathorWallet; + + beforeAll(async () => { + const { hWallet: ghWallet } = await GenesisWalletHelper.getSingleton(); + gWallet = ghWallet; + hWallet = await generateWalletHelper(); + }); + + afterAll(async () => { + await hWallet.stop(); + }); + + it('should test the debug methods', async () => { + expect(gWallet.debug).toStrictEqual(false); + + gWallet.enableDebugMode(); + expect(gWallet.debug).toStrictEqual(true); + + gWallet.disableDebugMode(); + expect(gWallet.debug).toStrictEqual(false); + }); + + it('should call processHistory when connection state changes to CONNECTED', async () => { + await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n); + const spy = jest.spyOn(hWallet.storage, 'processHistory'); + // Simulate that we received an event of the connection becoming active + await hWallet.onConnectionChangedState(ConnectionState.CONNECTED); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/__tests__/integration/hathorwallet_others.test.ts b/__tests__/integration/hathorwallet_others.test.ts index 6145a5444..bc086b753 100644 --- a/__tests__/integration/hathorwallet_others.test.ts +++ b/__tests__/integration/hathorwallet_others.test.ts @@ -9,16 +9,10 @@ import { waitUntilNextTimestamp, } from './helpers/wallet.helper'; import { NATIVE_TOKEN_UID, TOKEN_MELT_MASK, TOKEN_MINT_MASK } from '../../src/constants'; -import { - FULLNODE_NETWORK_NAME, - FULLNODE_URL, - NETWORK_NAME, - WALLET_CONSTANTS, -} from './configuration/test-constants'; +import { WALLET_CONSTANTS } from './configuration/test-constants'; import dateFormatter from '../../src/utils/date'; import { AddressError } from '../../src/errors'; import { precalculationHelpers } from './helpers/wallet-precalculation.helper'; -import { ConnectionState } from '../../src/wallet/types'; import HathorWallet from '../../src/new/wallet'; import { MemoryStore } from '../../src/storage'; import { IHistoryTx } from '../../src/types'; @@ -1617,94 +1611,6 @@ describe('getAuthorityUtxos', () => { }); }); -// This section tests methods that have side effects impacting the whole wallet. Executing it last. -describe('internal methods', () => { - /** @type HathorWallet */ - let gWallet; - /** @type HathorWallet */ - let hWallet; - beforeAll(async () => { - const { hWallet: ghWallet } = await GenesisWalletHelper.getSingleton(); - gWallet = ghWallet; - hWallet = await generateWalletHelper(); - }); - - afterAll(async () => { - hWallet.stop(); - await GenesisWalletHelper.clearListeners(); - await gWallet.stop(); - }); - - it('should test the debug methods', async () => { - expect(gWallet.debug).toStrictEqual(false); - - gWallet.enableDebugMode(); - expect(gWallet.debug).toStrictEqual(true); - - gWallet.disableDebugMode(); - expect(gWallet.debug).toStrictEqual(false); - }); - - it('should test network-related methods', async () => { - // GetServerUrl fetching from the live fullnode connection - expect(await gWallet.getServerUrl()).toStrictEqual(FULLNODE_URL); - expect(await gWallet.getNetwork()).toStrictEqual(NETWORK_NAME); - expect(await gWallet.getNetworkObject()).toMatchObject({ - name: NETWORK_NAME, - versionBytes: { p2pkh: 73, p2sh: 135 }, // Calculated for the privnet.py config file - bitcoreNetwork: { - name: expect.stringContaining(NETWORK_NAME), - alias: 'test', // this is the alias for the testnet network - pubkeyhash: 73, - scripthash: 135, - }, - }); - - // GetVersionData fetching from the live fullnode server - expect(await gWallet.getVersionData()).toMatchObject({ - timestamp: expect.any(Number), - version: expect.any(String), - network: FULLNODE_NETWORK_NAME, - minWeight: expect.any(Number), - minTxWeight: expect.any(Number), - minTxWeightCoefficient: expect.any(Number), - minTxWeightK: expect.any(Number), - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: expect.any(Number), - maxNumberInputs: 255, - maxNumberOutputs: 255, - }); - }); - - it('should change servers', async () => { - // Changing from our integration test privatenet to the testnet - gWallet.changeServer('https://node1.testnet.hathor.network/v1a/'); - const serverChangeTime = Date.now().valueOf(); - await delay(100); - - // Validating the server change with getVersionData - let networkData = await gWallet.getVersionData(); - expect(networkData.timestamp).toBeGreaterThan(serverChangeTime); - expect(networkData.network).toMatch(/^testnet.*/); - - await gWallet.changeServer(FULLNODE_URL); - await delay(100); - - // Reverting to the privatenet - networkData = await gWallet.getVersionData(); - expect(networkData.timestamp).toBeGreaterThan(serverChangeTime + 200); - expect(networkData.network).toStrictEqual(FULLNODE_NETWORK_NAME); - }); - - it('should reload the storage', async () => { - await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n); - const spy = jest.spyOn(hWallet.storage, 'processHistory'); - // Simulate that we received an event of the connection becoming active - await hWallet.onConnectionChangedState(ConnectionState.CONNECTED); - expect(spy).toHaveBeenCalledTimes(1); - }); -}); - describe('index-limit address scanning policy', () => { /** @type HathorWallet */ let hWallet; diff --git a/__tests__/integration/shared/internal.test.ts b/__tests__/integration/shared/internal.test.ts new file mode 100644 index 000000000..f480b4639 --- /dev/null +++ b/__tests__/integration/shared/internal.test.ts @@ -0,0 +1,137 @@ +/** + * 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. + */ + +import axios from 'axios'; +import type { FuzzyWalletType, IWalletTestAdapter } from '../adapters/types'; +import { FullnodeWalletTestAdapter } from '../adapters/fullnode.adapter'; +import { ServiceWalletTestAdapter } from '../adapters/service.adapter'; +import { FULLNODE_NETWORK_NAME, FULLNODE_URL, NETWORK_NAME } from '../configuration/test-constants'; +import Network from '../../../src/models/network'; +import { loggers } from '../utils/logger.util'; + +// XXX: onConnectionChangedState has different behavior between facades +// (fullnode calls reloadStorage/processHistory, service emits 'reload-data'). +// It needs refactoring before it can be tested here as a shared test. + +const adapters: IWalletTestAdapter[] = [ + new FullnodeWalletTestAdapter(), + new ServiceWalletTestAdapter(), +]; + +/** + * Minimum expected shape for getVersionData across both facades. + * Both facades should query the fullnode /version endpoint and return + * the same data. If a backend inconsistency is found for a specific + * facade, adjust the corresponding adapter's `versionDataOverrides` + * or skip individual fields here rather than duplicating the test. + */ +const baseVersionDataExpectation = { + timestamp: expect.any(Number), + version: expect.any(String), + network: FULLNODE_NETWORK_NAME, + minWeight: expect.any(Number), + minTxWeight: expect.any(Number), + minTxWeightCoefficient: expect.any(Number), + minTxWeightK: expect.any(Number), + tokenDepositPercentage: expect.any(Number), + rewardSpendMinBlocks: expect.any(Number), + maxNumberInputs: expect.any(Number), + maxNumberOutputs: expect.any(Number), +}; + +describe.each(adapters)('[Shared] internal methods — $name', adapter => { + beforeAll(async () => { + await adapter.suiteSetup(); + }); + + afterAll(async () => { + await adapter.suiteTeardown(); + }); + + describe('network query methods', () => { + let wallet: FuzzyWalletType; + + beforeAll(async () => { + const result = await adapter.createWallet(); + wallet = result.wallet; + }); + + afterAll(async () => { + await adapter.stopWallet(wallet); + }); + + it('getServerUrl returns the configured fullnode URL', () => { + expect(wallet.getServerUrl()).toBe(FULLNODE_URL); + }); + + it('getNetwork returns the correct network name', () => { + expect(wallet.getNetwork()).toBe(NETWORK_NAME); + }); + + it('getNetworkObject returns a Network instance with correct properties', () => { + const networkObj = wallet.getNetworkObject(); + expect(networkObj).toBeInstanceOf(Network); + expect(networkObj.name).toBe(NETWORK_NAME); + expect(networkObj).toMatchObject({ + versionBytes: { p2pkh: 73, p2sh: 135 }, + bitcoreNetwork: { + name: expect.stringContaining(NETWORK_NAME), + alias: 'test', + pubkeyhash: 73, + scripthash: 135, + }, + }); + }); + + it('getVersionData returns valid version info from the fullnode', async () => { + const versionData = await wallet.getVersionData(); + expect(versionData).toMatchObject(baseVersionDataExpectation); + }); + + it('getVersionData matches data from a direct fullnode request', async () => { + const versionData = await wallet.getVersionData(); + + // The raw fullnode /version endpoint returns snake_case keys. + // Both facades transform these to camelCase in getVersionData(). + const directResponse = await axios + .get('version', { + baseURL: FULLNODE_URL, + headers: { 'Content-Type': 'application/json' }, + }) + .catch(e => { + loggers.test!.log(`Received an error on /version: ${e}`); + if (e.response) { + return e.response; + } + throw e; + }); + expect(directResponse.status).toBe(200); + + // Map raw snake_case keys to the camelCase keys returned by the facades. + // This allows us to verify both facades faithfully represent the fullnode. + const rawToFacade: Record = { + version: 'version', + network: 'network', + min_weight: 'minWeight', + min_tx_weight: 'minTxWeight', + min_tx_weight_coefficient: 'minTxWeightCoefficient', + min_tx_weight_k: 'minTxWeightK', + token_deposit_percentage: 'tokenDepositPercentage', + reward_spend_min_blocks: 'rewardSpendMinBlocks', + max_number_inputs: 'maxNumberInputs', + max_number_outputs: 'maxNumberOutputs', + }; + + const fullnodeData = directResponse.data; + for (const [rawKey, facadeKey] of Object.entries(rawToFacade)) { + expect(fullnodeData).toHaveProperty(rawKey); + expect(versionData).toHaveProperty(facadeKey); + expect(versionData[facadeKey]).toStrictEqual(fullnodeData[rawKey]); + } + }); + }); +}); diff --git a/__tests__/integration/shared/server_changes.test.ts b/__tests__/integration/shared/server_changes.test.ts new file mode 100644 index 000000000..1a3d3dfad --- /dev/null +++ b/__tests__/integration/shared/server_changes.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. + */ + +/** + * Shared changeServer tests. + * + * Both facades implement changeServer on IHathorWallet. Although the + * underlying URL they modify differs (fullnode connection URL vs. + * wallet-service base URL), getVersionData() is the universal + * observable side-effect: it routes through whichever URL changeServer + * modifies, returning FullNodeVersionData with a `network` field that + * distinguishes testnet from privatenet. + * + * Each adapter provides: + * - `originalServerUrl`: the URL to revert to after tests + * - `testnetServerUrl`: a real testnet endpoint for validation + */ + +import type { FuzzyWalletType, IWalletTestAdapter } from '../adapters/types'; +import { delay } from '../utils/core.util'; +import { FullnodeWalletTestAdapter } from '../adapters/fullnode.adapter'; +import { ServiceWalletTestAdapter } from '../adapters/service.adapter'; +import { FULLNODE_NETWORK_NAME } from '../configuration/test-constants'; + +const adapters: IWalletTestAdapter[] = [ + new FullnodeWalletTestAdapter(), + new ServiceWalletTestAdapter(), +]; + +describe.each(adapters)('[Shared] server changes — $name', adapter => { + // Captured after suiteSetup (config initialized) but before any test + // mutates it via changeServer. Safe to use in afterAll for revert. + let serverUrlBeforeTests: string; + + beforeAll(async () => { + await adapter.suiteSetup(); + serverUrlBeforeTests = adapter.originalServerUrl; + }); + + afterAll(async () => { + await adapter.suiteTeardown(); + }); + + describe('changeServer', () => { + let wallet: FuzzyWalletType; + + beforeAll(async () => { + const result = await adapter.createWallet(); + wallet = result.wallet; + }); + + afterAll(async () => { + if (wallet) { + await wallet.changeServer(serverUrlBeforeTests); + await adapter.stopWallet(wallet); + } + }); + + it('should change to a testnet server and verify via getVersionData', async () => { + await wallet.changeServer(adapter.testnetServerUrl); + + try { + await delay(100); + + const testnetData = await wallet.getVersionData(); + expect(testnetData.network).toMatch(/^testnet.*/); + } finally { + // Always revert, even if assertions fail + await wallet.changeServer(serverUrlBeforeTests); + } + + await delay(100); + + // Verify the revert to the privatenet + const revertedData = await wallet.getVersionData(); + expect(revertedData.network).toStrictEqual(FULLNODE_NETWORK_NAME); + }); + }); +}); diff --git a/__tests__/integration/walletservice_facade.test.ts b/__tests__/integration/walletservice_facade.test.ts index 95c9a827a..7b8146312 100644 --- a/__tests__/integration/walletservice_facade.test.ts +++ b/__tests__/integration/walletservice_facade.test.ts @@ -1,16 +1,7 @@ -import axios from 'axios'; -import config from '../../src/config'; -import { loggers } from './utils/logger.util'; import HathorWalletServiceWallet from '../../src/wallet/wallet'; import { CreateTokenTransaction, FeeHeader, Output, transactionUtils } from '../../src'; -import { - FULLNODE_NETWORK_NAME, - FULLNODE_URL, - NETWORK_NAME, - WALLET_CONSTANTS, -} from './configuration/test-constants'; +import { WALLET_CONSTANTS } from './configuration/test-constants'; import { NATIVE_TOKEN_UID, TOKEN_MELT_MASK, TOKEN_MINT_MASK } from '../../src/constants'; -import Network from '../../src/models/network'; import { buildWalletInstance, emptyWallet, @@ -162,76 +153,6 @@ afterAll(async () => { await GenesisWalletServiceHelper.stop(); }); -describe('wallet public methods', () => { - beforeEach(async () => { - ({ wallet } = buildWalletInstance({ words: emptyWallet.words })); - await wallet.start({ pinCode, password }); - }); - - afterEach(async () => { - if (wallet) { - await wallet.stop({ cleanStorage: true }); - } - }); - - it('getServerUrl returns the configured base URL', () => { - expect(wallet.getServerUrl()).toBe(FULLNODE_URL); - }); - - it('getVersionData returns valid version info', async () => { - const versionData = await wallet.getVersionData(); - expect(versionData).toBeDefined(); - expect(versionData).toEqual( - expect.objectContaining({ - timestamp: expect.any(Number), - version: expect.any(String), - network: FULLNODE_NETWORK_NAME, - minWeight: expect.any(Number), - minTxWeight: expect.any(Number), - minTxWeightCoefficient: expect.any(Number), - minTxWeightK: expect.any(Number), - tokenDepositPercentage: expect.any(Number), - rewardSpendMinBlocks: expect.any(Number), - maxNumberInputs: expect.any(Number), - maxNumberOutputs: expect.any(Number), - decimalPlaces: expect.any(Number), - nativeTokenName: expect.any(String), - nativeTokenSymbol: expect.any(String), - }) - ); - - // Make sure it contains the same data as a direct fullnode request - const fullnodeResponse = await axios - .get('version', { - baseURL: config.getWalletServiceBaseUrl(), - headers: { - 'Content-Type': 'application/json', - }, - }) - .catch(e => { - loggers.test!.log(`Received an error on /version: ${e}`); - if (e.response) { - return e.response; - } - return {}; - }); - expect(fullnodeResponse.status).toBe(200); - expect(fullnodeResponse.data?.success).toBe(true); - - expect(versionData).toEqual(fullnodeResponse.data.data); - }); - - it('getNetwork returns the correct network name', () => { - expect(wallet.getNetwork()).toBe(NETWORK_NAME); - }); - - it('getNetworkObject returns a Network instance with correct name', () => { - const networkObj = wallet.getNetworkObject(); - expect(networkObj).toBeInstanceOf(Network); - expect(networkObj.name).toBe(NETWORK_NAME); - }); -}); - describe('empty wallet address methods', () => { const knownAddresses = emptyWallet.addresses; const unknownAddress = WALLET_CONSTANTS.miner.addresses[0]; diff --git a/src/new/wallet.ts b/src/new/wallet.ts index ad7f0b651..6c31b23a4 100644 --- a/src/new/wallet.ts +++ b/src/new/wallet.ts @@ -470,7 +470,7 @@ class HathorWallet extends EventEmitter { * @memberof HathorWallet * @inner * */ - changeServer(newServer: string): void { + async changeServer(newServer: string): Promise { this.storage.config.setServerUrl(newServer); } diff --git a/src/wallet/types.ts b/src/wallet/types.ts index e714e0d3e..9f70d1652 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -417,6 +417,7 @@ export interface IHathorWallet { checkPin(pin: string): Promise; checkPassword(password: string): Promise; checkPinAndPassword(pin: string, password: string): Promise; + changeServer(newServer: string): Promise; getServerUrl(): string; getNetwork(): string; getAddressPathForIndex(index: number): Promise;