Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -140,3 +140,6 @@ yarn-error.log
!.yarn/sdks
!.yarn/versions
/.husky/_

# Maestro run output
/maestro/screenshots
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
- added: WalletConnect Bitcoin (bip122) message signing for proof of ownership with existing BTC wallets
- added: Native Edge API HMAC signer (`edgeKey.json` + XOR-split C shards) so login-server requests can be signed outside the JS bundle via `apiSigner`, with JS `KEYS.EDGE_API_*` remaining as a fallback.
- added: Remote signed `GET /v1/infoRollup/:appId` `appKeys` fetch so plugin secrets can rotate without an app release, with DeviceSettings cache and baked-in `keys.json` fallback
- added: Stealth Send, sending privately to an address on any chain the Houdini provider serves
- added: Stealth Swap, routing a swap through the Houdini privacy provider
- added: A pasted address from another chain sets up the cross-chain send
- added: Swap-sends and stealth sends get their own titles in the transaction list and details
- added: The send scene notes when a swap provider pays the recipient
- changed: Standardize wallet list automation test IDs to use period separators.
- changed: Lock the send confirmation slider for the rest of the scene once a broadcast has been attempted, whether the broadcast reported success or failure, and replace the generic failure card with a message that the transaction may have gone through, pointing at the block explorer or confirmation email before trying again.
- changed: Prevent sending to the same wallet's own address for EVM assets.
Expand Down
16 changes: 8 additions & 8 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 @@ -165,7 +172,6 @@ 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 @@ -224,10 +230,8 @@ 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/StateProvinceListModal.tsx',

'src/components/modals/TransferModal.tsx',
Expand Down Expand Up @@ -257,10 +261,7 @@ 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 @@ -380,7 +381,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
134 changes: 134 additions & 0 deletions maestro/14-stealth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 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` | `30` | fiat, above the 25 USD private floor |
| `STEALTH_BELOW_MIN_AMOUNT` | `15` | fiat, between the 10 and 25 USD floors |
| `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.
- **The source wallet must hold more than the amount asked for.** The amounts are
fiat, so they sit against the floors on their own and need no re-checking as
prices move, but a wallet that has drifted below `STEALTH_AMOUNT` in value
fails the private branches on the balance rather than on the branch.

`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
WALLET_CODE: TRX
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: My Litecoin (new)
DEST_CODE: LTC
- runFlow:
file: ../common/stealth-set-amount.yaml
env:
ROW: You send
AMOUNT: '30'
- 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`.
43 changes: 43 additions & 0 deletions maestro/14-stealth/stealth-below-private-minimum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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'}
SRC_CODE: ${STEALTH_SRC_CODE || 'XLM'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
DEST_CODE: ${STEALTH_DEST_CODE || 'S'}
# Fiat, between the 10 USD transparent floor and the 25 USD private one:
AMOUNT: ${STEALTH_BELOW_MIN_AMOUNT || '15'}
tags:
- stealth
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
WALLET_CODE: ${SRC_CODE}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
DEST_CODE: ${DEST_CODE}
- 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
40 changes: 40 additions & 0 deletions maestro/14-stealth/stealth-cross-asset-quote.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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'}
SRC_CODE: ${STEALTH_SRC_CODE || 'XLM'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
DEST_CODE: ${STEALTH_DEST_CODE || 'S'}
# Fiat, and above the 25 USD private floor:
AMOUNT: ${STEALTH_AMOUNT || '30'}
tags:
- stealth
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
WALLET_CODE: ${SRC_CODE}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
DEST_CODE: ${DEST_CODE}
- 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
49 changes: 49 additions & 0 deletions maestro/14-stealth/stealth-execute-send.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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'}
SRC_CODE: ${STEALTH_SRC_CODE || 'XLM'}
DEST_WALLET: ${STEALTH_DEST_WALLET || 'My Sonic'}
DEST_CODE: ${STEALTH_DEST_CODE || 'S'}
# Fiat, and above the 25 USD private floor:
AMOUNT: ${STEALTH_AMOUNT || '30'}
tags:
- stealth-spend
---
- runFlow:
file: ../common/stealth-launch.yaml
- runFlow:
file: ../common/stealth-open-send.yaml
env:
WALLET: ${SRC_WALLET}
WALLET_CODE: ${SRC_CODE}
- runFlow:
file: ../common/stealth-pick-myself.yaml
env:
DEST_WALLET: ${DEST_WALLET}
DEST_CODE: ${DEST_CODE}
- 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
Loading