Skip to content

feat(ripple): dedicated XRP Destination Tag field (+ X-addr autofill, RequireDest gate) - #5247

Merged
johnnyluo merged 7 commits into
mainfrom
johnny/5184_xrp_destination_tag
Jul 12, 2026
Merged

johnnyluo merged 7 commits into
mainfrom
johnny/5184_xrp_destination_tag

Conversation

@johnnyluo

@johnnyluo johnnyluo commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds first-class XRPL Destination Tag support to the XRP send flow, mirroring the iOS combo design (vultisig-ios#4749).

  • First-class carrier: syncs the commondata submodule to include RippleSpecific.destination_tag (already merged on commondata main, PR Added error dialog when transaction info is incorrect on SendScreen #95). The tag rides that proto field, independent of the free-text memo.
  • Send form: a collapsible Destination Tag field under Memo for XRP (numeric, optional). Auto-expands + locks when an X-address paste autofills it.
  • X-address (XLS-5d): hand-rolled codec (WalletCore exposes none on JNI) — paste an X… address to normalize to the classic r… address and autofill the embedded tag; a tag-0 flag-1 X-address is rejected outright.
  • RequireDest gate: blocks a tagless send to an account with lsfRequireDestTag set (fails closed on lookup error, matching the existing reserve check).
  • Signing (RippleHelper): prefers the tag field; supports tag+memo via rawJSON DestinationTag+Memos; legacy numeric-memo→tag and swap text-memo→Memos paths preserved.
  • Verify (initiator and join-device — both via the shared TransactionToUiModelMapper + VerifySendScreen): shows a Destination Tag row from the proto field alongside Memo, when non-empty.
  • New strings localized across all 10 locales.

Issue

Closes #5184

Design note / tradeoff

The tag is carried only in the destination_tag proto field (no memo dual-write — impossible now that memo carries independent free-text). A keysign ceremony with a device on a pre-destination_tag build will fail to sign a tagged XRP send rather than silently drop the tag — fail-safe (no fund loss, no wrong tx), but all devices must update. The tag+memo combo path likewise requires all co-signers on this build.

Test Plan

  • RippleDestinationTagTest — canonical parser + X-address codec vectors (with/without tag, uint32-max, testnet reject, tag-0 reject, corrupt checksum)
  • AddressManagerTest — X-address autofill/lock, no-tag leaves editable, clearing releases lock and drops derived tag
  • Unit tests: mapper / DefaultSendStrategy / ChainValidationService / VerifyTransaction green
  • ./gradlew assembleDebug succeeds
  • ./gradlew lint (running)
  • Manual: send XRP with a tag → confirm DestinationTag on-chain; paste X-address → autofill/lock; tagless send to a RequireDest account → blocked; verify tag shows on both initiator and joining-device Verify screens

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

On chain test tx link:
https://xrpscan.com/tx/5624D7D4CDC445492C84DF38384196CA67AD4261CB93499B19CEAD408D7950C9
https://xrpscan.com/tx/7A0E125D5BFA1885019E861A3AB4BF7B95EFA9AAABE0B1A9D81B2CAC86D8E94D
https://xrpscan.com/tx/72DD773D9216731A204BA97AF5E6C348030354711A41CDE09BFF919A1193DAF1

Summary by CodeRabbit

  • New Features
    • Added an XRP-only “destination tag” field to the send flow (numeric, optional hinting) with XRP-specific validation.
    • Decodes destination tags from pasted XRP X-addresses, normalizes the classic address, and auto-locks the tag when derived.
    • Displays the destination tag as a dedicated row during send verification, suppressing duplicate display when it mirrors the memo.
  • Bug Fixes
    • Prevents XRP sends when a required destination tag is missing or invalid.
    • Updates XRP fee calculation to remove an unintended destination activation reserve component.
  • Localization
    • Added destination-tag UI text and XRP destination-tag error messages across supported languages.

… RequireDest gate)

Adds first-class XRPL destination-tag support to the XRP send flow, mirroring
the iOS combo design (vultisig-ios#4749).

- Sync commondata submodule to include RippleSpecific.destination_tag; carry
  the tag in that first-class proto field (independent of the free-text memo).
- Send form: collapsible "Destination Tag" field shown below Memo for XRP,
  numeric, optional; auto-expands + locks when an X-address paste autofills it.
- Hand-rolled XLS-5d X-address codec (WalletCore exposes none on JNI): paste an
  X-address to normalize to the classic r-address and autofill the embedded tag.
- RequireDest gate: block a tagless send to an account with lsfRequireDestTag
  (fails closed on lookup error, matching the reserve check).
- RippleHelper prefers the tag field; supports tag+memo via rawJSON
  DestinationTag+Memos; legacy numeric-memo and swap-memo paths preserved.
- Verify (initiator + join-device) shows a Destination Tag row from the proto
  field, alongside the Memo row.
- Localized new strings across all 10 locales.

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

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e640240f-f401-4b70-ad99-6331c7d44766

📥 Commits

Reviewing files that changed from the base of the PR and between e4d56fa and 6fb3799.

📒 Files selected for processing (2)
  • app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt

📝 Walkthrough

Walkthrough

The PR adds dedicated XRP destination-tag state, X-address decoding and autofill, RequireDestTag validation, Ripple payload and payment propagation, fee simplification, localized UI strings, and separate destination-tag display during transaction verification.

Changes

XRP destination tag flow

Layer / File(s) Summary
Ripple data contracts and X-address decoding
data/src/main/kotlin/com/vultisig/wallet/data/..., data/src/test/kotlin/com/vultisig/wallet/data/..., commondata
Ripple payloads and account responses carry destination tags; canonical parsing, X-address decoding, protobuf mapping, and tests are added.
X-address resolution and send state
app/src/main/java/com/vultisig/wallet/ui/models/send/AddressManager.kt, SendFormGraph.kt, app/src/test/java/com/vultisig/wallet/ui/models/send/AddressManagerTest.kt
X-addresses are normalized, embedded tags are autofilled and locked, and replacing the normalized address releases the lock and clears the derived tag.
Destination-tag send UI
app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt, app/src/main/java/com/vultisig/wallet/ui/screens/send/*, app/src/main/res/values*/strings.xml
Native XRP displays a numeric destination-tag field with expansion, lock, paste behavior, previews, and localized labels and errors.
Tag validation and payment construction
app/src/main/java/com/vultisig/wallet/ui/models/send/submit/*, ChainValidationService.kt, data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt
Canonical tags are validated, destination account flags are checked, and tags are included in Ripple payloads and payments alongside memos.
Ripple fee calculation
data/src/main/kotlin/com/vultisig/wallet/data/blockchain/model/Fees.kt, data/src/main/kotlin/com/vultisig/wallet/data/blockchain/xrp/RippleFeeService.kt
Ripple fee models and calculations no longer include destination account activation-reserve amounts.
Transaction verification display
app/src/main/java/com/vultisig/wallet/ui/models/VerifyTransactionViewModel.kt, TransactionToUiModelMapper.kt, VerifySendScreen.kt
Mapped XRP destination tags are rendered as a separate verification row.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SendForm
  participant AddressManager
  participant DefaultSendStrategy
  participant RippleApi
  participant RippleHelpter
  User->>SendForm: Paste X-address or enter tag
  SendForm->>AddressManager: Handle destination input
  AddressManager->>AddressManager: Decode X-address and sync tag state
  User->>DefaultSendStrategy: Submit XRP transaction
  DefaultSendStrategy->>RippleApi: Check destination account flags
  RippleApi-->>DefaultSendStrategy: Return RequireDestTag status
  DefaultSendStrategy->>RippleHelpter: Build signed Payment
  RippleHelpter-->>DefaultSendStrategy: Payment with DestinationTag
Loading

Possibly related PRs

Suggested reviewers: aminsato, rkokhatskyi

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The Ripple fee model/service changes remove account-activation fee handling, which is unrelated to the destination-tag requirements. Split out or justify the fee-related refactor separately, since it is not required for the XRP destination-tag feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Ripple destination-tag UX and validation changes.
Linked Issues check ✅ Passed The PR adds the XRP Destination Tag field, X-address autofill/locking, RequireDest validation, and carries the tag into signed payments.
✨ 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 johnny/5184_xrp_destination_tag

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

When both fields carry the same value (memo == the tag's canonical decimal),
the memo is not a distinct memo — sign it as tag-only instead of a rawJSON
DestinationTag+Memos combo. This keeps a co-signer that predates the
destination_tag field byte-identical: it reads the numeric memo and rebuilds
the same DestinationTag, so the pre-image matches. The echoed memo is also
hidden from the Verify Memo row (it shows in the Destination Tag row).

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
app/src/test/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategyTest.kt (1)

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

Consider adding coverage for the new destination-tag validation paths.

The fixture now wires destinationTagFieldState, but no test in this file exercises the non-canonical-tag rejection (send_error_xrp_invalid_destination_tag) or the validateRippleDestinationTag RequireDestTag gate added in submit(). The existing submit blocks XRP send to an unfunded destination below the reserve test provides a ready template (same WalletCore-JNI skip pattern) to adapt for these new checks.

Also applies to: 499-499

🤖 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
`@app/src/test/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategyTest.kt`
at line 69, The test fixture includes destinationTagFieldState but lacks
coverage for the new destination-tag validation paths. In
DefaultSendStrategyTest, add tests based on the existing unfunded XRP
destination test, preserving its WalletCore-JNI skip pattern, to verify
non-canonical tags return send_error_xrp_invalid_destination_tag and
RequireDestTag from validateRippleDestinationTag blocks submission; configure
destinationTagFieldState and assert the expected failures.
app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt (1)

274-285: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Duplicate fetchAccountsInfo call for the same address.

validateRippleDestinationReserve and validateRippleDestinationTag (ChainValidationService.kt, Lines 213-283) each independently call rippleApi.fetchAccountsInfo(dstAddress). Calling both here means every native XRP send now performs the same RPC twice before submission.

Consider fetching the account info once in DefaultSendStrategy and passing the result into both validators (or merging the two checks into a single validator call) to halve this network round-trip.

🤖 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
`@app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt`
around lines 274 - 285, The XRP validation flow in DefaultSendStrategy currently
triggers duplicate account-info requests through
validateRippleDestinationReserve and validateRippleDestinationTag. Fetch
rippleApi account information once and pass the shared result into both
validators, or introduce a combined validator that performs both checks from one
response; update the corresponding ChainValidationService method signatures and
callers accordingly.
🤖 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 `@app/src/main/java/com/vultisig/wallet/ui/models/send/AddressManager.kt`:
- Around line 164-181: Clear destinationTagFieldState when decoded.tag is null
before unlocking it, while preserving the existing behavior of populating and
locking the field when a tag is present. Update applyXAddress so a tag-less
X-address cannot retain a stale tag from a previously processed address.
- Around line 122-140: Release the stale destination-tag lock when a new invalid
X-address is entered. In the Ripple handling within AddressManager, ensure the
failed RippleDestinationTag.decodeXAddress path applies the same lock-clearing
and lockedClassicAddress reset logic currently used for changed non-X addresses,
while preserving normal invalid-address fallthrough.

In `@app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormGraph.kt`:
- Line 195: Update AddressManager.applyXAddress() so that when decoded.tag is
null, it both unlocks and clears destinationTagFieldState, preventing a prior
tag from persisting; add a regression test covering replacement of a tagged
X-address with an untagged X-address and verifying the field is empty.

In
`@data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt`:
- Around line 103-147: In buildPaymentRawJson, convert the ULong values sequence
and lastLedgerSequence to Long before inserting them into txJson, so JSONObject
serializes Sequence and LastLedgerSequence as numeric JSON scalars. Keep the
remaining payload construction unchanged.

In
`@data/src/test/kotlin/com/vultisig/wallet/data/chains/helpers/RippleDestinationTagTest.kt`:
- Around line 27-46: Replace the non-null assertions in the `decode X-address
canonical vectors` test with `requireNotNull(...)` or nullable-safe assertions.
Apply this to each `RippleDestinationTag.decodeXAddress` result before accessing
`classicAddress` and `tag`, so malformed fixtures produce a clear test failure
instead of an NPE.

---

Nitpick comments:
In
`@app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt`:
- Around line 274-285: The XRP validation flow in DefaultSendStrategy currently
triggers duplicate account-info requests through
validateRippleDestinationReserve and validateRippleDestinationTag. Fetch
rippleApi account information once and pass the shared result into both
validators, or introduce a combined validator that performs both checks from one
response; update the corresponding ChainValidationService method signatures and
callers accordingly.

In
`@app/src/test/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategyTest.kt`:
- Line 69: The test fixture includes destinationTagFieldState but lacks coverage
for the new destination-tag validation paths. In DefaultSendStrategyTest, add
tests based on the existing unfunded XRP destination test, preserving its
WalletCore-JNI skip pattern, to verify non-canonical tags return
send_error_xrp_invalid_destination_tag and RequireDestTag from
validateRippleDestinationTag blocks submission; configure
destinationTagFieldState and assert the expected failures.
🪄 Autofix (Beta)

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

Run ID: f2daa4e1-4ff3-4643-92ef-e4482b0e95d7

📥 Commits

Reviewing files that changed from the base of the PR and between ccfa1fd and aae99a7.

📒 Files selected for processing (33)
  • app/src/main/java/com/vultisig/wallet/ui/models/VerifyTransactionViewModel.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/mappers/TransactionToUiModelMapper.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/AddressManager.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/ChainValidationService.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormGraph.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormUiModel.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/SendFormViewModel.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategy.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/send/submit/SendStrategyFactory.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/send/SendFormAmountSection.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/send/SendFormScreen.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/send/VerifySendScreen.kt
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-hr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-nl/strings.xml
  • app/src/main/res/values-pt/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/com/vultisig/wallet/ui/models/send/AddressManagerTest.kt
  • app/src/test/java/com/vultisig/wallet/ui/models/send/submit/DefaultSendStrategyTest.kt
  • commondata
  • data/src/main/kotlin/com/vultisig/wallet/data/api/RippleApi.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleDestinationTag.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/mappers/KeysignPayloadProtoMapper.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/mappers/PayloadToProtoMapper.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/models/payload/BlockChainSpecfic.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/repositories/BlockChainSpecificRepository.kt
  • data/src/test/kotlin/com/vultisig/wallet/data/chains/helpers/RippleDestinationTagTest.kt

johnnyluo and others added 3 commits July 10, 2026 10:49
The XRP fee service folded the destination account-activation reserve (~1 XRP)
into the fee amount, so an unactivated destination showed a "network fee" of
1.00002 XRP — and worse, that inflated amount flowed into the XRPL Fee field
(burning the reserve) and the max-amount math.

The reserve is not a fee: it is part of the amount sent to the destination and
is already enforced as an amount constraint by validateRippleDestinationReserve.
The fee now reports the network fee only; the dead accountActivationFee field
and its redundant account probe are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AddressManager: drop a stale X-address-derived destination tag when an invalid
  X-address is entered or a tagged X-address is replaced with an untagged one,
  so a tag can't ride onto a different destination (a hand-typed tag is kept).
- RippleHelper: serialize Sequence/LastLedgerSequence as Long — org.json can't
  wrap a Kotlin ULong value class, which would drop them from the rawJSON.
- Tests: requireNotNull instead of !! ; add tagged->untagged X-address regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/RippleHelpter.kt Outdated
- RippleHelper: parse the legacy memo-as-tag carrier with the canonical parser
  (rejects 0, leading zeros, out-of-uint32) instead of memo.toLongOrNull(), so
  a stray numeric memo can't be silently reinterpreted or overflow a tag (#6).
- DefaultSendStrategy: dual-write the tag into the memo when a tag is set and no
  distinct memo, so a not-yet-updated co-signer rebuilds the same DestinationTag
  (byte-identical sighash), mirroring iOS dualWritingRippleTag (#8). RequireDest
  validation also treats a canonical numeric memo as a present tag (#10).
- TransactionToUiModelMapper: surface the tag whether it rides in the proto
  field or the canonical-numeric memo, and suppress that memo when it merely
  echoes the tag so it isn't shown twice on Verify (#7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DefaultSendStrategy: treat whitespace-only memo as absent (isNotBlank),
  aligning the dual-write gate with RippleHelper so a blank memo can't
  suppress the tag dual-write and leave an old co-signer untagged.
- RippleHelper: correct the comment — iOS rejects a bare "0" memo, we
  intentionally keep it as free text rather than claim exact iOS parity.

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

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

LGTM

@johnnyluo
johnnyluo added this pull request to the merge queue Jul 12, 2026
Merged via the queue into main with commit 0d7c97c Jul 12, 2026
4 checks passed
@johnnyluo
johnnyluo deleted the johnny/5184_xrp_destination_tag branch July 12, 2026 10:08
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.

feat(ripple): dedicated Destination Tag field under Memo in XRP send (+ X-addr autofill, RequireDest warning)

2 participants