Skip to content

feat(dash-dex): finish buy swap flow#1504

Open
HashEngineering wants to merge 100 commits into
feat/dash-dexfrom
feat/dash-dex-buy-swap-two
Open

feat(dash-dex): finish buy swap flow#1504
HashEngineering wants to merge 100 commits into
feat/dash-dexfrom
feat/dash-dex-buy-swap-two

Conversation

@HashEngineering

@HashEngineering HashEngineering commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Related PR's and Dependencies

Screenshots / Videos

How Has This Been Tested?

  • QA (Mobile Team)

Checklist:

  • I have performed a self-review of my own code and added comments where necessary
  • I have added or updated relevant unit/integration/functional/e2e tests

Summary by CodeRabbit

  • New Features

    • Added a new Dash DEX shortcut and home-screen entry point for quicker access.
    • Introduced a new buy/sell DEX flow with dedicated amount, refund address, and receive screens.
    • Added clearer address entry and balance controls, plus localized balance visibility labels.
  • Bug Fixes

    • Improved address validation to better reject invalid crypto addresses.
    • Refined search/no-results messaging and fixed layout sizing in amount selection.
    • Updated receive screens to show QR, expiry guidance, and copy actions more reliably.

HashEngineering and others added 2 commits June 29, 2026 14:26
…t fund loss

commitSwapTransaction chose the memo-less deposit path purely on a 'MAYA'
substring match of the joined provider string (routeName defaults to "").
If SwapKit returned a Maya-settling best route with an empty/absent providers
list — or a provider not literally named 'MAYA' — the swap sent DASH to the
Asgard vault with no OP_RETURN memo, which cannot be matched to a swap and
strands the funds.

Make the presence of a memo the authoritative signal: route through the Maya
OP_RETURN builder whenever the route is classified Maya OR a memo is present.
Maya routes always carry a memo; NEAR deposits never do, so legitimate NEAR
swaps are unaffected, and any mislabelled memo-bearing route now fails loudly
(80-byte guard) instead of losing funds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…switch

Wire the SwapKitApiAggregator to the per-direction snapshot keys (the BUY key
was added previously): hydrateFromSnapshot/persistSnapshot now read and write
the cache for the active swapDirection, so a leaner BUY refresh never clobbers
the richer SELL snapshot (preferred routes + Maya halt status) and vice versa.
reset() clears both keys.

On a direction switch, drop the now-wrong in-memory list and re-hydrate the new
direction's snapshot. Because observePoolList (which normally triggers the
background refresh) is not re-invoked on a direction change, setSwapDirection
now kicks the refresh itself using the last fiat rate seen by observePoolList —
otherwise the buy coin list would show only a spinner with an empty buy cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HashEngineering HashEngineering self-assigned this Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Wait, I made an XML structural error above. Let me restate the artifact correctly below (ignore the malformed block above).

Walkthrough

This PR adds a BUY direction to the Maya/SwapKit DEX integration. It introduces a BuyOrder model, SwapDirection enum, and extends SwapProvider/SwapKitApiAggregator with buy-order creation/validation and direction-aware caching. New Compose screens (enter amount, refund address, receive) and a home-screen shortcut are added, alongside currency URI generation changes, shared UI components, and navigation wiring.

Changes

DashDEX Buy Flow

Layer / File(s) Summary
Shared validation, UI components, and analytics constants
common/.../ServiceName.kt, AnalyticsConstants.kt, AddressParser.kt, AddressField.kt, EnterAmount.kt, TopIntroSend.kt, strings.xml
Adds ServiceName.Swapkit, SHORTCUT_DASH_DEX, strengthens AddressParser.exactMatch, adds the AddressField composable, adjusts EnterAmount picker sizing, restyles TopIntroSend, and adds balance toggle strings.
BuyOrder/SwapDirection contracts
model/BuyOrder.kt, utils/SwapDirection.kt, utils/SwapBackend.kt, api/SwapProvider.kt, api/DispatchingSwapProvider.kt
Adds BuyOrder, SwapDirection, SwapBackend.supportsBuy, and extends provider interfaces with setSwapDirection, createBuyOrder, validateBuyOrder.
SwapKitApiAggregator BUY implementation
swapkit/SwapKitApiAggregator.kt, proguard-rules.pro, utils/MayaConfig.kt
Implements direction-aware caching/hydration, classification gating, NEAR-pinned BUY routing, deposit-address derivation, memo-aware transaction dispatch, and per-direction snapshot persistence.
Per-currency payment URI generation and tests
payments/MayaCryptoCurrency.kt, test/.../MayaCryptoCurrencyTest.kt
Reworks payment URI generation across chains, adds networkName/registeredCurrencies, and strengthens parser/duplicate-key tests.
DEX Enter Amount flow
ui/DEXEnterAmountFragment.kt, DEXEnterAmountScreen.kt, DEXEnterAmountViewModel.kt
Adds the amount-entry screen with currency switching and quote-only validation.
DEX Refund Address flow
ui/DEXRefundAddressFragment.kt, DEXRefundAddressScreen.kt, DEXRefundAddressViewModel.kt
Adds the refund-address screen with scan/paste and address validation.
DEX Receive flow
ui/DEXReceiveFragment.kt, DEXReceiveScreen.kt, DEXReceiveViewModel.kt
Adds the deposit-address/QR screen sourced from createBuyOrder.
Portal, picker, and view model wiring
ui/MayaCryptoCurrencyPickerFragment.kt, MayaCryptoCurrencyPickerScreen.kt, MayaPortalFragment.kt, MayaPortalScreen.kt, MayaViewModel.kt, convert_currency/ConvertViewFragment.kt
Wires buy/sell direction through portal menu, picker routing, and coin filtering.
DEX navigation graph, drawables, strings
res/navigation/nav_maya.xml, res/drawable/ic_dash_dex_*.xml, res/values/strings-maya.xml
Wires new fragments into the nav graph and adds DEX icons/strings.
Home screen shortcut integration
wallet/res/drawable/ic_shortcut_dash_dex.xml, nav_home.xml, strings-extra.xml, MainViewModel.kt, WalletFragment.kt, ShortcutOption.kt
Adds the DASH_DEX shortcut, sets SwapKit backend, and navigates to the Maya flow.
Protocol documentation
integrations/maya/SWAPKIT_PROTOCOL.md, NEAR_INTENTS_PROTOCOL.md
Fixes heading and documents deposit-address expiry behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • abaranouski

