Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3973d46
Fix lint warnings in SwapDetailsCard
j0ntz Jul 3, 2026
25ae577
Handle optional swap destination wallet and payout wallet id
j0ntz Jul 3, 2026
1b38595
Share the swap price-impact calculation and display
j0ntz Jul 3, 2026
3767f2a
Add Houdini destination-chain metadata and plugin registration
j0ntz Jul 3, 2026
5f40022
Turn SendScene2 into a send-to-address swap (Stealth Send)
j0ntz Jul 3, 2026
5ade828
Add Stealth Swap to the existing swap flow
j0ntz Jul 3, 2026
031fe1a
Add Stealth Send and Stealth Swap maestro walks
j0ntz Jul 3, 2026
8883d0a
test: add missing testIDs for maestro selectors
j0ntz Jul 3, 2026
2d75ca0
Correct two HoudiniSwap address-validation regexes
j0ntz Jul 28, 2026
5bfa9f6
Adopt a cross-chain recipient address on any entry path
j0ntz Jul 28, 2026
5c28a62
Add the Stealth Send and Stealth Swap design document
j0ntz Jul 28, 2026
99b2406
Reflect live route availability in the Stealth Send and Swap UI
j0ntz Jul 28, 2026
a0f767b
Document the availability UX in the design doc
j0ntz Jul 28, 2026
2765be3
Enter swap-send amounts with the flip input
j0ntz Jul 28, 2026
32be2c5
Document Houdini exclusivity across the design doc
j0ntz Jul 28, 2026
28a7281
Offer every routable destination in the Myself picker
j0ntz Jul 28, 2026
4f4acca
Distinguish the three send-shaped swap flows in history
j0ntz Jul 28, 2026
e04734b
test: add missing testIDs for maestro selectors
j0ntz Jul 29, 2026
eff8dd5
Gate swap-send behind the PIN spending limit
j0ntz Jul 29, 2026
61bb85a
Record the PIN gate reversal in the design doc
j0ntz Jul 29, 2026
2773b7f
Show the real swap failure instead of a generic error card
j0ntz Jul 29, 2026
04a9ae6
Close the PIN-gate timing hole and two stealth leaks
j0ntz Jul 29, 2026
edb9abe
Record the error-display decision in the design doc
j0ntz Jul 29, 2026
d9bea43
Infer the payout token from the payout wallet
j0ntz Jul 29, 2026
4719a67
Report the destination wallet type in swap error tracking
j0ntz Jul 29, 2026
4d93bc5
Escape the dots in the Hedera address pattern
j0ntz Jul 29, 2026
77bafae
Record the review-round corrections in the design doc
j0ntz Jul 29, 2026
6c02cf3
Fix the phase-entry wording in the design doc
j0ntz Jul 29, 2026
d0feca3
Reconcile the Houdini chain table with the routes it claims
j0ntz Jul 30, 2026
0015a6b
Ask for privacy per request and enforce the provider's minimums
j0ntz Jul 30, 2026
5c7ceb5
Show the exchange order details on a swap-send
j0ntz Jul 30, 2026
56a6754
Record the feedback round in the design doc
j0ntz Jul 30, 2026
3af52f7
Read live rates in the fixed-to fallback and retry once
j0ntz Jul 30, 2026
cbca746
Close three quote-state and privacy gaps
j0ntz Jul 30, 2026
345c0b6
Retire a held quote on a wallet switch and label token sends
j0ntz Jul 30, 2026
845a609
Show the swap row fiat value inline, not on its own line
j0ntz Jul 30, 2026
6155fef
Retract a plain-send error when swap-send mode takes over
j0ntz Jul 30, 2026
fef114d
Record the review round in the design doc
j0ntz Jul 30, 2026
8722830
Cover the stealth swap options and swap error mapping
j0ntz Jul 30, 2026
fc37165
test: add testIDs for the wallet picker and its rows
j0ntz Jul 30, 2026
e24c82c
Add a composable maestro suite for the stealth branches
j0ntz Jul 30, 2026
5fe80ba
Record the follow-up sweep in the design doc
j0ntz Jul 30, 2026
fcdfe52
Correct the chain-mapping claim in the design doc
j0ntz Jul 30, 2026
571da74
Show the provider floor in the user's own fiat
j0ntz Jul 30, 2026
350a508
Clear the destination address when the source wallet changes
j0ntz Jul 30, 2026
8c113b9
Record phase 12 and the retry-expiry decision in the TDD
j0ntz Jul 31, 2026
dab17fa
Retire a quote the moment its terms change
j0ntz Jul 31, 2026
2e1d113
Let a wallet switch and a mode switch finish cleanly
j0ntz Jul 31, 2026
b0a8e4f
Show Myself when any routable wallet exists
j0ntz Jul 31, 2026
c2a5e22
Keep the expiry message on screen with its own slider gate
j0ntz Jul 31, 2026
750be41
Bring the TDD current with the second review round
j0ntz Jul 31, 2026
b5fe24b
Fix lint warnings in EdgeRow
j0ntz Aug 1, 2026
ce5ccd9
Let a row show a tinted state word in its title
j0ntz Aug 1, 2026
671f792
Move the guaranteed and estimated state into the amount row titles
j0ntz Aug 1, 2026
ef7ee28
Let the amount subflow set a row that already holds a value
j0ntz Aug 1, 2026
dc0b868
Record the amount-row title states in the design doc
j0ntz Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ yarn-error.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Maestro run output
/maestro/screenshots
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

