Skip to content
Merged
6 changes: 5 additions & 1 deletion __tests__/integration/adapters/fullnode.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -51,6 +51,10 @@ export class FullnodeWalletTestAdapter implements IWalletTestAdapter {

defaultPassword = DEFAULT_PASSWORD;

originalServerUrl = FULLNODE_URL;

testnetServerUrl = 'https://node1.testnet.hathor.network/v1a/';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using the published url?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to switch to another valid network to validate the connection works well and the internal methods return actual content and not stale cache. The public testnet seemed like a good candidate for that.


capabilities: WalletCapabilities = {
supportsMultisig: true,
supportsTokenScope: true,
Expand Down
23 changes: 18 additions & 5 deletions __tests__/integration/adapters/service.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<void> {
initializeServiceGlobalConfigs();
this._originalServerUrl = config.getWalletServiceBaseUrl();
await GenesisWalletServiceHelper.start();
}
Comment thread
tuliomir marked this conversation as resolved.

capabilities: WalletCapabilities = {
supportsMultisig: false,
supportsTokenScope: false,
Expand Down Expand Up @@ -80,11 +98,6 @@ export class ServiceWalletTestAdapter implements IWalletTestAdapter {
return wallet as unknown as HathorWalletServiceWallet;
}

async suiteSetup(): Promise<void> {
initializeServiceGlobalConfigs();
await GenesisWalletServiceHelper.start();
}

async suiteTeardown(): Promise<void> {
await this.stopAllWallets();
await GenesisWalletServiceHelper.stop();
Expand Down
14 changes: 14 additions & 0 deletions __tests__/integration/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
55 changes: 55 additions & 0 deletions __tests__/integration/fullnode-specific/internal.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
96 changes: 1 addition & 95 deletions __tests__/integration/hathorwallet_others.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1617,94 +1611,6 @@ describe('getAuthorityUtxos', () => {
});
});

// This section tests methods that have side effects impacting the whole wallet. Executing it last.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

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;
Expand Down
137 changes: 137 additions & 0 deletions __tests__/integration/shared/internal.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
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]);
}
});
});
});
Loading
Loading