Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
828150c
test: shared sendTransaction tests for both facades
tuliomir Mar 31, 2026
ca9c346
Merge remote-tracking branch 'origin/master' into test/shared-send-tr…
tuliomir Apr 1, 2026
e2624ee
fix(test): resolve integration test failures
tuliomir Apr 1, 2026
6240d5a
fix: null guard
tuliomir Apr 1, 2026
9acf2d4
fix(test): ensure walletTxs cleanup and assertions
tuliomir Apr 1, 2026
fc3fb68
Merge branch 'master' into test/shared-send-transaction
tuliomir Apr 1, 2026
baf474f
fix(test): address PR review comments for send-transaction tests
tuliomir Apr 1, 2026
59ab89a
refact(test): move service-specific sendTransaction tests to shared
tuliomir Apr 2, 2026
629eed7
chore: remove unused imports in hathorwallet_facade test
tuliomir Apr 2, 2026
89ccc84
test: migrate token send tests to shared suite
tuliomir Apr 2, 2026
2f7804f
test: migrate address tracking test to shared suite
tuliomir Apr 2, 2026
9490cee
fix: address review comments and itest failures
tuliomir Apr 2, 2026
43059a9
test: add fee token edge case tests
tuliomir Apr 2, 2026
ef3e2b1
fix: native token version defaulting to deposit (#1054)
raul-oliveira Apr 2, 2026
a8ad5f7
fix: isolates change address test
tuliomir Apr 3, 2026
c1bcc92
chore: workaround for input bug
tuliomir Apr 6, 2026
3e77f0f
Merge branch 'master' into test/shared-send-transaction
tuliomir Apr 6, 2026
4c027f2
fix: address PR review on fee validation
tuliomir Apr 8, 2026
dc321ca
Merge remote-tracking branch 'origin/master' into test/shared-send-tr…
tuliomir Apr 8, 2026
5b978aa
fix: linter
tuliomir Apr 8, 2026
b81a2d0
docs: improves comment messages
tuliomir Apr 9, 2026
18bc8e9
Merge remote-tracking branch 'origin/master'
tuliomir Apr 17, 2026
fdef446
feat(test): add recvWallet option to adapters
tuliomir Apr 22, 2026
10e61f1
test: address Carneiro's review on PR 1053
tuliomir Apr 22, 2026
d200936
refactor(test): migrate remaining sendTransaction
tuliomir Apr 22, 2026
a3147fd
Merge branch 'master' into test/shared-send-transaction
tuliomir Apr 22, 2026
4a58909
refact(test): move recvWallet to waitForTx helper
tuliomir Apr 22, 2026
b556e5c
fix(test): restore dropped coverage from migration
tuliomir Apr 22, 2026
87ff71d
fix(test): fix integration test failures
tuliomir Apr 23, 2026
eef4dad
refact(test): rename TTS to DBT for clarity
tuliomir Apr 23, 2026
9619ae2
Merge remote-tracking branch 'origin/master' into test/shared-send-tr…
tuliomir Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions __tests__/integration/adapters/fullnode.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type {
WalletCapabilities,
CreateWalletOptions,
CreateWalletResult,
SendTransactionOptions,
SendTransactionResult,
} from './types';
import type { PrecalculatedWalletData } from '../helpers/wallet-precalculation.helper';

Expand Down Expand Up @@ -180,6 +182,22 @@ export class FullnodeWalletTestAdapter implements IWalletTestAdapter {
return precalculationHelpers.test!.getPrecalculatedWallet();
}

async sendTransaction(
Comment thread
r4mmer marked this conversation as resolved.
wallet: FuzzyWalletType,
address: string,
amount: bigint,
options?: SendTransactionOptions
): Promise<SendTransactionResult> {
const hWallet = this.concrete(wallet);
const result = await hWallet.sendTransaction(address, amount, {
pinCode: DEFAULT_PIN_CODE,
...options,
});
await waitForTxReceived(hWallet, result.hash);
await waitUntilNextTimestamp(hWallet, result.hash);
return { hash: result.hash };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// --- Private helpers ---

/**
Expand Down
20 changes: 20 additions & 0 deletions __tests__/integration/adapters/service.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
WalletCapabilities,
CreateWalletOptions,
CreateWalletResult,
SendTransactionOptions,
SendTransactionResult,
} from './types';
import type { PrecalculatedWalletData } from '../helpers/wallet-precalculation.helper';

Expand Down Expand Up @@ -200,4 +202,22 @@ export class ServiceWalletTestAdapter implements IWalletTestAdapter {
getPrecalculatedWallet(): PrecalculatedWalletData {
return precalculationHelpers.test!.getPrecalculatedWallet();
}

async sendTransaction(
wallet: FuzzyWalletType,
address: string,
amount: bigint,
options?: SendTransactionOptions
): Promise<SendTransactionResult> {
const sw = this.concrete(wallet);
const result = await sw.sendTransaction(address, amount, {
...options,
pinCode: SERVICE_PIN,
});
if (!result.hash) {
throw new Error('sendTransaction: transaction had no hash');
}
await pollForTx(sw, result.hash);
return { hash: result.hash };
}
}
29 changes: 29 additions & 0 deletions __tests__/integration/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,33 @@ export interface IWalletTestAdapter {

/** Returns a fresh precalculated wallet for tests that need one */
getPrecalculatedWallet(): PrecalculatedWalletData;

// --- Transaction operations ---

/**
* Sends a transaction from the wallet to the given address.
* Handles pinCode injection for facades that require per-call credentials.
* Returns a normalized result with at least a `hash` field.
*/
sendTransaction(
wallet: FuzzyWalletType,
address: string,
amount: bigint,
options?: SendTransactionOptions
): Promise<SendTransactionResult>;
}

/**
* Options for sending a transaction via the adapter.
*/
export interface SendTransactionOptions {
token?: string;
changeAddress?: string;
}

/**
* Result of sending a transaction.
*/
export interface SendTransactionResult {
hash: string;
}
277 changes: 277 additions & 0 deletions __tests__/integration/fullnode-specific/send-transaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/**
* 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 sendTransaction tests.
*
* Tests that rely on fullnode-only APIs: storage.getAddressInfo,
* custom token transactions, fee tokens, multisig, sendManyOutputsTransaction.
*
* Shared sendTransaction tests live in `shared/send-transaction.test.ts`.
*/

import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper';
import {
createTokenHelper,
DEFAULT_PIN_CODE,
generateMultisigWalletHelper,
generateWalletHelper,
stopAllWallets,
waitForTxReceived,
waitUntilNextTimestamp,
} from '../helpers/wallet.helper';
import { NATIVE_TOKEN_UID } from '../../../src/constants';
import SendTransaction from '../../../src/new/sendTransaction';
import transaction from '../../../src/utils/transaction';
import { TokenVersion } from '../../../src/types';
import FeeHeader from '../../../src/headers/fee';

/**
* Validates the total fee amount in a list of headers.
*/
function validateFeeAmount(headers: unknown[], expectedFee: bigint) {

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.

headers can be a instante of Header[]

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.

Thanks, implemented on baf474f

const feeHeaders = headers.filter(h => h instanceof FeeHeader);
expect(feeHeaders).toHaveLength(1);
const totalFee = (feeHeaders[0] as FeeHeader).entries.reduce(
(sum, entry) => sum + entry.amount,
0n
);
expect(totalFee).toBe(expectedFee);
}

describe('[Fullnode] sendTransaction — address tracking', () => {
afterEach(async () => {
await stopAllWallets();
});

it('should track address usage for HTR transactions', async () => {
const hWallet = await generateWalletHelper();
await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n);

const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(2), 6n);
await waitForTxReceived(hWallet, tx1.hash);

expect(tx1).toMatchObject({
hash: expect.any(String),
inputs: expect.any(Array),
outputs: expect.any(Array),
version: expect.any(Number),
weight: expect.any(Number),
nonce: expect.any(Number),
timestamp: expect.any(Number),
parents: expect.any(Array),
tokens: expect.any(Array),
});

expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(0))).toHaveProperty(
'numTransactions',
2
);
expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(1))).toHaveProperty(
'numTransactions',
1
);
expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(2))).toHaveProperty(
'numTransactions',
1
);
Comment thread
r4mmer marked this conversation as resolved.

