Skip to content

feat(sdk): expose OP_RETURN, output-order and change-to-VIN0 controls - #4286

Open
romchornyi wants to merge 3 commits into
v4.2-devfrom
feat/maya-op-return
Open

feat(sdk): expose OP_RETURN, output-order and change-to-VIN0 controls#4286
romchornyi wants to merge 3 commits into
v4.2-devfrom
feat/maya-op-return

Conversation

@romchornyi

@romchornyi romchornyi commented Aug 4, 2026

Copy link
Copy Markdown

Issue being fixed or feature implemented

The Dash iOS wallet is restoring MAYACHAIN swap routes, which requires the DASH
deposit to carry the swap memo in an OP_RETURN. CoreTransactionBuilder could
not express that, so Maya was disabled during the DashSync unlink.

MAYAChain's UTXO deposit contract (docs,
"UTXO Chains") demands a specific shape: VOUT0 = Asgard vault, VOUT1 = the
memo as a zero-value OP_RETURN, VOUT2 = change paid back to the VIN0
address
, and no output reordering. The change rule matters because MAYAChain
identifies the depositor by VIN0 and pays refunds there — routing change to a
fresh HD address fails silently, with only a later refund going astray.

Because finalize/build_signed fund and sign inside a single FFI call, none of
this can be applied after the fact. It has to be expressible on the builder.

What was done?

  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs:
    core_wallet_tx_builder_add_op_return, ..._preserve_output_order and
    ..._change_to_first_input, following the existing setter style. An over-long
    payload is rejected before take_builder() runs, so a refused memo cannot
    leave the slot holding a mem::take default and silently drop outputs the
    caller already configured.
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs:
    core_wallet_signed_transaction_v2_bytes — read a finalized transaction's
    bytes without broadcasting, so the deposit shape can be asserted pre-broadcast.
  • packages/swift-sdk/.../CoreTransactionBuilder.swift: addOpReturn(_:),
    preserveOutputOrder(), changeToFirstInput(), and
    FinalizedCoreTransaction.serializedData().
  • .github/workflows/tests-rs-workspace.yml: fail the workflow if a local
    [patch."https://github.com/dashpay/rust-dashcore"] override is left in
    Cargo.toml — that override is invisible in review and produces a build that
    only works on one machine.

Depends on dashpay/rust-dashcore#922, which adds the underlying add_op_return,
preserve_output_order and change_to_first_input to key-wallet. Until that
merges and the rev in Cargo.toml is bumped, building this locally needs the
patch override — deliberately not committed, which is what the new CI guard
enforces.

How Has This Been Tested?

Added packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift,
which builds short- and long-memo deposits against a local dashmate devnet and
asserts output count and order, the OP_RETURN payload, VOUT2 == VIN0
scriptPubKey, the 80-byte memo ceiling, Maya's dust floor and a ≥ 1 duff/byte
fee — then checks fee parity for ordinary, multi-recipient, selected-input,
drain and asset-lock shapes so the precise output sizing does not move existing
fees.

Also verified: cargo check -p platform-wallet-ffi,
./build_ios.sh --target ios --target sim, and a green dashpay build of the
consuming wallet app.

Known gap: the integration test currently stalls in SPV bootstrap on a
22k-block devnet (compact filters lag past the 180 s wait) and has not yet run
its assertions end to end.

Breaking Changes

None. All three builder controls are opt-in and default behaviour is unchanged.

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

  • New Features
    • Added support for including OP_RETURN data in wallet transactions.
    • Added options to preserve transaction output order and route change to the first selected input.
    • Added the ability to retrieve serialized bytes from finalized transactions without consuming them.
  • Bug Fixes
    • Improved validation and error reporting for invalid transaction data and oversized OP_RETURN payloads.
    • Preserved transaction builder state when rejecting oversized OP_RETURN payloads.

…trols

MAYAChain requires a UTXO deposit shaped as VOUT0=vault, VOUT1=OP_RETURN memo,
VOUT2=change paid back to the VIN0 address, with no output reordering, and it
identifies the depositor by VIN0 for refunds.
https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions

`CoreTransactionBuilder.buildSigned` builds and signs in one FFI call, so none
of this can be applied after the fact — it has to be expressed on the builder.

FFI (rs-platform-wallet-ffi):
- core_wallet_tx_builder_add_op_return / _preserve_output_order /
  _change_to_first_input, mirroring the existing setter style
- an over-long payload is rejected before take_builder() runs, so a refused memo
  cannot leave the slot holding a mem::take default and silently drop outputs
  the caller already configured
- core_wallet_signed_transaction_v2_bytes: read the finalized transaction bytes
  without broadcasting, so the deposit shape can be asserted pre-broadcast

Swift SDK:
- addOpReturn / preserveOutputOrder / changeToFirstInput
- FinalizedCoreTransaction.serializedData()

Tests: MayaDepositVerificationIntegrationTests builds short- and long-memo
deposits and asserts output count/order, the OP_RETURN payload, VOUT2 == VIN0
scriptPubKey, the memo ceiling, the dust floor and a >= 1 duff/byte fee, then
checks fee parity for ordinary, multi-recipient, selected-input, drain and
asset-lock shapes so the precise output sizing does not move existing fees.

CI: fail the workspace workflow if the local rust-dashcore [patch] override is
still present in Cargo.toml.

Depends on key-wallet gaining add_op_return / preserve_output_order /
change_to_first_input (dashpay/rust-dashcore, branch feat/tx-builder-op-return).
Until that lands and the rev in Cargo.toml is bumped, building this needs a
local [patch] override, which is deliberately NOT committed.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds finalized V2 transaction serialization, OP_RETURN and output-routing controls across the Rust FFI and Swift SDK, and opt-in Maya deposit and fee verification tests. The macOS workflow rejects local rust-dashcore patch overrides.

Changes

Core wallet transaction flow

Layer / File(s) Summary
Finalized transaction serialization
packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
The FFI returns owned consensus bytes for finalized V2 transactions. Swift exposes them as copied Data.
Transaction builder controls
packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
The builder validates OP_RETURN payloads, preserves output insertion order, and routes change to the first selected input.
Maya deposit and fee verification
packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift
Opt-in integration tests validate deposit structure, memo boundaries, builder state preservation, UTXO selection, transaction decoding, and fee parity across multiple transaction shapes.

Workspace validation

Layer / File(s) Summary
macOS patch override guard
.github/workflows/tests-rs-workspace.yml
The macOS workflow fails when Cargo.toml contains a local rust-dashcore patch override.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MayaDepositVerificationIntegrationTests
  participant CoreTransactionBuilder
  participant CoreWalletFFI
  participant SPVWallet
  MayaDepositVerificationIntegrationTests->>SPVWallet: fund wallet and select UTXOs
  MayaDepositVerificationIntegrationTests->>CoreTransactionBuilder: build deposit transaction
  CoreTransactionBuilder->>CoreWalletFFI: add OP_RETURN and configure outputs
  CoreWalletFFI-->>CoreTransactionBuilder: finalize transaction
  CoreTransactionBuilder-->>MayaDepositVerificationIntegrationTests: return transaction
  MayaDepositVerificationIntegrationTests->>SPVWallet: decode transaction and calculate fee
  SPVWallet-->>MayaDepositVerificationIntegrationTests: transaction data and fee
Loading

Possibly related PRs

  • dashpay/platform#4247: The PR changes the same transaction-builder and broadcast FFI areas with separate OP_RETURN and serialization functionality.

Suggested reviewers: llbartekll, lklimek

🚥 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 identifies the primary SDK controls added for OP_RETURN outputs, output order, and change routing.
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/maya-op-return

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 Aug 4, 2026

Copy link
Copy Markdown
Collaborator

ℹ️ Review skipped (commit c3ffb89)
Last checked: 2026-08-04 21:30 UTC

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.60%. Comparing base (97904ed) to head (c3ffb89).

Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4286      +/-   ##
============================================
+ Coverage     87.54%   87.60%   +0.06%     
============================================
  Files          2679     2679              
  Lines        341312   343263    +1951     
============================================
+ Hits         298799   300718    +1919     
- Misses        42513    42545      +32     
Components Coverage Δ
dpp 88.66% <ø> (+0.11%) ⬆️
drive 86.28% <ø> (+0.02%) ⬆️
drive-abci 89.66% <ø> (+0.09%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.88% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.60% <ø> (ø)
🚀 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.

@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 (1)
packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs (1)

311-321: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Restore the builder when add_op_return fails.

take_builder() replaces the stored TransactionBuilder with the mem::take default. If b.add_op_return(bytes) returns Err, b is dropped and the slot keeps that default, so later builder operations or finalization are no longer based on previously configured inputs, outputs, and options. TransactionBuilder does not derive Clone, so the error path needs to avoid requiring b.clone() unless this dependency is changed to support it.

🤖 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-ffi/src/core_wallet/transaction_builder.rs`
around lines 311 - 321, Update the add_op_return error path in the
transaction-building method to restore the original TransactionBuilder into the
shared builder slot before returning the error. Preserve b without cloning,
store it through the existing store_builder mechanism on failure, and keep the
current error result unchanged.
🤖 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
`@packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift`:
- Around line 147-158: Guard all transaction collection accesses in the
verification flow before indexing: replace direct access to decoded.outputs[1],
decoded.outputs[0], and decoded.inputs[0] with safe first-element handling via
XCTUnwrap or equivalent count assertions. Ensure malformed transaction shapes
produce readable XCTest failures before evaluating opReturnPayload or
findMatchedUTXO, while preserving the existing outputTwoMatchesInputZeroScript
logic.
- Around line 55-57: Prevent testPrompt04StaticProofAndLegacyFeeParity from
hanging local Swift SDK CI by skipping it or splitting it so the long SPV
bootstrap and waitForSpendable flow is not run by the enabled run_tests.sh suite
until bootstrap stalls are resolved; do not add a local stopSpv call because
IntegrationTestCase.tearDown and suite cleanup already handle SPV teardown.

---

Nitpick comments:
In `@packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs`:
- Around line 311-321: Update the add_op_return error path in the
transaction-building method to restore the original TransactionBuilder into the
shared builder slot before returning the error. Preserve b without cloning,
store it through the existing store_builder mechanism on failure, and keep the
current error result unchanged.
🪄 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: b079eaf5-5ec2-4fda-b22a-a9b48a57753f

📥 Commits

Reviewing files that changed from the base of the PR and between 97904ed and 6f70092.

📒 Files selected for processing (5)
  • .github/workflows/tests-rs-workspace.yml
  • packages/rs-platform-wallet-ffi/src/core_wallet/broadcast.rs
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/CoreWallet/CoreTransactionBuilder.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift

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

Preliminary review — Codex only

The FFI allocation and Swift ownership plumbing follow existing repository conventions, but the exact PR head is not buildable because the pinned key-wallet revision lacks every newly referenced builder API and the imported constant has a different name upstream. The Maya integration test also does not require the expected VOUT2 change output and does not exercise the 80/81-byte OP_RETURN boundary or builder-state preservation after rejection.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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 `Cargo.toml`:
- [BLOCKING] Cargo.toml:55-62: Pin a key-wallet revision that provides the new builder API
  The workspace still pins rust-dashcore and key-wallet to `70d4bf8e36057c58e02d56769a6e9760f701dd06`, which does not provide `TransactionBuilder::add_op_return`, `preserve_output_order`, or `change_to_first_input`, nor the imported OP_RETURN limit constant. At this exact head, `cargo check -p platform-wallet-ffi --locked` fails with E0432/E0599, so the native library and the new Swift API cannot be built. Upstream rust-dashcore PR #922 currently provides the methods at `eebacae3d1152609b04e9c9af05acc87ea9b32ad`, but exports the limit as `DEFAULT_MAX_OP_RETURN_BYTES`, not `MAX_STANDARD_OP_RETURN_BYTES`. After the upstream change is merged, pin all rust-dashcore workspace dependencies to a compatible revision, update `Cargo.lock`, and align the import with the exported name.

In `packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift:181-191: Require VOUT2 for fixtures that necessarily produce change
  Both deposit fixtures leave millions of duffs after the vault payment and fee, so each transaction must contain exactly three outputs: vault, memo, and change. Allowing `outputCount == 2` and conditionally skipping the VIN0-script assertion lets a regression that suppresses change pass, even though change-to-VIN0 is the load-bearing behavior this test claims to verify. Require exactly three outputs and always validate VOUT2 against VIN0.
- [SUGGESTION] packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift:13-17: Exercise the OP_RETURN 80/81-byte boundary
  The long memo fixture is 79 UTF-8 bytes, and the later `memoBytes <= 80` assertion only verifies the fixture rather than the new API's boundary behavior. No test proves that an 80-byte payload succeeds, an 81-byte payload is rejected, or that a rejection preserves outputs and options already stored in the FFI builder. Add deterministic boundary coverage that rejects 81 bytes and then successfully finalizes the same builder with its earlier configuration intact, which directly tests the pre-`take_builder()` guarantee introduced by this PR.

Comment on lines +181 to +191
XCTAssertGreaterThanOrEqual(observation.outputCount, 2, "\(observation.name) output count below Maya minimum")
XCTAssertLessThanOrEqual(observation.outputCount, 3, "\(observation.name) output count above Maya maximum")
XCTAssertEqual(observation.actualVaultAmount, observation.depositAmount, "\(observation.name) VOUT0 amount mismatch")
XCTAssertTrue(observation.outputOneIsOpReturn, "\(observation.name) VOUT1 is not OP_RETURN")
XCTAssertEqual(observation.decodedMemo, observation.memo, "\(observation.name) memo payload mismatch")
XCTAssertGreaterThanOrEqual(observation.depositAmount, Constants.minimumDepositDuffs, "\(observation.name) deposit fell below Maya dust floor")
XCTAssertLessThanOrEqual(observation.memoBytes, Constants.maxMemoBytes, "\(observation.name) memo exceeded 80 bytes")
XCTAssertGreaterThanOrEqual(observation.feeDuffs, UInt64(observation.serializedSize), "\(observation.name) fee fell below 1 duff/byte")
if observation.outputCount == 3 {
XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey")
}

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.

🟡 Suggestion: Require VOUT2 for fixtures that necessarily produce change

Both deposit fixtures leave millions of duffs after the vault payment and fee, so each transaction must contain exactly three outputs: vault, memo, and change. Allowing outputCount == 2 and conditionally skipping the VIN0-script assertion lets a regression that suppresses change pass, even though change-to-VIN0 is the load-bearing behavior this test claims to verify. Require exactly three outputs and always validate VOUT2 against VIN0.

Suggested change
XCTAssertGreaterThanOrEqual(observation.outputCount, 2, "\(observation.name) output count below Maya minimum")
XCTAssertLessThanOrEqual(observation.outputCount, 3, "\(observation.name) output count above Maya maximum")
XCTAssertEqual(observation.actualVaultAmount, observation.depositAmount, "\(observation.name) VOUT0 amount mismatch")
XCTAssertTrue(observation.outputOneIsOpReturn, "\(observation.name) VOUT1 is not OP_RETURN")
XCTAssertEqual(observation.decodedMemo, observation.memo, "\(observation.name) memo payload mismatch")
XCTAssertGreaterThanOrEqual(observation.depositAmount, Constants.minimumDepositDuffs, "\(observation.name) deposit fell below Maya dust floor")
XCTAssertLessThanOrEqual(observation.memoBytes, Constants.maxMemoBytes, "\(observation.name) memo exceeded 80 bytes")
XCTAssertGreaterThanOrEqual(observation.feeDuffs, UInt64(observation.serializedSize), "\(observation.name) fee fell below 1 duff/byte")
if observation.outputCount == 3 {
XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey")
}
XCTAssertEqual(
observation.outputCount,
3,
"\(observation.name) must contain vault, memo, and change outputs"
)
XCTAssertEqual(observation.actualVaultAmount, observation.depositAmount, "\(observation.name) VOUT0 amount mismatch")
XCTAssertTrue(observation.outputOneIsOpReturn, "\(observation.name) VOUT1 is not OP_RETURN")
XCTAssertEqual(observation.decodedMemo, observation.memo, "\(observation.name) memo payload mismatch")
XCTAssertGreaterThanOrEqual(observation.depositAmount, Constants.minimumDepositDuffs, "\(observation.name) deposit fell below Maya dust floor")
XCTAssertLessThanOrEqual(observation.memoBytes, Constants.maxMemoBytes, "\(observation.name) memo exceeded 80 bytes")
XCTAssertGreaterThanOrEqual(observation.feeDuffs, UInt64(observation.serializedSize), "\(observation.name) fee fell below 1 duff/byte")
XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey")

source: ['codex']

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, and this was the one worth catching. Fixed in 8370b6a2.

You are right that the fixtures make change mandatory: 0.5 DASH funding against a 200_000-duff deposit, and 0.4 DASH against 35_000_000, both leave a change output far above the 546-duff dust threshold. outputCount == 2 was unreachable for them, so the range check bought nothing and cost the assertion — a regression that dropped change entirely would have satisfied >= 2 && <= 3 and then skipped the VIN0 check under if observation.outputCount == 3, which is precisely the behaviour this test exists to prove.

Now:

XCTAssertEqual(observation.outputCount, 3, "\(observation.name) must be vault + memo + change")
...
XCTAssertTrue(observation.outputTwoMatchesInputZeroScript, "\(observation.name) VOUT2 does not return change to VIN0 scriptPubKey")

with the VOUT2 assertion no longer conditional. I also moved the shape check ahead of the subscripting in buildDepositObservation, so a wrong shape fails readably instead of trapping on decoded.outputs[2].

One deliberate asymmetry worth recording, since it looks like the same check in two places: the app-side guard in dashpay/dashwallet-ios#916 (assertSwapDepositShape) still accepts 2 or 3 outputs. There the transaction is real, so change genuinely can fall below dust and be dropped by the builder — MAYAChain still identifies the depositor by VIN0 in that case, so a two-output deposit is valid. It is only here, where the fixture amounts rule that out, that three is the correct fixed expectation.

Comment on lines +13 to +17
static let maxMemoBytes = 80
static let longMemo =
"=:ARB.GLD:0x51a1449b3B6D635EddeC781cD47a99221712De97:344233230e4/1/0:_/def:15/0"
static let shortMemo =
"=:r:thor166n4w5039meulfa3p6ydg60ve6ueac7tlt0jws:669458827/1/0:_/def:15/0"

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.

🟡 Suggestion: Exercise the OP_RETURN 80/81-byte boundary

The long memo fixture is 79 UTF-8 bytes, and the later memoBytes <= 80 assertion only verifies the fixture rather than the new API's boundary behavior. No test proves that an 80-byte payload succeeds, an 81-byte payload is rejected, or that a rejection preserves outputs and options already stored in the FFI builder. Add deterministic boundary coverage that rejects 81 bytes and then successfully finalizes the same builder with its earlier configuration intact, which directly tests the pre-take_builder() guarantee introduced by this PR.

source: ['codex']

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch on both halves — the fixture proved nothing about the boundary, and nothing at all covered the rejection path. Added testOpReturnCeilingBoundaryAndRejectionPreservesBuilder in 8370b6a2.

It does exactly the two things you asked for:

let builder = try CoreTransactionBuilder(network: .regtest)
try builder.addOutput(address: vaultAddress, amountDuffs: depositAmount)
try builder.preserveOutputOrder()
try builder.changeToFirstInput()

XCTAssertThrowsError(try builder.addOpReturn(overCeiling))   // 81 bytes

// Same builder instance: if the rejection had consumed it, this would build a
// transaction missing the vault output and the ordering flags.
try builder.addOpReturn(atCeiling)                            // exactly 80 bytes
let tx = try builder.finalizeAtomic(...)

then asserts three outputs, that VOUT0 is still the vault payment for the original amount, and that the 80-byte payload round-trips verbatim out of VOUT1.

The reuse-after-rejection part is the one I care about most, because it pins a guarantee that is otherwise invisible: core_wallet_tx_builder_add_op_return takes the builder by value on the Rust side, so the naive implementation drops it on error and take_builder's mem::take leaves a default in the slot — the vault output and both ordering flags would vanish silently and the next finalize would produce a plain send. The FFI validates the payload before take_builder() specifically to avoid that, and until now nothing exercised it.

Two caveats I would rather state than have you discover: the new test is gated behind MAYA_DEPOSIT_VERIFICATION=1 along with the rest of the suite (CodeRabbit flagged that run_tests.sh runs this bundle and a bootstrap stall would hang the job), and it has not been run yet — the SDK does not currently build locally because platform-wallet-ffi needs the key-wallet API from dashpay/rust-dashcore#922, which is still open. So treat it as written-and-reviewed, not as passing.

Review follow-ups on the deposit verification test:

- Require exactly three outputs and always assert VOUT2 against VIN0. Both
  fixtures leave millions of duffs after the vault payment and fee, so change is
  mandatory; accepting two outputs let a regression that suppresses change pass
  the very test that exists to prove change-to-VIN0.
- Assert the output and input counts before indexing, so a wrong shape fails
  readably instead of trapping on an out-of-range subscript and taking the test
  process down.
- Cover the 80/81-byte OP_RETURN boundary rather than just the fixture, and
  reuse the same builder after a rejected payload. That pins the FFI guarantee
  this branch adds: the size check runs before `take_builder()`, so a refused
  memo must leave already-configured outputs and options intact.
- Gate the suite behind MAYA_DEPOSIT_VERIFICATION=1. `run_tests.sh` runs this
  bundle in CI, and these tests sit behind several 90-second waits on top of a
  full SPV bootstrap, so a bootstrap stall would hang the job rather than fail
  it.

Also renames MAX_STANDARD_OP_RETURN_BYTES to DEFAULT_MAX_OP_RETURN_BYTES,
following key-wallet making the ceiling configurable per builder.
@romchornyi

Copy link
Copy Markdown
Author

Pushed 8370b6a2 — all four review comments addressed.

Require exactly three outputs, always assert VOUT2 → VIN0

Agreed, and this was the important one. Both fixtures leave millions of duffs after the vault payment and fee, so change is mandatory; outputCount == 2 was never reachable for them, and allowing it meant a regression that suppressed change would pass the very test that exists to prove change goes to VIN0. Now XCTAssertEqual(outputCount, 3) and the VOUT2 assertion is unconditional.

(For contrast, the app-side assertion in dashpay/dashwallet-ios#916 deliberately accepts 2 or 3, because a real deposit can drop change below the 546-duff dust threshold. Here the fixture amounts rule that out.)

Guard counts before indexing

Done. Shape assertions now run before any subscripting, so a wrong shape produces a readable failure instead of trapping and killing the test process.

Exercise the 80/81-byte boundary

Added testOpReturnCeilingBoundaryAndRejectionPreservesBuilder. It rejects 81 bytes, accepts exactly 80, and — the part worth having — reuses the same builder after the rejection and finalizes it, asserting the vault output and ordering flags configured before the failed call are still there. That directly pins the guarantee this branch introduces: core_wallet_tx_builder_add_op_return validates the payload before take_builder(), so a refused memo can't leave a mem::take default behind and silently discard the caller's configuration.

Keep the suite from hanging local Swift SDK CI

Gated behind MAYA_DEPOSIT_VERIFICATION=1. You are right that run_tests.sh picks this bundle up and that a bootstrap stall would hang rather than fail the job — that stall is exactly what has stopped these tests reaching their assertions so far. Also confirmed no local stopSpv is needed, per your note about IntegrationTestCase.tearDown / cleanupSpvCache().


On the red CI

Every failing check has one root cause: Cargo.toml still pins rust-dashcore at 70d4bf8e, which predates the key-wallet API this PR wraps.

error[E0432]: unresolved import `...transaction_builder::MAX_STANDARD_OP_RETURN_BYTES`
error[E0599]: no method named `add_op_return` found for struct `TransactionBuilder`

Same failure in Rust workspace tests (macOS) and Kotlin SDK build; CodeRabbit or PastaClaw review failed only because no review had been submitted at that point and re-runs on its own. It clears when dashpay/rust-dashcore#922 merges and the rev here is bumped.

One thing to flag before that bump, because it is not a one-liner: current dev in rust-dashcore is 6 commits ahead of the pinned rev, and two of them are breaking for this repo — #818 removed AddressInfo.used / generated_at / used_at, and #919 changed the select_coins_with_size arity. Building platform-wallet-ffi against a rebased key-wallet today fails in platform-wallet itself, on those, entirely independently of anything in this PR:

error[E0609]: no field `used` on type `key_wallet::AddressInfo`
error[E0061]: this method takes 6 arguments but 5 arguments were supplied
error: could not compile `platform-wallet` (lib) due to 7 previous errors

So the rev bump wants either a companion change adapting platform-wallet to those two upstream changes, or a rev that carries #922 without them. Happy to take the adaptation if that is the preferred route.

Verification status, honestly: the Swift changes are not compile-verified locally — the SDK cannot be built here right now, since DashSDKFFI needs platform-wallet-ffi, which is blocked by exactly the mismatch above. They are assertion and gating changes over existing helpers, but I would not call them proven until CI or a local build runs them.

@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

🤖 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
`@packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift`:
- Around line 55-68: Reindent the changed Swift code in the Maya deposit
verification integration tests, including the isEnabled declaration,
skipUnlessEnabled(), and the additional referenced blocks, to use 2 spaces per
nesting level as required by the repository EditorConfig. Preserve all logic and
formatting content aside from indentation.
🪄 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: 8a1944b3-0642-4c4b-a2e7-2ef01a88468e

📥 Commits

Reviewing files that changed from the base of the PR and between 6f70092 and 8370b6a.

📒 Files selected for processing (2)
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs
  • packages/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-platform-wallet-ffi/src/core_wallet/transaction_builder.rs

Comment on lines +55 to +68
/// Opt-in gate. `run_tests.sh` (which CI runs for `swift-sdk-build`) executes this bundle,
/// and these tests sit behind several 90-second `waitForSpendable` windows on top of a full
/// SPV bootstrap. A bootstrap stall would hang the job rather than fail it, so they run only
/// when explicitly requested until the bootstrap path is reliable.
private static let isEnabled =
ProcessInfo.processInfo.environment["MAYA_DEPOSIT_VERIFICATION"] == "1"

private func skipUnlessEnabled() throws {
try XCTSkipUnless(
Self.isEnabled,
"Set MAYA_DEPOSIT_VERIFICATION=1 to run the Maya deposit verification suite "
+ "(requires a local dashmate devnet and a completed SPV bootstrap)."
)
}

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

Use 2-space indentation in the changed Swift code.

The changed blocks use 4 spaces per nesting level. Reindent them to match the repository EditorConfig requirement.

As per coding guidelines, “Follow repository EditorConfig settings: 2-space indentation.” <coding_guidelines>

Also applies to: 70-71, 127-183, 226-241, 264-274

🤖 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/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/MayaDepositVerificationIntegrationTests.swift`
around lines 55 - 68, Reindent the changed Swift code in the Maya deposit
verification integration tests, including the isEnabled declaration,
skipUnlessEnabled(), and the additional referenced blocks, to use 2 spaces per
nesting level as required by the repository EditorConfig. Preserve all logic and
formatting content aside from indentation.

Source: Coding guidelines

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.

3 participants