## Unreleased (develop)

- added: Stealth Send: the send scene offers a "Recipient receives" asset selector and a Stealth Send toggle, turning the send into a live swap-to-address quote routed exclusively through the Houdini privacy provider, with linked You send/Recipient gets amounts, quote expiry countdown, and a destination tag row on memo-required chains.
- added: The "Myself" recipient picker offers every asset a send can route to, not just the source asset, with same-asset wallets grouped at the top.
- added: Swap-sends, stealth sends, and stealth swap-sends are titled separately in the transaction list and details. The two private flows do not display the recipient, but still show the exchange order details so a stuck order can be traced.
- added: The send scene tells you when an amount is under the privacy provider's minimum before contacting them, and the Stealth toggle explains itself when an asset or amount cannot route privately.
- added: Stealth Swap: a toggle on the swap amount-entry scene routes the swap through the Houdini privacy provider as a fixed, non-tappable provider.
- added: Entering a recipient address from another chain now sets up the cross-chain send by itself. Pasting, typing, or scanning an address the sending wallet cannot read no longer reports an invalid address; Edge identifies the network it belongs to, or asks which one when the format is shared, and sets "Recipient receives" to match.
- added: Remote enable/disable of gift card providers via the info server's giftCardInfo config, supporting whole-provider disabling for Phaze and Bitrefill and per-brand disabling for Phaze.
- changed: Show fiat values on the Stealth Send amount rows.
- changed: Reorganize the wallet list menu so Asset Settings is reached through Wallet Settings, and rename the Monero "Backend" card to "Server Settings".
- fixed: Drop four destination chains the privacy provider does not serve, and correct the native-asset lookup that made six others unquotable.
- fixed: Prevent imported Monero wallets from using the Edge LWS backend. Choosing to import now prompts the user to continue with a full node or configure a custom LWS server, matching the wallet settings rule.

