Skip to content

feat: return the network fee when sending tx with FBT#139

Merged
pedroferreira1 merged 10 commits intomasterfrom
feat/fee-based-tokens-send-tx
Mar 6, 2026
Merged

feat: return the network fee when sending tx with FBT#139
pedroferreira1 merged 10 commits intomasterfrom
feat/fee-based-tokens-send-tx

Conversation

@pedroferreira1
Copy link
Copy Markdown
Member

@pedroferreira1 pedroferreira1 commented Mar 4, 2026

Acceptance Criteria

  • Return network fee in the send transaction prompt
  • Split send transaction into prepare tx, then sign/mine/push only after user approval.

NOTE: Missing new wallet lib version

Checklist

  • If you are requesting a merge into master, confirm this code is production-ready and can be included in future releases as soon as it gets merged
  • Make sure either the unit tests and/or the QA tests are capable of testing the new features
  • Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. More dependencies increase the possibility of one of them being hijacked and affecting us.

Summary by CodeRabbit

  • New Features

    • Confirmation prompts now include token details, computed network fees, and a preview of the prepared transaction; users can opt to receive the signed transaction hex without broadcasting.
  • Improvements

    • Transaction flow clarified to explicit prepare → sign → execute steps so signing happens before submission.
    • Confirmation dialogs enforce zero-amount checks and default native-token behavior; push_tx false returns hex, push_tx true submits for mining.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces sendTransaction’s prepareTxData→run flow with a unified ISendTransactionObject exposing prepareTx → signTx → runFromMining, computes networkFee via tokensUtils, includes preparedTx and tokenDetails in confirmation prompts, and updates tests/mocks to the new prepare→sign→run sequence. (≈33 words)

Changes

Cohort / File(s) Summary
RPC method: sendTransaction
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
Replaces prior prepareTxData/run usage with a unified ISendTransactionObject (prepareTx(), signTx(pin), runFromMining()). Imports tokensUtils to compute networkFee, derives token UIDs/details from params.outputs, includes preparedTx and networkFee in the confirmation prompt, signs after PIN, returns hex when push_tx=false, and calls runFromMining when push_tx=true.
Prompt types
packages/hathor-rpc-handler/src/types/prompt.ts
Updates SendTransactionConfirmationPrompt.data: removes outputs/inputs, adds networkFee?: bigint and preparedTx: Transaction, and replaces prior data input/output imports with Transaction type import.
Unit tests: sendTransaction
packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
Updates mocks and assertions for the new flow: prepareTxsignTxrunFromMining; adds preparedTx and signed-tx mocks; adjusts fee/header handling and networkFee expectations; updates error-paths and push_tx true/false behaviors; removes references to prepareTxData and run.

Sequence Diagram

sequenceDiagram
    participant User
    participant RPC as sendTransaction
    participant Service as ISendTransactionObject
    participant Wallet

    User->>RPC: call sendTransaction(params, push_tx)
    RPC->>Service: prepareTx()
    Service->>Wallet: prepareTx(params)
    Wallet-->>Service: preparedTx (inputs, outputs, feeHeader)
    Service-->>RPC: preparedTx

    RPC->>RPC: compute networkFee (tokensUtils) & fetch tokenDetails
    RPC->>User: prompt with preparedTx, tokenDetails, networkFee
    User->>RPC: confirm + PIN

    RPC->>Service: signTx(pin)
    Service->>Wallet: signTx(pin)
    Wallet-->>Service: signedTx
    Service-->>RPC: signedTx

    alt push_tx = true
        RPC->>Service: runFromMining()
        Service->>Wallet: runFromMining(signedTx)
        Wallet-->>Service: txResult
        Service-->>RPC: txResult
        RPC-->>User: execution result
    else push_tx = false
        RPC-->>User: signedTx.toHex()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I prepped a tx with careful paws,
I signed with hops and checked the laws.
If miners call, I dash and run,
Else I hand you hex — the job’s well done.
A tiny rabbit cheer for the new three-step cause!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective of this pull request: implementing network fee return functionality for transactions with Fee-Based Tokens (FBT). The changes across all files focus on this feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/fee-based-tokens-send-tx

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

217-246: ⚠️ Potential issue | 🟠 Major

Always emit loading-finished trigger, including error paths.

If signTx or runFromMining throws, SendTransactionLoadingFinishedTrigger is never sent, which can leave clients stuck in a loading state.

