Skip to content

feat: support BIP21 op_return and callback URI parameters - #771

Open
sag333ar wants to merge 3 commits into
dashpay:masterfrom
sag333ar:sagar
Open

sag333ar wants to merge 3 commits into
dashpay:masterfrom
sag333ar:sagar

Conversation

@sag333ar

Copy link
Copy Markdown

Feature implemented

Surfaces the BIP21 callback=<https-url> parameter (added in
DashSync) to the iOS app's UI layer. When a user scans/pays a URI
that carried a callback=, the wallet opens that URL in an
SFSafariViewController immediately after the send completes so
merchants can continue their checkout flow (THORChain swaps,
hosted gift-card flows, etc.) without leaving the wallet app.

Depends on the corresponding DashSync change which:

  • Parses and validates callback=<https-url> from BIP21 URIs.
  • Adds the openInAppBrowserWithURL: optional delegate method
    on DWPaymentProcessorDelegate that's invoked from
    handleCallbackSchemeIfNeeded: after a successful broadcast.

(Link to DashSync PR: )

Also hardens an unrelated force-unwrap in
ProvideAmountViewController that was the actual crash site
during development testing — described under "What was done?".

What was done?

PaymentController.swift

  • New paymentProcessor(_:openInAppBrowserWith:) delegate
    implementation: after the confirm-screen dismiss + nav-pop
    animations settle (~0.4 s), presents an SFSafariViewController
    pointing at the callback URL.
  • Built via the existing SFSafariViewController.dw_controller(with:)
    factory for consistent tint with the rest of the app.
  • Presented with modalPresentationStyle = .overFullScreen and
    modalPresentationCapturesStatusBarAppearance = true — matches
    the pattern already used by TXDetailViewController for its
    "View on Explorer" button.
  • Presents from the external presentation context
    (presentationContextProvider), not from
    provideAmountViewController. The latter is popped from the
    nav stack on send-success, so presenting on it would reveal a
    detached controller on dismiss and crash inside its
    viewWillAppear. Using the stable anchor + .overFullScreen
    keeps the underlying view in the hierarchy throughout.

DWPaymentProcessor.h / .m

  • New optional delegate method
    paymentProcessor:openInAppBrowserWithURL: on
    DWPaymentProcessorDelegate.
  • handleCallbackSchemeIfNeeded:address:tx: now also reads
    self.paymentInput.request.callback (the new NSURL
    property added in DashSync) and, if present and the delegate
    responds, dispatches the callback URL to the host.
  • The DashSync layer is responsible for validation (https scheme
    enforcement happens at parse time on the DashSync side), so this
    layer trusts the URL.

ProvideAmountViewController.swift

  • Replaces the navigationController! force-unwrap in
    viewWillAppear(_:) with a guard let early return.
    viewWillAppear can fire after this VC has been popped from
    the navigation stack (e.g. when a modal presented from it is
    dismissed), at which point navigationController is nil and
    the force-unwrap crashes. Defense-in-depth so this category of
    dismiss-after-pop crash cannot recur from other code paths.

How Has This Been Tested?