## 4.49.0 (staging)
Expand Down
21 changes: 11 additions & 10 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ export default [
'react-native/no-raw-text': [
'error',
{
skip: ['B', 'EdgeText', 'Paragraph', 'SmallText', 'WarningText']
skip: [
'B',
'EdgeText',
'Paragraph',
'PositiveText',
'SmallText',
'WarningText'
]
}
],
'react-native/sort-styles': 'off',
Expand Down Expand Up @@ -160,7 +167,7 @@ export default [
'src/components/cards/StakingOptionCard.tsx',
'src/components/cards/StakingReturnsCard.tsx',
'src/components/cards/SupportCard.tsx',
'src/components/cards/SwapDetailsCard.tsx',

'src/components/cards/TappableAccountCard.tsx',
'src/components/cards/TappableCard.tsx',
'src/components/cards/UnderlinedNumInputCard.tsx',
Expand Down Expand Up @@ -219,13 +226,11 @@ export default [
'src/components/modals/PasswordReminderModal.tsx',
'src/components/modals/PermissionsSettingModal.tsx',

'src/components/modals/RadioListModal.tsx',
'src/components/modals/RawTextModal.tsx',
'src/components/modals/ScamWarningModal.tsx',
'src/components/modals/ScanModal.tsx',

'src/components/modals/StateProvinceListModal.tsx',

'src/components/modals/TextInputModal.tsx',
'src/components/modals/TransferModal.tsx',

'src/components/modals/WalletListSortModal.tsx',
Expand Down Expand Up @@ -255,10 +260,8 @@ export default [

'src/components/rows/CryptoFiatAmountRow.tsx',

'src/components/rows/EdgeRow.tsx',

'src/components/rows/PaymentMethodRow.tsx',
'src/components/rows/SwapProviderRow.tsx',

'src/components/rows/TxCryptoAmountRow.tsx',

'src/components/scenes/ChangeMiningFeeScene.tsx',
Expand Down Expand Up @@ -372,7 +375,6 @@ export default [

'src/components/themed/SceneHeader.tsx',

'src/components/themed/SearchFooter.tsx',
'src/components/themed/SelectableRow.tsx',

'src/components/themed/ShareButtons.tsx',
Expand All @@ -386,7 +388,6 @@ export default [
'src/components/themed/TransactionListComponents.tsx',

'src/components/themed/VectorIcon.tsx',
'src/components/themed/WalletList.tsx',

'src/components/themed/WalletListErrorRow.tsx',
'src/components/themed/WalletListHeader.tsx',
Expand Down
133 changes: 133 additions & 0 deletions maestro/14-stealth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Stealth Send and Stealth Swap flows

Maestro coverage for the send-to-address swap UI. Every user-visible branch of
the feature has a flow, and the pieces those flows are built from live in
`../common/stealth-*.yaml` so a later session can drive one specific state
without walking the whole thing by hand.

## Running

The whole suite, minus the two flows that spend money:

```bash
npm run maestro -- test --include-tags stealth maestro
```

One flow:

```bash
npm run maestro -- test maestro/14-stealth/stealth-private-quote.yaml
```

The two funded flows are tagged `stealth-spend` and nothing else, so the command
above never triggers them. Run them deliberately, and in this order:

```bash
npm run maestro -- test maestro/14-stealth/stealth-execute-send.yaml
npm run maestro -- test maestro/14-stealth/stealth-execute-swap.yaml
```

They run opposite directions, so the pair returns the funds to where they
started and costs two spreads rather than emptying one wallet. Let the first
one's deposit confirm before starting the second: a wallet with an unconfirmed
outgoing transaction cannot quote a new send.

Run flows ONE AT A TIME rather than as a tagged batch. A failing flow takes the
maestro driver down with it, and every flow after it then reports
`Failed to connect`, which reads as a suite-wide breakage instead of one bad
flow.

On a machine with more than one simulator booted, pin the device and the driver
port, or the run may attach to the wrong one:

```bash
maestro --device <udid> --driver-host-port <port> test maestro/14-stealth
```

## Account expectations

The flows read wallet names from env vars so they can point at whatever the
signed-in account actually holds. Defaults assume the `edge-funds` roster
account as of 2026-07-30:

| Env var | Default | Needs |
| -------------------------- | ------------ | -------------------------------------------- |
| `STEALTH_SRC_WALLET` | `My Stellar` | funded above 25 USD for the private branches |
| `STEALTH_DEST_WALLET` | `My Sonic` | exists; no balance needed |
| `STEALTH_AMOUNT` | `158` | worth more than 25 USD in source units |
| `STEALTH_BELOW_MIN_AMOUNT` | `80` | worth between 10 and 25 USD |
| `STEALTH_PIN_DIGIT` | `0` | the account's single repeated relogin digit |
| `STEALTH_MEMO_CHAIN` | `Ripple` | a memo-required destination chain |

Two preconditions the flows cannot check for you:

- **The source wallet must have no unconfirmed outgoing transaction.** A pending
send blocks the next one, so the quote never arms and the flow fails on the
slider rather than on anything it is testing. Running the funded flows
back-to-back on one wallet hits this.
- **Balances drift.** The amounts above are chosen against a live price; when the
source asset moves far enough, `STEALTH_AMOUNT` stops clearing 25 USD or
`STEALTH_BELOW_MIN_AMOUNT` stops sitting between the two floors, and the
private branches quietly test the wrong thing. Both are worth re-checking
before reading a failure as a regression.

`stealth-send.yaml`, `stealth-swap.yaml` and `stealth-qr-payment-uri.yaml`
request no quote, so they need no balance at all.

## The flows

| Flow | Branch it drives |
| ----------------------------------- | -------------------------------------------------------------------- |
| `stealth-send.yaml` | Send scene controls before address entry, plus a memo-chain tag row |
| `stealth-swap.yaml` | Exchange scene toggle card |
| `stealth-qr-payment-uri.yaml` | scanned payment URI, cross-chain, amount on the recipient side |
| `stealth-myself-picker.yaml` | own-wallet destinations grouped same-asset first |
| `stealth-cross-asset-quote.yaml` | plain Swap & Send on a transparent route |
| `stealth-private-quote.yaml` | the toggle invalidating a held quote and refetching a private route |
| `stealth-below-private-minimum.yaml`| the toggle refusing under the private floor, with no request sent |
| `stealth-tx-details.yaml` | a completed stealth transaction's identity rows |
| `stealth-execute-send.yaml` | **spends** a private Stealth Send to the success scene |
| `stealth-execute-swap.yaml` | **spends** a Stealth Swap from the Exchange scene |

## Composing your own

Each subflow states its env contract in its header. A walk that needs a live
private quote on a pair the suite does not cover is five `runFlow` steps:

```yaml
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: My Tron 2
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: My Litecoin (new)
- runFlow:
file: ../common/stealth-set-amount.yaml
env:
ROW: You send
AMOUNT: '120'
- runFlow:
file: ../common/stealth-toggle.yaml
- runFlow:
file: ../common/stealth-await-quote.yaml
```

Two things bite when writing these by hand:

- **The confirm slider is a pan gesture.** A coordinate swipe across the track
does nothing at all. `stealth-slide-to-confirm.yaml` swipes from the thumb by
id, which is the only form that completes it.
- **Notification cards float over the bottom of every scene**, including the
slider and the amount rows. `stealth-launch.yaml` swipes them away; skip it
and later taps land on a card instead of the control underneath.

## Assertions

These flows are for driving the app to a state, not for asserting behavior. Each
one asserts only what it must to gate the next step (that a scene arrived, that
a quote settled, that the slider is live). Behavioral claims belong in the unit
tests and in `src/docs/stealth-send-swap.md`.
40 changes: 40 additions & 0 deletions maestro/14-stealth/stealth-below-private-minimum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Below the private floor the toggle refuses, client-side.
#
# The provider serves no private route under 25 USD, so an order under it is
# pre-empted before a request goes out rather than sent and refused. The toggle
# stays off and the card explains the floor. The transparent route is still
# available at the same amount, which is what keeps a plain Swap & Send working
# in the band between the two floors.
appId: ${APP_ID}
env:
APP_ID: ${MAESTRO_APP_ID || 'co.edgesecure.app'}
SRC_WALLET: ${STEALTH_SRC_WALLET || 'My Stellar'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
# Worth more than the 10 USD transparent floor and less than the 25 USD
# private one, in the source wallet's own denomination:
AMOUNT: ${STEALTH_BELOW_MIN_AMOUNT || '80'}
tags:
- stealth
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
- runFlow:
file: ../common/stealth-set-amount.yaml
env:
ROW: 'You send'
AMOUNT: ${AMOUNT}
- runFlow:
file: ../common/stealth-await-quote.yaml

- runFlow:
file: ../common/stealth-toggle.yaml
- assertVisible: 'Private routing needs at least.*'
- takeScreenshot: maestro/screenshots/stealth-below-min-01-toggle-refused
35 changes: 35 additions & 0 deletions maestro/14-stealth/stealth-cross-asset-quote.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# A plain cross-asset Swap & Send: no privacy requested, so a transparent
# (single-exchange) route is acceptable and the order clears the lower floor.
#
# Ends on a live quote with the slider armed. Nothing is sent.
appId: ${APP_ID}
env:
APP_ID: ${MAESTRO_APP_ID || 'co.edgesecure.app'}
SRC_WALLET: ${STEALTH_SRC_WALLET || 'My Stellar'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
AMOUNT: ${STEALTH_AMOUNT || '158'}
tags:
- stealth
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
- runFlow:
file: ../common/stealth-set-amount.yaml
env:
ROW: 'You send'
AMOUNT: ${AMOUNT}
- runFlow:
file: ../common/stealth-await-quote.yaml
# The edited side owns the guarantee, and each row states which it is in its
# own title. Asserting both catches a regression that swaps or drops them.
- assertVisible: 'You send \(Guaranteed\)'
- assertVisible: 'Recipient gets \(Estimated\)'
- takeScreenshot: maestro/screenshots/stealth-cross-asset-01-standard-quote
44 changes: 44 additions & 0 deletions maestro/14-stealth/stealth-execute-send.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Executes a private cross-asset Stealth Send end to end, to the success scene.
#
# THIS SPENDS FUNDS. It carries the `stealth-spend` tag ONLY, so a run of the
# rest of the suite by the `stealth` tag never triggers it. See README.md in
# this directory for how to invoke it deliberately.
#
# Sending to one of the account's own wallets keeps the principal inside the
# account, so the cost is the route's spread plus network fees.
appId: ${APP_ID}
env:
APP_ID: ${MAESTRO_APP_ID || 'co.edgesecure.app'}
SRC_WALLET: ${STEALTH_SRC_WALLET || 'My Stellar'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
AMOUNT: ${STEALTH_AMOUNT || '158'}
tags:
- stealth-spend
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
- runFlow:
file: ../common/stealth-set-amount.yaml
env:
ROW: 'You send'
AMOUNT: ${AMOUNT}
- runFlow:
file: ../common/stealth-toggle.yaml
- runFlow:
file: ../common/stealth-await-quote.yaml
env:
LABEL: 'Slide to send stealthily'
- runFlow:
file: ../common/stealth-slide-to-confirm.yaml
- extendedWaitUntil:
visible: 'Transaction Details'
timeout: 90000
- takeScreenshot: maestro/screenshots/stealth-execute-01-success
Loading