🔧 Suggested fix with `finally`
   try {
     // 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) {
       // Return the signed transaction as hex without mining/pushing
       response = signedTx.toHex();
     } else {
       // Mine and push the signed transaction
       response = await sendTransactionService.runFromMining();
     }

-    const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = {
-      type: TriggerTypes.SendTransactionLoadingFinishedTrigger,
-    };
-    promptHandler(loadingFinishedTrigger, requestMetadata);
-
     return {
       type: RpcResponseTypes.SendTransactionResponse,
       response,
     } as RpcResponse;
   } catch (err) {
     throw new SendTransactionError(err instanceof Error ? err.message : 'An unknown error occurred while sending the transaction');
+  } finally {
+    const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = {
+      type: TriggerTypes.SendTransactionLoadingFinishedTrigger,
+    };
+    promptHandler(loadingFinishedTrigger, requestMetadata);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
217 - 246, The SendTransactionLoadingFinishedTrigger must be emitted on all code
paths; move creation/call of loadingFinishedTrigger into a finally block so
promptHandler(loadingFinishedTrigger, requestMetadata) always runs even if
sendTransactionService.signTx or sendTransactionService.runFromMining throws.
Keep the existing catch that wraps errors into SendTransactionError (using
err.message when err is Error) but rethrow after the finally to preserve error
behavior; reference the
SendTransactionLoadingTrigger/SendTransactionLoadingFinishedTrigger objects,
promptHandler, sendTransactionService.signTx,
sendTransactionService.runFromMining, and the existing catch block when making
this change.
🧹 Nitpick comments (1)
packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts (1)

418-446: Consider extracting a buildMockPreparedTransaction helper.

The prepared transaction mock setup is duplicated across tests, which makes future changes to tx shape more error-prone.

Also applies to: 480-502

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`
around lines 418 - 446, Extract the duplicated prepared-transaction fixture into
a helper named buildMockPreparedTransaction that returns the object currently
assigned to mockPreparedTransaction (including inputs, outputs with
getTokenIndex and parseScript, tokens, and getFeeHeader) and replace inline
constructions where prepareTx is mocked (e.g., in the
sendManyOutputsSendTransaction mock and the other occurrence at lines ~480-502)
to call buildMockPreparedTransaction(); ensure tests still mock signTx and
runFromMining (signTxMock and sendTransactionMock) separately so only the
prepared-transaction shape is centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Around line 57-76: The test suite only covers networkFee: 0n with
native-token-like outputs; add a new test in sendTransaction.test.ts that
constructs a mockTransaction whose outputs include an FBT output (set
output.tokenData to a non-zero token index or FBT-identifying value and
parseScript.address as needed), stub getFeeHeader on the mockTransaction to
return a non-zero fee entry (e.g., entries: [{ tokenIndex: X, amount: 123n }])
and assert that the sendTransaction path returns that non-zero networkFee for
FBT; locate and update the mockTransaction fixture and the test cases around
getFeeHeader/outputs to include this FBT scenario and an explicit expect
asserting the returned networkFee equals the stubbed non-zero amount.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 177-184: The fee aggregation incorrectly sums feeHeader.entries
across different tokenIndex values; update the logic around
preparedTx.getFeeHeader() so fees are either filtered to the native token or
aggregated per token rather than combined into a single networkFee.
Specifically, in the block that computes feeHeaderAmount and networkFee (using
preparedTx.getFeeHeader(), feeHeader.entries and tokensUtils.getDataFee), either
sum only entries where entry.tokenIndex corresponds to the native token (e.g.,
tokenIndex === -1 or the known native index) before adding data fees, or produce
a map of tokenIndex -> fee amount (including data fees per token) and surface
that instead of a single networkFee; ensure downstream code that reads
networkFee is adjusted to expect the chosen representation.

---

Outside diff comments:
In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 217-246: The SendTransactionLoadingFinishedTrigger must be emitted
on all code paths; move creation/call of loadingFinishedTrigger into a finally
block so promptHandler(loadingFinishedTrigger, requestMetadata) always runs even
if sendTransactionService.signTx or sendTransactionService.runFromMining throws.
Keep the existing catch that wraps errors into SendTransactionError (using
err.message when err is Error) but rethrow after the finally to preserve error
behavior; reference the
SendTransactionLoadingTrigger/SendTransactionLoadingFinishedTrigger objects,
promptHandler, sendTransactionService.signTx,
sendTransactionService.runFromMining, and the existing catch block when making
this change.

---

Nitpick comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Around line 418-446: Extract the duplicated prepared-transaction fixture into
a helper named buildMockPreparedTransaction that returns the object currently
assigned to mockPreparedTransaction (including inputs, outputs with
getTokenIndex and parseScript, tokens, and getFeeHeader) and replace inline
constructions where prepareTx is mocked (e.g., in the
sendManyOutputsSendTransaction mock and the other occurrence at lines ~480-502)
to call buildMockPreparedTransaction(); ensure tests still mock signTx and
runFromMining (signTxMock and sendTransactionMock) separately so only the
prepared-transaction shape is centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d96a1ee3-8823-4b52-a39a-3af2aaeae7c5

📥 Commits

Reviewing files that changed from the base of the PR and between df38d49 and e24866a.

📒 Files selected for processing (3)
  • packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
  • packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
  • packages/hathor-rpc-handler/src/types/prompt.ts

Comment thread packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts Outdated
Comment thread packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

120-126: Unsafe double type cast bypasses type safety.

The as unknown as ISendTransactionService pattern completely disables TypeScript's type checking. If the wallet-lib API changes, this code will fail at runtime with no compile-time warning.

Consider adding runtime validation or a type guard to verify the returned object has the expected methods.

🛡️ Proposed defensive check
   const sendTransactionService = await wallet.sendManyOutputsSendTransaction(params.outputs, {
     inputs: params.inputs || [],
     changeAddress: params.changeAddress,
   }) as unknown as ISendTransactionService;
+
+  // Runtime validation until wallet-lib exports proper types
+  if (typeof sendTransactionService.prepareTx !== 'function' ||
+      typeof sendTransactionService.signTx !== 'function' ||
+      typeof sendTransactionService.runFromMining !== 'function') {
+    throw new Error('Wallet does not support the required transaction service interface');
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
120 - 126, The code currently force-casts the result of
wallet.sendManyOutputsSendTransaction(...) to ISendTransactionService using "as
unknown as ISendTransactionService", which disables type safety; replace this
with a runtime type guard that validates the returned object
(sendTransactionService) implements the ISendTransactionService shape (e.g., has
the expected methods like start, getResult, cancel or whatever methods
ISendTransactionService defines) and throw a descriptive error if validation
fails; call the type-guard function (e.g., isSendTransactionService(result))
right after obtaining the result from wallet.sendManyOutputsSendTransaction and
only proceed using sendTransactionService when the guard returns true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 120-126: The code currently force-casts the result of
wallet.sendManyOutputsSendTransaction(...) to ISendTransactionService using "as
unknown as ISendTransactionService", which disables type safety; replace this
with a runtime type guard that validates the returned object
(sendTransactionService) implements the ISendTransactionService shape (e.g., has
the expected methods like start, getResult, cancel or whatever methods
ISendTransactionService defines) and throw a descriptive error if validation
fails; call the type-guard function (e.g., isSendTransactionService(result))
right after obtaining the result from wallet.sendManyOutputsSendTransaction and
only proceed using sendTransactionService when the guard returns true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72115a6b-8faa-428c-b48b-5348754a1d80

📥 Commits

Reviewing files that changed from the base of the PR and between e24866a and a3fdc2a.

📒 Files selected for processing (3)
  • packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
  • packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
  • packages/hathor-rpc-handler/src/types/prompt.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts (1)

60-69: ⚠️ Potential issue | 🟠 Major

Add a dedicated non-zero FBT fee assertion test.

Current fixtures only validate networkFee: 0n, so the core FBT objective (returning fee for fee-based-token tx) is still not explicitly covered.

Suggested test addition
+it('should include non-zero networkFee for FBT transaction in confirmation prompt', async () => {
+  const fbtUid = 'fbt-token-uid';
+  rpcRequest.params.outputs = [{ address: 'testAddress', value: '100', token: fbtUid }];
+
+  (wallet.sendManyOutputsSendTransaction as jest.Mock).mockResolvedValue({
+    prepareTx: jest.fn().mockResolvedValue({
+      inputs: [{ hash: 'testTxId', index: 0 }],
+      outputs: [{ value: 100n, tokenData: 1, script: p2pkhScript }],
+      tokens: [fbtUid],
+      getFeeHeader: jest.fn().mockReturnValue({
+        entries: [{ tokenIndex: 0, amount: 15n }],
+      }),
+    }),
+    signTx: jest.fn().mockResolvedValue({ toHex: jest.fn().mockReturnValue('00') }),
+    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);
+
+  expect(promptHandler).toHaveBeenNthCalledWith(
+    1,
+    expect.objectContaining({
+      data: expect.objectContaining({ networkFee: 15n }),
+    }),
+    {},
+  );
+});

Also applies to: 91-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`
around lines 60 - 69, The test fixture currently only covers networkFee: 0n; add
a dedicated test that simulates a fee-based-token (FBT) transaction returning a
non-zero fee by adjusting mockTransaction.getFeeHeader to return entries with a
non-zero amount (e.g., amount: 100n) and/or set the appropriate tokenIndex, then
call the sendTransaction RPC method (the same test flow using
prepareTx()/mockTransaction) and assert that the response includes the expected
non-zero FBT fee (compare BigInt values) instead of 0n; reference
mockTransaction, getFeeHeader, prepareTx, and the sendTransaction RPC handler to
locate where to change the fixture and where to add the assertion.
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

151-157: ⚠️ Potential issue | 🟠 Major

Keep fee aggregation token-aware instead of collapsing all entries.

feeHeader.entries can represent different tokens (tokenIndex), so summing all entries into one networkFee bigint loses unit/token semantics and can misreport the prompt fee.

Safer intermediate fix (while prompt still expects a scalar)
-const feeHeaderAmount = feeHeader
-  ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n)
-  : 0n;
+const feeByTokenIndex = new Map<number, bigint>();
+for (const entry of feeHeader?.entries ?? []) {
+  feeByTokenIndex.set(
+    entry.tokenIndex,
+    (feeByTokenIndex.get(entry.tokenIndex) ?? 0n) + entry.amount,
+  );
+}
+if (feeByTokenIndex.size > 1) {
+  throw new PrepareSendTransactionError(
+    'Multiple fee tokens found. Expose fee per token in prompt instead of a single bigint.'
+  );
+}
+const feeHeaderAmount = [...feeByTokenIndex.values()][0] ?? 0n;
In hathor-wallet-lib FeeHeader, can a single transaction contain multiple fee entries with different tokenIndex values, and does tokenIndex reference tx.tokens[tokenIndex]?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
151 - 157, The current code collapses all feeHeader.entries into one bigint
losing token units; change this to aggregate fees per tokenIndex by iterating
preparedTx.getFeeHeader().entries and summing amounts into a Map<number, bigint>
(tokenIndex -> totalFee), compute dataOutputCount and dataFee via
tokensUtils.getDataFee(dataOutputCount) and add that dataFee to the appropriate
token bucket (e.g., if there is only one tokenIndex use that bucket, otherwise
decide a clear rule or include dataFee as a separate entry), then use this
token-aware structure instead of a single networkFee bigint; if the external API
still requires a scalar keep a compatibility branch: if the Map.size === 1
return that single token's total (amount + dataFee) as networkFee, otherwise
surface the full token-indexed fees object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Around line 60-69: The test fixture currently only covers networkFee: 0n; add
a dedicated test that simulates a fee-based-token (FBT) transaction returning a
non-zero fee by adjusting mockTransaction.getFeeHeader to return entries with a
non-zero amount (e.g., amount: 100n) and/or set the appropriate tokenIndex, then
call the sendTransaction RPC method (the same test flow using
prepareTx()/mockTransaction) and assert that the response includes the expected
non-zero FBT fee (compare BigInt values) instead of 0n; reference
mockTransaction, getFeeHeader, prepareTx, and the sendTransaction RPC handler to
locate where to change the fixture and where to add the assertion.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 151-157: The current code collapses all feeHeader.entries into one
bigint losing token units; change this to aggregate fees per tokenIndex by
iterating preparedTx.getFeeHeader().entries and summing amounts into a
Map<number, bigint> (tokenIndex -> totalFee), compute dataOutputCount and
dataFee via tokensUtils.getDataFee(dataOutputCount) and add that dataFee to the
appropriate token bucket (e.g., if there is only one tokenIndex use that bucket,
otherwise decide a clear rule or include dataFee as a separate entry), then use
this token-aware structure instead of a single networkFee bigint; if the
external API still requires a scalar keep a compatibility branch: if the
Map.size === 1 return that single token's total (amount + dataFee) as
networkFee, otherwise surface the full token-indexed fees object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dcfff01c-120f-4684-929a-8ee8cf03ff1a

📥 Commits

Reviewing files that changed from the base of the PR and between a3fdc2a and 90f16a9.

📒 Files selected for processing (3)
  • packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
  • packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
  • packages/hathor-rpc-handler/src/types/prompt.ts

@pedroferreira1 pedroferreira1 changed the title feat: latest changes for returning the network fee when sending tx with FBT feat: return the network fee when sending tx with FBT Mar 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

189-218: ⚠️ Potential issue | 🟠 Major

Always emit SendTransactionLoadingFinishedTrigger, including failures.

If signTx/runFromMining fails, the loading-finished trigger is never sent, so the client can stay stuck in loading.

💡 Suggested fix (move loading-finished to finally)
   const loadingTrigger: SendTransactionLoadingTrigger = {
     type: TriggerTypes.SendTransactionLoadingTrigger,
   };
   promptHandler(loadingTrigger, requestMetadata);

   try {
     // Sign the prepared transaction with the user's PIN
     const signedTx = await sendTransactionObject.signTx(pinResponse.data.pinCode);

     let response: Transaction | string;
     if (params.pushTx === false) {
       // Return the signed transaction as hex without mining/pushing
       response = signedTx.toHex();
     } else {
       // Mine and push the signed transaction
       response = await sendTransactionObject.runFromMining();
     }
-
-    const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = {
-      type: TriggerTypes.SendTransactionLoadingFinishedTrigger,
-    };
-    promptHandler(loadingFinishedTrigger, requestMetadata);

     return {
       type: RpcResponseTypes.SendTransactionResponse,
       response,
     } as RpcResponse;
   } catch (err) {
     throw new SendTransactionError(err instanceof Error ? err.message : 'An unknown error occurred while sending the transaction');
+  } finally {
+    const loadingFinishedTrigger: SendTransactionLoadingFinishedTrigger = {
+      type: TriggerTypes.SendTransactionLoadingFinishedTrigger,
+    };
+    promptHandler(loadingFinishedTrigger, requestMetadata);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
189 - 218, The SendTransactionLoadingFinishedTrigger is not emitted on errors
because the promptHandler call for loadingFinishedTrigger is inside the try
block; move the creation/call of loadingFinishedTrigger and promptHandler into a
finally block so promptHandler(loadingFinishedTrigger, requestMetadata) always
runs even if sendTransactionObject.signTx or sendTransactionObject.runFromMining
throws; keep the current SendTransactionError rethrow logic (wrap the thrown
error message) and ensure you reference the existing loadingTrigger,
loadingFinishedTrigger, promptHandler, sendTransactionObject.signTx, and
sendTransactionObject.runFromMining symbols when making this change.
♻️ Duplicate comments (2)
packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts (1)

60-69: ⚠️ Potential issue | 🟠 Major

Add one non-zero FBT fee assertion test to validate the feature objective.

Current assertions only validate networkFee: 0n. The PR goal is FBT network fee return behavior, so a non-zero FBT fee-header scenario should be explicitly tested.

💡 Suggested test addition
+  it('should include non-zero networkFee for FBT fee header entries', async () => {
+    const fbtUid = 'fbt-token-uid';
+    mockTransaction.tokens = [fbtUid];
+    mockTransaction.getFeeHeader = jest.fn().mockReturnValue({
+      entries: [{ tokenIndex: 0, amount: 15n }],
+    });
+
+    promptHandler
+      .mockResolvedValueOnce({
+        type: TriggerResponseTypes.SendTransactionConfirmationResponse,
+        data: { accepted: true },
+      })
+      .mockResolvedValueOnce({
+        type: TriggerResponseTypes.PinRequestResponse,
+        data: { accepted: true, pinCode: '1234' },
+      });
+
+    sendTransactionMock.mockResolvedValue({ hash: 'txHash123' });
+
+    await sendTransaction(rpcRequest, wallet, {}, promptHandler);
+
+    expect(promptHandler).toHaveBeenNthCalledWith(
+      1,
+      expect.objectContaining({
+        data: expect.objectContaining({
+          networkFee: 15n,
+        }),
+      }),
+      {}
+    );
+  });

Also applies to: 91-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`
around lines 60 - 69, The test currently only asserts a zero FBT network fee;
add a new assertion case in sendTransaction.test.ts that sets
mockTransaction.getFeeHeader to return a non-zero fee (e.g., entries: [{
tokenIndex: 0, amount: 123n }]) on the mockTransaction object and assert that
the RPC response (from the sendTransaction invocation in the test) contains
networkFee equal to that non-zero BigInt; ensure the mockTransaction used in the
test has tokens/fee token alignment so getFeeHeader's tokenIndex matches the
expected fee token and add an explicit expect(response.networkFee).toBe(123n)
(or similar) to validate non-zero FBT handling.
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

150-156: ⚠️ Potential issue | 🟠 Major

Don’t aggregate fee entries from different fee tokens into a single networkFee.

Line 153 currently collapses all feeHeader.entries amounts into one bigint. If entries have different tokenIndex values, this mixes units and can display a misleading fee in the confirmation prompt.

In `@hathor/wallet-lib` FeeHeader, can one transaction contain entries with different tokenIndex values, and does tokenIndex map directly to tx.tokens[]?
💡 Suggested fix (guard single-token representation)
-  const feeHeaderAmount = feeHeader
-    ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n)
-    : 0n;
+  const feeByTokenIndex = feeHeader?.entries.reduce((acc, entry) => {
+    acc.set(entry.tokenIndex, (acc.get(entry.tokenIndex) ?? 0n) + entry.amount);
+    return acc;
+  }, new Map<number, bigint>()) ?? new Map<number, bigint>();
+
+  if (feeByTokenIndex.size > 1) {
+    throw new PrepareSendTransactionError(
+      'Cannot represent multi-token fee as a single networkFee value.'
+    );
+  }
+
+  const feeHeaderAmount = feeByTokenIndex.values().next().value ?? 0n;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
150 - 156, The current code sums all feeHeader.entries amounts into one
networkFee, which mixes different token units; change logic in the block using
preparedTx.getFeeHeader(), feeHeader.entries and networkFee so you either (a)
validate that all feeHeader.entries share the same tokenIndex and only then
compute a single bigint feeHeaderAmount + tokensUtils.getDataFee(...), or (b)
when multiple tokenIndex values exist, build a per-token fee map (keyed by
tokenIndex) and surface a per-token fee breakdown (or return/error) instead of
aggregating; reference feeHeader, feeHeader.entries, tokenIndex, networkFee,
tokensUtils.getDataFee and params.outputs to locate and update the calculation
and the confirmation/response path accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Line 31: The test declares let mockTransaction: any which breaks the
no-explicit-any rule; replace it by declaring a concrete type for
mockTransaction (either a TestTransaction interface or an existing Transaction
type) that includes the properties used in the test (e.g., inputs, outputs,
hash, serialize or methods referenced) and update the declaration in
sendTransaction.test.ts so mockTransaction: TestTransaction (or the exact type
name) is used instead of any; if no existing type fits, add a minimal local type
alias or interface above the tests capturing the mock shape and reference that
in the mockTransaction declaration.

---

Outside diff comments:
In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 189-218: The SendTransactionLoadingFinishedTrigger is not emitted
on errors because the promptHandler call for loadingFinishedTrigger is inside
the try block; move the creation/call of loadingFinishedTrigger and
promptHandler into a finally block so promptHandler(loadingFinishedTrigger,
requestMetadata) always runs even if sendTransactionObject.signTx or
sendTransactionObject.runFromMining throws; keep the current
SendTransactionError rethrow logic (wrap the thrown error message) and ensure
you reference the existing loadingTrigger, loadingFinishedTrigger,
promptHandler, sendTransactionObject.signTx, and
sendTransactionObject.runFromMining symbols when making this change.

---

Duplicate comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Around line 60-69: The test currently only asserts a zero FBT network fee; add
a new assertion case in sendTransaction.test.ts that sets
mockTransaction.getFeeHeader to return a non-zero fee (e.g., entries: [{
tokenIndex: 0, amount: 123n }]) on the mockTransaction object and assert that
the RPC response (from the sendTransaction invocation in the test) contains
networkFee equal to that non-zero BigInt; ensure the mockTransaction used in the
test has tokens/fee token alignment so getFeeHeader's tokenIndex matches the
expected fee token and add an explicit expect(response.networkFee).toBe(123n)
(or similar) to validate non-zero FBT handling.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 150-156: The current code sums all feeHeader.entries amounts into
one networkFee, which mixes different token units; change logic in the block
using preparedTx.getFeeHeader(), feeHeader.entries and networkFee so you either
(a) validate that all feeHeader.entries share the same tokenIndex and only then
compute a single bigint feeHeaderAmount + tokensUtils.getDataFee(...), or (b)
when multiple tokenIndex values exist, build a per-token fee map (keyed by
tokenIndex) and surface a per-token fee breakdown (or return/error) instead of
aggregating; reference feeHeader, feeHeader.entries, tokenIndex, networkFee,
tokensUtils.getDataFee and params.outputs to locate and update the calculation
and the confirmation/response path accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8418ae4-1c8e-4cab-8747-523730ffdc15

📥 Commits

Reviewing files that changed from the base of the PR and between 90f16a9 and a58117d.

📒 Files selected for processing (3)
  • packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
  • packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts
  • packages/hathor-rpc-handler/src/types/prompt.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/hathor-rpc-handler/src/types/prompt.ts

Comment thread packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts Outdated
@pedroferreira1
Copy link
Copy Markdown
Member Author

@coderabbitai about your comment outside the diff: If signTx/runFromMining throws, the error propagates up to the client, which catches it and can dismiss the loading state itself. The finally would be redundant since the client already handles errors.
This change is not desired.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@pedroferreira1 Understood, thanks for the clarification! Since the client already catches errors from signTx/runFromMining and handles the loading state dismissal itself, a finally block at the RPC handler level would indeed be redundant. I'll keep this in mind for future reviews.

(´• ω •`) 🐇