Manual end-to-end testing on Dash mainnet using a TestFlight-style
local build (iOS 26.5, Xcode 16) against Dash Core:23.1.2
peers:

  • Scanned a BIP21 URI of the form
    dash:<addr>?amount=0.001&op_return=74657374&callback=https%3A%2F%2Fgoogle.com.
    After the send completes (1 InstantSend lock), SFSafariViewController
    opens to https://google.com and loads cleanly. Tapping Done
    dismisses without crashing. Reproduced multiple times across
    different recipient addresses.
  • URI without a callback= parameter — confirms no Safari sheet
    is presented (delegate method simply isn't invoked).
  • Regression-tested the original crash: tapping Done in Safari
    no longer triggers the
    ProvideAmountViewController.viewWillAppear force-unwrap path.
  • All other payment paths (no-callback URIs, BIP70 invoices,
    sweeps) continue to work as before — the new delegate is
    @optional, so behaviour for clients that don't implement it
    is unchanged.

No automated UI / unit-test coverage was added in this PR.

Breaking Changes

None. The new paymentProcessor:openInAppBrowserWithURL: delegate
method is declared @optional, and the
handleCallbackSchemeIfNeeded: callsite uses
respondsToSelector: before invoking it. Existing consumers of
DWPaymentProcessorDelegate continue to compile and behave
identically.

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 made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

sag333ar and others added 2 commits May 7, 2026 15:15
Wires DashSync's `openInAppBrowserWithURL:` payment-processor
delegate into the iOS app: after a successful send for a BIP21
URI that carried `callback=<https-url>`, the wallet opens the URL
in an `SFSafariViewController` so merchants can complete their
checkout flow.

The Safari sheet is presented from the external presentation
context (not the popped `ProvideAmountViewController`) with
`.overFullScreen` modal style. This keeps the underlying view in
the hierarchy on dismiss and avoids revealing a detached
controller that would otherwise crash inside its `viewWillAppear`.
Mirrors the pattern already used by `TxDetailViewController` for
the block-explorer button.

Also adds a defensive guard around `ProvideAmountViewController`'s
`navigationController!` force-unwrap so this category of
dismiss-after-pop crash cannot recur from any other code path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sag333ar, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 37 minutes and 27 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74ff6d04-09a6-493c-bc05-41d43df13bf5

📥 Commits

Reviewing files that changed from the base of the PR and between 6aaecc1 and c4a5b0d.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .gitignore
  • DashWallet/Sources/Models/Explore Dash/ExploreDash.swift
  • DashWallet/Sources/UI/Main/MainTabbarController.swift
  • DashWallet/Sources/UI/Payment Controller/Enter Amount/ProvideAmountViewController.swift
  • DashWallet/Sources/UI/Payment Controller/PaymentController.swift
  • DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.h
  • DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m
  • Gemfile
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@HashEngineering

Copy link
Copy Markdown
Contributor

@sag333ar
Can you show the place in BIP21 that discusses callback? Or is it a different standard, such as BIP321? Which services or apps provide a callback parameter that this app needs to handle?

@bfoss765 bfoss765 changed the title feat(payment): present in-app browser for BIP21 callback URLs feat: BIP21 URI - Add OP-RETURN Jun 30, 2026
@bfoss765 bfoss765 changed the title feat: BIP21 URI - Add OP-RETURN feat: support BIP21 op_return and callback URI parameters Jun 30, 2026
@thepastaclaw

thepastaclaw commented Aug 31, 2026

Copy link
Copy Markdown

⛔ Final review complete — 4 blocking finding(s) (commit c4a5b0d)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Final validation — Sol-only technical fallback

At exact head c4a5b0d, the callback feature is not merge-ready: it references a DashSync API absent from both the recorded dependency commit and current DashSync master, loses callback metadata on scanned BIP70 requests, and races the payment-flow transitions. The callback is also neither transaction-bound nor authenticated as belonging to the confirmed payee, allowing the wrong or an attacker-controlled destination to open after payment.

Source: reviewer 1: gpt-5.6-sol (agent: sol-fallback-reviewer, role: general); reviewer 2: gpt-5.6-sol (agent: sol-fallback-reviewer, role: security-auditor); reviewer 3: gpt-5.6-sol (agent: sol-fallback-reviewer, role: ffi-engineer); final verifier: gpt-5.6-sol (agent: sol-verifier, role: final-verifier)

One or more required Phase-1 GLM Flash lanes remained technically unusable after the bounded exact-model retry. Their evidence was discarded as authoritative, and the complete selected role cohort was rerun fresh on exact gpt-5.6-sol before this fresh Sol verifier produced the final decision. No additional Phase-2 reviewer pass ran.

Review provenance

  • Phase 1 GLM evidence: technically unusable after bounded retry; discarded from the decision
  • GLM failure attempts: codex-ffi-engineer-b91b409f64454a609c030cef1de031c4 (failed), codex-ffi-engineer-1610beae09504ee58a188fc74a5caaab (failed), codex-general-87b0672bd2a34a60b821cb4a2a329100 (failed), codex-general-cfb173673cac4d97901fda698d55ae7a (failed), codex-security-auditor-3a99d8a4ecad4a35a3247c9b8dd6ff9b (failed), codex-security-auditor-9b3869c916e44f98a3803b34f8341269 (failed)
  • Sol-only fallback reasons: launch_transport_or_nonzero_exit, launch_transport_or_nonzero_exit, launch_transport_or_nonzero_exit
  • Sol-only fallback reviewers: gpt-5.6-sol — general (completed); agent sol-fallback-reviewer, gpt-5.6-sol — security-auditor (completed); agent sol-fallback-reviewer, gpt-5.6-sol — ffi-engineer (completed); agent sol-fallback-reviewer
  • Fresh verifier (Sol): gpt-5.6-sol — final-verifier; agent sol-verifier
  • Additional Phase 2 pass: not run; the Sol-only fallback is final

🔴 4 blocking

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 `DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m`:
- [BLOCKING] DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m:706: The required DashSync callback API is not available to a clean build
  This line accesses `DSPaymentRequest.callback`, but the repository records DashSync commit `13fc5bea98a6c0f1b80c4e2c146ff5bf536baf76`, whose `DSPaymentRequest.h` declares `callbackScheme` but no `callback` property. Current DashSync master (`d3c1497a7b4323a9f21ce3f8f0c5c4e262cab4ca`) also lacks it; the property exists only on the still-separate DashSync PR #608 head (`aca6889b43961db91ffc8bcc603ebeba5b3f935e`). Because the Podfile uses an unpinned `../DashSync/` checkout and this PR updates neither that dependency nor `DashSyncCurrentCommit`, the exact app head cannot compile against its recorded or normal upstream dependency. Land and record the corresponding DashSync change, or otherwise make the dependency reproducible, before merging this app change.
- [BLOCKING] DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m:705-708: Callback metadata is neither preserved nor bound to the completed payment
  Reading the callback from mutable `self.paymentInput.request` creates two concrete failures. First, `DWQRScanModel.m` eagerly fetches a scanned URI's `r=` request and, on success, places only the resulting `protocolRequest` in the new `DWPaymentInput`; the original `DSPaymentRequest` carrying `callback` is discarded, so this expression is nil and the advertised scanned BIP70 flow never opens its callback. Second, `PaymentController.performPayment(with:)` resets and reuses the processor, including the Home controller used by incoming payment URLs. If payment A still has a publication or BIP70 relay completion pending when payment B replaces `paymentInput`, A's completion can read and launch B's callback even though B was not the completed transaction. Preserve callback metadata when converting scanned requests and snapshot it with the protocol request/transaction when starting each send; completion handlers must use that immutable snapshot rather than current processor state.
- [BLOCKING] DashWallet/Sources/UI/Payments/PaymentModels/DWPaymentProcessor.m:705-708: Untrusted callback is opened as a trusted post-payment continuation
  The callback comes from an untrusted QR code or app URL, yet it is opened automatically after payment without displaying or approving its destination and without proving that it belongs to the confirmed payee. DashSync PR #608 only checks that the parsed URL's scheme is `https`; TLS authenticates the callback host, not its relationship to the payment. An attacker can therefore wrap a legitimate, PKI-authenticated BIP70 `r=` request in an outer BIP21 URI with an attacker-controlled callback: the confirmation authenticates the real merchant, but the post-payment browser opens the attacker's site in that merchant checkout context. Bind the callback to authenticated payment metadata, or show its host and require explicit approval before navigation.

In `DashWallet/Sources/UI/Payment Controller/PaymentController.swift`:
- [BLOCKING] DashWallet/Sources/UI/Payment Controller/PaymentController.swift:249-264: The fixed delay races the payment screen transitions
  The 0.4-second timer begins immediately after `didSend` starts dismissing `ConfirmPaymentViewController`. That dismissal's completion then starts an animated navigation pop and asynchronously reports success; the standard tab flow subsequently dismisses the payments modal and presents `SuccessTxDetailViewController`. A fixed delay does not wait for this chain, and `stableAnchor.view.window != nil` remains true while the hierarchy is being dismissed. Safari can therefore be presented from a transitioning controller, be ignored by UIKit, or compete with the success controller. Chain callback presentation from the actual payment-container dismissal/navigation completion, or coordinate it through the active transition coordinator, instead of inferring completion from elapsed time.

}