🐇 A rabbit hops where DASH meets DEX,
Buy and sell now share one nexus.
Deposit address, QR aglow,
Refund paths now safely flow.
Hop hop hooray, the swap's complete! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: completing the DashDEX buy swap flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dash-dex-buy-swap-two

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.

@HashEngineering

Copy link
Copy Markdown
Collaborator Author

@coderabbitai - review - hopefully not too many commits, most don't have any code in this PR

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@HashEngineering Sounds good, I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 20

🧹 Nitpick comments (1)
integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt (1)

158-165: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Fold swapDirection into the ViewModel’s UI state instead of adding another standalone flow.

This change introduces one more parallel state stream in MayaViewModel, which pushes the screen farther away from the repo’s single-UIState pattern. Please carry the selected direction inside the existing UI state model rather than exposing a separate swapDirection flow. As per coding guidelines, ViewModels should use a single UIState data class rather than multiple separate flows.

🤖 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/MayaViewModel.kt`
around lines 158 - 165, The new standalone swapDirection flow in MayaViewModel
breaks the single-UIState pattern; move the selected direction into the existing
UI state data class and expose it through the ViewModel’s current state stream
instead of a separate StateFlow. Update the state holder and any
reducers/emitters that currently read or write _swapDirection in MayaViewModel
so the BUY/SELL selection is stored and observed alongside the other UI fields.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@common/src/main/java/org/dash/wallet/common/ui/components/AddressField.kt`:
- Around line 214-216: The trailing Icon in AddressField’s clickable trailing
action is currently unlabeled for accessibility because contentDescription is
always null. Update the trailing action logic in AddressField so each trailing
state (scan and clear) provides a meaningful spoken label via the Icon’s
contentDescription, using the existing trailing state/painter selection code to
differentiate the labels.
- Around line 157-165: The AddressField interaction handlers ignore the enabled
state, so long-press paste and the trailing scan/clear actions can still run
when the field is disabled. Update the AddressField composable to gate every
interactive path on enabled, including the pointerInput/detectTapGestures
long-press block and the trailing action callback logic, so they become no-ops
unless enabled is true.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt`:
- Around line 157-162: The URI shape emitted by
MayaCryptoCurrency.getPaymentRequestURI is no longer compatible with the current
EthereumPaymentIntentParser/createPaymentIntent flow, which will treat the
EIP-681 suffix as part of the address. Either update EthereumPaymentIntentParser
to strip EIP-681 components like @<chain> and ?value= before handing off to
createPaymentIntent, or keep MayaCryptoCurrency (and the other affected payment
URI generator) emitting the simpler scheme-only address format until parser
support is added.
- Around line 85-97: The Bech32 fallback in
MayaCryptoCurrency.getPaymentRequestURI is building an invalid BIP-21 payment
URI; update the fallback path in this method so the amount parameter is appended
as the first query string using the correct URI separator, and keep the existing
AddressFormatException/SegwitAddress.fromBech32 handling intact while fixing the
returned bitcoin URI format.
- Around line 615-618: The `getPaymentRequestURI` flow in `MayaCryptoCurrency`
is building the Solana `spl-token` value from `asset.substring(4)`, which leaves
the `SYMBOL-` prefix in place. Update the token extraction logic so it returns
only the mint address for Solana assets (for example by splitting on the first
dash and using the part after it), and keep the `tokenContract` value used by
`getPaymentRequestURI` as the clean mint address.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/swapkit/SwapKitApiAggregator.kt`:
- Around line 549-568: The fallback in markMayaInfo is turning every pool into
mayaOnly when isClassificationAvailable() is false, which makes the BUY picker
empty during a transient /tokens miss. Update SwapKitApiAggregator so the
first-launch / classification-failure path does not publish fail-closed
metadata: preserve the last known snapshot/classification state, or propagate a
retryable error before updating pool fields. Keep the logic around markMayaInfo
and isClassificationAvailable aligned so BUY filtering on !mayaOnly does not
hide otherwise valid routes that requestBuyRoute() can still validate.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountFragment.kt`:
- Around line 67-73: The shared DEXEnterAmountViewModel is being reinitialized
every time DEXEnterAmountFragment.onCreateView() runs because setArguments()
resets amount and validation state on each recreation. Update the fragment so
setArguments() is only called once when the view model is first seeded, or add a
guard that skips it if the model already has arguments/entered state, preserving
in-progress amount across rotation and back navigation.
- Around line 79-87: The onValidationPassed observer is being registered too
early, which can access viewLifecycleOwner before the fragment’s view lifecycle
is ready and crash on first render. Move the observer setup from the current
onCreateView flow into DEXEnterAmountFragment.onViewCreated(), keeping the
existing navigation logic and args.asset/args.currency usage unchanged so it is
registered only after the view lifecycle owner is initialized.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountViewModel.kt`:
- Around line 159-170: The amount validation flow is using stale in-flight
results because `validationJob` is not canceled when `onKeyInput`, currency
changes, or `onContinueClick`-adjacent state updates modify the tracked
`Amount`. Update `DEXEnterAmountViewModel` so any input/currency change cancels
the active `validationJob`, and guard the success path in the validation
response handling by checking that the returned `sellAmount` still matches the
current state before navigating or setting `isAmountValid`.
- Around line 120-125: The fallback in DEXEnterAmountViewModel’s exchange-rate
setup is fabricating a 1:1 price when DEXEnterAmountFragment passes 0 for
missing pool data, which makes later sellAmount calculations look valid. Update
the rate handling in the view model so zero/absent dashPriceFiat or
assetPriceFiat is treated as unavailable state instead of substituting
BigDecimal.ONE, and make the downstream conversion/validation path in
DEXEnterAmountViewModel reject or disable calculations until real pool prices
are present.
- Around line 99-101: The navigation trigger is currently split between
`onValidationPassed` and the `uiState` StateFlow, which introduces a second
event channel in `DEXEnterAmountViewModel`. Move this one-shot navigation signal
into the existing `UIState` contract and have the Fragment observe it from
`uiState` instead of `SingleLiveEvent<Unit>`, updating the ViewModel logic and
any UI consumers that currently read `onValidationPassed`.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveFragment.kt`:
- Around line 78-80: The info log in DEXReceiveFragment’s logging statement is
exposing a sensitive wallet identifier by including the refund address. Update
the log call in the receive-screen flow to omit args.refundAddress entirely,
keeping only non-sensitive fields like the asset and sell amount, and ensure any
other logging in this fragment follows the same privacy-safe pattern.
- Around line 72-82: The DEX receive flow is creating a new buy order every time
the view is recreated because `DEXReceiveFragment.onCreateView()` always calls
`viewModel.loadDepositAddress()`, and `DEXReceiveViewModel.loadDepositAddress()`
immediately triggers `createBuyOrder(...)`. Add a one-time guard in the
fragment/view model flow so the order is only created once per buy session, and
skip повторed calls during rotations or back-stack recreation while still
allowing normal initial loading.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveViewModel.kt`:
- Around line 50-61: The receive state currently drops memo/tag data, so assets
that require a routing identifier can’t be shown or encoded correctly. Update
DEXReceiveUIState and the receive flow in DEXReceiveViewModel/loadDepositAddress
to carry the memo/tag alongside address and uri, then render it in the UI and
pass it into buildUri() so the QR/payment URI includes it when the parser
supports memo-bearing destinations.
- Around line 124-131: The receive-address metadata is being written before the
buy order is actually created in DEXReceiveViewModel’s create-buy flow, which
can persist an incorrect Swapkit income tag if the order fails or is abandoned.
Move the call to transactionMetadataProvider.markAddressWithTaxCategory() so it
runs only in the successful branch after swapProvider.createBuyOrder() returns a
successful result, using the same destinationAddress and existing
TaxCategory.Income/ServiceName.Swapkit values.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressFragment.kt`:
- Around line 108-111: The info log in DEXRefundAddressFragment currently prints
the full refund address and exact entered amounts, which exposes sensitive
transaction metadata in normal logs. Update the log.info call in the
refund-address flow to either omit these values entirely or mask them before
logging, and keep the message limited to non-sensitive context such as the asset
or a generic continuation status.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressViewModel.kt`:
- Around line 62-75: `DEXRefundAddressViewModel.setArguments()` is resetting the
refund address state on every `DEXRefundAddressFragment.onCreateView()` call,
which wipes user input on recreation. Update `setArguments` to only reinitialize
`_uiState` when the incoming asset/currency differs from the current state, and
otherwise preserve `address` and `continueEnabled` while still updating the
display code as needed. Use the existing `asset`, `currencyCode`, and
`errorCurrencyCode` fields in `DEXRefundAddressViewModel` to compare and avoid
clearing state unnecessarily.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerScreen.kt`:
- Around line 169-178: The empty-state branch in MayaCryptoCurrencyPickerScreen
should not treat a stale hidden search query as an active search when offline.
Update the logic around displayItems.isEmpty so it only shows
maya_no_search_results when the search UI is actually available and the query is
user-visible/active; otherwise fall back to maya_no_available_coins even if
searchQuery still contains text. Use the existing query/searchQuery handling in
MayaCryptoCurrencyPickerScreen to gate the message selection.

In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt`:
- Around line 240-248: The BUY route label logic is duplicated and can diverge
between the picker block and getRouteLabelResId(), so route labels may disagree
for the same SwapDirection.BUY case. Extract the BUY decision into a single
helper in MayaViewModel and have both the picker flow and getRouteLabelResId()
call that shared helper, keeping the existing preferred-route logic only for
non-BUY paths.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 654-662: The explicit backend selection in
MainViewModel.setSwapBackend can still be overwritten by
DispatchingSwapProvider’s async restore from init, so make the provider’s
backend initialization deterministic before this helper is used for Dash DEX
navigation. Update DispatchingSwapProvider so the persisted activeBackend
restore cannot race with or clobber a later setBackend call, and ensure the
init-time persistScope.launch ordering preserves the most recent explicit
selection.

