Skip to content

feat(key-wallet): OP_RETURN outputs, output-order control and change-to-VIN0 - #922

Merged
QuantumExplorer merged 3 commits into
devfrom
feat/tx-builder-op-return
Aug 6, 2026
Merged

feat(key-wallet): OP_RETURN outputs, output-order control and change-to-VIN0#922
QuantumExplorer merged 3 commits into
devfrom
feat/tx-builder-op-return

Conversation

@romchornyi

@romchornyi romchornyi commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

The Dash iOS wallet is restoring MAYACHAIN swap routes. MAYAChain's UTXO deposit
contract (docs,
"UTXO Chains") requires a very specific transaction shape:

  • VOUT0 — Asgard vault payment
  • VOUT1 — the swap memo in an OP_RETURN
  • VOUT2 — change paid back to the VIN0 address
  • no output reordering

TransactionBuilder could express none of it. The change rule is the
load-bearing one: "Do not use HD wallets that forward the change to a new
address, because MAYAChain IDs the user as the address in VIN0. The user must
keep their VIN0 address funded for refunds."
set_funding assigns
next_change_address() — exactly the pattern that breaks — and it breaks
silently: the swap succeeds, and only a later refund goes to an address the
user was never told to watch.

This cannot be worked around downstream. Consumers that fund and sign in a
single call (the Swift SDK's FFI builder) have no seam to patch outputs
afterwards, so the shape has to be expressible on the builder itself.

What was done?

All in key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs.

  • add_op_return(&[u8]) -> Result<Self, BuilderError> — appends a
    zero-value OP_RETURN output. Payloads over the 80-byte standardness limit
    return the new BuilderError::OpReturnDataTooLarge instead of panicking
    inside ScriptBuf. MAX_STANDARD_OP_RETURN_BYTES is pub so FFI callers can
    pre-check before handing over a builder the call would consume.
  • preserve_output_order() — skips BIP-69 output sorting.
  • change_to_first_input() — routes change to the address of the first
    input after the BIP-69 input sort. This required moving
    selected_inputs.sort_by(bip69_input_sorter) above change construction: VIN0
    is not known until the sort has run, while change was previously pushed
    before it.
  • calculate_base_size now measures each output's real serialized size
    (8 + varint(script_len) + script_len) instead of charging a flat
    TX_OUTPUT_SIZE per output.

The fee change is not cosmetic. For the canonical Maya shape — 1 input, vault +
80-byte memo + change — the flat estimate gives 260 bytes against a real 318,
i.e. 0.82 duff/byte, under the 1 duff/byte relay minimum, so the transaction
can be rejected outright rather than merely underpaying.

Deliberately conservative choices, called out for review:

  • the asset-lock burn output is still charged TX_OUTPUT_SIZE rather than its
    real ~11 bytes, so identity-funding fees stay byte-identical;
  • both new flags are opt-in — default behaviour is unchanged for every existing
    caller.

How Has This Been Tested?

cargo test -p key-wallet — 21/21 in the transaction_builder module, run on
this branch rebased onto current dev.

New tests:

  • test_maya_deposit_shape_preserves_output_order_and_routes_change_to_first_input
    — asserts output count and order, the OP_RETURN payload round-trip,
    output[2].script_pubkey == VIN0's address script (built with two inputs, so
    it genuinely exercises the post-sort behaviour), and that the fee covers the
    signed size. The last point matters: build_unsigned leaves every
    script_sig empty, so comparing the fee against the serialized bytes as-is
    would pass regardless of how badly the estimate under-counted.
  • test_add_op_return_rejects_oversized_payload — returns the error rather than
    panicking.
  • test_default_ordinary_send_matches_legacy_bytes — an ordinary
    two-recipient send assembles byte-identically to the pre-change logic.
  • test_base_size_unchanged_for_pre_op_return_shapes — P2PKH and asset-lock
    size estimates match the pre-change formula exactly, pinning the "no fee
    movement for existing shapes" claim.