const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton();
await waitUntilNextTimestamp(hWallet, tx1.hash);
const { hash: tx2Hash } = await hWallet.sendTransaction(
await gWallet.getAddressAtIndex(0),
8n,
{ changeAddress: await hWallet.getAddressAtIndex(5) }
);
await waitForTxReceived(hWallet, tx2Hash);

const htrBalance = await hWallet.getBalance(NATIVE_TOKEN_UID);
expect(htrBalance[0].balance.unlocked).toEqual(2n);

expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(5))).toHaveProperty(
'numTransactions',
1
);
expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(6))).toHaveProperty(
'numTransactions',
0
);
});

it('should send custom token transactions', async () => {
const hWallet = await generateWalletHelper();
await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n);
const { hash: tokenUid } = await createTokenHelper(hWallet, 'Token to Send', 'TTS', 100n);

const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(5), 30n, {
token: tokenUid,
changeAddress: await hWallet.getAddressAtIndex(6),
});
await waitForTxReceived(hWallet, tx1.hash);

let htrBalance = await hWallet.getBalance(tokenUid);
expect(htrBalance[0].balance.unlocked).toEqual(100n);

expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(5))).toHaveProperty(
'numTransactions',
1
);
expect(await hWallet.storage.getAddressInfo(await hWallet.getAddressAtIndex(6))).toHaveProperty(
'numTransactions',
1
);

const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton();
await waitUntilNextTimestamp(hWallet, tx1.hash);
const { hash: tx2Hash } = await hWallet.sendTransaction(
await gWallet.getAddressAtIndex(0),
80n,
{
token: tokenUid,
changeAddress: await hWallet.getAddressAtIndex(12),
}
);
await waitForTxReceived(hWallet, tx2Hash);
await waitForTxReceived(gWallet, tx2Hash);

htrBalance = await hWallet.getBalance(tokenUid);
expect(htrBalance[0].balance.unlocked).toEqual(20n);
});

it('should send custom fee token transactions', async () => {
const hWallet = await generateWalletHelper();
await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n);
const { hash: tokenUid } = await createTokenHelper(hWallet, 'FeeBasedToken', 'FBT', 8582n, {
tokenVersion: TokenVersion.FEE,
});

const tx1 = await hWallet.sendTransaction(await hWallet.getAddressAtIndex(5), 8000n, {
token: tokenUid,
changeAddress: await hWallet.getAddressAtIndex(6),
});
validateFeeAmount(tx1.headers, 2n);
await waitForTxReceived(hWallet, tx1.hash);

let fbtBalance = await hWallet.getBalance(tokenUid);
expect(fbtBalance[0].balance.unlocked).toEqual(8582n);

const { hWallet: gWallet } = await GenesisWalletHelper.getSingleton();
await waitUntilNextTimestamp(hWallet, tx1.hash);
const { hash: tx2Hash, headers: tx2Headers } = await hWallet.sendTransaction(
await gWallet.getAddressAtIndex(0),
82n,
{
token: tokenUid,
changeAddress: await hWallet.getAddressAtIndex(12),
}
);
validateFeeAmount(tx2Headers, 2n);
await waitForTxReceived(hWallet, tx2Hash);
await waitForTxReceived(gWallet, tx2Hash);

fbtBalance = await hWallet.getBalance(tokenUid);
expect(fbtBalance[0].balance.unlocked).toEqual(8500n);

const htrBalance = await hWallet.getBalance(NATIVE_TOKEN_UID);
expect(htrBalance[0].balance.unlocked).toEqual(5n);
});