---

Nitpick comments:
In
`@integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt`:
- Around line 158-165: The new standalone swapDirection flow in MayaViewModel
breaks the single-UIState pattern; move the selected direction into the existing
UI state data class and expose it through the ViewModel’s current state stream
instead of a separate StateFlow. Update the state holder and any
reducers/emitters that currently read or write _swapDirection in MayaViewModel
so the BUY/SELL selection is stored and observed alongside the other UI fields.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 632e9b5d-baab-42cd-ba8d-ed53b762729a

📥 Commits

Reviewing files that changed from the base of the PR and between c1baf29 and e12ce22.

📒 Files selected for processing (46)
  • common/src/main/java/org/dash/wallet/common/data/ServiceName.kt
  • common/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.kt
  • common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/AddressField.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt
  • common/src/main/res/drawable/ic_warning_triangle.xml
  • common/src/main/res/values/strings.xml
  • integrations/maya/NEAR_INTENTS_PROTOCOL.md
  • integrations/maya/SWAPKIT_PROTOCOL.md
  • integrations/maya/proguard-rules.pro
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/DispatchingSwapProvider.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/SwapProvider.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/BuyOrder.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/swapkit/SwapKitApiAggregator.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountScreen.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountViewModel.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveViewModel.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressScreen.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressViewModel.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerScreen.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalScreen.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/SwapBackend.kt
  • integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/SwapDirection.kt
  • integrations/maya/src/main/res/drawable/ic_dash_dex_buy.xml
  • integrations/maya/src/main/res/drawable/ic_dash_dex_illustration.xml
  • integrations/maya/src/main/res/drawable/ic_dash_dex_sell.xml
  • integrations/maya/src/main/res/navigation/nav_maya.xml
  • integrations/maya/src/main/res/values/strings-maya.xml
  • integrations/maya/src/test/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrencyTest.kt
  • wallet/res/drawable/ic_shortcut_dash_dex.xml
  • wallet/res/navigation/nav_home.xml
  • wallet/res/values/strings-extra.xml
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt
  • wallet/src/de/schildbach/wallet/ui/main/shortcuts/ShortcutOption.kt