Downstream verification: consumed by dashpay/platform#4286, which adds an
integration test building real Maya-shaped deposits, and manually smoke-tested
via a full MAYACHAIN swap from the Dash iOS wallet (dashpay/dashwallet-ios#916).

Breaking Changes

None. The three new methods are additive and opt-in; calculate_base_size
produces identical results for every transaction shape that existed before this
change, which test_base_size_unchanged_for_pre_op_return_shapes enforces.

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for OP_RETURN data outputs with an 80-byte relay-policy limit.
    • Added options to preserve output order and route change to the first input.
    • Added network compatibility validation for change routing.
  • Improvements

    • Improved transaction size estimation and output handling for data-bearing transactions while preserving legacy behavior.
  • Bug Fixes

    • Added clear errors when OP_RETURN data exceeds the supported limit.
  • Tests

    • Expanded coverage for data outputs, fee sizing, network validation, and transaction compatibility.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 71013666-79fb-47a0-8d4a-2a4dc5cfcf13

📥 Commits

Reviewing files that changed from the base of the PR and between ac5a210 and 3990667.

📒 Files selected for processing (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

📝 Walkthrough

Walkthrough

This PR adds OP_RETURN output support with an 80-byte limit, output-order preservation, and first-input change routing. Transaction size estimation now uses serialized output and script sizes. Tests cover routing, network validation, fee sizing, limits, dust thresholds, and legacy compatibility.

Changes

Transaction Builder OP_RETURN and Change Routing

Layer / File(s) Summary
Builder contracts and configuration
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Adds OP_RETURN limits, network helpers, builder options, serialized output sizing, and legacy-compatible defaults.
OP_RETURN and routing APIs
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Adds add_op_return, preserve_output_order, and change_to_first_input. Oversized payloads return BuilderError::OpReturnDataTooLarge.
Size estimation and coin selection
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Updates change and base-size estimates to use serialized output and script sizes. AssetLock sizing remains fixed. Drain selection disables first-input routing.
Input sorting and transaction assembly
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Sorts inputs before routing change, validates network compatibility, selects the effective change script, and conditionally preserves output insertion order.
Feature and compatibility validation
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Adds helpers and tests for OP_RETURN limits, output ordering, first-input routing, network mismatch handling, fee sizing, dust thresholds, AssetLock estimates, and ordinary-send serialization.

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

Sequence Diagram(s)

sequenceDiagram
  participant Wallet
  participant TransactionBuilder
  participant CoinSelection
  participant Transaction
  Wallet->>TransactionBuilder: add outputs and builder options
  TransactionBuilder->>CoinSelection: estimate serialized size and select inputs
  CoinSelection-->>TransactionBuilder: selected inputs
  TransactionBuilder->>Transaction: sort inputs and assemble outputs
  Transaction-->>Wallet: built transaction and fee
Loading
🚥 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 clearly and concisely identifies the three main changes: OP_RETURN outputs, output-order control, and change routing to the first input.
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/tx-builder-op-return

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

@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.

Actionable comments posted: 5

🤖 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.

Inline comments:
In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 188-192: Update the documentation for add_output and
preserve_output_order in the transaction builder to state that BIP-69 output
sorting is enabled by default, while calling preserve_output_order disables
sorting and retains insertion order.
- Around line 29-31: Replace the hardcoded MAX_STANDARD_OP_RETURN_BYTES constant
with a relay-limit value supplied by the selected network policy or
TransactionBuilder configuration, and thread that value through add_op_return
and related builder construction paths. Preserve the existing payload validation
behavior while allowing networks or nodes with different OP_RETURN limits to
provide their policy-specific value.
- Around line 816-825: Update the legacy helper’s
CoinSelector::select_coins_with_size call to pass CHANGE_OUTPUT_SIZE when
change_addr exists and 0 for drain builds, matching the production selector
path. Adjust the surrounding build_unsigned_legacy logic as needed so the
regression test uses the same selector input and fee behavior as production.
- Around line 462-472: Update the change-output selection in TransactionBuilder
around change_to_first_input and set_change_address so input-derived change is
validated against the configured change address network. Reject mismatched
first_input.address.network and change_addr.network with the existing builder
error mechanism before assembling outputs, while preserving the current behavior
for matching networks and explicit change addresses.
- Around line 220-222: Update the change-output selector in the transaction size
estimation flow to use should_estimate_change_output() rather than checking only
change_addr. Ensure calculate_base_size(), coin selection, and final assembly
consistently budget a change output when change_to_first_input is enabled.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 404b3092-33ba-4f3f-bf97-cdebb1a812ea

📥 Commits

Reviewing files that changed from the base of the PR and between 9cbe4e7 and df1fe31.

📒 Files selected for processing (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs Outdated
Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.75362% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.06%. Comparing base (08bf729) to head (3990667).
⚠️ Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
.../wallet/managed_wallet_info/transaction_builder.rs 92.75% 25 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #922      +/-   ##
==========================================
- Coverage   75.06%   75.06%   -0.01%     
==========================================
  Files         328      328              
  Lines       77255    77587     +332     
==========================================
+ Hits        57992    58241     +249     
- Misses      19263    19346      +83     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 49.84% <ø> (-0.76%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.37% <ø> (+0.10%) ⬆️
wallet 76.02% <92.75%> (+0.25%) ⬆️
Files with missing lines Coverage Δ
.../wallet/managed_wallet_info/transaction_builder.rs 88.91% <92.75%> (+1.58%) ⬆️

... and 20 files with indirect coverage changes

@romchornyi
romchornyi force-pushed the feat/tx-builder-op-return branch from df1fe31 to 8677e3d Compare August 4, 2026 12:52
@romchornyi

Copy link
Copy Markdown
Contributor Author

Pushed 8677e3d6: rustfmt (that was the whole Pre-commit failure) plus fixes for three of the five review comments.

Fixed

  1. change_output_size ignored change_to_first_input. select_coins_with_size was budgeting a change output only when change_addr.is_some(), while calculate_base_size budgets one whenever should_estimate_change_output() is true. A builder using add_inputs + change_to_first_input without a preset change address therefore told the selector change was free while the assembler emitted one anyway. Both now key off should_estimate_change_output(), and the stale "Matches calculate_base_size" comment is corrected.

    To be precise about severity: the change output was already counted in base_size, so this is not a fee undercount — it skewed branch-and-bound's change-vs-changeless decision. Real inconsistency, not a money bug.

  2. The legacy regression helper passed 148 as the selector's last argument. That parameter is the change-output size since fix(key-wallet): rewrite branch-and-bound coin selection (#918) #919; 148 was the per-input size under the older signature. build_unsigned_legacy now passes CHANGE_OUTPUT_SIZE/0 exactly as the pre-change production path did, so test_default_ordinary_send_matches_legacy_bytes compares like with like. Both of these came from rebasing onto fix(key-wallet): rewrite branch-and-bound coin selection (#918) #919 — the visible conflict was resolved without auditing the neighbouring call whose semantics had changed.

  3. add_output doc. It stated BIP-69 sorting as unconditional; now says it is the default and points at preserve_output_order.

Not changed, with reasons

  1. Make MAX_STANDARD_OP_RETURN_BYTES network/policy-configurable. 80 bytes is the standard relay policy this builder already targets elsewhere, and no caller needs a different value today. Threading a policy parameter through add_op_return and the construction paths is API surface this PR does not need; better raised on its own if a node with a different -datacarriersize ever has to be supported.

  2. Validate first_input.address.network against change_addr.network. When change_to_first_input is set the change address is taken from a UTXO the wallet itself selected, so it is the wallet's network by construction, and TransactionBuilder holds no network of its own to check against. The mismatch the comment guards against isn't reachable from here.

cargo test -p key-wallet — 21/21 in transaction_builder; cargo clippy -p key-wallet --all-targets clean.

@romchornyi
romchornyi force-pushed the feat/tx-builder-op-return branch from 8677e3d to c9d33a4 Compare August 4, 2026 13:59
@romchornyi

Copy link
Copy Markdown
Contributor Author

Pushed c9d33a4e — the two remaining comments are now addressed, one by change and one with evidence for declining.

Injected the OP_RETURN relay limit (comment 1)

MAX_STANDARD_OP_RETURN_BYTES is now DEFAULT_MAX_OP_RETURN_BYTES, and the ceiling lives on the builder:

pub fn set_max_op_return_bytes(mut self, max_bytes: usize) -> Self

add_op_return validates against the configured value, so a node or network with a different -datacarriersize can supply its own policy without the builder hardcoding one. The constant remains public as the default and as a pre-check for callers that must reject a payload before handing over a builder add_op_return would consume. Covered by test_max_op_return_bytes_is_configurable, which exercises a raised ceiling (accepted, and still enforced one byte past it) and a lowered one, asserting the error reports the configured maximum rather than the default.

Network validation for input-derived change (comment 4) — not adding

Address::script_pubkey() delegates to payload().script_pubkey(); the network affects only the base58/bech32 encoding, never the output script. A P2PKH address for the same hash160 produces byte-identical scriptPubKey on mainnet and testnet, so pairing a testnet Utxo with a mainnet set_change_address yields exactly the same change output either way — the check would reject a caller mistake with no observable on-chain consequence.

There is also nothing to validate against: TransactionBuilder carries no network of its own, and Address<NetworkChecked> exposes no network accessor, so implementing this would mean either widening the builder's API or adding an accessor to the dash crate. Happy to add it if you would still like the caller-hygiene guard, but it seemed the wrong trade for this PR.

cargo test -p key-wallet — 22/22 in transaction_builder; cargo clippy -p key-wallet --all-targets clean.


Unrelated heads-up for whoever picks this up downstream: rebasing onto current dev pulls in #818 (AddressInfo.used removed) and #919 (select_coins_with_size arity), which dashpay/platform has not absorbed yet — cargo check -p platform-wallet-ffi fails against this branch on those two, not on anything in this PR.

@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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs (1)

752-756: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use thiserror for BuilderError.

BuilderError still manually implements fmt::Display and std::error::Error. Add thiserror to key-wallet/Cargo.toml, derive thiserror::Error, and move the variant messages into #[error(...)]; keep CoinSelection as a source for proper cause support.

🤖 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 `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs` around
lines 752 - 756, Add the thiserror crate to key-wallet/Cargo.toml, then refactor
the BuilderError enum to derive thiserror::Error. Move the error messages from
the manual fmt::Display implementation into #[error(...)] attributes on each
variant, including OpReturnDataTooLarge. Preserve the CoinSelection variant as a
source by using #[source] to maintain proper error-cause support. Remove the old
manual Display and Error trait implementations once all variants have error
messages.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 240-254: Derive a conservative serialized change-output size from
all eligible input scripts before coin selection, accounting for routed change
addresses that may produce WitnessProgram scripts rather than assuming
CHANGE_OUTPUT_SIZE. Use this derived size consistently in calculate_base_size()
and select_coins_with_size() whenever change_to_first_input() or another change
path is enabled, while preserving existing behavior for P2PKH change. Extend the
tests around the existing change-selection cases near the routed change logic to
cover a larger change script and verify the estimate remains sufficient.

---

Nitpick comments:
In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 752-756: Add the thiserror crate to key-wallet/Cargo.toml, then
refactor the BuilderError enum to derive thiserror::Error. Move the error
messages from the manual fmt::Display implementation into #[error(...)]
attributes on each variant, including OpReturnDataTooLarge. Preserve the
CoinSelection variant as a source by using #[source] to maintain proper
error-cause support. Remove the old manual Display and Error trait
implementations once all variants have error messages.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 15f53f53-9841-4632-8338-07da4aadf7d2

📥 Commits

Reviewing files that changed from the base of the PR and between df1fe31 and c9d33a4.

📒 Files selected for processing (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 6, 2026
Estimate routed change using the largest eligible input script and reject configured network mismatches. Add regression coverage for P2WSH sizing and cross-network change routing.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs (2)

884-985: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider pinning golden bytes instead of duplicating the assembly path.

build_unsigned_legacy re-implements about 100 lines of the pre-change assembly. The copy drifts as assemble_unsigned changes, and a future change can be mirrored into both paths, which hides the regression the helper exists to catch. A recorded hex transaction plus the expected fee gives the same guarantee without the duplicate logic.

This is optional. Keep the helper if you prefer the behavioural comparison.

🤖 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 `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs` around
lines 884 - 985, Optionally replace the duplicated assembly logic in
build_unsigned_legacy with a pinned golden transaction hex and expected fee,
using the recorded bytes to validate the pre-change behavior. If retaining the
helper, no change is required because the review explicitly allows the
behavioral comparison.

512-519: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer a typed error over InvalidData.

The network mismatch returns BuilderError::InvalidData with a message. The test at Lines 1449-1453 asserts on a substring of that message, so any wording change breaks the test. Add a dedicated variant, in the same way as the new OpReturnDataTooLarge.

♻️ Proposed variant
     /// OP_RETURN payload exceeds the standard relay-policy size.
     OpReturnDataTooLarge {
         len: usize,
         max: usize,
     },
+    /// The first-input change address and the configured change address
+    /// belong to different networks.
+    ChangeAddressNetworkMismatch,
-                    return Err(BuilderError::InvalidData(
-                        "Input-derived change address network does not match configured change address"
-                            .into(),
-                    ));
+                    return Err(BuilderError::ChangeAddressNetworkMismatch);
🤖 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 `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs` around
lines 512 - 519, Replace the network-mismatch `BuilderError::InvalidData` return
in the transaction builder with a dedicated typed `BuilderError` variant,
following the existing `OpReturnDataTooLarge` pattern. Add and use the new
variant so callers and tests no longer depend on the current error-message
wording.
🤖 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.

Inline comments:
In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 182-206: Update the pull request title to use the required
Conventional Commit prefix, using “feat” because this change adds
transaction-builder capability; preserve the existing title description after
the prefix.
- Around line 506-532: Replace the hardcoded 546 threshold in the change-output
branch with the script-specific threshold derived from
estimated_change_output_size(), and use that same threshold during coin
selection and output construction. Ensure routed change scripts, including
P2WSH, are rejected when change_amount is at or below their calculated dust
threshold.

---

Nitpick comments:
In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 884-985: Optionally replace the duplicated assembly logic in
build_unsigned_legacy with a pinned golden transaction hex and expected fee,
using the recorded bytes to validate the pre-change behavior. If retaining the
helper, no change is required because the review explicitly allows the
behavioral comparison.
- Around line 512-519: Replace the network-mismatch `BuilderError::InvalidData`
return in the transaction builder with a dedicated typed `BuilderError` variant,
following the existing `OpReturnDataTooLarge` pattern. Add and use the new
variant so callers and tests no longer depend on the current error-message
wording.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 71f74a8a-d9b0-4c1f-be9c-7b85a55d02e8

📥 Commits

Reviewing files that changed from the base of the PR and between 08bf729 and ac5a210.

📒 Files selected for processing (1)
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

Comment on lines +182 to +206
/// Add an OP_RETURN output carrying `data` (value 0).
///
/// Errors if `data` exceeds [`MAX_STANDARD_OP_RETURN_BYTES`].
pub fn add_op_return(mut self, data: &[u8]) -> Result<Self, BuilderError> {
if data.len() > MAX_STANDARD_OP_RETURN_BYTES {
return Err(BuilderError::OpReturnDataTooLarge {
len: data.len(),
max: MAX_STANDARD_OP_RETURN_BYTES,
});
}

let push_bytes =
<&PushBytes>::try_from(data).map_err(|_| BuilderError::OpReturnDataTooLarge {
len: data.len(),
max: MAX_STANDARD_OP_RETURN_BYTES,
})?;
self.outputs.push(TxOut {
value: 0,
script_pubkey: Builder::new()
.push_opcode(opcodes::all::OP_RETURN)
.push_slice(push_bytes)
.into_script(),
});
Ok(self)
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a Conventional Commit prefix to the PR title.

The title reads "OP_RETURN Support and Change Routing in Transaction Builder". The pr-title.yml check requires one of build, chore, ci, docs, feat, fix, refactor, or test. This PR adds new builder capability, so use a feat prefix, for example feat: OP_RETURN support and change routing in transaction builder.

As per path instructions, "Check whether the PR title prefix allowed in the pr-title.yml workflow accurately describes the changes."

🤖 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 `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs` around
lines 182 - 206, Update the pull request title to use the required Conventional
Commit prefix, using “feat” because this change adds transaction-builder
capability; preserve the existing title description after the prefix.

Source: Path instructions

Comment thread key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs Outdated
ZocoLini
ZocoLini previously approved these changes Aug 6, 2026
`change_amount > 546` hard-codes the dust threshold for a 34-byte P2PKH output.
With `change_to_first_input` the change script is whatever VIN0 uses, and Dash
Core derives dust from the serialized output plus the 148-byte input that would
spend it — so a 43-byte output has a 573-duff threshold. A 550-duff routed
change output cleared the flat 546 check and could then be rejected as dust by
the network.

Derive the threshold with `3 * (output_size + 148)`, sized from the same
`estimated_change_output_size()` that coin selection already uses, so selection
and output construction agree. Hoisted above the point `self.outputs` is moved.

Nothing moves for the ordinary path: the formula is exactly 546 for a 34-byte
P2PKH output, which `test_dust_threshold_follows_the_change_output_size` pins
alongside the 43-byte case.
@romchornyi

Copy link
Copy Markdown
Contributor Author

Pushed 39906674, addressing the dust-threshold finding. Follow-up commit rather than an amend this time, so the diff since your last review stays visible.

Dust threshold now follows the routed change script

You are right, and the arithmetic lines up exactly: 3 * (34 + 148) = 546, so the flat literal was only ever correct for a P2PKH output. With change_to_first_input the change script is whatever VIN0 uses, and a 43-byte output has a 3 * (43 + 148) = 573 threshold — a 550-duff change output cleared the old check and could then be rejected as dust by the network.

assemble_unsigned now derives the threshold from the same estimated_change_output_size() that coin selection uses, so selection and output construction agree on the size. It is computed just above the point self.outputs is moved out, since the check sits after that move.

Behaviour is unchanged for the ordinary path — the formula returns exactly 546 for P2PKH — and test_dust_threshold_follows_the_change_output_size pins both that and the 43-byte case so the equivalence cannot drift.

PR title

That comment looks like it was raised against an earlier revision of the title. It currently reads feat(key-wallet): OP_RETURN outputs, output-order control and change-to-VIN0 and the check-title job passes, so there is nothing to change.

cargo test -p key-wallet — 24/24 in transaction_builder; cargo clippy -p key-wallet --all-targets clean.

@QuantumExplorer
QuantumExplorer merged commit dca5b05 into dev Aug 6, 2026
34 of 35 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/tx-builder-op-return branch August 6, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants