Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
139 changes: 135 additions & 4 deletions __tests__/integration/storage/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
waitForWalletReady,
waitForTxReceived,
} from '../helpers/wallet.helper';
import { InvalidPasswdError } from '../../../src/errors';
import { InvalidPasswdError, SendTxError } from '../../../src/errors';
import HathorWallet from '../../../src/new/wallet';
import { loggers } from '../utils/logger.util';
import SendTransaction from '../../../src/new/sendTransaction';
Expand Down Expand Up @@ -83,12 +83,15 @@ describe('locked utxos', () => {
],
pin: 'xxx', // instance with wrong pin to validate pin as parameter
});
await expect(sendTx.prepareTx()).rejects.toThrow(InvalidPasswdError);
// prepareTx creates an unsigned transaction (pin is not validated here)
await sendTx.prepareTx();
// signTx with the wrong pin should fail
await expect(sendTx.signTx()).rejects.toThrow(InvalidPasswdError);
// Will work with the correct PIN as parameter
await sendTx.prepareTx(DEFAULT_PIN_CODE);
await sendTx.signTx(DEFAULT_PIN_CODE);
await sendTx.updateOutputSelected(true);
// This shouldn't fail since if we did not have tokens the prepareTx should have failed
const input = sendTx.transaction.inputs[0];
const input = sendTx.transaction!.inputs[0];
const utxoId = { txId: input.hash, index: input.index };
await expect(hwallet.storage.isUtxoSelectedAsInput(utxoId)).resolves.toBe(true);
// Send a transaction spending the only utxo on the wallet.
Expand All @@ -104,6 +107,134 @@ describe('locked utxos', () => {
const storageMem = new Storage(storeMem);
await testUnlockWhenSpent(storageMem, walletDataMem);
});

it('should wrap prepareTx errors into SendTxError', async () => {
const walletData = precalculationHelpers.test.getPrecalculatedWallet();
const store = new MemoryStore();
const storage = new Storage(store);
const hwallet = await startWallet(storage, walletData);
const address = await hwallet.getAddressAtIndex(0);
await GenesisWalletHelper.injectFunds(hwallet, address, 1n);

const sendTx = new SendTransaction({
storage: hwallet.storage,
outputs: [
{
type: 'p2pkh',
address: await hwallet.getAddressAtIndex(1),
// Sending more than available should cause prepareTx to fail
value: 9999n,
token: NATIVE_TOKEN_UID,
},
],
});

await expect(sendTx.prepareTx()).rejects.toThrow(SendTxError);
});
});

describe('run(until) state machine', () => {
afterEach(async () => {
await stopWallets();
await stopAllWallets();
await GenesisWalletHelper.clearListeners();
});

it('should stop at prepare-tx and resume to completion', async () => {
const walletData = precalculationHelpers.test.getPrecalculatedWallet();
const store = new MemoryStore();
const storage = new Storage(store);
const hwallet = await startWallet(storage, walletData);
const address = await hwallet.getAddressAtIndex(0);
await GenesisWalletHelper.injectFunds(hwallet, address, 2n);

const sendTx = new SendTransaction({
storage: hwallet.storage,
pin: DEFAULT_PIN_CODE,
outputs: [
{
type: 'p2pkh',
address: await hwallet.getAddressAtIndex(1),
value: 1n,
token: NATIVE_TOKEN_UID,
},
],
});

// Stop after prepare — transaction should exist but be unsigned (no input data)
const preparedTx = await sendTx.run('prepare-tx');
expect(preparedTx).toBeDefined();
expect(preparedTx.inputs.length).toBeGreaterThan(0);
for (const input of preparedTx.inputs) {
expect(input.data).toBeNull();
}

// Resume from where we left off — should sign, mine and push
const finalTx = await sendTx.run(null);
expect(finalTx.hash).toBeDefined();
await waitForTxReceived(hwallet, finalTx.hash);
});

it('should stop at sign-tx and resume to completion', async () => {
const walletData = precalculationHelpers.test.getPrecalculatedWallet();
const store = new MemoryStore();
const storage = new Storage(store);
const hwallet = await startWallet(storage, walletData);
const address = await hwallet.getAddressAtIndex(0);
await GenesisWalletHelper.injectFunds(hwallet, address, 2n);

const sendTx = new SendTransaction({
storage: hwallet.storage,
pin: DEFAULT_PIN_CODE,
outputs: [
{
type: 'p2pkh',
address: await hwallet.getAddressAtIndex(1),
value: 1n,
token: NATIVE_TOKEN_UID,
},
],
});

// Stop after sign — transaction should be signed (input data populated)
const signedTx = await sendTx.run('sign-tx');
expect(signedTx).toBeDefined();
for (const input of signedTx.inputs) {
expect(input.data).not.toBeNull();
}

// Resume — should mine and push
const finalTx = await sendTx.run(null);
expect(finalTx.hash).toBeDefined();
await waitForTxReceived(hwallet, finalTx.hash);
});

it('should complete the full flow with run(null)', async () => {
const walletData = precalculationHelpers.test.getPrecalculatedWallet();
const store = new MemoryStore();
const storage = new Storage(store);
const hwallet = await startWallet(storage, walletData);
const address = await hwallet.getAddressAtIndex(0);
await GenesisWalletHelper.injectFunds(hwallet, address, 2n);

const sendTx = new SendTransaction({
storage: hwallet.storage,
pin: DEFAULT_PIN_CODE,
outputs: [
{
type: 'p2pkh',
address: await hwallet.getAddressAtIndex(1),
value: 1n,
token: NATIVE_TOKEN_UID,
},
],
});

// Full flow in one call
const tx = await sendTx.run(null);
expect(tx.hash).toBeDefined();
await waitForTxReceived(hwallet, tx.hash);
});
});