Comment on lines +157 to +165
if (onLongPress != null) {
Modifier.pointerInput(Unit) {
detectTapGestures(onLongPress = { onLongPress() })
}
} else {
Modifier
}
)
.padding(start = 20.dp, end = 10.dp),

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Honor enabled for every interaction.

enabled currently only disables the text field. Long-press paste and the trailing scan/clear action still fire, so a disabled AddressField can still mutate state or launch scanning.

Suggested fix
                 .then(
-                    if (onLongPress != null) {
+                    if (enabled && onLongPress != null) {
                         Modifier.pointerInput(Unit) {
                             detectTapGestures(onLongPress = { onLongPress() })
                         }
                     } else {
                         Modifier
@@
                     modifier = Modifier
                         .size(40.dp)
                         .clip(CircleShape)
-                        .clickable(onClick = trailing.second),
+                        .clickable(enabled = enabled, onClick = trailing.second),
                     contentAlignment = Alignment.Center
                 ) {

Also applies to: 206-211

🤖 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 `@common/src/main/java/org/dash/wallet/common/ui/components/AddressField.kt`
around lines 157 - 165, The AddressField interaction handlers ignore the enabled
state, so long-press paste and the trailing scan/clear actions can still run
when the field is disabled. Update the AddressField composable to gate every
interactive path on enabled, including the pointerInput/detectTapGestures
long-press block and the trailing action callback logic, so they become no-ops
unless enabled is true.

Comment on lines +214 to +216
Icon(
painter = painterResource(trailing.first),
contentDescription = null,

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Label the trailing action for accessibility.

This icon is clickable, but contentDescription is always null, so TalkBack exposes an unlabeled control for both the scan and clear states. Please give each state a spoken label.

🤖 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 `@common/src/main/java/org/dash/wallet/common/ui/components/AddressField.kt`
around lines 214 - 216, The trailing Icon in AddressField’s clickable trailing
action is currently unlabeled for accessibility because contentDescription is
always null. Update the trailing action logic in AddressField so each trailing
state (scan and clear) provides a meaningful spoken label via the Icon’s
contentDescription, using the existing trailing state/painter selection code to
differentiate the labels.

Comment on lines +85 to +97
override fun getPaymentRequestURI(address: String, amount: String): String {
return try {
BitcoinURI.convertToBitcoinURI(
Address.fromBase58(BitcoinMainNetParams(), address),
Coin.parseCoin(amount),
null,
null
)
} catch (e: AddressFormatException) {
SegwitAddress.fromBech32(BitcoinMainNetParams(), address)
"bitcoin:$address&amount=$amount"
}
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use ?amount= in the SegWit fallback URI.

Line 95 builds bitcoin:$address&amount=$amount, which is not a valid BIP-21 URI because the first query parameter must start with ?. Any QR/code path that hits the Bech32 fallback will emit a malformed BTC payment URI.

Suggested fix
-            "bitcoin:$address&amount=$amount"
+            "bitcoin:$address?amount=$amount"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun getPaymentRequestURI(address: String, amount: String): String {
return try {
BitcoinURI.convertToBitcoinURI(
Address.fromBase58(BitcoinMainNetParams(), address),
Coin.parseCoin(amount),
null,
null
)
} catch (e: AddressFormatException) {
SegwitAddress.fromBech32(BitcoinMainNetParams(), address)
"bitcoin:$address&amount=$amount"
}
}
override fun getPaymentRequestURI(address: String, amount: String): String {
return try {
BitcoinURI.convertToBitcoinURI(
Address.fromBase58(BitcoinMainNetParams(), address),
Coin.parseCoin(amount),
null,
null
)
} catch (e: AddressFormatException) {
SegwitAddress.fromBech32(BitcoinMainNetParams(), address)
"bitcoin:$address?amount=$amount"
}
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 93-93: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 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/payments/MayaCryptoCurrency.kt`
around lines 85 - 97, The Bech32 fallback in
MayaCryptoCurrency.getPaymentRequestURI is building an invalid BIP-21 payment
URI; update the fallback path in this method so the amount parameter is appended
as the first query string using the correct URI separator, and keep the existing
AddressFormatException/SegwitAddress.fromBech32 handling intact while fixing the
returned bitcoin URI format.

Comment on lines +157 to +162

// EIP-681 native transfer: ethereum:<recipient>@<chainId>?value=<wei> (value in 1e18 base units).
override fun getPaymentRequestURI(address: String, amount: String): String {
val weiAmount = BigDecimal(amount).movePointRight(NATIVE_DECIMALS).toBigInteger()
return "ethereum:$address@$chain?value=$weiAmount"
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

These EVM URIs no longer match the app's parser contract.

EthereumPaymentIntentParser still forwards everything after the scheme into createPaymentIntent(), and createPaymentIntent() only strips <prefix>: before embedding the remainder into the OP_RETURN metadata. With the new @<chain>?value=... and /transfer?... shapes, any flow that scans/pastes one of these URIs back into the app will persist the suffix as part of the destination address instead of the recipient. Please either teach the parser to strip EIP-681 components first or avoid emitting EIP-681 here until that parser work lands.

Also applies to: 221-226

🤖 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/payments/MayaCryptoCurrency.kt`
around lines 157 - 162, The URI shape emitted by
MayaCryptoCurrency.getPaymentRequestURI is no longer compatible with the current
EthereumPaymentIntentParser/createPaymentIntent flow, which will treat the
EIP-681 suffix as part of the address. Either update EthereumPaymentIntentParser
to strip EIP-681 components like @<chain> and ?value= before handing off to
createPaymentIntent, or keep MayaCryptoCurrency (and the other affected payment
URI generator) emitting the simpler scheme-only address format until parser
support is added.

Comment on lines +615 to +618
val tokenContract = asset.substring(4)
override fun getPaymentRequestURI(address: String, amount: String): String {
return "solana:$address?amount=$amount&spl-token=$tokenContract"
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Extract the Solana mint address, not SYMBOL-<mint>.

Line 615 uses asset.substring(4), so SOL.USDC-EPj... becomes USDC-EPj.... That makes every generated spl-token parameter wrong, because it includes the symbol and dash instead of just the mint address.

Suggested fix
-    val tokenContract = asset.substring(4)
+    val tokenContract = asset.substringAfter("-")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val tokenContract = asset.substring(4)
override fun getPaymentRequestURI(address: String, amount: String): String {
return "solana:$address?amount=$amount&spl-token=$tokenContract"
}
val tokenContract = asset.substringAfter("-")
override fun getPaymentRequestURI(address: String, amount: String): String {
return "solana:$address?amount=$amount&spl-token=$tokenContract"
}
🤖 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/payments/MayaCryptoCurrency.kt`
around lines 615 - 618, The `getPaymentRequestURI` flow in `MayaCryptoCurrency`
is building the Solana `spl-token` value from `asset.substring(4)`, which leaves
the `SYMBOL-` prefix in place. Update the token extraction logic so it returns
only the mint address for Solana assets (for example by splitting on the first
dash and using the part after it), and keep the `tokenContract` value used by
`getPaymentRequestURI` as the clean mint address.

Comment on lines +108 to +111
log.info(
"DEX buy: continue with refund address={} for asset={} amount(dash={} fiat={} crypto={})",
address, args.asset, amount.dash, amount.fiat, amount.crypto
)

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove full wallet data from this info log.

This writes the refund address and exact entered amounts to normal app logs. Those values are sensitive transaction metadata and should be masked or omitted entirely.

🤖 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/DEXRefundAddressFragment.kt`
around lines 108 - 111, The info log in DEXRefundAddressFragment currently
prints the full refund address and exact entered amounts, which exposes
sensitive transaction metadata in normal logs. Update the log.info call in the
refund-address flow to either omit these values entirely or mask them before
logging, and keep the message limited to non-sensitive context such as the asset
or a generic continuation status.

Comment on lines +62 to +75
fun setArguments(asset: String, currencyCode: String) {
// Qualify tokens with their host network (e.g. "ETH (Ethereum)") so the user knows which
// chain the refund address must be valid for; native L1 coins show just the code.
val network = MayaCurrencyList.networkName(asset)
val displayCode = if (network != null) "$currencyCode ($network)" else currencyCode
_uiState.update {
it.copy(
asset = asset,
currencyCode = displayCode,
address = "",
continueEnabled = false,
errorCurrencyCode = null
)
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't wipe the refund address when the fragment is recreated.

setArguments() always clears address and disables Continue. Since DEXRefundAddressFragment calls this from onCreateView() on a nav-graph-scoped ViewModel, rotating the device or popping back from the receive step erases the user's input. Only reinitialize when the asset/currency actually changes. Cross-file: DEXRefundAddressFragment.onCreateView() invokes this unconditionally.

Possible fix
 fun setArguments(asset: String, currencyCode: String) {
     val network = MayaCurrencyList.networkName(asset)
     val displayCode = if (network != null) "$currencyCode ($network)" else currencyCode
-    _uiState.update {
-        it.copy(
-            asset = asset,
-            currencyCode = displayCode,
-            address = "",
-            continueEnabled = false,
-            errorCurrencyCode = null
-        )
-    }
+    _uiState.update { state ->
+        if (state.asset == asset && state.currencyCode == displayCode) {
+            state.copy(errorCurrencyCode = null)
+        } else {
+            state.copy(
+                asset = asset,
+                currencyCode = displayCode,
+                address = "",
+                continueEnabled = false,
+                errorCurrencyCode = null
+            )
+        }
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun setArguments(asset: String, currencyCode: String) {
// Qualify tokens with their host network (e.g. "ETH (Ethereum)") so the user knows which
// chain the refund address must be valid for; native L1 coins show just the code.
val network = MayaCurrencyList.networkName(asset)
val displayCode = if (network != null) "$currencyCode ($network)" else currencyCode
_uiState.update {
it.copy(
asset = asset,
currencyCode = displayCode,
address = "",
continueEnabled = false,
errorCurrencyCode = null
)
}
fun setArguments(asset: String, currencyCode: String) {
// Qualify tokens with their host network (e.g. "ETH (Ethereum)") so the user knows which
// chain the refund address must be valid for; native L1 coins show just the code.
val network = MayaCurrencyList.networkName(asset)
val displayCode = if (network != null) "$currencyCode ($network)" else currencyCode
_uiState.update { state ->
if (state.asset == asset && state.currencyCode == displayCode) {
state.copy(errorCurrencyCode = null)
} else {
state.copy(
asset = asset,
currencyCode = displayCode,
address = "",
continueEnabled = false,
errorCurrencyCode = null
)
}
}
}
🤖 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/DEXRefundAddressViewModel.kt`
around lines 62 - 75, `DEXRefundAddressViewModel.setArguments()` is resetting
the refund address state on every `DEXRefundAddressFragment.onCreateView()`
call, which wipes user input on recreation. Update `setArguments` to only
reinitialize `_uiState` when the incoming asset/currency differs from the
current state, and otherwise preserve `address` and `continueEnabled` while
still updating the display code as needed. Use the existing `asset`,
`currencyCode`, and `errorCurrencyCode` fields in `DEXRefundAddressViewModel` to
compare and avoid clearing state unnecessarily.

Comment on lines +169 to +178
displayItems.isEmpty() -> {
// Empty list area. Check the rendered list (displayItems), not the full
// dataset, so this also covers a search that filters every coin out.
// Show a search-specific message when filtering is the cause, otherwise
// the generic "No available coins" (offline with no cache / empty list).
val emptyMessage = if (query.isEmpty()) {
R.string.maya_no_available_coins
} else {
R.string.maya_no_search_results
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t let hidden offline searches switch the empty state to “no search results”.

searchQuery can survive after connectivity drops, but the search field is hidden offline and the ViewModel already empties items in that state. With a stale non-empty query, this branch shows maya_no_search_results instead of the intended offline “no available coins” message.

Suggested fix
-                    val emptyMessage = if (query.isEmpty()) {
+                    val emptyMessage = if (!isOnline || query.isEmpty()) {
                         R.string.maya_no_available_coins
                     } else {
                         R.string.maya_no_search_results
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
displayItems.isEmpty() -> {
// Empty list area. Check the rendered list (displayItems), not the full
// dataset, so this also covers a search that filters every coin out.
// Show a search-specific message when filtering is the cause, otherwise
// the generic "No available coins" (offline with no cache / empty list).
val emptyMessage = if (query.isEmpty()) {
R.string.maya_no_available_coins
} else {
R.string.maya_no_search_results
}
displayItems.isEmpty() -> {
// Empty list area. Check the rendered list (displayItems), not the full
// dataset, so this also covers a search that filters every coin out.
// Show a search-specific message when filtering is the cause, otherwise
// the generic "No available coins" (offline with no cache / empty list).
val emptyMessage = if (!isOnline || query.isEmpty()) {
R.string.maya_no_available_coins
} else {
R.string.maya_no_search_results
}
🤖 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/MayaCryptoCurrencyPickerScreen.kt`
around lines 169 - 178, The empty-state branch in MayaCryptoCurrencyPickerScreen
should not treat a stale hidden search query as an active search when offline.
Update the logic around displayItems.isEmpty so it only shows
maya_no_search_results when the search UI is actually available and the query is
user-visible/active; otherwise fall back to maya_no_available_coins even if
searchQuery still contains text. Use the existing query/searchQuery handling in
MayaCryptoCurrencyPickerScreen to gate the message selection.

Comment on lines +240 to +248
direction == SwapDirection.BUY -> {
// BUY routes via NEAR (Maya can't buy DASH), so a both-provider
// asset is bought via NEAR. Checked BEFORE the preferred-route map:
// that map can still hold a stale MAYA entry from a prior SELL
// session or the persisted snapshot, which would otherwise show as
// "Maya*" here. No preferred-route quote is run for BUY.
routeLabelId = R.string.maya_route_label_near
routeCalculated = false
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep BUY route labeling in one shared code path.

The picker now forces dual-provider assets to NEAR during BUY here, but getRouteLabelResId() later in this file still follows the old preferred-route logic. That leaves two label calculators that can disagree for BUY. Please extract this decision into one helper and reuse it from both places.

🤖 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/MayaViewModel.kt`
around lines 240 - 248, The BUY route label logic is duplicated and can diverge
between the picker block and getRouteLabelResId(), so route labels may disagree
for the same SwapDirection.BUY case. Extract the BUY decision into a single
helper in MayaViewModel and have both the picker flow and getRouteLabelResId()
call that shared helper, keeping the existing preferred-route logic only for
non-BUY paths.

Comment on lines +654 to +662
/**
* Selects the cross-chain swap backend before entering the Dash DEX portal so
* the portal doesn't inherit a stale provider left over from a previous Buy/Sell
* selection. Mirrors BuyAndSellViewModel.setSwapBackend. SwapKit exposes both Buy
* and Sell and falls back to Maya automatically when no SwapKit API key is set.
*/
fun setSwapBackend(backend: SwapBackend) {
swapProvider.setBackend(backend)
}

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Prevent the async backend restore from clobbering this selection.

Line 660 assumes setBackend() makes the upcoming Dash DEX navigation deterministic, but DispatchingSwapProvider still restores activeBackend in a background persistScope.launch during init. On a cold start, that late restore can overwrite this explicit choice after the shortcut is tapped, so the portal may still open on the previously persisted backend. Fix the ordering in the provider before relying on this helper for the Dash DEX entry point.

🤖 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/ui/main/MainViewModel.kt` around lines 654 -
662, The explicit backend selection in MainViewModel.setSwapBackend can still be
overwritten by DispatchingSwapProvider’s async restore from init, so make the
provider’s backend initialization deterministic before this helper is used for
Dash DEX navigation. Update DispatchingSwapProvider so the persisted
activeBackend restore cannot race with or clobber a later setBackend call, and
ensure the init-time persistScope.launch ordering preserves the most recent
explicit selection.

HashEngineering and others added 17 commits July 1, 2026 15:40
…mount

DEXEnterAmountViewModel accepted arbitrarily large amounts, while the common
EnterAmountFragment rejects any keystroke whose DASH-equivalent exceeds
Constants.MAX_MONEY. Add the same guard in onKeyInput: reject the key and
restore the previous value when the resulting DASH amount would exceed the
protocol maximum. Uses the Amount model's own conversion so the cap applies
whether the user is typing in fiat, DASH, or the asset currency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Map the noRoutesFound validation failure to a "higher than the allowed
maximum" message when the entered amount is large, keeping the existing
"below the allowed minimum" message otherwise. Add the corresponding
maya_error_below_allowed_maximum string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Import ordering, remove unused imports, wrap long argument lists and log
statements, and add missing trailing newlines across the maya module.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The buy flow surfaced SwapKit's raw error codes (e.g. "validation_error")
to the user. The root cause of that particular error was the entered amount
being lost: DEXEnterAmountViewModel.setArguments runs on every onCreateView
(including back-navigation) and reset the shared, nav-graph-scoped amount to
zero, so the receive step sent a zero sell amount that SwapKit rejects.

- DEXEnterAmountViewModel: don't re-seed (and wipe) the committed amount when
  re-entering the same asset; only seed for a genuinely new entry.
- DEXReceiveViewModel: reject a non-positive sellAmount before calling the API,
  and map SwapKit failures to friendly, localized @stringres messages.
- SwapKitErrors: new Context-free mapper from SwapKit error codes to message
  resources, reusable across swap surfaces.
- DEXEnterAmountScreen: replace the unreliable min/max guess (which compared the
  raw selected-currency string to a fixed threshold) with one neutral,
  amount-focused message.
- strings: add dex_error_* messages; reword dex_enter_amount_invalid.
- Remove the now-unused onToggleBalance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two robustness improvements across the DashDEX buy screens (enter-amount,
refund-address, receive):

1. Survive process death. The enter-amount and refund-address ViewModels now
   persist the user's input to SavedStateHandle (the entered Amount — @parcelize,
   rates + anchor included — and the refund address), asset-gated, and restore it
   when the screen is recreated. The amount is also restored in the enter-amount
   ViewModel's init so downstream steps (refund / receive) read a correct
   enteredAmount() even when the OS relaunches straight onto their screen.

2. Handle no network connectivity (mirroring the coin picker). Each ViewModel
   injects NetworkStateInt and mirrors connectivity into its UI state; each screen
   shows a non-dismissable no-connection toast while offline and disables all
   interaction: the numeric keypad is dimmed and inert (new `enabled` param on the
   shared NumericKeyboardCompose), the refund address field is disabled, and the
   Continue buttons are disabled. The ViewModels also guard their input/continue
   handlers on isOnline as defense-in-depth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve screen

Pressing Continue on the refund-address screen now validates the swap end-to-end
with SwapKit (createBuyOrder) and passes the resolved deposit address forward,
instead of the receive screen calling createBuyOrder again.

- DEXRefundAddressViewModel.submitOrder: validate the address, mark the DASH
  destination as income, then create the buy order; on success emit onOrderCreated
  (deposit address + sell amount), on failure surface a friendly SwapKitErrors
  message inline. Shows a submitting state.
- DEXRefundAddressScreen: Continue shows a spinner and is disabled while
  submitting; the address field is locked during submission; SwapKit order errors
  render inline, distinct from the address-format error.
- nav: dexReceiveFragment args change from refundAddress to sellAmount +
  depositAddress.
- DEXReceiveViewModel/Fragment: now purely presentational — dropped SwapProvider,
  WalletDataProvider, TransactionMetadataProvider and the loadDepositAddress /
  createBuyOrder path; setArguments just displays the passed-in deposit address and
  builds the QR/URI. Removes the dependency on the shared enter-amount ViewModel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tinue

When the wallet has no DASH to convert (userDashAccountEmptyError), the convert
screen popped a blocking dialog while Continue could still be enabled by typing
any non-zero amount (checkTheUserEnteredValue only checked non-zero, not balance).

- MayaConvertCryptoFragment: show a Toast instead of the AdaptiveDialog, and lock
  Continue via fragment.setInputEnabled(false).
- ConvertViewFragment: add an inputEnabled gate + public setInputEnabled(); every
  continueBtn.isEnabled assignment now respects it, so Continue stays disabled
  regardless of the entered amount. Buffered until the view exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te when too low

Add errorMessageRes()/isAmountTooLowError() to SwapProvider so each backend
owns its own error vocabulary (Maya via getMayaErrorString, SwapKit via
SwapKitErrors) and DispatchingSwapProvider delegates to the active one.
Screens (address input, convert crypto, DEX refund) no longer reference a
specific backend's error mapping.

The sell address-input screen now surfaces quote errors inline under the
field instead of a blocking dialog, and AddressInputFragment restores the
address-format error text after a subclass replaced it with a swap error.

The bootstrap quote on that screen (1 DASH) can sit below a route's minimum;
on an amount-too-low error the amount is doubled and retried, at most twice
(2 then 4 DASH), before the error is surfaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Address()

Add MayaCryptoCurrency.getNewExampleAddress(): every chain class derives a
format-valid address (checksummed wherever the format defines one) from a
single per-session ECKey, so within a session each chain's generated address
is stable. Key-derived for real where the chain's derivation is available
(BTC/LTC segwit, DASH/DOGE/TRON/BCH/ZEC Base58Check, Cosmos-family bech32,
XRP ripple Base58Check); otherwise deterministic bytes expanded from the
session pubkey (EVM hex, Solana, NEAR, SUI, Starknet, TON with CRC16,
Cardano and Radix bech32). Encoders live in the new AddressGenerator object.

Use it for the placeholder addresses sent to providers: validateBuyOrder's
refund address (DEXEnterAmountViewModel) and the legacy Maya no-destination
default quote (MayaWebApi), instead of the well-known hardcoded examples.

Also fix MayaDashCryptoCurrency, which inherited the Bitcoin-params address
parser and rejected Dash addresses (latent — DASH isn't in MayaCurrencyList).

Unit tests cover every override: parser exact-match, session stability, and
checksum round-trips through real decoders where available.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Changes generated by the kotlin-master-engineer agent review of this
branch (see claude-fable-5-review.md):

- L1: derive the receive address off the main thread (Dispatchers.IO)
  in DEXRefundAddressViewModel.submitOrder
- L2: replace validationError: String? with validationFailed: Boolean;
  the screen only ever shows a fixed localized message and the raw
  provider message is already logged
- L3: unify both getDefaultSwapQuote defaults on a shared
  DEFAULT_QUOTE_DUFFS (10 DASH) constant; the 1->10 bump was deliberate
  (52a04fb) and all two-arg callers pass value explicitly
- L4: collapse dead aggregator-type branches in isTradingActive();
  unwrap DispatchingSwapProvider.active once
- L5: drop stale "buy flow not built yet" TODO in MayaPortalFragment
- L6: delete DEBUG_FORCE_MAYA_ONLY_HALT test scaffolding and the
  forcedHalt branch in SwapKitApiAggregator
- L7: BigDecimal.valueOf for double USD prices; document the
  outbound-fee-as-total display estimate in mapToSwapQuote

L8 (negative bad-checksum fixtures) deferred: blocked on the H1/H2
address-validation hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Changes generated by the kotlin-master-engineer agent review of this
branch (see claude-fable-5-review.md):

- M1: block amount input (keypad + currency switch) while the buy
  validation quote is in flight, so a Success can never navigate with
  an amount other than the one validated; keypad dims like offline
- M2: tag the receive address with the Income tax category only after
  createBuyOrder succeeds, not before the attempt
- M3: BTC deposit URI now validates base58 or bech32 addresses and
  always emits proper BIP-21 (?amount=); previously bech32 deposits
  (what SwapKit/NEAR return) got a malformed &amount= fallback that
  scanning wallets drop
- M4: DispatchingSwapProvider init no longer clobbers an explicit
  setBackend that races the persisted-value load (lock + flag)
- M5: Starknet parser requires 50-64 hex chars and rejects the zero
  felt; 0x0 / truncated felts are burn destinations
- M6: NEAR named-account grammar per account-id rules (labels can't
  lead/trail with -/_, no double separators) and 64-char overall cap

Parser tests updated: 0xdeadbeef now invalid, new negative cases for
zero felt, short felts, and malformed NEAR account ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical fix from the kotlin-master-engineer agent review (see
claude-fable-5-review.md): SwapKit's BuyOrder.memo was captured but
dropped before the receive screen — a deposit sent without a required
chain memo/tag (XRP destination tag, TON comment, Cosmos-style memo)
cannot be attributed to the swap and the funds are lost.

Buys are NEAR Intents-pinned, whose SIMPLE deposit mode issues a unique
address with no memo, so the common case is unaffected — but the 1Click
spec returns depositMemo when "the chain requires it", and this now
reaches the user instead of being silently discarded:

- DEXRefundOrderResult carries memo; submitOrder passes it through
- new nav arg memo (default "") on dexReceiveFragment
- DEXReceiveViewModel puts the trimmed memo in the UI state
- receive screen shows a copyable "Memo / Tag" row (UriRow generalized
  to LabeledCopyRow) plus a red warning that transfers without the
  memo will be lost; preview added for the XRP tag case

The memo is deliberately NOT encoded into the QR/payment URI: there is
no cross-chain URI standard for memos, and a misparsed URI is worse
than a clearly displayed field (the exchange-deposit UX).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/H2, L8)

High-severity fixes from the kotlin-master-engineer agent review (see
claude-fable-5-review.md): these four parsers accepted any string of
the right shape, so a single-character typo in a sell destination or
buy refund address passed the client and relied entirely on
server-side rejection. All four formats are checksummed; the checksums
are now verified in verifyAddress overrides:

- XRP: ripple-alphabet Base58Check decode (new
  AddressGenerator.rippleBase58Decode, inverse of the existing
  encoder), 0x00 account version byte, double-SHA256 checksum
- TRON: Base58.decodeChecked + 0x41 mainnet version byte
- Cardano: Shelley bech32 checksum with hrp "addr" (Bech32.decode
  gained a max-length overload — Shelley's ~103 chars exceed
  BIP-173's 90-char cap); Byron via minimal CBOR parse of
  [tag24(payload), crc32] with the CRC32 verified
- TON: base64url decode, CRC16-XMODEM check (crc16Xmodem now shared
  from AddressGenerator), testnet tag bit rejected (only 0x11/0x51
  accepted), workchain must be 0 or -1

The hardcoded Cardano exampleAddress turned out to be fabricated (its
bech32 checksum is invalid) — replaced with the CIP-19 mainnet test
vector; the fabricated address is kept as a negative test fixture.

L8: negative bad-checksum fixtures added for all four chains, each
pre-verified computationally: single-char typos (checksum fails), a
testnet-flagged TON address with a valid CRC (exercises the tag
check), and an unknown-workchain raw TON form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ache (H3/H4)

Remaining high-severity fixes from the kotlin-master-engineer agent
review (see claude-fable-5-review.md):

- H3: buildAndSendSwapTx caught CancellationException in its broad
  catch (Exception) and converted it to ResponseResource.Failure. If
  the coroutine was cancelled after sendTransaction() had already
  broadcast the DASH swap tx, the caller saw "swap failed" and could
  retry — a double swap. CancellationException is now rethrown ahead
  of the generic handler; both Maya and SwapKit commit paths route
  through this function.

- H4: usdPriceCache was a plain HashMap written on responseScope
  (pool refresh / snapshot hydration / reset) while read on the main
  dispatcher via applyPoolPrices — torn reads or a resize spin were
  possible. Now a ConcurrentHashMap; all call sites unchanged.

This completes the review: C1, H1-H4, M1-M6 and L1-L8 are all
addressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The enter-amount ViewModel is nav-graph scoped, so backing out to the
coin picker (or exiting via "Back home" on the receive screen) kept the
committed amount and its SavedStateHandle copy alive, restoring a stale
amount on the next entry into the buy flow. Clear both when the
enter-amount fragment is popped off the back stack; isRemoving keeps
config changes and process-death restore intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SwapKit reports both an out-of-range amount and a temporarily
unavailable provider route as noRoutesFound; the messages on the
enter-amount and refund screens only suggested changing the amount.
Add a "try again shortly" hint, since NEAR route outages observed in
testing recovered within a minute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… display

Two inconsistencies: the validation_error message mentioned a refund
address on the sell address-input screen (where the user enters a
destination), and SwapKit's ambiguous noRoutesFound showed "amount too
low" on the sell enter-amount banner but neutral no-route copy
elsewhere. Make the validation message address-neutral and feed the
banner from the active backend's errorMessageRes (Maya's genuine
amount-too-low keeps its minimum copy).

Document the full flow/screen/code/text matrix in SWAPKIT_PROTOCOL.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HashEngineering and others added 2 commits July 13, 2026 07:41
DEXEnterAmountFragment.onDestroy() forced the nav-graph-scoped viewModel
lazy to resolve, which re-runs getBackStackEntry(nav_maya). When the whole
flow is popped to Home ("Back home" on the receive screen), that graph is
already off the back stack, so resolution threw IllegalArgumentException.

Capture the ViewModel in onCreateView (graph guaranteed alive) and clear
the entered amount via that captured instance in onDestroy, never touching
the lazy after teardown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants