feat(dash-dex): finish buy swap flow#1504
Conversation
…feat/swapkit-poc
…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>
📝 WalkthroughWait, I made an XML structural error above. Let me restate the artifact correctly below (ignore the malformed block above). WalkthroughThis PR adds a BUY direction to the Maya/SwapKit DEX integration. It introduces a ChangesDashDEX Buy Flow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…into feat/dash-dex-buy-swap-two
|
@coderabbitai - review - hopefully not too many commits, most don't have any code in this PR |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 liftFold
swapDirectioninto 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-UIStatepattern. Please carry the selected direction inside the existing UI state model rather than exposing a separateswapDirectionflow. 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
📒 Files selected for processing (46)
common/src/main/java/org/dash/wallet/common/data/ServiceName.ktcommon/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.ktcommon/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.ktcommon/src/main/java/org/dash/wallet/common/ui/components/AddressField.ktcommon/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.ktcommon/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.ktcommon/src/main/res/drawable/ic_warning_triangle.xmlcommon/src/main/res/values/strings.xmlintegrations/maya/NEAR_INTENTS_PROTOCOL.mdintegrations/maya/SWAPKIT_PROTOCOL.mdintegrations/maya/proguard-rules.prointegrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/DispatchingSwapProvider.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/SwapProvider.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/BuyOrder.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/swapkit/SwapKitApiAggregator.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountScreen.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXEnterAmountViewModel.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveViewModel.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressScreen.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXRefundAddressViewModel.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaCryptoCurrencyPickerScreen.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaPortalScreen.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConfig.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/SwapBackend.ktintegrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/SwapDirection.ktintegrations/maya/src/main/res/drawable/ic_dash_dex_buy.xmlintegrations/maya/src/main/res/drawable/ic_dash_dex_illustration.xmlintegrations/maya/src/main/res/drawable/ic_dash_dex_sell.xmlintegrations/maya/src/main/res/navigation/nav_maya.xmlintegrations/maya/src/main/res/values/strings-maya.xmlintegrations/maya/src/test/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrencyTest.ktwallet/res/drawable/ic_shortcut_dash_dex.xmlwallet/res/navigation/nav_home.xmlwallet/res/values/strings-extra.xmlwallet/src/de/schildbach/wallet/ui/main/MainViewModel.ktwallet/src/de/schildbach/wallet/ui/main/WalletFragment.ktwallet/src/de/schildbach/wallet/ui/main/shortcuts/ShortcutOption.kt
| if (onLongPress != null) { | ||
| Modifier.pointerInput(Unit) { | ||
| detectTapGestures(onLongPress = { onLongPress() }) | ||
| } | ||
| } else { | ||
| Modifier | ||
| } | ||
| ) | ||
| .padding(start = 20.dp, end = 10.dp), |
There was a problem hiding this comment.
🎯 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.
| Icon( | ||
| painter = painterResource(trailing.first), | ||
| contentDescription = null, |
There was a problem hiding this comment.
🎯 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.
| 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" | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
|
|
||
| // 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" | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| val tokenContract = asset.substring(4) | ||
| override fun getPaymentRequestURI(address: String, amount: String): String { | ||
| return "solana:$address?amount=$amount&spl-token=$tokenContract" | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| log.info( | ||
| "DEX buy: continue with refund address={} for asset={} amount(dash={} fiat={} crypto={})", | ||
| address, args.asset, amount.dash, amount.fiat, amount.crypto | ||
| ) |
There was a problem hiding this comment.
🔒 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.
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
| /** | ||
| * 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) | ||
| } |
There was a problem hiding this comment.
🩺 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.
…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>
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>
…into feat/dash-dex-buy-swap-two
Issue being fixed or feature implemented
Related PR's and Dependencies
Screenshots / Videos
How Has This Been Tested?
Checklist:
Summary by CodeRabbit
New Features
Bug Fixes