✏️ Learnings added
Learnt from: pedroferreira1
URL: https://github.com/HathorNetwork/hathor-rpc-lib/pull/139

Timestamp: 2026-03-04T14:05:39.986Z
Learning: In the `hathor-rpc-lib` repository (`packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`), errors thrown by `signTx` or `runFromMining` intentionally propagate up to the client layer, which is responsible for catching them and dismissing any loading state. A `finally` block at the RPC handler level for this purpose is considered redundant and is not desired.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@pedroferreira1 pedroferreira1 self-assigned this Mar 4, 2026
@pedroferreira1 pedroferreira1 moved this from Todo to In Progress (Done) in Hathor Network Mar 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts (2)

489-517: Make the FBT fee test use a real non-native output token.

Line 489 names this as an FBT scenario, but it still runs with the default request output token ('00'). This can pass even if non-native token handling regresses. Set a non-native UID in rpcRequest.params.outputs and assert token details are fetched/surfaced in the first prompt.

Suggested test tightening
   it('should calculate non-zero networkFee when FBT fee header is present', async () => {
+    rpcRequest.params.outputs = [{
+      address: 'testAddress',
+      value: '100',
+      token: 'token-uid-123',
+    }];
+
     const fbtMockTransaction = {
       inputs: [{ hash: 'testTxId', index: 0 }],
       outputs: [{ value: BigInt(100), tokenData: 0, script: p2pkhScript }],
       tokens: ['token-uid-123'],
@@
     await sendTransaction(rpcRequest, wallet, {}, promptHandler);
 
+    expect(wallet.getTokenDetails).toHaveBeenCalledWith('token-uid-123');
     expect(promptHandler).toHaveBeenNthCalledWith(1,
       expect.objectContaining({
         type: TriggerTypes.SendTransactionConfirmationPrompt,
         data: expect.objectContaining({
           networkFee: 500n,
+          tokenDetails: expect.any(Map),
         }),
       }),
       {},
     );
   });

Also applies to: 519-527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`
around lines 489 - 517, The test currently labeled as an FBT scenario uses the
default/native output token, so update the test to set a non-native token UID in
rpcRequest.params.outputs (e.g., a real non-native token UID) and ensure the
mocked fbt transaction (fbtMockTransaction) reflects that token in
tokens/outputs; then assert that the first prompt from promptHandler includes
the token details (use TriggerResponseTypes.SendTransactionConfirmationResponse
and check prompt data contains the non-native token UID or label) while keeping
the same mocks for wallet.sendManyOutputsSendTransaction, prepareTx, signTx, and
sendTransaction call to sendTransaction(rpcRequest, wallet, {}, promptHandler).

60-69: Extract a prepared-transaction factory to reduce mock drift.

The prepared transaction mock shape is duplicated in several places; a small factory will make future flow updates safer.

Also applies to: 402-409, 454-461, 490-498

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`
around lines 60 - 69, The test duplicates the same prepared-transaction mock in
multiple places (mockTransaction in sendTransaction.test.ts) causing mock drift;
extract a single factory function (e.g., createPreparedTransactionMock or
buildPreparedTxMock) that returns the common shape (inputs, outputs with BigInt
value, tokens, getFeeHeader mock, toHex mock) and replace each inline
mockTransaction/prepareTx mock with calls to this factory (update usages in the
blocks around the existing mocks noted and at the other duplicated ranges).
Ensure the factory returns fresh objects per call so tests don’t share mutated
state and keep the same property names used by tests (inputs, outputs, tokens,
getFeeHeader, toHex).
packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts (1)

123-127: The double-cast works around an undocumented wallet-lib API; error handling exists but assertion would improve clarity.

The sendManyOutputsSendTransaction method is not in the wallet-lib v2.14.0 documented API (as noted in the TODO at lines 43–44). The double-cast at line 123 is intentional, and the subsequent try-catch block at lines 132–141 catches any shape mismatches and converts them to PrepareSendTransactionError. If prepareTx is missing, the error is already handled with a meaningful message rather than propagating a cryptic "not a function" error.

A runtime assertion as shown in the original suggestion would provide earlier failure detection and clearer logging during development, but isn't required given the existing error-handling layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts` around lines
123 - 127, The code double-casts the return of
wallet.sendManyOutputsSendTransaction to ISendTransactionObject which masks an
undocumented API and can produce an unclear runtime "not a function" if the
shape is wrong; add a runtime assertion right after the call to verify the
returned object has the expected properties (e.g., is non-null and includes
prepareTx as a function and any other required fields from
ISendTransactionObject) and throw a clear, descriptive error if the check fails
so failures are detected earlier and logged clearly; reference the
wallet.sendManyOutputsSendTransaction call and ISendTransactionObject (and
prepareTx) when adding this assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts`:
- Around line 489-517: The test currently labeled as an FBT scenario uses the
default/native output token, so update the test to set a non-native token UID in
rpcRequest.params.outputs (e.g., a real non-native token UID) and ensure the
mocked fbt transaction (fbtMockTransaction) reflects that token in
tokens/outputs; then assert that the first prompt from promptHandler includes
the token details (use TriggerResponseTypes.SendTransactionConfirmationResponse
and check prompt data contains the non-native token UID or label) while keeping
the same mocks for wallet.sendManyOutputsSendTransaction, prepareTx, signTx, and
sendTransaction call to sendTransaction(rpcRequest, wallet, {}, promptHandler).
- Around line 60-69: The test duplicates the same prepared-transaction mock in
multiple places (mockTransaction in sendTransaction.test.ts) causing mock drift;
extract a single factory function (e.g., createPreparedTransactionMock or
buildPreparedTxMock) that returns the common shape (inputs, outputs with BigInt
value, tokens, getFeeHeader mock, toHex mock) and replace each inline
mockTransaction/prepareTx mock with calls to this factory (update usages in the
blocks around the existing mocks noted and at the other duplicated ranges).
Ensure the factory returns fresh objects per call so tests don’t share mutated
state and keep the same property names used by tests (inputs, outputs, tokens,
getFeeHeader, toHex).

In `@packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts`:
- Around line 123-127: The code double-casts the return of
wallet.sendManyOutputsSendTransaction to ISendTransactionObject which masks an
undocumented API and can produce an unclear runtime "not a function" if the
shape is wrong; add a runtime assertion right after the call to verify the
returned object has the expected properties (e.g., is non-null and includes
prepareTx as a function and any other required fields from
ISendTransactionObject) and throw a clear, descriptive error if the check fails
so failures are detected earlier and logged clearly; reference the
wallet.sendManyOutputsSendTransaction call and ISendTransactionObject (and
prepareTx) when adding this assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fbddddda-4ad3-4402-a0e1-780b605a260d

📥 Commits

Reviewing files that changed from the base of the PR and between a58117d and a2b9d86.

📒 Files selected for processing (2)
  • packages/hathor-rpc-handler/__tests__/rpcMethods/sendTransaction.test.ts
  • packages/hathor-rpc-handler/src/rpcMethods/sendTransaction.ts

Comment on lines +144 to +147
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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can't you also filter for output.token !== constants.NATIVE_TOKEN_UID so you don't have to loop again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done 821d51e

raul-oliveira
raul-oliveira previously approved these changes Mar 4, 2026
@github-project-automation github-project-automation bot moved this from In Progress (Done) to In Review (WIP) in Hathor Network Mar 4, 2026
andreabadesso
andreabadesso previously approved these changes Mar 6, 2026
@raul-oliveira raul-oliveira self-requested a review March 6, 2026 13:45
@pedroferreira1 pedroferreira1 merged commit 92ccf8a into master Mar 6, 2026
2 checks passed
@github-project-automation github-project-automation bot moved this from In Review (WIP) to Waiting to be deployed in Hathor Network Mar 6, 2026
@pedroferreira1 pedroferreira1 deleted the feat/fee-based-tokens-send-tx branch March 6, 2026 13:46
export type SendTransactionConfirmationPrompt = BaseConfirmationPrompt & {
type: TriggerTypes.SendTransactionConfirmationPrompt;
data: {
outputs: IDataOutput[],
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants