feat(maya)!: swap deposits on the SDK deferred surface — dashj construction deleted - #1535
feat(maya)!: swap deposits on the SDK deferred surface — dashj construction deleted#1535HashEngineering wants to merge 14 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR moves Maya deposit construction and broadcasting to the Kotlin SDK deferred-payment flow. It adds deposit validation, reservation mirroring, SDK lock waiting, updated swap display merging, and tests. Manual wallet transaction APIs and an obsolete Maya exception are removed. ChangesMaya SDK cutover
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
wallet/test/de/schildbach/wallet/payments/MayaDepositShapeTest.kt (1)
94-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for a VOUT0 without a decodable address.
verifyMayaDepositShapereturns"VOUT0 is not a plain address output"whenoutputs[0].addressis null. No test reaches that branch. The engine can produce a non-address VOUT0 if the builder contract changes, so cover it.💚 Proposed test
+ `@Test` + fun nonAddressVault0Fails() { + val tx = DecodedTransaction( + ByteArray(32), + listOf(input()), + listOf( + DecodedTransaction.Output(null, vaultDuffs, p2pkhScript(1)), + memoOutput() + ) + ) + val error = verify(tx) + assertNotNull(error) + assertTrue(error!!.contains("not a plain address output")) + } +🤖 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 `@wallet/test/de/schildbach/wallet/payments/MayaDepositShapeTest.kt` around lines 94 - 107, Add a test in MayaDepositShapeTest alongside wrongVaultAmountFails and wrongVaultAddressFails that constructs a deposit transaction whose VOUT0 has no decodable address, invokes verifyMayaDepositShape, and asserts the returned error is non-null and contains "VOUT0 is not a plain address output".integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt (1)
135-142: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRethrow
CancellationExceptionbefore the general catch.
kotlinx.coroutines.CancellationExceptionextendsjava.util.concurrent.CancellationException, which extendsIllegalStateException. Socatch (e: Exception)also swallows a cancellation ofviewModelScopethat arrives while the coroutine suspends inwaitUntilLocked.When that happens, the handler logs a warning, sets
locked = false, and execution continues into the rest of the success branch._showLoading.value = falseandcommitSwapTradeSuccessState.value = ...are non-suspending, so they still run on a screen that is already going away. The cancellation only resurfaces at the next suspension point, which isswapOrderDao.insertOrder.
MayaBlockchainApiImpl.buildAndSendSwapTxuses the explicit rethrow pattern for the same reason (seewallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.ktLines 356-361). Apply it here so a cancelled screen unwinds instead of continuing.♻️ Proposed refactor
val locked = try { withTimeoutOrNull(IS_LOCK_TIMEOUT_MS) { walletDataProvider.waitUntilLocked(txId) } != null + } catch (e: CancellationException) { + // CancellationException is an IllegalStateException, so the + // general catch below would swallow an outer-scope cancel and + // let the rest of the success branch run on a dead screen. + throw e } catch (e: Exception) { log.warn("could not watch maya swap tx {} for a lock", txId, e) false }Add the import:
+import kotlinx.coroutines.CancellationExceptionNote that
withTimeoutOrNullreturns null on timeout instead of throwing, so this rethrow does not defeat the timeout fallback.🤖 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 `@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt` around lines 135 - 142, Update the try/catch around waitUntilLocked in the Maya conversion preview flow to rethrow kotlinx.coroutines.CancellationException before the general Exception handler, following the existing pattern in buildAndSendSwapTx. Preserve the current warning log and false fallback for non-cancellation errors, while allowing coroutine cancellation to unwind immediately.wallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.kt (1)
378-387: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPrefer the typed SDK error over message matching.
Use
DashSdkError.PlatformWallet.CoreInsufficientFundsfromorg.dashfoundation.dashsdk.errorsbefore checking the"insufficient funds"message across causes. The predicate controls both the maximum-sell retry and theInsufficientFundsExceptionmapping.🤖 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 `@wallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.kt` around lines 378 - 387, Update isInsufficientFunds to first detect DashSdkError.PlatformWallet.CoreInsufficientFunds from org.dashfoundation.dashsdk.errors while traversing the throwable cause chain, then retain the case-insensitive “insufficient funds” message fallback across causes. Preserve the existing predicate behavior for maximum-sell retries and InsufficientFundsException mapping.build.gradle (1)
11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin the Maya SDK to an immutable version for release builds.
dashSdkVersionuses a mutableSNAPSHOT, whileSdkL1SendService.buildDeferredMayaDepositdepends on SDK-specific overload parameters and output-order behavior. Use an immutable artifact that contains this API, and retain a test for the VOUT0/VOUT1 contract.🤖 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 `@build.gradle` at line 11, Pin dashSdkVersion in build.gradle to an immutable released artifact containing the overloads required by SdkL1SendService.buildDeferredMayaDeposit, replacing the mutable SNAPSHOT version. Retain or add coverage for the VOUT0/VOUT1 output-order contract in wallet/src/de/schildbach/wallet/service/platform/sdk/SdkL1SendService.kt; no direct production-code change is required there unless needed to preserve that behavior.
🤖 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 `@wallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.kt`:
- Around line 308-323: In the SdkWriteResult.Broadcast branch, clear the
mirrored reservation locks after the successful broadcast by calling
reservationLockMirror.setLocks for the payment with locked=false. Place this
alongside the existing success handling, independently of
bridgedTransactionFactory.bridge so locks are released even when the non-fatal
display bridge returns NotBridged; leave the NotBroadcast and Ambiguous
lifecycle behavior unchanged.
---
Nitpick comments:
In `@build.gradle`:
- Line 11: Pin dashSdkVersion in build.gradle to an immutable released artifact
containing the overloads required by SdkL1SendService.buildDeferredMayaDeposit,
replacing the mutable SNAPSHOT version. Retain or add coverage for the
VOUT0/VOUT1 output-order contract in
wallet/src/de/schildbach/wallet/service/platform/sdk/SdkL1SendService.kt; no
direct production-code change is required there unless needed to preserve that
behavior.
In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt`:
- Around line 135-142: Update the try/catch around waitUntilLocked in the Maya
conversion preview flow to rethrow kotlinx.coroutines.CancellationException
before the general Exception handler, following the existing pattern in
buildAndSendSwapTx. Preserve the current warning log and false fallback for
non-cancellation errors, while allowing coroutine cancellation to unwind
immediately.
In `@wallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.kt`:
- Around line 378-387: Update isInsufficientFunds to first detect
DashSdkError.PlatformWallet.CoreInsufficientFunds from
org.dashfoundation.dashsdk.errors while traversing the throwable cause chain,
then retain the case-insensitive “insufficient funds” message fallback across
causes. Preserve the existing predicate behavior for maximum-sell retries and
InsufficientFundsException mapping.
In `@wallet/test/de/schildbach/wallet/payments/MayaDepositShapeTest.kt`:
- Around line 94-107: Add a test in MayaDepositShapeTest alongside
wrongVaultAmountFails and wrongVaultAddressFails that constructs a deposit
transaction whose VOUT0 has no decodable address, invokes
verifyMayaDepositShape, and asserts the returned error is non-null and contains
"VOUT0 is not a plain address output".
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d77315a1-547b-4dc4-a840-2e164b351eaa
📒 Files selected for processing (15)
build.gradleintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.ktwallet/src/de/schildbach/wallet/data/WalletDataAdapter.ktwallet/src/de/schildbach/wallet/payments/FakeDashSpendService.ktwallet/src/de/schildbach/wallet/payments/MayaBlockchainApiImpl.ktwallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.ktwallet/src/de/schildbach/wallet/payments/WalletSendPaymentService.ktwallet/src/de/schildbach/wallet/service/TxDisplayCacheService.ktwallet/src/de/schildbach/wallet/service/platform/sdk/ReservationLockMirror.ktwallet/src/de/schildbach/wallet/service/platform/sdk/SdkL1SendService.ktwallet/test/de/schildbach/wallet/payments/MayaDepositShapeTest.ktwallet/test/de/schildbach/wallet/service/TxDisplayCacheMergeGuardTest.kt
💤 Files with no reviewable changes (4)
- wallet/src/de/schildbach/wallet/payments/WalletSendPaymentService.kt
- wallet/src/de/schildbach/wallet/payments/FakeDashSpendService.kt
- integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.kt
- wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt
|
not ready, needs to handle the MAX button |
|
Follow-up pushed (4a17d29) after an audit of the MAX button, which the mainnet field test did not exercise. The bug: the max-sell fee reserve could come in under the real fee, making the deposit pay the vault less than quoted. NEAR Intents refuses under-delivery (the deposit sits ~1h, then refunds minus 0.001 DASH), and Maya would execute a swap for an amount the user never agreed to. Three problems with one root cause — nobody had revisited the max path for the cutover:
The fix: Both routes now quote exactly that figure and deposit exactly what they quoted. The silent retry is gone — a balance drop between quote and build aborts with a re-quote error. Re-measuring at build time uses the real memo, which can only raise the ceiling set by the worst-case quote, so the guard cannot fire spuriously. Side effect worth noting: the maya module no longer calls 7 new |
|
One more max-sell fix (20513dd), found by asking whether the max path actually drains the wallet. It does not — by design (see below) — but it does spend all but 1 000 duffs, which means coin selection reaches every UTXO in the wallet. Fixed by extracting the drain's fail-closed preflight — dashj wallet locks OR seam-registered locks on SDK-only txs, blocking on a check failure too — into For the record, what a MAX sell actually leaves behind:
It is not a true send-all ( |
|
|
…uction deleted MayaBlockchainApiImpl now builds the MAYACHAIN deposit with the Kotlin SDK's deferred build/broadcast primitive (buildDeferredMayaDeposit: vault VOUT0, OP_RETURN memo VOUT1, change back to VIN0's address VOUT2, no BIP-69 reordering — the new builder controls from platform#4286/#4288, engine work in rust-dashcore#922), verifies the deposit shape from the signed bytes BEFORE broadcasting (verifyMayaDepositShape — a mis-shaped vault deposit strands funds), mirrors the reservation into wallet locks for the transition window, and bridges the broadcast tx for display. The SwapKit Maya-protocol legs converge automatically — they delegate to the same buildAndSendSwapTx; the NEAR-Intents legs already ride the neutral SDK-routed send. Replace-then-delete, as with BIP70: the dashj leg is gone — manual SendRequest/OP_RETURN script construction, output clearing/re-signing, the fresh-Transaction confidence workaround, the post-completeTx output checks, and with it the now-orphaned manual-tx surface (WalletSendPaymentService.completeTransaction/signTransaction/ sendTransaction and IncorrectSwapOutputCount) whose only consumer was this path. Failure semantics: build/verify failures release the reservation (recoverable, nothing moved); a provably pre-network broadcast refusal releases; an AMBIGUOUS broadcast outcome keeps the reservation and reports non-retryable — releasing would let a rebuilt retry pay the vault twice (the BIP70 field-test lesson). Max sells retry the build once with a 10k-duff fee reserve carved out on an engine-reported shortfall (pre-broadcast by construction). Pins dash-sdk-android 0.1.0-v41int13-maya2-SNAPSHOT (qa5-plus-maya + the buildSignedPayment Maya options). Unit tests: 10-case MayaDepositShapeTest; payments + sdk-service suites 747/747 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Shape verification runs on the SDK's own consensus decoder (TransactionDecoder) instead of bitcoinj parsing; the memo check is now byte-for-byte against the expected OP_RETURN script, and the change-to-VIN0 check uses the decoder's recovered sender address. A decode failure counts as a failed shape check (released, recoverable), never a broadcastable pass. - The transition-only lockOutput reservation mirror moves to its own clearly-marked helper (ReservationLockMirror) so the last dashj on this flow is quarantined in one Phase-2-deletable class. - Duffs conversion is pure decimal arithmetic (no Coin types). - MayaDepositShapeTest rebuilt on hand-built DecodedTransaction fixtures — 13 host cases, no dashj, no native library. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-tested on mainnet 2026-08-05 (tx 59f7d755…d8ce): the engine had the InstantSend lock 1.6s after broadcast, but the confirmation screen still waited out its whole 10s timeout. Two compounding causes: - The ViewModel watched observeTransactionLocked, a change-only stream — on the SDK route the lock usually lands BEFORE the send call returns, so the subscription missed the event and nothing ever re-fired. It now calls waitUntilLocked, which returns immediately for an already-locked tx (same timeout as the outer bound). - WalletDataAdapter.waitUntilLocked preferred the held dashj wallet whenever the tx existed there — true for every bridged SDK send — and a bridged copy's confidence is frozen (no peergroup ever delivers it an IS-lock), so that wait could never complete post-cutover. The seam path (live engine lock state, race-free current-state replay) now runs first; the dashj-confidence path remains for pre-cutover and for txs the SDK store never learned. Also benefits CrowdNode's top-up lock wait, the other waitUntilLocked caller, which had the same frozen-confidence exposure for bridged sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08-05 mainnet field test left the swap row stale until the long-tap History rebuild. The chain was intact up to the last step: the swap-orders write re-emitted presentable metadata, the display cache computed the changed row and rebuilt it with the fresh swap decoration — and then the SDK-stamped shape freeze threw the new title/icon/status away (both the inline metadata-path merge and mergeDisplayEntryPreservingSdkStamped keep the CACHED shape for SDK-authoritative rows, because a dashj rebuild cannot be trusted to re-derive value or direction). The swap decoration is the exception the freeze must admit: it is metadata-authoritative — the very swap-orders table whose change triggered the rebuild — not a dashj recomputation. Both merge sites now pass icon/title/status through when the rebuild carries swap metadata (entry.swapStatus != null), while value, exchange rate, contact identity and the filter bucket stay frozen. A rebuild WITHOUT swap metadata still never undresses an existing swap row. Covered by three new TxDisplayCacheMergeGuardTest cases (decoration passes the freeze; PENDING→COMPLETED retitles; a metadata-less rebuild keeps the swap shape); service suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dashj The max-sell fee reserve could come in under the real fee, which makes the deposit pay the vault LESS than quoted. That is never acceptable: NEAR Intents refuses under-delivery (the deposit sits ~1h, then refunds minus 0.001 DASH) and Maya would execute a swap for an amount the user never agreed to. Three problems, one cause — nobody had revisited the max path for the cutover: - SwapKit sized the sweep with dashj (estimateNetworkFee → completeTx on the HELD wallet). Post-cutover incoming SDK transactions never reach that wallet, so its coin set is frozen at cutover time: the estimate either throws (any funds received since the cutover are invisible to it, so a max quote fails outright) or prices the wrong shape (no OP_RETURN). - The direct Maya route had no reserve at all — it quoted the full balance and relied on a silent adjust-down retry at build time, i.e. exactly the under-delivery this commit refuses. - The retry itself masked the problem instead of reporting it. Replaced with a MEASURED figure: SdkL1SendService.maxMayaDepositDuffs builds a throwaway deposit through the real engine (same builder, same three options, the wallet's own address as a size stand-in), reads the fee off the reservation and releases it. Biased HIGH by construction — a worst-case 80-byte memo, a change output kept in the probe so the measured size matches the real one, and 1 000 duffs of headroom to keep that change clear of dust — so the reserve can never fall short. Exposed neutrally as MayaBlockchainApi.maxSwapDepositAmount. Both routes now quote exactly that figure and deposit exactly what they quoted. The silent retry is gone: if the balance drops between quote and build, the deposit aborts with a re-quote error. Re-measuring at build time uses the REAL memo, which can only raise the ceiling set by the worst-case quote, so the guard cannot fire spuriously. The maya module no longer calls estimateNetworkFee anywhere — the last dashj on the swap path is gone. 7 new SdkL1SendServiceTest cases; payments + service suites and ktlint green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A max deposit spends all but the change headroom, so coin selection reaches every UTXO — including app-locked ones. spendableBalanceDuffs deliberately INCLUDES app-locked outputs (the engine has no lock concept and the FFI exposes no exclusion API), which is exactly why the send-all drain refuses to run while any lock exists. The Maya max path bypassed that guard and would have swept CrowdNode-protected funds into a swap. Extracted the drain's fail-closed preflight (dashj wallet locks OR seam-registered locks on SDK-only txs, blocking on a check failure too) into hasProtectedOutputs, and applied it in maxMayaDepositDuffs: refuse to quote rather than build a deposit that spends protected funds. A partial (non-max) deposit is unchanged — it carries the same exposure as any ordinary send. commitSwapTransaction contains the refusal as a recoverable failure instead of letting it escape the caller's scope. 3 new tests (dashj lock, seam lock, check-throws); nothing is built or measured in any of those cases. Suites and ktlint green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maxMayaDepositDuffs read the WALLET-WIDE spendable balance, subtracted a probe-measured fee and a change-headroom constant, and quoted the result — but the deposit funds from BIP44 account 0 alone. Any wallet holding DIP-15 contact-received or CoinJoin funds therefore quoted against money the build could not reach: the probe failed with CoreInsufficientFunds and MAX died. It failed safely, but it failed. A max deposit IS a drain, so build one and read what the engine says it delivers: every BIP44 UTXO selected, this memo's bytes priced into the fee, no change, the vault output set to (total inputs - fee). That is the same computation the real deposit performs, so quote and deposit cannot disagree — which subtracting a guess from the wrong pool could not promise. - SdkDeferredPayment carries `deliverableDuffs`: what the transaction actually pays. Supplied by the caller for an explicit build, computed by the engine for a drain. - buildDeferredMayaDeposit takes `drain`, passing SelectionStrategy.ALL. A drain supplies no amount, so 0 is passed and the engine sets the output (requires the maya7 AAR, whose JNI accepts a zero output). - maxMayaDepositDuffs builds a drain to an own address, reads its deliverable, and releases the reservation. Gone: the probe reserve, the change headroom, the spendable-minus-fee arithmetic. A drain the engine will not fund means "nothing depositable" and returns 0. - A MAX sell now BUILDS as a drain, and the built transaction is checked against the quote before any broadcast decision. The pre-build guard compared a re-measurement; this compares the signed transaction that would actually reach the vault, so nothing moving in between can defeat it. Under-delivery is refused: Maya would execute a swap the user never agreed to, and NEAR Intents rejects it outright. The CrowdNode app-locked-output refusal and the funding-gate check are unchanged — a max deposit still will not sweep protected outputs. Pin moved to the maya7 AAR. Tests: five rewritten to the drain contract (the engine-computed amount, the drain-shaped probe and its release, worst-case and explicit memo sizing, and an unfundable drain reporting 0), plus a fake that throws to model the engine's refusal. Wallet unit suite shows no regression against the same baseline; the pre-existing failures are an unrelated leaked dashj Context between test classes. Verified on-device (testnet, emulator): drain-measured max deposit 27442985 duffs, real fee 432, 80-byte memo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
will be rebasing against the latest phase1 branch tip |
…quote A max sell aborted pre-broadcast with "VOUT0 carries 7442734 duffs, expected 7442725" and released a perfectly good deposit. A MAX sell builds as a DRAIN, so the ENGINE sets the vault output to (total inputs - fee); the app never supplies that number and the quote is only a FLOOR. Both guards around the build already treat it that way -- each aborts on `<` and neither on `>` -- but verifyMayaDepositShape demanded exact equality with the quote, so a drain delivering MORE than quoted was rejected as mis-shaped. Nine duffs, and the ordinary result of the balance moving between quote and build. Verify a max sell against payment.deliverableDuffs instead: the value Rust computes from the REGISTERED transaction. That keeps the check exact rather than loosening it to a range, and turns it into a cross-check -- the decoded host bytes must agree with what the engine registered, which is the disagreement the gate exists to catch. Ordinary sells are unchanged: the app chose the amount, so the quote is the expectation. Four tests, including the failing transaction end to end: rejected against the quote, accepted against the engine's amount, and still rejected when the bytes disagree with the engine by one duff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a7f60b0 to
48eac3d
Compare
bfoss765
left a comment
There was a problem hiding this comment.
The design here is the right shape for the port — the reservation lifecycle (build/verify failure and provable pre-network refusal → release; ambiguous → keep + non-retryable), the fail-closed byte-exact shape verifier, and the drain-measured MAX with the engine-deliverable cross-check are all genuinely strong fund-safety work. Requesting changes on a small set of items, most of which your own description already declares:
Blocking
-
SdkL1SendService.kt(companion object):MAYA_DEPOSIT_PROBE_RESERVE_DUFFSandMAYA_DEPOSIT_CHANGE_HEADROOM_DUFFSare added but never referenced by code (only KDoc), and the PR description says they were removed. The KDocs ofmaxMayaDepositDuffsandMayaBlockchainApi.maxSwapDepositAmountalso still describe the retiredspendable − measured fee − headroommodel while the implementation is drain-measured with no headroom. Please delete the dead constants and rewrite both KDocs to the drain model — as written, a future reader will trust the doc and re-introduce the headroom arithmetic the drain rework deliberately killed. -
Coordination: this was cut against the pre-pooling
SdkL1SendService.kt. The integration line has since moved to the pooled ALL_SPENDABLE funding default (v41int19+ AARs; platform #944/#4350 are now merged upstream), and there are further in-flight changes to this file on the branch tip — a merge simulation shows content conflicts in bothSdkL1SendService.ktandSdkL1SendServiceTest.kt. Two consequences: (a)buildDeferredMayaDepositand the MAX drain will select across BIP44 + BIP32 + DashPay receival accounts under the pooled default, not "every BIP44 UTXO" as the comments/tests state — change/refund provenance can be a receival-account address (still wallet-owned, funds-safe, but the docs/tests are stale); (b) funding shortfalls become the typedCoreInsufficientFunds(FFI 22) rather than the string shapes. Suggest reconstructing the Maya additions on top of the pooled version rather than resolving hunks mechanically, and re-running the testnet MAX sell under the pooled default — the drain's sweep scope is materially different. -
Dependency chain (declared): rust-dashcore#928 and platform#4324 are still open and the
maya8AAR is local-only, so nobody else can build this. Flagging that the integration pin has moved tov41int21, which also lacks the drain surface — the fold target is now "int21 + #922 + #928 + #4324".
Non-blocking
-
buildDeferredMayaDepositwithdrain = truedoesn't runhasProtectedOutputs()— the only guard against sweeping CrowdNode-locked outputs is the caller having calledmaxMayaDepositDuffsfirst.MayaBlockchainApiImpldoes that today, but the guard is a call-site convention. Suggest moving the check into the primitive itself underdrain = true, same as the send-all drain — then no future caller can sweep protected funds by skipping the measurement step. -
MayaBlockchainApiImpl,SdkWriteResult.Broadcastarm — on the CodeRabbit thread asking for the mirror locks to be cleared after successful broadcast: I'd argue keeping them is the safe behavior and clearing would be wrong. Post-cutover the held dashj wallet never learns the deposit spent those outpoints; if the display bridge returnsNotBridged, the stale lock is the only thing preventing the mixer double-selecting an already-spent coin. Worth a short code comment in theBroadcastarm saying the lock is intentionally left in place, so the next reader doesn't "fix" it. -
Testing section: "Testnet MAX sell — 2026-08-07 … Re-tested and successful" vs "The commit path — building the drain and broadcasting it after the guard — has not been run against the network yet." These read as contradictory — please clarify which statement covers the broadcasting MAX drain. If a MAX drain has never actually hit the network, that should gate the merge given it sweeps the wallet into a vault.
-
Nit:
MayaConversionPreviewViewModel'swithTimeoutOrNull(...) { waitUntilLocked(txId) } != nullworks becauseUnit != null, but reads oddly. Fine to leave.
…red-model docs Review items 1, 4, 5 and 7 from #1535. The max-deposit guard was a call-site convention: buildDeferredMayaDeposit would happily drain the wallet, and the only thing standing between a MAX deposit and CrowdNode-locked funds was the caller having measured first. MayaBlockchainApiImpl does measure, but a convention is one refactor away from being skipped and the money does not come back. Move the fail-closed check into the primitive under drain = true, where no caller can miss it. maxMayaDepositDuffs keeps its own copy deliberately -- its probe runs inside a catch-all that turns any failure into a quote of 0, which would otherwise swallow the refusal and report "your maximum is 0" instead of "you hold locked funds". Three tests cover the gap: a direct drain build is refused for dashj locks and for seam-registered locks with nothing reserved, and a partial deposit is still allowed through. The docs described a model the drain rework deleted. Two constants (MAYA_DEPOSIT_PROBE_RESERVE_DUFFS, MAYA_DEPOSIT_CHANGE_HEADROOM_DUFFS) survived only in KDoc references, and three doc blocks still explained the max deposit as "spendable - measured fee - headroom". Left alone, the next reader would trust them and reintroduce the headroom arithmetic that the drain measurement exists to remove. Delete the constants and rewrite maxMayaDepositDuffs, MayaBlockchainApi.maxSwapDepositAmount and the MayaBlockchainApiImpl class doc to the drain model, including why a headroom must not come back. Also: say in the Broadcast arm that the mirrored reservation locks are left in place on purpose -- post-cutover the held dashj wallet never learns the deposit spent those outpoints, so on a NotBridged result the stale lock is the only thing keeping the mixer off an already-spent coin. And make the IS-lock timeout read as what it is rather than relying on Unit != null. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the review. Items 1, 3, 4, 5, 6 and 7 are addressed in Item 6 first — the testing claim was wrong, but in the other directionYou were right that the two statements contradicted each other, and right to say it should gate the merge. Chasing it down turned up a bigger error than the contradiction. They describe two different runs, which is why the numbers never lined up: one was measurement-only (27,442,985 duffs, 432-duff fee, 80-byte memo), the other the 2026-08-07 MAX sell (7,442,734 duffs, 423-duff fee, 72-byte memo). The MAX sell was broadcast — and it was mainnet with real funds, not testnet. Every "testnet" label on this work was wrong, in this description and in platform#4324. Both are corrected. The transaction is
InstantSend-locked, confirmed in block 2517981. The absent change output is the part that matters: it proves this was a real drain and not a large ordinary send, and So the commit path has been exercised end to end on the real network. The description no longer claims otherwise. Item 4 — guard moved, with one wrinkle worth naming
Three tests cover the gap you identified: a direct drain build is refused for dashj locks and for seam-registered locks with nothing reserved, and a partial deposit still passes through unguarded. Item 1 — three stale KDocs, not twoYou named Items 5 and 7The Item 2 — what
|
Pins 0.1.0-v41int21-SNAPSHOT -- the official integration AAR, with no local suffix, so this branch builds for anyone once int21 is published. Testing was done against a locally built 0.1.0-v41int21-maya10-SNAPSHOT (int21 with rust-dashcore#928, platform #4286/#4288/#4324, the asset-lock fixes #4336/#4337, and message signing #4319/#4321), which exists only in the author's local Maven repository and is therefore deliberately NOT committed. The SDK's send APIs default accountType to ALL_SPENDABLE from #4329, so this pin changes funding scope without a call-site edit: sends and the MAX Maya drain now draw on BIP44 + BIP32 + every DashPay contact-receiving account, with change returning to BIP44. That is the intent -- a send should reach the user's whole spendable balance, and the app-side sweep-then-send machinery exists only because the SDK could not do this before. No call site names an account type, deliberately. Comments updated where they still asserted the old single-account scope: sendToAddress's "BIP44 account 0 is the default", and buildDeferredMayaDeposit's "a DRAIN spends every BIP44 UTXO". Also records that the protected-outputs guard is wallet-wide rather than per-account, so it still covers the sweep now that the pooled default has widened it -- checked against hasAppLockedSpendableOutputs, whose narrow per-account sibling is the separate CoinJoin-drain guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item 2(b) from #1535. MayaBlockchainApiImpl.isInsufficientFunds still walked the cause chain looking for the substring "insufficient funds" -- written when the shortfall only reached us as key-wallet's Display wrapped in a build failure. The SDK types it now, so match DashSdkError.PlatformWallet.CoreInsufficientFunds (FFI 22) instead. A matcher keyed on wording stops recognising the shortfall the moment the wording changes, and the failure is silent: "not enough funds" degrades into an opaque swap failure with no route to the familiar UI. The cause chain is still walked, since the typed error can arrive wrapped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 2 — the pooled fold is done, with one piece deliberately outstandingPushed as What
|
MAX sell exercised on the pooled build — and it surfaced a real bugThe MAX sell has now been run on the pooled Reproduced on the pooled buildThe MAX sell run today on On device immediately after, the wallet's own mirror shows:
So the wallet reports 0.05 for a wallet it has just emptied. This is the same defect the earlier drain produced, now confirmed on the pooled build with a second independent transaction — it is reproducible, not a one-off. The bug: a drain's inputs are never marked spentAfter a MAX deposit the wallet permanently over-reports its balance by the amount it just sent. Observed: 0.12443157 DASH displayed when the true balance was 0.05000000 — inflated by exactly 0.07443157, the drain's input total. Verified against the on-device SDK mirror rather than inferred:
Root causeA drain pays the vault at
The SDK already documents this exact failure mode at A rescan does not fix this. It re-runs the same script-only block matching and misses the drain again for the same reason. One cause, three symptomsThe stuck
The tagging itself is fine: Does this affect the primary SendCoinsActivity/Fragment?Yes, but not yet — it lands with the cutover. This is not Maya-specific. The trigger is "a transaction with no wallet-owned output", and the send-all path produces exactly that shape. From
So:
So the blast radius is every SDK drain to a third party, and the main send screen inherits it the moment the cutover flips. Worth fixing before that, independently of Maya. Fixes in flightTwo work streams, tracked separately:
The acceptance test for the first should be |
A MAX Maya sell is now an ordinary fixed-amount send of `spendable - mayaMaxFeeReserveDuffs(...)`, the same system the shielded max-shield and Buy Credits already use, replacing SelectionStrategy.ALL. The drain had to go because of what it produced, not what it computed: vault output, zero-value OP_RETURN, no change -- a transaction with NO wallet-owned script. Compact block filters match wallet script pubkeys only, so such a transaction is never matched in a block, its context never reaches CONTEXT_IN_BLOCK, and the wallet counts the spent inputs as spendable forever. Two mainnet drains proved it: a5c99aec (balance inflated by 0.07443157) and 1f608a9a on the pooled build (reported 0.05 for a wallet it had just emptied, row stuck on "Sending" across restarts). A rescan cannot recover either -- it re-runs the same script-only matching. Withholding a reserve restores a change output, so the deposit confirms and settles like any other send. It also makes quoting stricter rather than looser: the app names the amount and the transaction pays exactly that, so quote and payment are equal by construction and under-delivery is unreachable. The reserve's unused remainder returns as change, which is what makes over-reserving lossless. A MAX sell therefore leaves a small remnant rather than emptying to zero -- deliberate, and not surfaced, matching shielded. Sizing mirrors assetLockMaxFeeReserve (~148 vbytes per input, doubled) but sizes the data carrier exactly, since a Maya quote always knows its memo length. Floored at MAYA_MAX_RESERVE_MIN_INPUTS because the reachable dashj UTXO count freezes post-cutover and under-reserving is the failing direction; the overlaid count behind WalletDataProvider is the eventual source. Removed: the drain parameter through SdkL1SendSource, the probe build in maxMayaDepositDuffs, and expectedVaultDuffs -- max sells verify against the quote again, like every other sell. The app-locked-output guard STAYS and now keys on isMaxDeposit: a reserve leaves change but does not narrow which coins are selected, so a max deposit is still sweep-scale and can still reach CrowdNode-locked outputs. Revisit when the SDK computes MAX internally in the wallet engine; the engine should own the amount rather than this arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Maya/SwapKit MAX sell stayed titled "Sending" on the home screen forever (across app restarts) and never rendered "Conversion DASH/RUNE", while the transaction-details screen identified the swap correctly the whole time. Reproduced on-device by rewinding the cached row and restarting: it did not recover. Two independent display-layer faults combined. 1. The swap decoration was only derivable from a dashj transaction. TransactionRowView.fromTransaction was the sole reader of metadata.swapOrder, so every writer that produced it needed a resolvable dashj wrapper. TxDisplayCacheService's metadata flow silently writes nothing when no wrapper resolves -- after already assigning this.metadata -- so the swap-order change is consumed and never seen again. The row for this MAX sell was authored by CutoverUiDataService (the SDK path), which has no notion of swap orders at all. 2. The SDK planner then re-titled the row on every pass. Its never-touch carve-out keys off existing.service, but a row whose decoration was already lost carries service = null, so the definitive plain-send re-stamp claimed it -- and with the SDK record's context stuck at mempool, that re-stamp is permanently "Sending". Fix: derive the decoration (convert icon, "Conversion ..."/"Converted ..." title, swapStatus) from the swap_orders record by txid, with no dashj transaction involved -- the same source the details screen observes. The new pure planSwapRowDecorations runs from reconcileSwapRows on every metadata emission (not gated on the diff) and on every DisplayCacheRefreshBus tick, so an SDK-authored insert or re-stamp is corrected whichever writer got there first. Idempotent: only decoration fields are touched, and a settled row produces no write. Swap rows also join the SDK planner's never-touch set (swapStatus != null, in both planL1DisplaySync and planL1InstantLockRowUpdate) so a decorated row holds stable instead of flip-flopping once per sync pass. The title choice moves to TransactionRowView.swapTitleRes so renderer and reconciler cannot drift. Adds 12 host-JVM regression tests, including the full mempool -> in-block story: a row born plain gets decorated, survives the context advance without being re-titled, follows PENDING -> COMPLETED rather than being pinned to a stale rendering, then settles. One test asserts a non-swap row of the same shape is still re-stamped, so the carve-out is not over-broad. Not addressed: the swap vault address is still marked TaxCategory.Expense (MayaConversionPreviewViewModel) -- TaxCategory has no Trade value, and adding one reaches into the CSV export and the category picker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary of changes since the reviewEverything below is pushed. Branch head Review items
The substantive change: MAX no longer drains
The reason is the bug documented above: a drain produced destination + zero-value It also makes quoting stricter, not looser: the app names the amount and the transaction pays exactly that, so quote and payment are equal by construction and under-delivery is unreachable. The reserve's remainder returns as change, so over-reserving is lossless. A MAX sell leaves a small remnant rather than emptying to zero — deliberate, unsurfaced, matching shielded. The app-locked-output guard stayed and now keys on Verified on mainnet: the MAX sell on this build worked through Dash DEX/Maya and left a change output with no stale TXO rows — the failure mode the drain produced twice. Two issues a reviewer should know about1. CI cannot build this branch yet. The committed pin is Worse, publishing int21 alone will not be enough: the qa5/int21 line does not carry #4286 or #4288 (verified — zero references to the OP_RETURN / output-order / change-to-VIN0 controls), which this branch needs. Testing used a local 2. The #4324 dependency is now nearly vestigial. Since MAX stopped draining, nothing in the wallet consumes Still open, tracked separatelyThe filter gap itself is unfixed: block/compact-filter matching watches script pubkeys only, so any changeless transaction paying a third party is invisible to it. The wallet now avoids producing that shape, but post-cutover |
…t/maya-sdk-route # Conflicts: # build.gradle # wallet/src/de/schildbach/wallet/service/platform/sdk/SdkL1SendService.kt # wallet/test/de/schildbach/wallet/service/platform/sdk/SdkL1SendServiceTest.kt
|
Re-reviewed at head Keeping changes-requested for the following. Blocker: does not compile against the merge targetMerged onto current base
Medium: the changeless-transaction bug may still be reachableThis is about the post-review MAX-reserve redesign (
If the engine folds sub-dust change into the fee, the transaction becomes vault + OP_RETURN with no wallet-owned output — and I could not determine from this repo what Item 1 regressed on the doc I named
Related new dead code in the same class: Item 4 weakened
Item 6 — PR descriptionThe description at head still describes the retired drain design end-to-end: it names pin Minor
|
|
@bfoss765 This is ready for a re-review — every item from your review is addressed, and the dependency picture has changed materially since then. Your items: 1, 3, 4, 5, 6, 7 and (b) are done, per the earlier comments. Item 2's fold is done too: the branch is on the pooled The dependency chain collapsed since your review:
One design change you should weigh as part of the re-review, since it happened after your pass: the MAX sell no longer drains. It is now an ordinary fixed-amount send of Testing on the current head: the MAX sell was run end to end on mainnet through Dash DEX/Maya on the reserve model and settled correctly — change output present, no stale TXO rows, quote equal to payment by construction. |
Issue being fixed or feature implemented
Maya/SwapKit swap deposits were the last L1 send still built by dashj. This moves them onto the Kotlin SDK's deferred build/broadcast surface and deletes the dashj leg, the same replace-then-delete treatment BIP70 got in #1531 — items 4 and 6 of #1520.
A MAYACHAIN UTXO deposit is not an ordinary send: the Asgard vault output must be
VOUT0, the swap memo must be a zero-valueOP_RETURNatVOUT1, change must go back toVIN0's address (MAYAChain identifies the depositor by the first input and pays refunds there), and no BIP-69 reordering may touch any of it. The SDK builder had no way to express that shape, which is why this path had to stay on dashj until now.The committed pin is
org.dashj:dash-sdk-android:0.1.0-v41int18-SNAPSHOT— byte-identical to what the base branchfeat/kotlin-sdk-phase1already uses, so this PR does not change the build for anyone reviewing it. That published AAR does not carry the drain surface, so the Maya deposit code will not compile against it until rows 4 and 5 below land and an officialintNAAR includes them.Testing was done against a locally built
0.1.0-v41int18-maya8-SNAPSHOT, which exists only in the author's local Maven repository. Nobody else can build this branch until rows 4 and 5 land and an officialintNAAR carries them — that is a statement about this PR's readiness to be built by a reviewer, not a request to take it on trust.The integration line has moved on. It is now at
v41int21, which also lacks the drain surface, so the fold target is no longer int18 — it isint21 + #922 + #928 + #4324. Re-cutting the local AAR against int21 and reconstructing the Maya additions on top of the newerSdkL1SendService.ktis review item 2, which is deliberately not part of this round: int21 also carries the pooledALL_SPENDABLEfunding default, and folding onto it changes the drain's sweep scope, so it needs its own MAX run against the network rather than a mechanical conflict resolution. Treat the numbers in this description as belonging to the int18 fold.What that local AAR is built from, so it can be reproduced:
bfoss765/integration/v41-keystore-qa5@a56a49a1e3— the integration branch that merges the in-flight feature work and produces the officialv41intNAARs;v41int18was cut from this commit. The Maya work branches off this rather thanv4.2-dev, on the integration owner's direction. It carries none of the drain work below, and is not expected to.fd3c491aon the same fork), which descends from neither. feat: merchant filter event tracking #922 is merged upstream but is not in that fork rev, so both are supplied locally.This branch is rebased onto
feat/kotlin-sdk-phase1@8476ae798.Required, in dependency order:
add_op_return,preserve_output_order,change_to_first_input, OP_RETURN-aware fee sizingSelectionStrategy::AllselectionStrategyonbuildSignedPaymentandSignedCoreTransaction.deliverableAmountDuffs— how a host asks for a drain and reads what it paysv4.2-devkey-wallet pin, which predates #928Rows 4 and 5 are what the newest commit here depends on; without them a drain-with-memo is rejected by the engine and the deliverable amount cannot be read back.
What was done?
MayaBlockchainApiImplrewritten on the SDK (buildAndSendSwapTx, used by both the direct Maya backend and SwapKit's Maya-protocol legs):SdkL1SendService.buildDeferredMayaDepositwraps the SDK's atomic select + reserve + sign with the three new builder controls, returning signed bytes with the inputs reserved and nothing broadcast. Memo size is checked before anything is reserved (an asset contract address plus a destination address can exceed the 80-byte OP_RETURN limit).verifyMayaDepositShapeasserts the full deposit shape from the signed bytes: vault atVOUT0for the exact amount, a zero-valueOP_RETURNatVOUT1whose script is byte-for-byte the expected push of the memo, at most one further output, and when present that change is P2PKH payingVIN0's own address. A mis-shaped deposit to a vault strands funds, so this replaces the old post-completeTxoutput count check with something considerably stricter. Decoding uses the SDK's own consensus decoder; a decode failure counts as a failed check, never a broadcastable pass.Deleted (replace-then-delete): all dashj transaction construction on this path — manual
SendRequest, the OP_RETURN script building, the output clear-and-re-add dance, re-signing, the fresh-Transactionconfidence workaround — plus the now-orphaned manual-tx surface it was the last consumer of (WalletSendPaymentService.completeTransaction/signTransaction/sendTransaction) and the unusedIncorrectSwapOutputCount.MayaBlockchainApiImplitself has zero dashj imports: the only dashj left on the flow is the transition-onlyReservationLockMirror(extracted here, dies with Phase 2) which locks the reserved outpoints so the background CoinJoin mixer cannot double-select them.Two field-test bugs fixed after the first live swap:
WalletDataAdapter.waitUntilLockedpreferred the held dashj wallet whenever the tx existed there — true for every bridged SDK send, whose dashj confidence is frozen because no peergroup ever delivers it a lock. The seam path (live engine lock state, race-free current-state replay) now runs first. CrowdNode's top-up wait, the only other caller, had the same exposure and is fixed with it.SwapKit needed no separate work: its Maya-protocol legs delegate to the same rewritten
buildAndSendSwapTx, and its NEAR-Intents legs already ride the SDK-routed neutral send on this branch.How Has This Been Tested?
Mainnet, end to end, real funds — 2026-08-05, Pixel 9 emulator,
prodDebug, SwapKit Maya route, tx59f7d755…d8ce: 0.05318952 DASH → 3.5205806 RUNE, completed. SDK deferred build reserved the inputs and priced the deposit at a 343-duff fee (the engine's OP_RETURN-aware sizing), shape verification passed, broadcast, engine InstantSend lock 1.6 s later, mined in block 2516852, display bridge committed the row, and SwapKit tracking ranPENDING → COMPLETEDwith the outbound RUNE tx041fb277…. Both fixes above were verified on a follow-up swap.Mainnet MAX sell, real funds — 2026-08-07, against the
maya8AAR, txa5c99aec…c873. (Earlier revisions of this description called this run "testnet"; the transaction is on mainnet — the label was wrong, the run was real.) The drain measured a 7,442,734-duff deposit (423-duff fee, 72-byte memo) and the build delivered exactly that. It first exposed a bug in this PR's own gate:verifyMayaDepositShapecomparedVOUT0against the quote (7,442,725) and aborted a sound deposit over a 9-duff difference. A MAX sell builds as a drain, so the engine sets the vault amount and the quote is only a floor — which is how both guards around the build already treat it (each aborts on<, neither on>). The shape check now verifies a max sell againstdeliverableAmountDuffs, the value Rust computes from the registered transaction; it stays an exact comparison, which also makes it a cross-check that the decoded host bytes agree with what the engine registered. Ordinary sells still verify against the quote.Re-run after the fix and broadcast, so this is the commit path end to end — drain built, guard cleared, transaction on the network — not a build-and-release rehearsal. The on-chain transaction confirms the drain shape exactly:
VOUT0→ vaultVOUT1OP_RETURN(the memo)VOUT0 + fee == inputsto the duffNo change output is the proof it was a real drain rather than a large ordinary send, and
VOUT0equals thedeliverableAmountDuffsthe engine reported at build time — the two values the shape check now compares. InstantSend-locked, confirmed in block 2517981.Automated: new
MayaDepositShapeTest(17 host cases over hand-built decoded-transaction fixtures — wrong vault address/amount, wrong or displaced memo, value-carrying OP_RETURN, foreign change, non-P2PKH change, the OP_PUSHDATA1 boundary at 76+ bytes, no inputs, and the max-sell expectation source — the failing transaction above rejected against the quote, accepted against the engine's amount, and still rejected when the bytes disagree with the engine by one duff); 3 newTxDisplayCacheMergeGuardTestcases for the swap-decoration exception. Wallet payments and SDK-service suites green (747 tests),compileStagingDebugKotlinand ktlint clean.Breaking Changes
WalletSendPaymentService.completeTransaction/signTransaction/sendTransactionare removed (no remaining callers), as isIncorrectSwapOutputCount.Max deposits are measured by draining, not by estimating
maxMayaDepositDuffsread the wallet-wide spendable balance, subtracted a probe-measured fee and a change-headroom constant, and quoted the result — but the deposit funds from BIP44 account 0 alone. The quote was therefore derived from a different pool than the build spends from, so it could name an amount the build could not fund, and it under-quoted by the headroom even when it worked.A max deposit is a drain, so this now builds one and reads what the engine says it delivers: every BIP44 UTXO selected, the memo's bytes priced into the fee, no change, the vault output set to
total inputs − fee. That is the same computation the real deposit performs, so quote and deposit cannot disagree.SdkDeferredPaymentcarriesdeliverableDuffs— what the transaction actually pays. Supplied by the caller for an explicit build, computed by the engine for a drain.buildDeferredMayaDeposittakesdrain, passingSelectionStrategy.ALL.MAYA_DEPOSIT_PROBE_RESERVE_DUFFS,MAYA_DEPOSIT_CHANGE_HEADROOM_DUFFS, thespendable − fee − headroomarithmetic, and the wallet-wide balance read.The CrowdNode app-locked-output refusal and the funding-gate check are unchanged — a max deposit still will not sweep protected outputs.
Verified on-device. Two separate runs, which an earlier version of this description ran together and contradicted itself over:
a5c99aec…c873. The drain measured 7,442,734 duffs (423-duff fee, 72-byte memo), the built transaction paid exactly that, and after thedeliverableAmountDuffsfix the deposit was broadcast, InstantSend-locked and confirmed. The chain shows two inputs fully consumed and no change output, which is what makes it a drain and not a large ordinary send.So the broadcasting MAX drain has been exercised against the real network, for real funds. The earlier "has not been run against the network yet" sentence described run 1 and was wrong about run 2; it is removed rather than reworded, since it was the source of the contradiction.
Checklist:
Summary by CodeRabbit
New Features
Bug Fixes
Tests