Skip to content

feat: add UTXO release mechanism for nano contract transactions#1050

Open
raul-oliveira wants to merge 10 commits intomasterfrom
raul-oliveira/feat/utxo-unlock
Open

feat: add UTXO release mechanism for nano contract transactions#1050
raul-oliveira wants to merge 10 commits intomasterfrom
raul-oliveira/feat/utxo-unlock

Conversation

@raul-oliveira
Copy link
Copy Markdown
Contributor

@raul-oliveira raul-oliveira commented Mar 30, 2026

Acceptance criteria

  • Add TTL (60s) to markUtxoSelected calls in NanoContractTransactionBuilder as safety net against permanent UTXO locks
  • Add releaseUtxos() to ISendTransaction interface and SendTransaction class for explicit UTXO cleanup on rejection/abandonment
  • Add no-op releaseUtxos() to SendTransactionWalletService
  • Add unit tests for releaseUtxos (happy path, null tx, null storage, mid-loop error)
  • Add integration test validating lock/unlock lifecycle: prepare -> fail ->release -> succeed

Security Checklist

  • 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

    • Users can now release previously locked UTXOs from prepared transactions, improving flexibility when reusing funds.
  • API

    • Send-transaction interfaces expose a release operation so clients can trigger UTXO release when needed.
  • Tests

    • Added unit and integration tests covering the UTXO lock/unlock lifecycle and release behavior to ensure robustness.

  - Add TTL (60s) to markUtxoSelected calls in NanoContractTransactionBuilder
    as safety net against permanent UTXO locks
  - Add releaseUtxos() to ISendTransaction interface and SendTransaction class
    for explicit UTXO cleanup on rejection/abandonment
  - Add no-op releaseUtxos() to SendTransactionWalletService
  - Add unit tests for releaseUtxos (happy path, null tx, null storage, mid-loop
   error)
  - Add integration test validating lock/unlock lifecycle: prepare -> fail ->
  release -> succeed
@raul-oliveira raul-oliveira self-assigned this Mar 30, 2026
@raul-oliveira raul-oliveira moved this from Todo to In Progress (Done) in Hathor Network Mar 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 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

Added a new releaseUtxos() lifecycle method to SendTransaction and its interface, updated UTXO selection calls to pass SELECT_OUTPUTS_TIMEOUT, and added unit and integration tests validating UTXO lock/unlock behavior and error resilience.

Changes

Cohort / File(s) Summary
Interface
src/wallet/types.ts
Added releaseUtxos(): Promise<void> to ISendTransaction.
SendTransaction implementation
src/new/sendTransaction.ts
New async releaseUtxos() method that iterates transaction inputs and calls storage.utxoSelectAsInput(..., false, SELECT_OUTPUTS_TIMEOUT) with per-call try/catch to continue on errors.
Wallet service
src/wallet/sendTransactionWalletService.ts
Added public releaseUtxos(): Promise<void> no-op method (server-managed UTXO state).
Nano contract builder
src/nano_contracts/builder.ts
All this.wallet.markUtxoSelected(...) calls updated to include SELECT_OUTPUTS_TIMEOUT parameter across deposit, grant authority, and fee selection flows.
Unit tests
__tests__/new/sendTransaction.test.ts
Added describe('releaseUtxos') suite: tests for unlocking calls per-input, noop when transaction is null, safe behavior when storage is unset, and resilience when some storage calls reject.
Integration tests
__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts
New integration test validating nano contract UTXO lock on prepare, blocking re-prepare, and successful re-prepare after releaseUtxos(); ensures release is invoked in teardown.
Test helper typing
__tests__/integration/helpers/wallet.helper.ts
Made waitTxConfirmed third timeout parameter optional (`timeout?: number

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SendTx as SendTransaction
    participant Storage
    participant Wallet as SendTransactionWalletService

    Client->>SendTx: prepareTx(inputs, outputs)
    SendTx->>Storage: utxoSelectAsInput({txId,index}, true, SELECT_OUTPUTS_TIMEOUT)
    Storage-->>SendTx: locked / ack
    SendTx-->>Client: transaction prepared

    Client->>SendTx: prepareTx() with same inputs
    SendTx->>Storage: utxoSelectAsInput({txId,index}, true, SELECT_OUTPUTS_TIMEOUT)
    Storage-->>SendTx: error (already locked)
    SendTx-->>Client: NanoContractTransactionError

    Client->>SendTx: releaseUtxos()
    loop per input
        SendTx->>Storage: utxoSelectAsInput({txId,index}, false, SELECT_OUTPUTS_TIMEOUT)
        opt success
            Storage-->>SendTx: unlocked
        end
        opt failure
            Storage-->>SendTx: error (logged, continue)
        end
    end
    SendTx-->>Client: releaseUtxos() complete

    Client->>SendTx: prepareTx() again
    SendTx->>Storage: utxoSelectAsInput({txId,index}, true, SELECT_OUTPUTS_TIMEOUT)
    Storage-->>SendTx: locked / ack
    SendTx-->>Client: transaction prepared
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

tests, enhancement

Suggested reviewers

  • pedroferreira1
  • r4mmer
  • tuliomir

Poem

🐰 I nudged each UTXO, set them free,

Locked for a moment in transaction's spree.
releaseUtxos() hops, one call at a time,
Unlocking the garden, restoring the lime. 🌿🔓

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a UTXO release mechanism for nano contract transactions, which is the primary feature across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch raul-oliveira/feat/utxo-unlock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.94%. Comparing base (0ccc910) to head (02733be).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1050      +/-   ##
==========================================
+ Coverage   87.93%   87.94%   +0.01%     
==========================================
  Files         114      114              
  Lines        8910     8918       +8     
  Branches     2030     2022       -8     
==========================================
+ Hits         7835     7843       +8     
  Misses       1047     1047              
  Partials       28       28              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/new/sendTransaction.test.ts`:
- Line 8: The import line including TOKEN_AUTHORITY_MASK, NATIVE_TOKEN_UID, and
SELECT_OUTPUTS_TIMEOUT in the test file has Prettier formatting violations; run
a formatter (e.g., run prettier --write on
__tests__/new/sendTransaction.test.ts) or adjust the import to match project
style (single quotes/double quotes, spacing, trailing semicolons) so the linter
stops flagging the file and the import statement conforms to the repository's
Prettier rules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e17c0e97-feff-46a0-9bf9-5ee99b5d2988

📥 Commits

Reviewing files that changed from the base of the PR and between 1991f55 and 97b872a.

📒 Files selected for processing (6)
  • __tests__/integration/nano_utxo_lock_unlock.test.ts
  • __tests__/new/sendTransaction.test.ts
  • src/nano_contracts/builder.ts
  • src/new/sendTransaction.ts
  • src/wallet/sendTransactionWalletService.ts
  • src/wallet/types.ts

Comment thread __tests__/new/sendTransaction.test.ts Outdated
@raul-oliveira raul-oliveira moved this from In Progress (Done) to In Progress (WIP) in Hathor Network Mar 31, 2026
@raul-oliveira raul-oliveira requested a review from tuliomir April 6, 2026 16:55
@raul-oliveira raul-oliveira moved this from In Progress (WIP) to In Progress (Done) in Hathor Network Apr 6, 2026
Comment thread src/new/sendTransaction.ts Outdated
*/
// eslint-disable-next-line class-methods-use-this
async releaseUtxos(): Promise<void> {
// No-op: wallet-service manages UTXO state server-side
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.

question: Is there a plan to implement this feature for the Wallet Service too? An API call to release the stuck UTXOs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Probably not, because the way wallet service locks the utxos is by binding them to a tx proposal. In this use case that we are dealing, we never create a tx proposal before the user approves

@tuliomir tuliomir moved this from In Progress (Done) to In Review (WIP) in Hathor Network Apr 6, 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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts`:
- Line 18: The test asserts failure by the NanoContractTransactionError type but
the second prepare-lock path throws a generic Error from the send-transaction
flow, so update the assertions in
__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts to assert
the error message content rather than the error class: find the two expectations
using toThrow(NanoContractTransactionError) (the second prepare-lock path around
lines ~102-120 and the earlier occurrence) and replace them with assertions that
capture the thrown error and assert on its message (e.g., expect(() =>
...).toThrowErrorMatching(/your expected substring/) or use try/catch and
expect(err.message).toMatch(/expected message/)), matching the actual error
string emitted by the send-transaction flow (e.g., the "already locked" or
prepare-lock failure text).
- Line 29: Replace explicit nulls used as "not provided" with omitted arguments
(or undefined) to follow the TypeScript convention: update calls to
waitTxConfirmed(wallet, txId, null) to simply waitTxConfirmed(wallet, txId) and
change generateWalletHelper(null) to generateWalletHelper() (or use undefined if
you prefer explicitness); ensure you only remove the third argument where the
function signatures for waitTxConfirmed and generateWalletHelper accept an
optional parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b88eb1d0-5962-48db-b331-f1ff658c3f64

📥 Commits

Reviewing files that changed from the base of the PR and between 2e4b998 and c394ff5.

📒 Files selected for processing (3)
  • __tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts
  • src/new/sendTransaction.ts
  • src/wallet/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/wallet/types.ts
  • src/new/sendTransaction.ts

Comment thread __tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.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.

♻️ Duplicate comments (1)
__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts (1)

102-120: ⚠️ Potential issue | 🟠 Major

Use message-based lock assertion instead of error class assertion.

At Line 120, asserting NanoContractTransactionError is brittle for this path; the failure may come as a generic error while still proving lock contention. Assert by lock-related message/content instead.

Suggested adjustment
-import { NanoContractTransactionError } from '../../../src/errors';
@@
-      ).rejects.toThrow(NanoContractTransactionError);
+      ).rejects.toThrow(/locked|being used/i);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts` around
lines 102 - 120, Replace the brittle class-based rejection assertion on
hWallet.createNanoContractTransaction with a message/content-based assertion
that verifies the error indicates a lock contention; specifically, call
hWallet.createNanoContractTransaction(...) with signTx: false as before but
change the expectation to .rejects.toThrow(/lock|locked|contention|already
locked/i) or assert the thrown error.message includes a lock-related substring
rather than expecting NanoContractTransactionError so the test validates lock
behavior regardless of error class.
🧹 Nitpick comments (1)
__tests__/integration/helpers/wallet.helper.ts (1)

455-459: Align optional timeout typing with TS migration convention.

At Line 458, prefer timeout?: number instead of timeout?: number | null to avoid using null as “not provided”.

Suggested adjustment
 export async function waitTxConfirmed(
   hWallet: HathorWallet,
   txId: string,
-  timeout?: number | null
+  timeout?: number
 ): Promise<void> {

Based on learnings, optional/unprovided TypeScript parameters should use undefined/? and avoid null.

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

In `@__tests__/integration/helpers/wallet.helper.ts` around lines 455 - 459, The
function signature waitTxConfirmed currently types the optional parameter as
timeout?: number | null; update it to use the TS-conventional optional form
timeout?: number (drop | null) and then adjust any internal checks inside
waitTxConfirmed (and any callers, if necessary) that compare explicitly to null
to instead check for undefined or use truthy/typeof checks so the logic behaves
the same when the timeout is omitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@__tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts`:
- Around line 102-120: Replace the brittle class-based rejection assertion on
hWallet.createNanoContractTransaction with a message/content-based assertion
that verifies the error indicates a lock contention; specifically, call
hWallet.createNanoContractTransaction(...) with signTx: false as before but
change the expectation to .rejects.toThrow(/lock|locked|contention|already
locked/i) or assert the thrown error.message includes a lock-related substring
rather than expecting NanoContractTransactionError so the test validates lock
behavior regardless of error class.

---

Nitpick comments:
In `@__tests__/integration/helpers/wallet.helper.ts`:
- Around line 455-459: The function signature waitTxConfirmed currently types
the optional parameter as timeout?: number | null; update it to use the
TS-conventional optional form timeout?: number (drop | null) and then adjust any
internal checks inside waitTxConfirmed (and any callers, if necessary) that
compare explicitly to null to instead check for undefined or use truthy/typeof
checks so the logic behaves the same when the timeout is omitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6beb2cbf-55b2-4af2-be90-8c754c16a6a6

📥 Commits

Reviewing files that changed from the base of the PR and between c394ff5 and 3b76e9b.

📒 Files selected for processing (2)
  • __tests__/integration/fullnode-specific/nano_utxo_lock_unlock.test.ts
  • __tests__/integration/helpers/wallet.helper.ts

@raul-oliveira raul-oliveira requested a review from tuliomir April 13, 2026 16:07
tuliomir
tuliomir previously approved these changes Apr 13, 2026
hWallet: HathorWallet,
txId: string,
timeout: number | null | undefined
timeout?: number | null
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.

suggestion(non-blocking): Reinforcing a suggestion from CodeRabbit, remove null from the parameter types, as it's not conceptually correct to pass this value here.

pedroferreira1
pedroferreira1 previously approved these changes Apr 15, 2026
Comment thread src/new/sendTransaction.ts Outdated
await this.storage.utxoSelectAsInput(
{ txId: input.hash, index: input.index },
false,
SELECT_OUTPUTS_TIMEOUT
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not a bug, it just has no use the timeout if we are setting to false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nice catch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@tuliomir tuliomir moved this from In Review (WIP) to In Review (Done) in Hathor Network Apr 15, 2026
@tuliomir tuliomir moved this from In Review (Done) to In Review (WIP) in Hathor Network Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review (WIP)

Development

Successfully merging this pull request may close these issues.

3 participants