// BIP21 `callback=<https-url>` — already https-validated at parse time.
NSURL *inAppCallback = self.paymentInput.request.callback;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: The required DashSync callback API is not available to a clean build

This line accesses DSPaymentRequest.callback, but the repository records DashSync commit 13fc5bea98a6c0f1b80c4e2c146ff5bf536baf76, whose DSPaymentRequest.h declares callbackScheme but no callback property. Current DashSync master (d3c1497a7b4323a9f21ce3f8f0c5c4e262cab4ca) also lacks it; the property exists only on the still-separate DashSync PR #608 head (aca6889b43961db91ffc8bcc603ebeba5b3f935e). Because the Podfile uses an unpinned ../DashSync/ checkout and this PR updates neither that dependency nor DashSyncCurrentCommit, the exact app head cannot compile against its recorded or normal upstream dependency. Land and record the corresponding DashSync change, or otherwise make the dependency reproducible, before merging this app change.

source: ['claude']

Comment on lines +705 to +708
// BIP21 `callback=<https-url>` — already https-validated at parse time.
NSURL *inAppCallback = self.paymentInput.request.callback;
if (inAppCallback && [self.delegate respondsToSelector:@selector(paymentProcessor:openInAppBrowserWithURL:)]) {
[self.delegate paymentProcessor:self openInAppBrowserWithURL:inAppCallback];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Callback metadata is neither preserved nor bound to the completed payment

Reading the callback from mutable self.paymentInput.request creates two concrete failures. First, DWQRScanModel.m eagerly fetches a scanned URI's r= request and, on success, places only the resulting protocolRequest in the new DWPaymentInput; the original DSPaymentRequest carrying callback is discarded, so this expression is nil and the advertised scanned BIP70 flow never opens its callback. Second, PaymentController.performPayment(with:) resets and reuses the processor, including the Home controller used by incoming payment URLs. If payment A still has a publication or BIP70 relay completion pending when payment B replaces paymentInput, A's completion can read and launch B's callback even though B was not the completed transaction. Preserve callback metadata when converting scanned requests and snapshot it with the protocol request/transaction when starting each send; completion handlers must use that immutable snapshot rather than current processor state.

source: ['claude']

Comment on lines +705 to +708
// BIP21 `callback=<https-url>` — already https-validated at parse time.
NSURL *inAppCallback = self.paymentInput.request.callback;
if (inAppCallback && [self.delegate respondsToSelector:@selector(paymentProcessor:openInAppBrowserWithURL:)]) {
[self.delegate paymentProcessor:self openInAppBrowserWithURL:inAppCallback];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Untrusted callback is opened as a trusted post-payment continuation

The callback comes from an untrusted QR code or app URL, yet it is opened automatically after payment without displaying or approving its destination and without proving that it belongs to the confirmed payee. DashSync PR #608 only checks that the parsed URL's scheme is https; TLS authenticates the callback host, not its relationship to the payment. An attacker can therefore wrap a legitimate, PKI-authenticated BIP70 r= request in an outer BIP21 URI with an attacker-controlled callback: the confirmation authenticates the real merchant, but the post-payment browser opens the attacker's site in that merchant checkout context. Bind the callback to authenticated payment metadata, or show its host and require explicit approval before navigation.

source: ['claude']

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