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: 2 additions & 1 deletion Bitkit/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,8 @@ extension AppViewModel {
toast(
type: .warning,
title: t("other__lnurl_pay_error"),
description: t("other__lnurl_pay_error_no_capacity")
description: t("other__lnurl_pay_error_no_capacity"),
accessibilityIdentifier: "LnurlPayNoCapacityToast"
)
return
}
Expand Down
25 changes: 20 additions & 5 deletions journeys/hardware-wallet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,29 @@ For the passphrase journeys, start it with passphrase protection instead:
TREZOR_PASSPHRASE_PROTECTION=true ./scripts/trezor-emulator start
```

Then build and run with the Bridge environment (mirrors the `xcodebuild` invocation in
`Docs/AI_DEVICE_TESTS.md`):
Then build and run with the Bridge settings, each passed as its own `--extra-args`:

```bash
TEST_TREZOR_EMU=1 TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 \
xcodebuildmcp simulator build-and-run \
--extra-args "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD TEST_TREZOR_EMU"
--extra-args "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD" \
--extra-args TREZOR_BRIDGE=true \
--extra-args TREZOR_BRIDGE_URL=http://127.0.0.1:21325
```

Use the address the simulator reaches the emulator at in `TREZOR_BRIDGE_URL` when it is not
`127.0.0.1`. Do not add the `TEST_TREZOR_EMU` compilation condition that `Docs/AI_DEVICE_TESTS.md`
passes for the UI test suite: it replaces the wallet with the Trezor test dashboard, so no
journey that starts on the wallet home screen can run.

## Order

Several journeys mutate pairing state. Run them in this order, or re-pair between runs:

1. `connect-home-tile.xml` — pairs the emulator; every other journey assumes it ran.
2. `settings-hardware-wallets.xml`, `connect-flow.xml`, `suggestion-intro-sheet.xml` — each forgets
and re-pairs the device, ending paired.
3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `transfer-to-spending*.xml`, `reconnect.xml`.
3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `transfer-to-spending*.xml`, `reconnect.xml`,
`send-onchain.xml`.
4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-transfer-to-spending.xml` →
`passphrase-settings-remove.xml`.
5. `detail-overview.xml` **last** — its final step forgets the device.
Expand All @@ -59,6 +65,15 @@ The device blocks until each prompt is acknowledged, once per address type for a
../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}'
```

The isolated `trezor-emulator` profile of `bitkit-docker` takes the same command through its own
container (`docker compose --profile trezor-emulator exec -T trezor-emulator
/trezor-user-env/.venv/bin/python3 /opt/bitkit-trezor/trezor-controller.py send-json
'{"type":"emulator-press-yes"}'`), which is how a stack that cannot use the host helper presses Yes.

`send-onchain.xml` spends from the emulator's account, so it needs funds beyond what the transfer
journeys leave; fund the account's receive address again from the regtest node before it, and send
to an address of that node.

## Checking for passphrase leaks

Unlike the notification extension, the app writes its logs as **files in the app group container**,
Expand Down
31 changes: 31 additions & 0 deletions journeys/hardware-wallet/send-onchain.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<journey name="Hardware Wallet Send On-chain">
<description>
Drives the on-chain Send sheet from a paired Trezor: Recipient -> Amount -> Review -> Sign With
Device -> Sent, then verifies the payment is listed once as an outgoing blue hardware activity
and that the hardware balance dropped by at least the amount. Requires a paired Bridge
emulator whose native-segwit account holds at least 50 000 confirmed regtest sats (fund the
emulator's receive address with the regtest bitcoin node and mine a block), and a valid regtest
destination address that does not belong to the emulator, for example a fresh address of the
regtest node. Start on the wallet home screen.
</description>
<actions>
<action>Note the sats amount shown on the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles</action>
<action>Tap the hardware wallet tile and verify the hardware wallet detail screen opens (id "HardwareWalletScreen")</action>
<action>Tap the Send button (id "Send") of the tab bar</action>
<action>If a camera permission dialog appears, dismiss it by choosing "Don't Allow"</action>
<action>Verify the Send sheet shows the recipient options, then tap "Enter Manually" (id "RecipientManual")</action>
<action>Type the destination regtest address into the recipient field (id "RecipientInput") and tap Continue (id "AddressContinue")</action>
<action>Verify the Send amount screen (id "SendAmount") is visible and its available amount (id "AvailableAmount") is greater than 0</action>
<action>Tap the keys "1", "0", "0", "0", "0" (ids "N1", "N0", "N0", "N0", "N0") to enter 10 000 sats and tap Continue (id "ContinueAmount")</action>
<action>Verify the review screen (id "SendConfirm") shows an amount of 10 000 sats and the paired hardware wallet as the funding source (blue accent)</action>
<action>Tap "Show Details" (id "SendConfirmToggleDetails") and verify the destination address and a non-zero network fee are listed</action>
<action>Swipe the send control (id "GRAB")</action>
<action>Verify the sign screen (id "HardwareSendSign") is titled "SIGN WITH DEVICE", shows the destination address under "To address (confirm on device)" and the amount (id prefix "HardwareSendSignAmount")</action>
<action>Tap "Open Trezor Connect" (id "HardwareSendOpenTrezorConnect")</action>
<action>Approve the four prompts on the Bridge emulator in order (recipient, amount, fee and summary) with the controller command that presses Yes, as the README describes</action>
<action>Wait up to 60 seconds for signing and broadcast, then verify the Sent screen (id "SendSuccess") appears and shows 10 000 sats</action>
<action>Tap "Close" (id "Close"), mine one regtest block, wait for the app to sync, and verify the app is back on the hardware wallet detail screen (id "HardwareWalletScreen")</action>
<action>Verify the activity list of the hardware wallet detail screen contains exactly one new blue sent row above the older rows, whose amount is 10 000 sats plus the network fee shown in the review details</action>
<action>Navigate back to the wallet home screen and verify the hardware wallet tile shows a lower amount than the one noted at the start, by at least 10 000 sats</action>
</actions>
</journey>
36 changes: 36 additions & 0 deletions journeys/lnurl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# LNURL journeys

## Setup

LNURL-pay needs a reachable LNURL server. Start the `lnurl-pay` fixture of a sibling `bitkit-docker`
checkout (a service with stable pay metadata whose invoice callback keeps returning an error until
it is switched healthy):

```bash
cd /path/to/bitkit-docker
docker compose --profile lnurl-pay up -d --build --wait lnurl-server-fixture
```

It serves on port 3010. Read the link to paste from it:

```bash
curl -fsS http://127.0.0.1:3010/generate/pay # {"url":..., "lnurl":"lnurl1...", ...}
curl -fsS -X POST -H 'content-type: application/json' \
-d '{"mode":"healthy"}' http://127.0.0.1:3010/fixture # or "error"; it starts in "error"
```

The callback URLs it returns follow the address the request used, so a simulator that reaches the
server at another address (a per-seat loopback address, `E2E_LOCAL_HOST`) gets callbacks at that
address. Build the app with the E2E condition and local backend as in [`../README.md`](../README.md)
so it may call plain HTTP on the local network.

The fixture issues signed regtest invoices for fetching and decoding only. It has no Lightning
node and cannot settle a payment.

## What is not covered

Everything after the capacity check needs a Lightning spending balance: a channel to the LSP, which
the journey environment does not provide on an isolated stack. The failing and recovering invoice
callback (the send failure screen with a retry that succeeds once the callback is healthy) therefore
has no journey here yet. `payment-requests/definite-pre-broadcast-retry.xml` covers it for a
Payment Request on a wallet that has that balance.
26 changes: 26 additions & 0 deletions journeys/lnurl/pay-no-spending-balance.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<journey name="lnurl pay without spending balance is refused">
<description>
Verifies that pasting an LNURL-pay link while the wallet has no Lightning spending balance is
refused with the capacity warning, and that the Send sheet stays on the recipient options instead
of opening the LNURL amount or confirmation screens. The app has to resolve the pay request from
the LNURL server first, because the minimum it compares the balance against comes from that
response, so this journey also proves the simulator reaches the server.

Precondition: an onboarded E2E build on regtest with the node running and no channels, so the
Spending balance is zero. The LNURL-pay link is the `lnurl` field of the `GET /generate/pay`
response of the `bitkit-docker` `lnurl-server` fixture, read from the server address the app
reaches (see README.md). Copy it to the simulator clipboard (`xcrun simctl pbcopy &lt;udid&gt;`).
The fixture mode does not matter here, the invoice callback is never called. Start on the
wallet home screen.
</description>
<actions>
<action>Verify the SPENDING balance on the home screen is 0</action>
<action>Tap the Send button (id "Send")</action>
<action>If a camera permission dialog appears, dismiss it by choosing "Don't Allow"</action>
<action>Tap "Paste Invoice" (id "RecipientInvoice")</action>
<action>If the iOS dialog about pasting from another app appears (the first paste of a build), choose "Allow Paste"</action>
<action>Straight after that, without waiting for another screen refresh because the toast fades after about 2.5 seconds, verify a warning toast titled "Unable To Pay (LNURL)" with the text "Not enough outbound/sending capacity to complete lnurl-pay request." appears (id "LnurlPayNoCapacityToast")</action>
<action>Verify the Send sheet is still on the recipient options (id "RecipientManual" is visible) and neither the LNURL amount screen (id "SendNumberField") nor a review screen (id "SendConfirm") was opened</action>
<action>Close the Send sheet and verify the wallet home screen is shown</action>
</actions>
</journey>
Loading