From e24866aac62d5997a1d15f7de380df8545a35e96 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Tue, 3 Mar 2026 15:57:09 -0300 Subject: [PATCH 01/10] feat: latest changes for returning the network fee when sending tx with FBT --- .../rpcMethods/sendTransaction.test.ts | 117 +++++++++++++----- .../src/rpcMethods/sendTransaction.ts | 99 +++++++++++---- .../hathor-rpc-handler/src/types/prompt.ts | 26 +++- 3 files changed, 188 insertions(+), 54 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index dfd85926..6e912344 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -51,6 +51,30 @@ describe('sendTransaction', () => { // Mock wallet sendTransactionMock = jest.fn(); + + // Create a mock Transaction object returned by prepareTx() + // Inputs have hash/index, outputs have value/tokenData with getTokenIndex()/parseScript() + const mockTransaction = { + inputs: [{ + hash: 'testTxId', + index: 0, + }], + outputs: [{ + value: BigInt(100), + tokenData: 0, + getTokenIndex: jest.fn().mockReturnValue(-1), // -1 = HTR + parseScript: jest.fn().mockReturnValue({ + address: { base58: 'testAddress' }, + timelock: null, + }), + }], + tokens: [], + getFeeHeader: jest.fn().mockReturnValue({ + entries: [{ tokenIndex: 0, amount: 0n }], + }), + toHex: jest.fn().mockReturnValue('mockedTxHex'), + }; + wallet = { getNetwork: jest.fn().mockReturnValue('testnet'), getTokenDetails: jest.fn().mockResolvedValue({ @@ -61,21 +85,9 @@ describe('sendTransaction', () => { }, }), sendManyOutputsSendTransaction: jest.fn().mockResolvedValue({ - prepareTxData: jest.fn().mockResolvedValue({ - inputs: [{ - txId: 'testTxId', - index: 0, - value: 100n, - address: 'testAddress', - token: '00', - }], - outputs: [{ - address: 'testAddress', - value: BigInt(100), - token: '00', - }], - }), - run: sendTransactionMock, + prepareTx: jest.fn().mockResolvedValue(mockTransaction), + signTx: jest.fn().mockResolvedValue(mockTransaction), + runFromMining: sendTransactionMock, }), } as unknown as jest.Mocked; @@ -118,18 +130,17 @@ describe('sendTransaction', () => { outputs: [{ address: 'testAddress', value: 100n, - token: '00', + token: constants.NATIVE_TOKEN_UID, + timelock: undefined, }], inputs: [{ txId: 'testTxId', index: 0, - value: 100n, - address: 'testAddress', - token: '00', }], changeAddress: 'changeAddress', pushTx: true, tokenDetails: new Map(), + networkFee: 0n, }, }, {}); expect(promptHandler).toHaveBeenNthCalledWith(2, { @@ -278,7 +289,7 @@ describe('sendTransaction', () => { it('should throw InsufficientFundsError when not enough funds available', async () => { (wallet.sendManyOutputsSendTransaction as jest.Mock).mockResolvedValue({ - prepareTxData: jest.fn().mockRejectedValue( + prepareTx: jest.fn().mockRejectedValue( new Error('Insufficient amount of tokens') ), }); @@ -293,7 +304,7 @@ describe('sendTransaction', () => { it('should throw SendTransactionError when transaction preparation fails', async () => { (wallet.sendManyOutputsSendTransaction as jest.Mock).mockImplementation(() => { return { - prepareTxData: jest.fn().mockRejectedValue(new Error('Failed to prepare transaction')), + prepareTx: jest.fn().mockRejectedValue(new Error('Failed to prepare transaction')), }; }); @@ -403,9 +414,36 @@ describe('sendTransaction', () => { } as SendTransactionRpcRequest; const mockHex = '00010203'; - const mockTransaction = { + + // Create a mock Transaction object returned by prepareTx() + const mockPreparedTransaction = { + inputs: [{ hash: 'testTxId', index: 0 }], + outputs: [{ + value: BigInt(100), + tokenData: 0, + getTokenIndex: jest.fn().mockReturnValue(-1), + parseScript: jest.fn().mockReturnValue({ + address: { base58: 'testAddress' }, + timelock: null, + }), + }], + tokens: [], + getFeeHeader: jest.fn().mockReturnValue({ + entries: [{ tokenIndex: 0, amount: 0n }], + }), + }; + + // signTx returns the signed Transaction with toHex + const mockSignedTransaction = { toHex: jest.fn().mockReturnValue(mockHex), }; + const signTxMock = jest.fn().mockResolvedValue(mockSignedTransaction); + + (wallet.sendManyOutputsSendTransaction as jest.Mock).mockResolvedValue({ + prepareTx: jest.fn().mockResolvedValue(mockPreparedTransaction), + signTx: signTxMock, + runFromMining: sendTransactionMock, + }); promptHandler .mockResolvedValueOnce({ @@ -417,11 +455,10 @@ describe('sendTransaction', () => { data: { accepted: true, pinCode: '1234' }, }); - sendTransactionMock.mockResolvedValue(mockTransaction); - const response = await sendTransaction(requestWithPushTxFalse, wallet, {}, promptHandler); - expect(sendTransactionMock).toHaveBeenCalledWith('prepare-tx', '1234'); + expect(signTxMock).toHaveBeenCalledWith('1234'); + expect(sendTransactionMock).not.toHaveBeenCalled(); // runFromMining should not be called expect(response).toEqual({ type: RpcResponseTypes.SendTransactionResponse, response: mockHex, @@ -438,6 +475,31 @@ describe('sendTransaction', () => { } as SendTransactionRpcRequest; const txResponse = { hash: 'txHash123' }; + const signTxMock = jest.fn().mockResolvedValue({ toHex: jest.fn() }); + + // Create a mock Transaction object returned by prepareTx() + const mockTransaction = { + inputs: [{ hash: 'testTxId', index: 0 }], + outputs: [{ + value: BigInt(100), + tokenData: 0, + getTokenIndex: jest.fn().mockReturnValue(-1), + parseScript: jest.fn().mockReturnValue({ + address: { base58: 'testAddress' }, + timelock: null, + }), + }], + tokens: [], + getFeeHeader: jest.fn().mockReturnValue({ + entries: [{ tokenIndex: 0, amount: 0n }], + }), + }; + + (wallet.sendManyOutputsSendTransaction as jest.Mock).mockResolvedValue({ + prepareTx: jest.fn().mockResolvedValue(mockTransaction), + signTx: signTxMock, + runFromMining: sendTransactionMock.mockResolvedValue(txResponse), + }); promptHandler .mockResolvedValueOnce({ @@ -449,11 +511,10 @@ describe('sendTransaction', () => { data: { accepted: true, pinCode: '1234' }, }); - sendTransactionMock.mockResolvedValue(txResponse); - const response = await sendTransaction(requestWithPushTxTrue, wallet, {}, promptHandler); - expect(sendTransactionMock).toHaveBeenCalledWith(null, '1234'); + expect(signTxMock).toHaveBeenCalledWith('1234'); + expect(sendTransactionMock).toHaveBeenCalled(); // runFromMining should be called expect(response).toEqual({ type: RpcResponseTypes.SendTransactionResponse, response: txResponse, diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 70b33530..5c0d4eb9 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -6,9 +6,8 @@ */ import { z } from 'zod'; -import { constants, Transaction } from '@hathor/wallet-lib'; +import { constants, Network, Transaction, tokensUtils } from '@hathor/wallet-lib'; import type { DataScriptOutputRequestObj, IHathorWallet } from '@hathor/wallet-lib'; -import type { IDataOutput } from '@hathor/wallet-lib/lib/types'; import { TriggerTypes, PinConfirmationPrompt, @@ -33,6 +32,23 @@ import { } from '../errors'; import { validateNetwork, fetchTokenDetails } from '../helpers'; +/** + * Unified send transaction interface for both HathorWallet and HathorWalletServiceWallet. + * + * Both wallet implementations provide sendTransaction services that support + * a prepare-then-sign flow through this interface. This allows building the + * transaction before requesting user confirmation, then signing with the PIN + * only after approval. + * + * TODO: Remove this once wallet-lib exports a unified ISendTransaction with + * prepareTx/signTx (see hathor-wallet-lib PR #1022). + */ +interface ISendTransactionService { + prepareTx(): Promise; + signTx(pin: string): Promise; + runFromMining(): Promise; +} + const OutputValueSchema = z.object({ address: z.string(), value: z.string().regex(/^\d+$/) @@ -101,23 +117,20 @@ export async function sendTransaction( const { params } = validationResult.data; validateNetwork(wallet, params.network); - // sendManyOutputsSendTransaction throws if it doesn't receive a pin, - // but doesn't use it until prepareTxData is called, so we can just assign - // an arbitrary value to it and then mutate the instance after we get the - // actual pin from the pin prompt. - const stubPinCode = '111111'; - - // Create the transaction service but don't run it yet - const sendTransaction = await wallet.sendManyOutputsSendTransaction(params.outputs, { + // Create the transaction service and cast to the unified interface that works + // with both HathorWallet (SendTransaction) and HathorWalletServiceWallet + // (SendTransactionWalletService) implementations. + const sendTransactionService = await wallet.sendManyOutputsSendTransaction(params.outputs, { inputs: params.inputs || [], changeAddress: params.changeAddress, - pinCode: stubPinCode, - }); + }) as unknown as ISendTransactionService; - // Prepare the transaction to get all inputs (including automatically selected ones) - let preparedTx; + // Prepare the full transaction without signing to get inputs, outputs, and fee. + // This builds the tx so we can show it to the user for confirmation before + // requesting their PIN. + let preparedTx: Transaction; try { - preparedTx = await sendTransaction.prepareTxData(); + preparedTx = await sendTransactionService.prepareTx(); } catch (err) { if (err instanceof Error) { if (err.message.includes('Insufficient amount of tokens')) { @@ -127,22 +140,59 @@ export async function sendTransaction( throw new PrepareSendTransactionError(err instanceof Error ? err.message : 'An unknown error occurred while preparing the transaction'); } + // Extract inputs and outputs from the Transaction object for the prompt. + const network = new Network(wallet.getNetwork()); + + const txInputs = preparedTx.inputs.map(input => ({ + txId: input.hash, + index: input.index, + })); + + const txOutputs = preparedTx.outputs.map(output => { + const tokenIndex = output.getTokenIndex(); + const token = tokenIndex === -1 ? constants.NATIVE_TOKEN_UID : preparedTx.tokens[tokenIndex]; + const decoded = output.parseScript(network); + + if (decoded && 'data' in decoded && !('address' in decoded)) { + // ScriptData output + return { data: (decoded as { data: string }).data, value: output.value, token }; + } + + // P2PKH or P2SH output + const addressScript = decoded as { address: { base58: string }, timelock: number | null } | null; + return { + address: addressScript?.address?.base58, + value: output.value, + token, + timelock: addressScript?.timelock ?? undefined, + }; + }); + // Extract token UIDs from outputs and fetch their details - const tokenUids = preparedTx.outputs - .filter((output): output is IDataOutput & { token: string } => 'token' in output && typeof output.token === 'string') + const tokenUids = txOutputs + .filter((output): output is typeof output & { token: string } => typeof output.token === 'string') .map(output => output.token); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); + // Get the network fee from the transaction's fee header and data outputs + const feeHeader = preparedTx.getFeeHeader(); + const feeHeaderAmount = feeHeader + ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n) + : 0n; + const dataOutputCount = txOutputs.filter(output => 'data' in output && output.data !== undefined).length; + const networkFee = feeHeaderAmount + tokensUtils.getDataFee(dataOutputCount); + // Show the complete transaction (with all inputs) to the user const prompt: SendTransactionConfirmationPrompt = { ...rpcRequest, type: TriggerTypes.SendTransactionConfirmationPrompt, data: { - outputs: preparedTx.outputs, - inputs: preparedTx.inputs, + outputs: txOutputs, + inputs: txInputs, changeAddress: params.changeAddress, pushTx: params.pushTx, tokenDetails, + networkFee, } }; @@ -170,13 +220,16 @@ export async function sendTransaction( promptHandler(loadingTrigger, requestMetadata); try { - // Now execute the prepared transaction + // Sign the prepared transaction with the user's PIN + const signedTx = await sendTransactionService.signTx(pinResponse.data.pinCode); + let response: Transaction | string; if (params.pushTx === false) { - const transaction = await sendTransaction.run('prepare-tx', pinResponse.data.pinCode); - response = transaction.toHex(); + // Return the signed transaction as hex without mining/pushing + response = signedTx.toHex(); } else { - response = await sendTransaction.run(null, pinResponse.data.pinCode); + // Mine and push the signed transaction + response = await sendTransactionService.runFromMining(); } const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = { diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index ca8406e1..c48d8c8e 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -6,7 +6,7 @@ */ import { AddressInfoObject, GetBalanceObject, TokenDetailsObject } from '@hathor/wallet-lib/lib/wallet/types'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; -import { IDataInput, IDataOutput } from '@hathor/wallet-lib/lib/types'; +import { OutputValueType } from '@hathor/wallet-lib/lib/types'; import { RequestMetadata, RpcRequest } from './rpcRequest'; export enum TriggerTypes { @@ -298,14 +298,34 @@ export interface SignOracleDataConfirmationResponse { data: boolean; } +/** Input data extracted from a prepared Transaction for display in prompts. */ +export interface TxPromptInput { + txId: string; + index: number; +} + +/** Output data extracted from a prepared Transaction for display in prompts. */ +export interface TxPromptOutput { + address?: string; + value: OutputValueType; + token: string; + timelock?: number | null; + data?: string; +} + export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & { type: TriggerTypes.SendTransactionConfirmationPrompt; data: { - outputs: IDataOutput[], - inputs: IDataInput[], + outputs: TxPromptOutput[], + inputs: TxPromptInput[], changeAddress?: string; pushTx: boolean; tokenDetails?: Map; + /** + * Calculated network fee for the transaction. + * This is calculated based on the token outputs and their versions. + */ + networkFee?: bigint; } } From a3fdc2a0afbd0b02262a579090c54510eb114f69 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 09:51:12 -0300 Subject: [PATCH 02/10] refactor: use params to show data in the dialogs --- .../rpcMethods/sendTransaction.test.ts | 55 ++++--------------- .../src/rpcMethods/sendTransaction.ts | 49 ++++------------- .../hathor-rpc-handler/src/types/prompt.ts | 46 +++++++++++----- 3 files changed, 53 insertions(+), 97 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index 6e912344..01fac286 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -29,6 +29,10 @@ describe('sendTransaction', () => { let promptHandler: jest.Mock; let sendTransactionMock: jest.Mock; + // A valid P2PKH script (25 bytes: OP_DUP OP_HASH160 pushdata(20) <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG) + // so that P2PKH.identify() returns true during fee calculation + const p2pkhScript = Buffer.from([0x76, 0xa9, 0x14, ...new Array(20).fill(0), 0x88, 0xac]); + beforeEach(() => { // Setup basic request rpcRequest = { @@ -53,21 +57,9 @@ describe('sendTransaction', () => { sendTransactionMock = jest.fn(); // Create a mock Transaction object returned by prepareTx() - // Inputs have hash/index, outputs have value/tokenData with getTokenIndex()/parseScript() const mockTransaction = { - inputs: [{ - hash: 'testTxId', - index: 0, - }], - outputs: [{ - value: BigInt(100), - tokenData: 0, - getTokenIndex: jest.fn().mockReturnValue(-1), // -1 = HTR - parseScript: jest.fn().mockReturnValue({ - address: { base58: 'testAddress' }, - timelock: null, - }), - }], + inputs: [{ hash: 'testTxId', index: 0 }], + outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], tokens: [], getFeeHeader: jest.fn().mockReturnValue({ entries: [{ tokenIndex: 0, amount: 0n }], @@ -127,16 +119,8 @@ describe('sendTransaction', () => { ...rpcRequest, type: TriggerTypes.SendTransactionConfirmationPrompt, data: { - outputs: [{ - address: 'testAddress', - value: 100n, - token: constants.NATIVE_TOKEN_UID, - timelock: undefined, - }], - inputs: [{ - txId: 'testTxId', - index: 0, - }], + inputs: [{ txId: 'testTxId', index: 0 }], + outputs: [{ address: 'testAddress', value: BigInt(100), token: '00' }], changeAddress: 'changeAddress', pushTx: true, tokenDetails: new Map(), @@ -415,25 +399,15 @@ describe('sendTransaction', () => { const mockHex = '00010203'; - // Create a mock Transaction object returned by prepareTx() const mockPreparedTransaction = { inputs: [{ hash: 'testTxId', index: 0 }], - outputs: [{ - value: BigInt(100), - tokenData: 0, - getTokenIndex: jest.fn().mockReturnValue(-1), - parseScript: jest.fn().mockReturnValue({ - address: { base58: 'testAddress' }, - timelock: null, - }), - }], + outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], tokens: [], getFeeHeader: jest.fn().mockReturnValue({ entries: [{ tokenIndex: 0, amount: 0n }], }), }; - // signTx returns the signed Transaction with toHex const mockSignedTransaction = { toHex: jest.fn().mockReturnValue(mockHex), }; @@ -477,18 +451,9 @@ describe('sendTransaction', () => { const txResponse = { hash: 'txHash123' }; const signTxMock = jest.fn().mockResolvedValue({ toHex: jest.fn() }); - // Create a mock Transaction object returned by prepareTx() const mockTransaction = { inputs: [{ hash: 'testTxId', index: 0 }], - outputs: [{ - value: BigInt(100), - tokenData: 0, - getTokenIndex: jest.fn().mockReturnValue(-1), - parseScript: jest.fn().mockReturnValue({ - address: { base58: 'testAddress' }, - timelock: null, - }), - }], + outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], tokens: [], getFeeHeader: jest.fn().mockReturnValue({ entries: [{ tokenIndex: 0, amount: 0n }], diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 5c0d4eb9..96f7ee9b 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -6,7 +6,7 @@ */ import { z } from 'zod'; -import { constants, Network, Transaction, tokensUtils } from '@hathor/wallet-lib'; +import { constants, Transaction, tokensUtils } from '@hathor/wallet-lib'; import type { DataScriptOutputRequestObj, IHathorWallet } from '@hathor/wallet-lib'; import { TriggerTypes, @@ -140,55 +140,28 @@ export async function sendTransaction( throw new PrepareSendTransactionError(err instanceof Error ? err.message : 'An unknown error occurred while preparing the transaction'); } - // Extract inputs and outputs from the Transaction object for the prompt. - const network = new Network(wallet.getNetwork()); - - const txInputs = preparedTx.inputs.map(input => ({ - txId: input.hash, - index: input.index, - })); - - const txOutputs = preparedTx.outputs.map(output => { - const tokenIndex = output.getTokenIndex(); - const token = tokenIndex === -1 ? constants.NATIVE_TOKEN_UID : preparedTx.tokens[tokenIndex]; - const decoded = output.parseScript(network); - - if (decoded && 'data' in decoded && !('address' in decoded)) { - // ScriptData output - return { data: (decoded as { data: string }).data, value: output.value, token }; - } - - // P2PKH or P2SH output - const addressScript = decoded as { address: { base58: string }, timelock: number | null } | null; - return { - address: addressScript?.address?.base58, - value: output.value, - token, - timelock: addressScript?.timelock ?? undefined, - }; - }); - - // Extract token UIDs from outputs and fetch their details - const tokenUids = txOutputs - .filter((output): output is typeof output & { token: string } => typeof output.token === 'string') - .map(output => output.token); + // Extract token UIDs from the user's requested outputs and fetch their details + const tokenUids = params.outputs + .filter((output): output is { token: string } & typeof output => 'token' in output && typeof output.token === 'string') + .map(output => output.token) + .filter(uid => uid !== constants.NATIVE_TOKEN_UID); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); - // Get the network fee from the transaction's fee header and data outputs + // Calculate network fee: fee header (fee-based tokens) + data output fees const feeHeader = preparedTx.getFeeHeader(); const feeHeaderAmount = feeHeader ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n) : 0n; - const dataOutputCount = txOutputs.filter(output => 'data' in output && output.data !== undefined).length; + const dataOutputCount = params.outputs.filter(output => 'data' in output).length; const networkFee = feeHeaderAmount + tokensUtils.getDataFee(dataOutputCount); - // Show the complete transaction (with all inputs) to the user + // Show the user's original parameters for confirmation const prompt: SendTransactionConfirmationPrompt = { ...rpcRequest, type: TriggerTypes.SendTransactionConfirmationPrompt, data: { - outputs: txOutputs, - inputs: txInputs, + outputs: params.outputs, + inputs: params.inputs, changeAddress: params.changeAddress, pushTx: params.pushTx, tokenDetails, diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index c48d8c8e..becd8af2 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -5,8 +5,9 @@ * LICENSE file in the root directory of this source tree. */ import { AddressInfoObject, GetBalanceObject, TokenDetailsObject } from '@hathor/wallet-lib/lib/wallet/types'; +import { TokenVersion } from '@hathor/wallet-lib'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; -import { OutputValueType } from '@hathor/wallet-lib/lib/types'; +import type { DataScriptOutputRequestObj } from '@hathor/wallet-lib'; import { RequestMetadata, RpcRequest } from './rpcRequest'; export enum TriggerTypes { @@ -186,6 +187,11 @@ export interface NanoContractParams { parsedArgs: unknown[]; pushTx: boolean; tokenDetails?: Map; + /** + * Calculated network fee for the transaction. + * This is calculated based on the token outputs and their versions. + */ + networkFee?: bigint; } export interface CreateTokenParams { @@ -202,6 +208,21 @@ export interface CreateTokenParams { meltAuthorityAddress: string | null, allowExternalMeltAuthorityAddress: boolean, data: string[] | null, + /** + * Token version for the new token. + * If not provided, the wallet-lib will use the default (NATIVE). + */ + tokenVersion?: TokenVersion, + /** + * Calculated network fee for the token creation. + * This is only applicable for fee tokens (TokenVersion.FEE). + */ + networkFee?: bigint, + /** + * Calculated deposit amount for the token creation. + * This is only applicable for deposit tokens (TokenVersion.DEPOSIT). + */ + depositAmount?: bigint, } // Extended type for nano contract token creation @@ -298,26 +319,23 @@ export interface SignOracleDataConfirmationResponse { data: boolean; } -/** Input data extracted from a prepared Transaction for display in prompts. */ -export interface TxPromptInput { - txId: string; - index: number; +export interface SendTransactionOutputParam { + address: string; + value: bigint; + token: string; + timelock?: number; } -/** Output data extracted from a prepared Transaction for display in prompts. */ -export interface TxPromptOutput { - address?: string; - value: OutputValueType; - token: string; - timelock?: number | null; - data?: string; +export interface SendTransactionInputParam { + txId: string; + index: number; } export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & { type: TriggerTypes.SendTransactionConfirmationPrompt; data: { - outputs: TxPromptOutput[], - inputs: TxPromptInput[], + outputs: (SendTransactionOutputParam | DataScriptOutputRequestObj)[]; + inputs?: SendTransactionInputParam[]; changeAddress?: string; pushTx: boolean; tokenDetails?: Map; From 90f16a960a6ee668d56a1f153ecf01b7a2ee5b6b Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 10:04:57 -0300 Subject: [PATCH 03/10] feat: add prepared tx to the prompt --- .../__tests__/rpcMethods/sendTransaction.test.ts | 4 +++- .../hathor-rpc-handler/src/rpcMethods/sendTransaction.ts | 1 + packages/hathor-rpc-handler/src/types/prompt.ts | 8 +++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index 01fac286..fed89088 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -28,6 +28,7 @@ describe('sendTransaction', () => { let wallet: jest.Mocked; let promptHandler: jest.Mock; let sendTransactionMock: jest.Mock; + let mockTransaction: any; // A valid P2PKH script (25 bytes: OP_DUP OP_HASH160 pushdata(20) <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG) // so that P2PKH.identify() returns true during fee calculation @@ -57,7 +58,7 @@ describe('sendTransaction', () => { sendTransactionMock = jest.fn(); // Create a mock Transaction object returned by prepareTx() - const mockTransaction = { + mockTransaction = { inputs: [{ hash: 'testTxId', index: 0 }], outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], tokens: [], @@ -125,6 +126,7 @@ describe('sendTransaction', () => { pushTx: true, tokenDetails: new Map(), networkFee: 0n, + preparedTx: mockTransaction, }, }, {}); expect(promptHandler).toHaveBeenNthCalledWith(2, { diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 96f7ee9b..60ec0878 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -166,6 +166,7 @@ export async function sendTransaction( pushTx: params.pushTx, tokenDetails, networkFee, + preparedTx, } }; diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index becd8af2..8adb8fb8 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -7,7 +7,7 @@ import { AddressInfoObject, GetBalanceObject, TokenDetailsObject } from '@hathor/wallet-lib/lib/wallet/types'; import { TokenVersion } from '@hathor/wallet-lib'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; -import type { DataScriptOutputRequestObj } from '@hathor/wallet-lib'; +import type { DataScriptOutputRequestObj, Transaction } from '@hathor/wallet-lib'; import { RequestMetadata, RpcRequest } from './rpcRequest'; export enum TriggerTypes { @@ -344,6 +344,12 @@ export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & { * This is calculated based on the token outputs and their versions. */ networkFee?: bigint; + /** + * The full prepared Transaction object before signing. + * Available for clients that need access to the complete transaction details + * (e.g. inputs with scripts, outputs with token indexes, etc.). + */ + preparedTx: Transaction; } } From a58117da813e16abc49ef3dc32aee686cad9f37f Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 10:37:37 -0300 Subject: [PATCH 04/10] refactor: add back types and improve naming/comments --- .../rpcMethods/sendTransaction.test.ts | 2 - .../src/rpcMethods/sendTransaction.ts | 14 +++---- .../hathor-rpc-handler/src/types/prompt.ts | 38 +------------------ 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index fed89088..7e0f1d1d 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -120,8 +120,6 @@ describe('sendTransaction', () => { ...rpcRequest, type: TriggerTypes.SendTransactionConfirmationPrompt, data: { - inputs: [{ txId: 'testTxId', index: 0 }], - outputs: [{ address: 'testAddress', value: BigInt(100), token: '00' }], changeAddress: 'changeAddress', pushTx: true, tokenDetails: new Map(), diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 60ec0878..cf2cc87f 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -43,7 +43,7 @@ import { validateNetwork, fetchTokenDetails } from '../helpers'; * TODO: Remove this once wallet-lib exports a unified ISendTransaction with * prepareTx/signTx (see hathor-wallet-lib PR #1022). */ -interface ISendTransactionService { +interface ISendTransactionObject { prepareTx(): Promise; signTx(pin: string): Promise; runFromMining(): Promise; @@ -120,17 +120,17 @@ export async function sendTransaction( // Create the transaction service and cast to the unified interface that works // with both HathorWallet (SendTransaction) and HathorWalletServiceWallet // (SendTransactionWalletService) implementations. - const sendTransactionService = await wallet.sendManyOutputsSendTransaction(params.outputs, { + const sendTransactionObject = await wallet.sendManyOutputsSendTransaction(params.outputs, { inputs: params.inputs || [], changeAddress: params.changeAddress, - }) as unknown as ISendTransactionService; + }) as unknown as ISendTransactionObject; // Prepare the full transaction without signing to get inputs, outputs, and fee. // This builds the tx so we can show it to the user for confirmation before // requesting their PIN. let preparedTx: Transaction; try { - preparedTx = await sendTransactionService.prepareTx(); + preparedTx = await sendTransactionObject.prepareTx(); } catch (err) { if (err instanceof Error) { if (err.message.includes('Insufficient amount of tokens')) { @@ -160,8 +160,6 @@ export async function sendTransaction( ...rpcRequest, type: TriggerTypes.SendTransactionConfirmationPrompt, data: { - outputs: params.outputs, - inputs: params.inputs, changeAddress: params.changeAddress, pushTx: params.pushTx, tokenDetails, @@ -195,7 +193,7 @@ export async function sendTransaction( try { // Sign the prepared transaction with the user's PIN - const signedTx = await sendTransactionService.signTx(pinResponse.data.pinCode); + const signedTx = await sendTransactionObject.signTx(pinResponse.data.pinCode); let response: Transaction | string; if (params.pushTx === false) { @@ -203,7 +201,7 @@ export async function sendTransaction( response = signedTx.toHex(); } else { // Mine and push the signed transaction - response = await sendTransactionService.runFromMining(); + response = await sendTransactionObject.runFromMining(); } const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = { diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index 8adb8fb8..456abb2b 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -5,9 +5,8 @@ * LICENSE file in the root directory of this source tree. */ import { AddressInfoObject, GetBalanceObject, TokenDetailsObject } from '@hathor/wallet-lib/lib/wallet/types'; -import { TokenVersion } from '@hathor/wallet-lib'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; -import type { DataScriptOutputRequestObj, Transaction } from '@hathor/wallet-lib'; +import type { Transaction } from '@hathor/wallet-lib'; import { RequestMetadata, RpcRequest } from './rpcRequest'; export enum TriggerTypes { @@ -187,11 +186,6 @@ export interface NanoContractParams { parsedArgs: unknown[]; pushTx: boolean; tokenDetails?: Map; - /** - * Calculated network fee for the transaction. - * This is calculated based on the token outputs and their versions. - */ - networkFee?: bigint; } export interface CreateTokenParams { @@ -208,21 +202,6 @@ export interface CreateTokenParams { meltAuthorityAddress: string | null, allowExternalMeltAuthorityAddress: boolean, data: string[] | null, - /** - * Token version for the new token. - * If not provided, the wallet-lib will use the default (NATIVE). - */ - tokenVersion?: TokenVersion, - /** - * Calculated network fee for the token creation. - * This is only applicable for fee tokens (TokenVersion.FEE). - */ - networkFee?: bigint, - /** - * Calculated deposit amount for the token creation. - * This is only applicable for deposit tokens (TokenVersion.DEPOSIT). - */ - depositAmount?: bigint, } // Extended type for nano contract token creation @@ -319,29 +298,14 @@ export interface SignOracleDataConfirmationResponse { data: boolean; } -export interface SendTransactionOutputParam { - address: string; - value: bigint; - token: string; - timelock?: number; -} - -export interface SendTransactionInputParam { - txId: string; - index: number; -} - export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & { type: TriggerTypes.SendTransactionConfirmationPrompt; data: { - outputs: (SendTransactionOutputParam | DataScriptOutputRequestObj)[]; - inputs?: SendTransactionInputParam[]; changeAddress?: string; pushTx: boolean; tokenDetails?: Map; /** * Calculated network fee for the transaction. - * This is calculated based on the token outputs and their versions. */ networkFee?: bigint; /** From a2b9d863b1df8567f756f204604d453678b7316d Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 11:04:24 -0300 Subject: [PATCH 05/10] feat: fee header are expected only to be in HTR --- .../rpcMethods/sendTransaction.test.ts | 43 ++++++++++++++++++- .../src/rpcMethods/sendTransaction.ts | 8 +++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index 7e0f1d1d..2e55fce7 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -28,7 +28,7 @@ describe('sendTransaction', () => { let wallet: jest.Mocked; let promptHandler: jest.Mock; let sendTransactionMock: jest.Mock; - let mockTransaction: any; + let mockTransaction: Record; // A valid P2PKH script (25 bytes: OP_DUP OP_HASH160 pushdata(20) <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG) // so that P2PKH.identify() returns true during fee calculation @@ -485,4 +485,45 @@ describe('sendTransaction', () => { response: txResponse, }); }); + + it('should calculate non-zero networkFee when FBT fee header is present', async () => { + const fbtMockTransaction = { + inputs: [{ hash: 'testTxId', index: 0 }], + outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], + tokens: ['token-uid-123'], + getFeeHeader: jest.fn().mockReturnValue({ + entries: [{ tokenIndex: 0, amount: 500n }], + }), + toHex: jest.fn().mockReturnValue('mockedTxHex'), + }; + + (wallet.sendManyOutputsSendTransaction as jest.Mock).mockResolvedValue({ + prepareTx: jest.fn().mockResolvedValue(fbtMockTransaction), + signTx: jest.fn().mockResolvedValue(fbtMockTransaction), + runFromMining: sendTransactionMock.mockResolvedValue({ hash: 'txHash123' }), + }); + + promptHandler + .mockResolvedValueOnce({ + type: TriggerResponseTypes.SendTransactionConfirmationResponse, + data: { accepted: true }, + }) + .mockResolvedValueOnce({ + type: TriggerResponseTypes.PinRequestResponse, + data: { accepted: true, pinCode: '1234' }, + }); + + await sendTransaction(rpcRequest, wallet, {}, promptHandler); + + // Verify the confirmation prompt was called with non-zero networkFee + expect(promptHandler).toHaveBeenNthCalledWith(1, + expect.objectContaining({ + type: TriggerTypes.SendTransactionConfirmationPrompt, + data: expect.objectContaining({ + networkFee: 500n, + }), + }), + {}, + ); + }); }); diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index cf2cc87f..7ed942ff 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -147,10 +147,14 @@ export async function sendTransaction( .filter(uid => uid !== constants.NATIVE_TOKEN_UID); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); - // Calculate network fee: fee header (fee-based tokens) + data output fees + // Calculate network fee: fee header (fee-based tokens) + data output fees. + // We expect all fees to be paid in HTR (tokenIndex 0). const feeHeader = preparedTx.getFeeHeader(); + if (feeHeader && feeHeader.entries.some(entry => entry.tokenIndex !== 0)) { + throw new PrepareSendTransactionError('Unexpected fee entry with non-HTR token index'); + } const feeHeaderAmount = feeHeader - ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n) + ? feeHeader.entries.filter(entry => entry.tokenIndex === 0).reduce((sum, entry) => sum + entry.amount, 0n) : 0n; const dataOutputCount = params.outputs.filter(output => 'data' in output).length; const networkFee = feeHeaderAmount + tokensUtils.getDataFee(dataOutputCount); From c5c7fbfcc02ddae06706bceb1af970d7dbacb084 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 11:26:34 -0300 Subject: [PATCH 06/10] refactor: rename networkFee -> fee --- .../__tests__/rpcMethods/sendTransaction.test.ts | 8 ++++---- .../hathor-rpc-handler/src/rpcMethods/sendTransaction.ts | 4 ++-- packages/hathor-rpc-handler/src/types/prompt.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index 2e55fce7..5ae6c428 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -123,7 +123,7 @@ describe('sendTransaction', () => { changeAddress: 'changeAddress', pushTx: true, tokenDetails: new Map(), - networkFee: 0n, + fee: 0n, preparedTx: mockTransaction, }, }, {}); @@ -486,7 +486,7 @@ describe('sendTransaction', () => { }); }); - it('should calculate non-zero networkFee when FBT fee header is present', async () => { + it('should calculate non-zero fee when FBT fee header is present', async () => { const fbtMockTransaction = { inputs: [{ hash: 'testTxId', index: 0 }], outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], @@ -515,12 +515,12 @@ describe('sendTransaction', () => { await sendTransaction(rpcRequest, wallet, {}, promptHandler); - // Verify the confirmation prompt was called with non-zero networkFee + // Verify the confirmation prompt was called with non-zero fee expect(promptHandler).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: TriggerTypes.SendTransactionConfirmationPrompt, data: expect.objectContaining({ - networkFee: 500n, + fee: 500n, }), }), {}, diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 7ed942ff..36718ad0 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -157,7 +157,7 @@ export async function sendTransaction( ? feeHeader.entries.filter(entry => entry.tokenIndex === 0).reduce((sum, entry) => sum + entry.amount, 0n) : 0n; const dataOutputCount = params.outputs.filter(output => 'data' in output).length; - const networkFee = feeHeaderAmount + tokensUtils.getDataFee(dataOutputCount); + const fee = feeHeaderAmount + tokensUtils.getDataFee(dataOutputCount); // Show the user's original parameters for confirmation const prompt: SendTransactionConfirmationPrompt = { @@ -167,7 +167,7 @@ export async function sendTransaction( changeAddress: params.changeAddress, pushTx: params.pushTx, tokenDetails, - networkFee, + fee, preparedTx, } }; diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index 456abb2b..ab43b142 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -307,7 +307,7 @@ export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & { /** * Calculated network fee for the transaction. */ - networkFee?: bigint; + fee?: bigint; /** * The full prepared Transaction object before signing. * Available for clients that need access to the complete transaction details From 4bdfeb3ce9637309d7780e523bc0a8667cd7d767 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 11:29:52 -0300 Subject: [PATCH 07/10] tests: use a fbt uid mock to make it more real --- .../rpcMethods/sendTransaction.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts index 5ae6c428..79b2f3b3 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts @@ -487,10 +487,19 @@ describe('sendTransaction', () => { }); it('should calculate non-zero fee when FBT fee header is present', async () => { + const fbtTokenUid = 'fbt-token-uid-abc123'; + + // Use a non-native token in the request outputs + rpcRequest.params.outputs = [{ + address: 'testAddress', + value: '100', + token: fbtTokenUid, + }]; + const fbtMockTransaction = { inputs: [{ hash: 'testTxId', index: 0 }], outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }], - tokens: ['token-uid-123'], + tokens: [fbtTokenUid], getFeeHeader: jest.fn().mockReturnValue({ entries: [{ tokenIndex: 0, amount: 500n }], }), @@ -515,12 +524,16 @@ describe('sendTransaction', () => { await sendTransaction(rpcRequest, wallet, {}, promptHandler); - // Verify the confirmation prompt was called with non-zero fee + // Verify token details were fetched for the non-native token + expect(wallet.getTokenDetails).toHaveBeenCalledWith(fbtTokenUid); + + // Verify the confirmation prompt was called with non-zero fee and token details expect(promptHandler).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: TriggerTypes.SendTransactionConfirmationPrompt, data: expect.objectContaining({ fee: 500n, + tokenDetails: expect.any(Map), }), }), {}, From 821d51e894ac4ce64213d4960e6809a95faf7d80 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 4 Mar 2026 17:57:32 -0300 Subject: [PATCH 08/10] refactor: do two filter in one to avoid two loops --- .../hathor-rpc-handler/src/rpcMethods/sendTransaction.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 36718ad0..7f359f8c 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -142,9 +142,8 @@ export async function sendTransaction( // Extract token UIDs from the user's requested outputs and fetch their details const tokenUids = params.outputs - .filter((output): output is { token: string } & typeof output => 'token' in output && typeof output.token === 'string') - .map(output => output.token) - .filter(uid => uid !== constants.NATIVE_TOKEN_UID); + .filter((output): output is { token: string } & typeof output => 'token' in output && typeof output.token === 'string' && output.token !== constants.NATIVE_TOKEN_UID) + .map(output => output.token); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); // Calculate network fee: fee header (fee-based tokens) + data output fees. From f7152def129a8760e98121012c9d7c13c1ce25a1 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Thu, 5 Mar 2026 21:47:26 -0300 Subject: [PATCH 09/10] refactor: use reduce instead of filter/map --- .../hathor-rpc-handler/src/rpcMethods/sendTransaction.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts index 7f359f8c..d96648d1 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts @@ -141,9 +141,12 @@ export async function sendTransaction( } // Extract token UIDs from the user's requested outputs and fetch their details - const tokenUids = params.outputs - .filter((output): output is { token: string } & typeof output => 'token' in output && typeof output.token === 'string' && output.token !== constants.NATIVE_TOKEN_UID) - .map(output => output.token); + const tokenUids = params.outputs.reduce((acc, output) => { + if ('token' in output && typeof output.token === 'string' && output.token !== constants.NATIVE_TOKEN_UID) { + acc.push(output.token); + } + return acc; + }, []); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); // Calculate network fee: fee header (fee-based tokens) + data output fees. From 8aa912bf226ee91de047f4d610a05a61ea123654 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Fri, 6 Mar 2026 10:40:14 -0300 Subject: [PATCH 10/10] chore: upgrade wallet lib to latest version --- packages/hathor-rpc-handler/package.json | 2 +- yarn.lock | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/hathor-rpc-handler/package.json b/packages/hathor-rpc-handler/package.json index 196dcd8b..d5af075b 100644 --- a/packages/hathor-rpc-handler/package.json +++ b/packages/hathor-rpc-handler/package.json @@ -30,7 +30,7 @@ "typescript-eslint": "7.13.0" }, "dependencies": { - "@hathor/wallet-lib": "2.14.0", + "@hathor/wallet-lib": "2.16.0", "zod": "3.23.8" } } diff --git a/yarn.lock b/yarn.lock index 583a9065..f42b8311 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2456,7 +2456,7 @@ __metadata: resolution: "@hathor/hathor-rpc-handler@workspace:packages/hathor-rpc-handler" dependencies: "@eslint/js": "npm:9.4.0" - "@hathor/wallet-lib": "npm:2.14.0" + "@hathor/wallet-lib": "npm:2.16.0" "@types/eslint__js": "npm:8.42.3" "@types/jest": "npm:29.5.12" "@types/node": "npm:20.14.2" @@ -2573,6 +2573,24 @@ __metadata: languageName: node linkType: hard +"@hathor/wallet-lib@npm:2.16.0": + version: 2.16.0 + resolution: "@hathor/wallet-lib@npm:2.16.0" + dependencies: + axios: "npm:1.7.7" + bitcore-lib: "npm:8.25.10" + bitcore-mnemonic: "npm:8.25.10" + buffer: "npm:6.0.3" + crypto-js: "npm:4.2.0" + isomorphic-ws: "npm:5.0.0" + lodash: "npm:4.17.21" + queue-microtask: "npm:1.2.3" + ws: "npm:8.17.1" + zod: "npm:3.23.8" + checksum: 10/808bb2269dbe14be02410a4ee976332d5f0cf20494df69a76f7430e7bea4dba9926ff6b1c709f142ba1377c309949de21d411f47c607b3805c937a3b69ebcc8f + languageName: node + linkType: hard + "@hathor/web-wallet@workspace:packages/web-wallet": version: 0.0.0-use.local resolution: "@hathor/web-wallet@workspace:packages/web-wallet"