describe('custom signature method', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/integration/walletservice_facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,7 +2307,7 @@ describe('Fee-based tokens', () => {
expect((txData.headers![0] as FeeHeader).entries[0].amount).toBe(2n);

// Sign the transaction and run from mining
await sendTx.signTx(sendTx.utxosAddressPath);
await sendTx.signTx();
const tx = await sendTx.runFromMining();
await pollForTx(feeWallet, tx.hash!);

Expand Down
7 changes: 5 additions & 2 deletions __tests__/new/sendTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,13 @@ test('prepareTxData', async () => {
tokens: ['01'],
});

await expect(sendTransaction.prepareTx()).rejects.toThrow('Pin is not set.');
sendTransaction.pin = '000000';
// prepareTx does not require a PIN (creates unsigned transaction)
await expect(sendTransaction.prepareTx()).resolves.toBe(preparedTx);

// signTx requires a PIN
await expect(sendTransaction.signTx()).rejects.toThrow('Pin is not set.');
sendTransaction.pin = '000000';

prepareSpy.mockRestore();
spyGetToken.mockRestore();
});
Expand Down
55 changes: 52 additions & 3 deletions __tests__/wallet/sendTransactionWalletService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Network from '../../src/models/network';
import Address from '../../src/models/address';
import { TokenVersion } from '../../src/types';
import FeeHeader from '../../src/headers/fee';
import { SendTxError } from '../../src/errors';
import { SendTxError, WalletError } from '../../src/errors';

describe('prepareTxData', () => {
let wallet;
Expand Down Expand Up @@ -2158,7 +2158,7 @@ describe('prepareTx - Fee Tokens', () => {
];

sendTransaction = new SendTransactionWalletService(wallet, { inputs, outputs });
const { transaction } = await sendTransaction.prepareTx();
const transaction = await sendTransaction.prepareTx();

// Verify inputs: 1 pre-selected fee token + 1 pre-selected HTR
expect(transaction.inputs).toHaveLength(2);
Expand Down Expand Up @@ -2254,7 +2254,7 @@ describe('prepareTx - Fee Tokens', () => {
];

sendTransaction = new SendTransactionWalletService(wallet, { outputs });
const { transaction } = await sendTransaction.prepareTx();
const transaction = await sendTransaction.prepareTx();

// Verify transaction was created
expect(transaction).toBeDefined();
Expand Down Expand Up @@ -2327,3 +2327,52 @@ describe('prepareTx - Fee Tokens', () => {
);
});
});

describe('signTx preconditions', () => {
let wallet;
const seed =
'purse orchard camera cloud piece joke hospital mechanic timber horror shoulder rebuild you decrease garlic derive rebuild random naive elbow depart okay parrot cliff';

beforeEach(() => {
wallet = new HathorWalletServiceWallet({
requestPassword: async () => '123',
seed,
network: new Network('testnet'),
});
});

it('should throw when transaction is null', async () => {
const sendTransaction = new SendTransactionWalletService(wallet);
await expect(sendTransaction.signTx('1234')).rejects.toThrow(WalletError);
await expect(sendTransaction.signTx('1234')).rejects.toThrow(
"Can't sign transaction if it's null."
);
});

it('should throw when pin is not set', async () => {
const sendTransaction = new SendTransactionWalletService(wallet);
// Manually set transaction to bypass the null check
sendTransaction.transaction = {
inputs: [],
getDataToSignHash: jest.fn(),
prepareToSend: jest.fn(),
};
await expect(sendTransaction.signTx()).rejects.toThrow(SendTxError);
await expect(sendTransaction.signTx()).rejects.toThrow('Pin is not set.');
});

it('should throw when utxosAddressPath does not match inputs', async () => {
const sendTransaction = new SendTransactionWalletService(wallet);
// Set transaction with inputs but leave utxosAddressPath empty
sendTransaction.transaction = {
inputs: [{ hash: 'tx1', index: 0 }],
getDataToSignHash: jest.fn(),
prepareToSend: jest.fn(),
};
sendTransaction.utxosAddressPath = []; // mismatch: 0 paths vs 1 input
await expect(sendTransaction.signTx('1234')).rejects.toThrow(SendTxError);
await expect(sendTransaction.signTx('1234')).rejects.toThrow(
'utxosAddressPath length does not match transaction inputs. Call prepareTx() first.'
);
});
});
12 changes: 7 additions & 5 deletions __tests__/wallet/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ jest.mock('../../src/wallet/sendTransactionWalletService', () => {
const instance = new ActualSendTransactionWalletService(...args);

// Mock all methods except run with appropriate return values
instance.prepareTx = jest.fn().mockResolvedValue({
utxosAddressPath: [], // Mock utxos address path array
});
// Set up the transaction mock for signTx to use
instance.transaction = {
const mockTransaction = {
getDataToSignHash: jest.fn().mockReturnValue(Buffer.from('mock-hash')),
inputs: [], // Mock empty inputs so the for loop doesn't execute
prepareToSend: jest.fn(),
};
instance.prepareTx = jest.fn().mockImplementation(() => {
instance.transaction = mockTransaction;
instance.utxosAddressPath = [];
instance._currentStep = 'prepared';
return Promise.resolve(mockTransaction);
});
instance.runFromMining = jest.fn().mockResolvedValue({});

return instance;
Expand Down
Loading
Loading