Skip to content

Fix #50: fail-fast spendable check and spendable-balance model#51

Merged
codewordneptune merged 7 commits into
mainfrom
cwn/spendable-check
Jul 15, 2026
Merged

Fix #50: fail-fast spendable check and spendable-balance model#51
codewordneptune merged 7 commits into
mainfrom
cwn/spendable-check

Conversation

@codewordneptune

@codewordneptune codewordneptune commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Sending while a previous transaction was still unconfirmed could build an unfunded 0-input transaction that failed with a cryptic VM error after a long proving run. This PR makes such sends fail instantly with a clear message, and reworks the balance model so every figure in the app accounts for pending transactions correctly.

Fixes #50

Backend (Rust)

  • Fail-fast spendable check: input selection errors before proving when spendable UTXOs cannot cover amount plus fee. The error distinguishes "Insufficient funds" from "Funds are awaiting confirmation", and the latter is only used when retrying after confirmation can actually succeed.
  • Pending balance model: get_all_balance returns (available, pending, total). Available excludes inputs of unconfirmed transactions, pending is the expected incoming change, and total drops at broadcast rather than at confirmation. The RPC balance response gains pending_balance.
  • Cache freshness: the cached per-account total is rewritten after send and after forget, not only on block sync.

Frontend

  • Ownership vs. action split: the Wallet page shows what you own (figure includes pending change), while the Send page shows "Spendable now" with an inline note explaining any gap.
  • Lifecycle refreshes: broadcast, forget, and block sync refetch balances and the accounts list. Idle sync heartbeats are height guarded so nothing flickers, and the accounts table shows its loading overlay only on first load.
  • Stale-state protection: the pending list is account stamped so a previous account's data never flashes after a switch, and confirmed transactions leave the pending section automatically.
  • Presentation: pending transaction cards get a proper header (sent time, Forget action), amount columns are right aligned with tabular figures, clicking anywhere in an account row switches accounts (copy button excepted), and the toast utility gains sticky and fixed-id options.

Verification

  • TypeScript: tsc --noEmit clean, no new warnings.
  • Rust: run continuously via tauri dev through two days of live testing with a dozen-plus real transactions, including failure cases surfaced gracefully by the new error paths.
  • The diff was also reviewed by a multi-agent adversarial pass. Suggestions out of scope here are tracked as follow-ups.

Screenshots

image image

🤖 Generated with Claude Code

codewordneptune and others added 2 commits July 12, 2026 02:30
…cted incoming change

Input selection now errors before proving when spendable UTXOs cannot
cover amount + fee, distinguishing "insufficient funds" from "funds
awaiting confirmation" (the latter only when the expected incoming
change actually covers the shortfall). Root cause of #50, where an
underfunded selection produced a 0-input transaction that failed the
VM's balance assertion after ~20s of proving.

get_all_balance returns (available, pending, total): available excludes
inputs of unconfirmed transactions, pending is the expected incoming
amount (change/self-sends) computed from stored pending transaction
details, and total drops by the sent amount at broadcast. The RPC
balance response gains pending_balance. The per-account cached total is
refreshed after send and forget, not only on block sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ifecycle

Wallet page shows the ownership view (figure includes pending change,
fixed 4-decimal precision, de-emphasized decimals, equal card heights);
the Send page shows "Spendable now" with an awaiting-confirmation hint.
Broadcast, forget, and block sync all refetch balance + wallets list;
the sync_finish listener is height-guarded so idle heartbeats fetch
nothing, and the accounts table only overlays on first load. The
pending list is account-stamped (executionAddressId) so a stale list
never flashes after an account switch, and confirmed transactions leave
the pending section on their own via a block-gated refetch.

Pending-transaction cards get a proper header (Sent <time> + absolute
timestamp on hover + Forget), the accounts-table balance column is
right-aligned with tabular figures, address-cell clicks switch accounts
(copy button excepted), and the toast utility gains sticky/fixed-id
options with an 8s error duration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codewordneptune and others added 3 commits July 12, 2026 02:42
Copying inside a clickable row (e.g. the accounts table, where a row
click switches the active account) must never also trigger the row
action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codewordneptune codewordneptune self-assigned this Jul 12, 2026

@Sword-Smith Sword-Smith left a comment

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.

Please fix docstring per my suggestion. And fix conflicts by rebasing this PR branch on top of origin/main (GitHub's default branch). Then merge at your own discretion.

Comment thread src-tauri/src/rpc/mod.rs Outdated
Comment on lines +100 to +101
// Expected incoming amount (change and self-sends) from this wallet's
// pending transactions; credited back to the balance once they are mined.

@Sword-Smith Sword-Smith Jul 13, 2026

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.

Is this always a non-negative number? I'm not 100 % sure what it means. It only tracks positive changes to balance, right? If I'm right, please add: "Always a non-negative number; only tracks positive changes to balance." If I'm wrong elaborate on what this means in the docstring of the field.

Also: Use docstring here instead of regular comments. Docstrings are preceded with three forward slashes: /// comment.

@codewordneptune codewordneptune Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added docstrings and renamed fields available_balance and pending_balance because I realized they no longer accurately reflected the underlying values after these changes.

The new names are:
available_balancespendable_balance
pending_balancepending_change (which is always >= 0)

The available balance shown on the Wallet page is calculated as:
available_balance = spendable_balance + pending_change

The updated names should make their intent clearer and the docstrings provide additional context where needed.

On a related note, many crypto wallets deduct a transaction amount from the available balance as soon as the transaction is broadcast without waiting for its confirmation. Neptune Wallet did not previously follow this convention, but with this change, the available balance shown in the UI will now reflect pending outgoing transactions immediately.

@codewordneptune codewordneptune Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Just one final check: do you approve the updated naming?

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.

Just one final check: do you approve the updated naming?

Yes :)

@codewordneptune codewordneptune changed the title Fix #50: fail-fast spendable check and pending-balance model Fix #50: fail-fast spendable check and spendable-balance model Jul 14, 2026
let pending_utxos = self.updater.get_pending_spent_utxos().await?;
utxos.retain(|utxo| !pending_utxos.contains(&utxo.id));
let pending_ids = self.updater.get_pending_spent_utxos().await?;
let utxos: Vec<_> = utxos

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.

Nice. I like shadowing when used like this, to overwrite an existing value with a "more refined" calculation.

Resolves the src/App.tsx conflict: keep main's unused-variable cleanup
(serverUrl-only destructure) and this branch's sync-height guard, and
restore the (event) parameter on the SYNC_FINISH_EVENT listener, which
the auto-merge dropped while the guard logic reads event.payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codewordneptune
codewordneptune merged commit b1e9ea3 into main Jul 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Send while funds are tied up in a pending transaction builds a 0-input transaction and fails with a cryptic VM error

2 participants