it('should send fee token with manually provided HTR input (no HTR output)', async () => {
const hWallet = await generateWalletHelper();
await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 10n);
const { hash: tokenUid } = await createTokenHelper(
hWallet,
'FeeTokenManualInput',
'FTMI',
100n,
{ tokenVersion: TokenVersion.FEE }
);

const { utxos: utxosHtr } = await hWallet.getUtxos({ token: NATIVE_TOKEN_UID });
const { utxos: utxosToken } = await hWallet.getUtxos({ token: tokenUid });

const htrUtxo = utxosHtr[0];
const tokenUtxo = utxosToken[0];

const tx = await hWallet.sendManyOutputsTransaction(
[
{
address: await hWallet.getAddressAtIndex(5),
value: 50n,
token: tokenUid,
},
],
{
inputs: [
{ txId: htrUtxo.tx_id, token: NATIVE_TOKEN_UID, index: htrUtxo.index },
{ txId: tokenUtxo.tx_id, token: tokenUid, index: tokenUtxo.index },
],
}
);
validateFeeAmount(tx.headers, 2n);
await waitForTxReceived(hWallet, tx.hash);

const decodedTx = await hWallet.getTx(tx.hash);
expect(decodedTx.inputs).toHaveLength(2);
expect(decodedTx.outputs).toContainEqual(
expect.objectContaining({ value: 50n, token: tokenUid })
);
});

it('should send a multisig transaction', async () => {
const mhWallet1 = await generateMultisigWalletHelper({ walletIndex: 0 });
const mhWallet2 = await generateMultisigWalletHelper({ walletIndex: 1 });
const mhWallet3 = await generateMultisigWalletHelper({ walletIndex: 2 });
await GenesisWalletHelper.injectFunds(mhWallet1, await mhWallet1.getAddressAtIndex(0), 10n);

const { tx_id: inputTxId, index: inputIndex } = (await mhWallet1.getUtxos()).utxos[0];
const network = mhWallet1.getNetworkObject();
const sendTransaction = new SendTransaction({
storage: mhWallet1.storage,
inputs: [{ txId: inputTxId, index: inputIndex }],
outputs: [
{
address: await mhWallet1.getAddressAtIndex(1),
value: 10n,
token: NATIVE_TOKEN_UID,
},
],
});
const tx = transaction.createTransactionFromData(
{ version: 1, ...(await sendTransaction.prepareTxData()) },
network
);
const txHex = tx.toHex();

const sig1 = await mhWallet1.getAllSignatures(txHex, DEFAULT_PIN_CODE);
const sig2 = await mhWallet2.getAllSignatures(txHex, DEFAULT_PIN_CODE);
const sig3 = await mhWallet3.getAllSignatures(txHex, DEFAULT_PIN_CODE);

await waitUntilNextTimestamp(mhWallet1, inputTxId);

const partiallyAssembledTx = await mhWallet1.assemblePartialTransaction(txHex, [
sig1,
sig2,
sig3,
]);
partiallyAssembledTx.prepareToSend();
const finalTx = new SendTransaction({
storage: mhWallet1.storage,
transaction: partiallyAssembledTx,
});

const sentTx = await finalTx.runFromMining();
expect(sentTx).toHaveProperty('hash');
await waitForTxReceived(mhWallet1, sentTx.hash, 10000);

const historyTx = await mhWallet1.getTx(sentTx.hash);
expect(historyTx).toMatchObject({
tx_id: partiallyAssembledTx.hash,
inputs: [expect.objectContaining({ tx_id: inputTxId, value: 10n })],
});
});
});
Loading
Loading