Skip to content

feat(platform-wallet): return the exact network fee from send_payment#4095

Merged
QuantumExplorer merged 1 commit into
v4.1-devfrom
feat/send-dashpay-payment-exact-fee
Jul 11, 2026
Merged

feat(platform-wallet): return the exact network fee from send_payment#4095
QuantumExplorer merged 1 commit into
v4.1-devfrom
feat/send-dashpay-payment-exact-fee

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 11, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

DashPay payment callers (dashwallet-ios in particular) need to show the exact network fee of the broadcast transaction. The transaction builder already computes this fee for every DashPay payment, but send_payment discarded it, so wallets could only re-estimate it after the fact.

This has been carried as a local patch on the dashwallet-ios platform pin branch and is flagged in that repo's DASHSYNC_MIGRATION.md as needing upstreaming — landing it here shrinks the next pin bump.

What was done?

Thread the fee the builder already computes through the full stack:

  • PlatformWallet::send_payment (rust): returns (txid, entry, fee) instead of dropping the fee.
  • platform_wallet_send_dashpay_payment (FFI): gains a nullable out_fee_duffs out-param.
  • ManagedPlatformWallet.sendDashPayPayment (Swift): returns (txid: Data, feeDuffs: UInt64).
  • SendDashPayPaymentSheet (SwiftExampleApp): adjusted to the tuple return.

How Has This Been Tested?

  • cargo check -p platform-wallet -p platform-wallet-ffi --all-targets passes.
  • cargo fmt --all --check clean.
  • SwiftExampleApp builds for the iOS simulator against a freshly rebuilt DashSDKFFI.xcframework containing the updated header.
  • The same change has been exercised end-to-end on the dashwallet-ios pin branch, where the returned fee is displayed after a DashPay payment broadcast.

Breaking Changes

None for consensus or the platform protocol. API-wise, send_payment's return type and the FFI/Swift signatures change; the FFI out-param is nullable so existing C callers can pass NULL.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • DashPay payment results now include the exact network fee in duffs, along with the transaction ID.
    • Swift SDK integrations can access the fee returned for each completed DashPay payment.
  • Updates
    • Existing payment flows continue to display success and handle errors as before.
    • Applications using the updated payment API should accommodate the additional fee value in the result.

The builder computed the fee for every DashPay payment and discarded
it. Thread it through: send_payment returns (txid, entry, fee);
platform_wallet_send_dashpay_payment gains a nullable out_fee_duffs;
Swift sendDashPayPayment returns (txid, feeDuffs) so wallets can show
the exact fee of the broadcast transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DashPay payment sending now propagates the exact network fee from transaction construction through the Rust FFI and Swift SDK. The example app adapts to the tuple return while continuing to use the transaction ID.

Changes

DashPay payment fee propagation

Layer / File(s) Summary
Capture and return payment fee
packages/rs-platform-wallet/src/wallet/identity/network/payments.rs
send_payment captures the fee from signed transaction construction and returns it with the transaction ID and payment entry.
Expose fee through FFI
packages/rs-platform-wallet-ffi/src/dashpay.rs
The exported function accepts an optional fee output pointer and writes the returned fee when provided.
Return and consume fee in Swift
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift, packages/swift-sdk/SwiftExampleApp/.../SendDashPayPaymentSheet.swift
The Swift SDK returns (txid, feeDuffs), while the example app continues using only the transaction ID.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SwiftSDK as ManagedPlatformWallet
  participant FFI as platform_wallet_send_dashpay_payment
  participant Rust as DashPayView
  participant Builder as TransactionBuilder
  SwiftSDK->>FFI: Provide txid and fee output pointers
  FFI->>Rust: Call send_payment
  Rust->>Builder: Build signed transaction
  Builder-->>Rust: Return transaction and fee
  Rust-->>FFI: Return txid, entry, and fee
  FFI-->>SwiftSDK: Populate txid and feeDuffs
Loading

Possibly related PRs

  • dashpay/platform#3985: Modifies the same send_payment flow around transaction build and broadcast handling.

Suggested reviewers: shumkov, lklimek, llbartekll, ZocoLini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: returning the exact network fee from the platform wallet payment flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/send-dashpay-payment-exact-fee

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.

@thepastaclaw

thepastaclaw commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 5f4bb14)

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
packages/rs-platform-wallet/src/wallet/identity/network/payments.rs (1)

539-542: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the # Returns doc to mention the network fee.

The return type now includes u64 for the fee (line 555), but the # Returns doc still describes only the Txid and PaymentEntry. The Swift counterpart at ManagedPlatformWallet.swift:2104-2105 was updated; the Rust doc should be too.

📝 Proposed doc update
 /// # Returns
 ///
-/// The `Txid` of the broadcast transaction and the newly created
-/// [`PaymentEntry`] recording the outgoing payment.
+/// The `Txid` of the broadcast transaction, the newly created
+/// [`PaymentEntry`] recording the outgoing payment, and the
+/// network fee in duffs from the broadcast transaction.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/identity/network/payments.rs` around
lines 539 - 542, Update the # Returns documentation for the payment-broadcasting
function near PaymentEntry to describe all returned values, including the u64
network fee alongside the Txid and PaymentEntry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/rs-platform-wallet/src/wallet/identity/network/payments.rs`:
- Around line 539-542: Update the # Returns documentation for the
payment-broadcasting function near PaymentEntry to describe all returned values,
including the u64 network fee alongside the Txid and PaymentEntry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff2754d6-85ee-44b5-8f7f-d32d5feafbb3

📥 Commits

Reviewing files that changed from the base of the PR and between 0a32757 and 5f4bb14.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-ffi/src/dashpay.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/payments.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/SendDashPayPaymentSheet.swift

@QuantumExplorer QuantumExplorer merged commit 807ba0e into v4.1-dev Jul 11, 2026
19 checks passed
@QuantumExplorer QuantumExplorer deleted the feat/send-dashpay-payment-exact-fee branch July 11, 2026 22:14

@thepastaclaw thepastaclaw left a comment

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.

Code Review

Small plumbing change that threads an already-computed fee value from TransactionBuilder::build_signed through send_payment -> FFI out-param -> Swift tuple. The Swift/FFI marshalling is correct and safe (nullable out-param, exhaustive tuple matching, proper lifetime handling of the resolver signer). However, tracing the fee value back to its source in the vendored key-wallet crate (TransactionBuilder::build_signed, pinned rev 1ee1c94) shows it is a size*rate recomputation from the final signed tx, not the actual inputs-minus-outputs amount baked into the transaction during coin selection/change calculation — these two values provably diverge whenever a dust remainder is folded into the fee (no change output created) and can diverge more generally from encoded-size estimation differences. This directly undermines the PR's stated goal of exposing the "exact network fee."

Source (experiment sonnet-primary-opus-quarter-sample-20260710, cohort sonnet_primary, bucket 2): reviewers codex/general=gpt-5.6-sol(completed_parseable_output_with_nonzero_wrapper_exit); sonnet5/general=claude-sonnet-5(completed); verifier=verifier-sonnet5-4095-1783808164=claude-sonnet-5; orchestrator=openai/gpt-5.6-sol reasoning=high (orchestration-only, not a reviewer/verifier).

🔴 1 blocking | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/wallet/identity/network/payments.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/identity/network/payments.rs:668-675: Piped-through fee is a size-based recomputation, not the actual inputs-minus-outputs fee
  I traced this through to `TransactionBuilder::build_signed` in the pinned `key-wallet` crate (rev `1ee1c94`, `key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`). The value now surfaced by this PR is `fee_rate.calculate_fee(encoded_size(&signed_tx))` — a fee-rate × final-serialized-size recomputation done *after* signing.

  That is provably not the same quantity as the fee the broadcast transaction actually pays. During `assemble_unsigned`, the change output (if any) is sized as `total_input - total_output - selection.estimated_fee`, so the true on-chain fee (inputs minus outputs) always equals `selection.estimated_fee` — a value computed *before* signing from an estimated per-input size (148 bytes) and the coin-selector's dust-folding rules, not from the final signed size. The two numbers only match by coincidence:
  - Dust case: when `change_amount` falls below the dust threshold, `accumulate_coins_with_size` folds the dust into `estimated_fee` (`total_value - target_amount`) so the true paid fee is inflated by up to 546 duffs. `build_signed`'s returned value is oblivious to this — it just does size × rate — so it undercounts the actual fee by exactly the folded-in dust amount whenever no change output is created.
  - General case: the 148-byte/input estimate used for coin selection and the real encoded size of a signed P2PKH input (variable-length DER signatures) aren't guaranteed to match, so the two fee numbers can diverge even when a change output exists.

  This PR's whole purpose is to let wallets show the *exact* fee instead of re-estimating after the fact — but the plumbed-through value inherits an estimate that's decoupled from what the change output / dust folding actually paid. Since `send_payment` previously discarded this value entirely (`_fee`), this inaccuracy had no consumer before; this PR is what turns it into an externally-relied-upon contract, which is why it's in scope here even though the root computation lives in the external `key-wallet` crate.

Comment on lines +668 to +675
let (tx, fee) = builder
.build_signed(signer, |addr| {
managed_account.address_derivation_path(&addr)
})
.await
.map_err(|e| PlatformWalletError::TransactionBuild(e.to_string()))?;

(payment_address, tx)
(payment_address, tx, fee)

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.

🔴 Blocking: Piped-through fee is a size-based recomputation, not the actual inputs-minus-outputs fee

I traced this through to TransactionBuilder::build_signed in the pinned key-wallet crate (rev 1ee1c94, key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs). The value now surfaced by this PR is fee_rate.calculate_fee(encoded_size(&signed_tx)) — a fee-rate × final-serialized-size recomputation done after signing.

That is provably not the same quantity as the fee the broadcast transaction actually pays. During assemble_unsigned, the change output (if any) is sized as total_input - total_output - selection.estimated_fee, so the true on-chain fee (inputs minus outputs) always equals selection.estimated_fee — a value computed before signing from an estimated per-input size (148 bytes) and the coin-selector's dust-folding rules, not from the final signed size. The two numbers only match by coincidence:

  • Dust case: when change_amount falls below the dust threshold, accumulate_coins_with_size folds the dust into estimated_fee (total_value - target_amount) so the true paid fee is inflated by up to 546 duffs. build_signed's returned value is oblivious to this — it just does size × rate — so it undercounts the actual fee by exactly the folded-in dust amount whenever no change output is created.
  • General case: the 148-byte/input estimate used for coin selection and the real encoded size of a signed P2PKH input (variable-length DER signatures) aren't guaranteed to match, so the two fee numbers can diverge even when a change output exists.

This PR's whole purpose is to let wallets show the exact fee instead of re-estimating after the fact — but the plumbed-through value inherits an estimate that's decoupled from what the change output / dust folding actually paid. Since send_payment previously discarded this value entirely (_fee), this inaccuracy had no consumer before; this PR is what turns it into an externally-relied-upon contract, which is why it's in scope here even though the root computation lives in the external key-wallet crate.

source: ['codex']

// `PaymentEntry.memo` slot stays available for
// future local-note wiring.
let txid = try await wallet.sendDashPayPayment(
let (txid, _) = try await wallet.sendDashPayPayment(

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.

💬 Nitpick: In-repo reference implementation discards the newly-exposed fee

This PR's stated motivation is that wallets could previously only re-estimate the fee after the fact. SendDashPayPaymentSheet is the in-tree DashPay send flow and the natural place to demonstrate the new capability, but it discards feeDuffs (let (txid, _) = try await wallet.sendDashPayPayment(...)) instead of surfacing it in the confirmation UI. Not a defect — the real consumer is dashwallet-ios — but the example app doesn't showcase the feature this PR adds.

source: ['claude']

bezibalazs added a commit that referenced this pull request Jul 13, 2026
…nd-example-app

Conflict: packages/rs-platform-wallet/src/wallet/identity/network/payments.rs
(test region) — kept BOTH sides: the branch's DIP-15 used-flip persistence
test (send_payment_persists_external_pool_used_flip, inline setup) and
base #4049/#4095's AcceptingBroadcaster / fund_bip44_account_0 /
register_sender_and_external_account helpers plus the two exact-fee tests.
Production code auto-merged (used-flip persist block + exact-fee return
both verified present). Adapted the JNI sendDashPayPayment wrapper to the
new out_fee_duffs FFI out-param (captured; Kotlin still reads the fee from
the persisted payment entry, as iOS does).

Verified: cargo fmt, JNI check (shielded), workspace clippy --all-features
-D warnings, the 3 payments tests (3 passed), gradle :sdk+:app unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants