From ad501b783d527cd998d6327804ec836bb976e50f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Aug 2026 13:48:02 -0300 Subject: [PATCH 1/2] feat: add trezor electrum url override --- .env.example | 2 + README.md | 3 +- app/build.gradle.kts | 2 + app/src/main/java/to/bitkit/env/Env.kt | 13 +++- .../to/bitkit/repositories/HwWalletRepo.kt | 7 +- .../java/to/bitkit/repositories/TrezorRepo.kt | 5 +- docs/trezor-emulator.md | 69 +++++++++++++++++++ journeys/hardware-wallet/README.md | 17 +++-- 8 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 docs/trezor-emulator.md diff --git a/.env.example b/.env.example index 78d23fa49..52b9df57b 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,5 @@ # TREZOR_BRIDGE=true # TREZOR_BRIDGE_URL=http://10.0.2.2:21325 +# Point Trezor and hardware wallet flows at the local regtest Electrum from bitkit-docker. +# TREZOR_ELECTRUM_URL=tcp://10.0.2.2:60001 diff --git a/README.md b/README.md index d89a536e3..f35875107 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,10 @@ When testing the Trezor Bridge emulator from bitkit-docker through Android Studi ```properties TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 +TREZOR_ELECTRUM_URL=tcp://10.0.2.2:60001 ``` -CLI builds can still pass the same values as environment variables. +CLI builds can still pass the same values as environment variables. See [docs/trezor-emulator.md](docs/trezor-emulator.md) for the full emulator setup. ### Lint diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1866008f4..99757265e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,7 @@ val geoEnv = envFlag("GEO", default = true) val paykitUiDisabledEnv = envFlag("PAYKIT_UI_DISABLED", default = false) val trezorBridgeEnv = localProp("TREZOR_BRIDGE").map { it.toBoolean().toString() }.orElse("false") val trezorBridgeUrlEnv = localProp("TREZOR_BRIDGE_URL").orElse("http://10.0.2.2:21325") +val trezorElectrumUrlEnv = localProp("TREZOR_ELECTRUM_URL").orElse("") val networkPerFlavor = mapOf("dev" to "REGTEST", "mainnet" to "BITCOIN", "tnet" to "TESTNET") val requestedNdkVersion = providers.environmentVariable("NDK_VERSION").orNull?.takeIf { it.isNotBlank() } val androidTestAnnotationPackage = "to.bitkit.test.annotations" @@ -325,6 +326,7 @@ androidComponents { buildConfigFields.put("E2E_HOMEGATE_URL", e2eHomegateUrlEnv.stringField()) buildConfigFields.put("TREZOR_BRIDGE", trezorBridgeEnv.booleanField()) buildConfigFields.put("TREZOR_BRIDGE_URL", trezorBridgeUrlEnv.stringField()) + buildConfigFields.put("TREZOR_ELECTRUM_URL", trezorElectrumUrlEnv.stringField()) buildConfigFields.put("GEO", geoEnv.booleanField()) buildConfigFields.put("FEATURE_PAYKIT_UI_DISABLED", paykitUiDisabledEnv.booleanField()) buildConfigFields.put("LOCALES", provider { bcp47Locales.joinToString(",") }.stringField()) diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index e857ef58f..1764ce4fc 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -184,6 +184,17 @@ internal object Env { else -> "https://bitkit.stag0.blocktank.to/backups-ldk" } + /** + * Debug/E2E-only Electrum override for Trezor and hardware wallet flows, set via `TREZOR_ELECTRUM_URL`. + * Points them at the local regtest Electrum from bitkit-docker while the rest of the app keeps its + * configured server. + */ + val trezorElectrumUrl: String? + get() = BuildConfig.TREZOR_ELECTRUM_URL.takeIf { it.isNotBlank() && (isDebug || isE2eTest) } + + fun trezorElectrumUrlOrDefault(configured: String): String = + trezorElectrumUrl?.takeIf { network == Network.REGTEST } ?: configured + fun electrumUrlForNetwork(network: BitkitCoreNetwork): String { val isE2eLocal = isE2eTest && e2eBackend == "local" return when (network) { @@ -191,7 +202,7 @@ internal object Env { BitkitCoreNetwork.TESTNET, BitkitCoreNetwork.TESTNET4, BitkitCoreNetwork.SIGNET -> ElectrumServers.TESTNET BitkitCoreNetwork.REGTEST -> - if (isE2eLocal) ElectrumServers.REGTEST.LOCAL else ElectrumServers.REGTEST.STAG + trezorElectrumUrl ?: if (isE2eLocal) ElectrumServers.REGTEST.LOCAL else ElectrumServers.REGTEST.STAG } } diff --git a/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt b/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt index 4e368f709..c56757e15 100644 --- a/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt @@ -636,7 +636,12 @@ class HwWalletRepo @Inject constructor( val desiredWatchers = combine( hwWalletStore.data, settingsStore.data - .map { WatcherSettings(it.addressTypesToMonitor.toSet(), it.electrumServer) } + .map { + WatcherSettings( + monitoredTypes = it.addressTypesToMonitor.toSet(), + electrumUrl = Env.trezorElectrumUrlOrDefault(it.electrumServer), + ) + } .distinctUntilChanged(), ) { data, settings -> data.knownDevices to settings diff --git a/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt b/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt index 2c66ba607..f4777f70d 100644 --- a/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt @@ -975,7 +975,7 @@ class TrezorRepo @Inject constructor( gapLimit = gapLimit, ) trezorService.startWatcher(params, eventBridge) - TrezorDebugLog.log(WATCHER_TAG, "Started watcher '$watcherId'") + TrezorDebugLog.log(WATCHER_TAG, "Started watcher '$watcherId' on '$electrumUrl'") }.onFailure { Logger.error("Start watcher failed", it, context = TAG) _state.update { s -> s.copy(error = trezorErrorMessage(it)) } @@ -1240,7 +1240,8 @@ class TrezorRepo @Inject constructor( private fun electrumUrlForNetwork(network: BitkitCoreNetwork): String = Env.electrumUrlForNetwork(network) - private suspend fun currentElectrumUrl(): String = settingsStore.data.first().electrumServer + private suspend fun currentElectrumUrl(): String = + Env.trezorElectrumUrlOrDefault(settingsStore.data.first().electrumServer) private suspend fun ensureConnected() { if (trezorService.isConnected()) return diff --git a/docs/trezor-emulator.md b/docs/trezor-emulator.md new file mode 100644 index 000000000..5412140f6 --- /dev/null +++ b/docs/trezor-emulator.md @@ -0,0 +1,69 @@ +# Trezor Emulator + +The deterministic Trezor User Env from [`synonymdev/bitkit-docker`](https://github.com/synonymdev/bitkit-docker) stands in for a physical Trezor during development. It exposes an emulated T2T1 through Trezor Bridge over HTTP, so the app talks to it with the same wallet protocol it uses over USB. Everything except the USB stack, BLE and the one-time pair code is covered; see `journeys/hardware-wallet/README.md` for the full breakdown of what the Bridge transport does and does not simulate. + +## Start the Emulator + +Run the stack and the helper from a sibling `bitkit-docker` checkout: + +```sh +cd ../bitkit-docker +docker compose up -d +./scripts/trezor-emulator start +``` + +The helper wipes and sets up a deterministic device: model `T2T1`, firmware `2-main`, seed `all all all all all all all all all all all all`, no PIN, label `Bitkit Test Trezor`. Bridge listens on `21325` and the User Env dashboard on `9002`. Linux hosts need the host-network service: `docker compose --profile trezor-linux up -d trezor-user-env-linux`. + +Other subcommands: `status` (bridge/emulator state plus a Bridge enumerate), `adb` (reverse the Bridge port for a physical phone), `logs`, `stop`. Helper internals, environment overrides and troubleshooting live in `bitkit-docker/docs/trezor-emulator.md`. + +## Build Flags + +| Flag | Default | Effect | +| - | - | - | +| `TREZOR_BRIDGE` | `false` | Enables the Bridge transport in `TrezorBridgeTransport`. Without it, Bridge devices are never enumerated. | +| `TREZOR_BRIDGE_URL` | `http://10.0.2.2:21325` | Bridge endpoint. Use `http://127.0.0.1:21325` on a physical phone after `trezor-emulator adb`. | +| `TREZOR_ELECTRUM_URL` | unset | Electrum server for Trezor and hardware wallet flows only. The rest of the app keeps the server configured under Settings → Advanced → Electrum Server. | + +Each resolves in the same order: environment variable, then `-P` Gradle property, then `local.properties`. Android Studio builds read `local.properties`; CLI and `just` builds read `.env` or inline environment variables. See `.env.example` for the commented template. + +`TREZOR_ELECTRUM_URL` applies only to debug and E2E builds, and only on regtest. It is resolved by `Env.trezorElectrumUrl` and applied in `Env.electrumUrlForNetwork` (the Dev Settings watcher), `TrezorRepo.currentElectrumUrl` (account info, compose, broadcast) and `HwWalletRepo` (the production watchers). + +## Install + +Android emulator: + +```sh +TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 \ + TREZOR_ELECTRUM_URL=tcp://10.0.2.2:60001 ./gradlew installDevDebug +``` + +Physical phone, after reversing both the Bridge and Electrum ports: + +```sh +../bitkit-docker/scripts/trezor-emulator adb +adb reverse tcp:60001 tcp:60001 +TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 \ + TREZOR_ELECTRUM_URL=tcp://127.0.0.1:60001 ./gradlew installDevDebug +``` + +## Funding the Emulator Wallet + +Copy an address from Dev Settings → Trezor → Get Address, then send and mine from `bitkit-docker`: + +```sh +cd ../bitkit-docker +./bitcoin-cli send 0.1
+``` + +The balance reaches the hardware wallet tile only when `TREZOR_ELECTRUM_URL` points at the local regtest node. Without it, watchers run against the staging regtest Electrum, which does not know about locally mined coins. `Started watcher '' on ''` in the app log confirms which server is in use. + +## Smoke Checklist + +Open Settings → Advanced → Dev Settings → Trezor, then verify: + +- Scan lists the Bridge emulator device +- Connect succeeds and device features are shown +- Get address and get public key succeed +- Sign and verify message succeed +- Send or compose reaches the expected funded or no-funds state +- Disconnect, reconnect and forget-device cleanup behave correctly diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 248acf806..617102ff7 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -2,9 +2,9 @@ AI-driven UI test journeys for the home-screen hardware wallet features, designed to run against the deterministic Trezor emulator from `synonymdev/bitkit-docker` — no physical -Trezor required. Journeys follow the `android` CLI journey XML format: natural-language -`` steps evaluated sequentially against the running app; any failed step fails -the journey. +Trezor required; see `docs/trezor-emulator.md` for the emulator and build-flag setup. +Journeys follow the `android` CLI journey XML format: natural-language `` steps +evaluated sequentially against the running app; any failed step fails the journey. ## What the Bridge emulator does and does not simulate @@ -48,12 +48,17 @@ instead of UI interactions. ```sh TREZOR_PASSPHRASE_PROTECTION=true ../bitkit-docker/scripts/trezor-emulator start ``` -3. For a physical phone, reverse the Bridge port and install with Bridge enabled: +3. For a physical phone, reverse the Bridge and Electrum ports and install with Bridge enabled: ```sh ../bitkit-docker/scripts/trezor-emulator adb - TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 ./gradlew installDevDebug + adb reverse tcp:60001 tcp:60001 + TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 \ + TREZOR_ELECTRUM_URL=tcp://127.0.0.1:60001 ./gradlew installDevDebug ``` - For an Android emulator use `TREZOR_BRIDGE_URL=http://10.0.2.2:21325`. + For an Android emulator use `http://10.0.2.2:21325` and `tcp://10.0.2.2:60001` instead. + `TREZOR_ELECTRUM_URL` keeps the hardware wallet watchers on the local regtest node so + coins mined from `bitkit-docker` show up; see `docs/trezor-emulator.md` for the full + flag reference. 4. A wallet must exist in the app (onboarding completed) on regtest (dev flavor). ## Journeys From 2046f5b91339d480b5ed7197f63399fd35253c4f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Aug 2026 08:53:33 -0300 Subject: [PATCH 2/2] fix: scope trezor electrum override per network Co-Authored-By: Claude Opus 5 (1M context) --- app/src/main/java/to/bitkit/env/Env.kt | 4 ++-- .../to/bitkit/repositories/HwWalletRepo.kt | 10 ++++++++-- .../java/to/bitkit/repositories/TrezorRepo.kt | 18 +++++++++++------- .../ui/screens/trezor/TrezorViewModel.kt | 2 +- .../to/bitkit/repositories/HwWalletRepoTest.kt | 12 ++++++------ .../ui/screens/trezor/TrezorViewModelTest.kt | 6 +++--- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt index 1764ce4fc..d414437c1 100644 --- a/app/src/main/java/to/bitkit/env/Env.kt +++ b/app/src/main/java/to/bitkit/env/Env.kt @@ -192,8 +192,8 @@ internal object Env { val trezorElectrumUrl: String? get() = BuildConfig.TREZOR_ELECTRUM_URL.takeIf { it.isNotBlank() && (isDebug || isE2eTest) } - fun trezorElectrumUrlOrDefault(configured: String): String = - trezorElectrumUrl?.takeIf { network == Network.REGTEST } ?: configured + fun trezorElectrumUrlOrDefault(configured: String, network: BitkitCoreNetwork): String = + trezorElectrumUrl?.takeIf { network == BitkitCoreNetwork.REGTEST } ?: configured fun electrumUrlForNetwork(network: BitkitCoreNetwork): String { val isE2eLocal = isE2eTest && e2eBackend == "local" diff --git a/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt b/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt index c56757e15..b8585ed4c 100644 --- a/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt @@ -403,7 +403,10 @@ class HwWalletRepo @Inject constructor( signedTx: HwFundingSignedTx, ): Result = withContext(ioDispatcher) { runSuspendCatching { - val txId = trezorRepo.broadcastRawTx(serializedTx = signedTx.serializedTx).getOrThrow() + val txId = trezorRepo.broadcastRawTx( + serializedTx = signedTx.serializedTx, + network = Env.network.toCoreNetwork(), + ).getOrThrow() HwFundingBroadcastResult( txId = txId, miningFeeSats = signedTx.miningFeeSats, @@ -639,7 +642,10 @@ class HwWalletRepo @Inject constructor( .map { WatcherSettings( monitoredTypes = it.addressTypesToMonitor.toSet(), - electrumUrl = Env.trezorElectrumUrlOrDefault(it.electrumServer), + electrumUrl = Env.trezorElectrumUrlOrDefault( + configured = it.electrumServer, + network = Env.network.toCoreNetwork(), + ), ) } .distinctUntilChanged(), diff --git a/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt b/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt index f4777f70d..bc3a06e71 100644 --- a/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/TrezorRepo.kt @@ -447,7 +447,7 @@ class TrezorRepo @Inject constructor( awaitSetup() trezorService.getTransactionHistory( extendedKey = extendedKey, - electrumUrl = currentElectrumUrl(), + electrumUrl = currentElectrumUrl(network), network = network, scriptType = scriptType, ) @@ -466,7 +466,7 @@ class TrezorRepo @Inject constructor( awaitSetup() trezorService.getAccountInfo( extendedKey = extendedKey, - electrumUrl = currentElectrumUrl(), + electrumUrl = currentElectrumUrl(network), network = network, scriptType = scriptType, ) @@ -484,7 +484,7 @@ class TrezorRepo @Inject constructor( awaitSetup() trezorService.getAddressInfo( address = address, - electrumUrl = currentElectrumUrl(), + electrumUrl = currentElectrumUrl(network), network = network, ) }.onFailure { e -> @@ -509,7 +509,7 @@ class TrezorRepo @Inject constructor( val params = ComposeParams( wallet = WalletParams( extendedKey = extendedKey, - electrumUrl = currentElectrumUrl(), + electrumUrl = currentElectrumUrl(network), fingerprint = fingerprint, network = network, accountType = accountType, @@ -542,12 +542,13 @@ class TrezorRepo @Inject constructor( suspend fun broadcastRawTx( serializedTx: String, + network: BitkitCoreNetwork, ): Result = withContext(ioDispatcher) { runSuspendCatching { awaitSetup() trezorService.broadcastRawTx( serializedTx = serializedTx, - electrumUrl = currentElectrumUrl(), + electrumUrl = currentElectrumUrl(network), ) }.onFailure { Logger.error("Trezor broadcastRawTx failed", it, context = TAG) @@ -1240,8 +1241,11 @@ class TrezorRepo @Inject constructor( private fun electrumUrlForNetwork(network: BitkitCoreNetwork): String = Env.electrumUrlForNetwork(network) - private suspend fun currentElectrumUrl(): String = - Env.trezorElectrumUrlOrDefault(settingsStore.data.first().electrumServer) + private suspend fun currentElectrumUrl(network: BitkitCoreNetwork): String = + Env.trezorElectrumUrlOrDefault( + configured = settingsStore.data.first().electrumServer, + network = network, + ) private suspend fun ensureConnected() { if (trezorService.isConnected()) return diff --git a/app/src/main/java/to/bitkit/ui/screens/trezor/TrezorViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/trezor/TrezorViewModel.kt index e79abfdef..67d6d6cf8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/trezor/TrezorViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/trezor/TrezorViewModel.kt @@ -447,7 +447,7 @@ class TrezorViewModel @Inject constructor( val signedStep = state.sendStep as? SendStep.Signed ?: return@launch val rawTx = signedStep.signedTx.serializedTx _uiState.update { it.copy(send = it.send.copy(isBroadcasting = true)) } - trezorRepo.broadcastRawTx(serializedTx = rawTx) + trezorRepo.broadcastRawTx(serializedTx = rawTx, network = state.selectedNetwork) .onSuccess { txid -> TrezorDebugLog.log("BROADCAST", "SUCCESS txid=$txid") _uiState.update { diff --git a/app/src/test/java/to/bitkit/repositories/HwWalletRepoTest.kt b/app/src/test/java/to/bitkit/repositories/HwWalletRepoTest.kt index 5eb92f8b6..1b2649ad4 100644 --- a/app/src/test/java/to/bitkit/repositories/HwWalletRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/HwWalletRepoTest.kt @@ -1555,7 +1555,7 @@ class HwWalletRepoTest : BaseUnitTest() { assertEquals(true, result.isFailure) verify(trezorRepo, never()).signTxFromPsbt(any(), anyOrNull()) - verify(trezorRepo, never()).broadcastRawTx(any()) + verify(trezorRepo, never()).broadcastRawTx(any(), any()) verify(trezorRepo, never()).disconnectStaleSession(any()) } @@ -1585,7 +1585,7 @@ class HwWalletRepoTest : BaseUnitTest() { assertEquals(1_250uL, result.getOrThrow().miningFeeSats) assertEquals(3uL, result.getOrThrow().feeRate) assertEquals(26_250uL, result.getOrThrow().totalSpent) - verify(trezorRepo, never()).broadcastRawTx(any()) + verify(trezorRepo, never()).broadcastRawTx(any(), any()) } @Test @@ -1596,7 +1596,7 @@ class HwWalletRepoTest : BaseUnitTest() { feeRate = 3uL, totalSpent = 26_250uL, ) - whenever(trezorRepo.broadcastRawTx("rawtx")).thenReturn(Result.success("broadcast-txid")) + whenever(trezorRepo.broadcastRawTx(eq("rawtx"), any())).thenReturn(Result.success("broadcast-txid")) val sut = createRepo() val result = sut.broadcastFunding(signedTx) @@ -1617,7 +1617,7 @@ class HwWalletRepoTest : BaseUnitTest() { feeRate = 3uL, totalSpent = 26_250uL, ) - whenever(trezorRepo.broadcastRawTx("rawtx")) + whenever(trezorRepo.broadcastRawTx(eq("rawtx"), any())) .thenReturn(Result.success("core-derived-txid")) val sut = createRepo() @@ -1645,7 +1645,7 @@ class HwWalletRepoTest : BaseUnitTest() { assertEquals(true, result.isFailure) verify(trezorRepo).disconnectStaleSession("dev1") - verify(trezorRepo, never()).broadcastRawTx(any()) + verify(trezorRepo, never()).broadcastRawTx(any(), any()) } @Test @@ -1666,7 +1666,7 @@ class HwWalletRepoTest : BaseUnitTest() { assertEquals(true, result.isFailure) verify(trezorRepo, never()).disconnectStaleSession(any()) - verify(trezorRepo, never()).broadcastRawTx(any()) + verify(trezorRepo, never()).broadcastRawTx(any(), any()) } @Test diff --git a/app/src/test/java/to/bitkit/ui/screens/trezor/TrezorViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/trezor/TrezorViewModelTest.kt index fa1bd073a..5e9901723 100644 --- a/app/src/test/java/to/bitkit/ui/screens/trezor/TrezorViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/trezor/TrezorViewModelTest.kt @@ -262,14 +262,14 @@ class TrezorViewModelTest : BaseUnitTest() { sut.broadcastSignedTx() advanceUntilIdle() - verify(trezorRepo, never()).broadcastRawTx(any()) + verify(trezorRepo, never()).broadcastRawTx(any(), any()) } @Test fun `broadcastSignedTx should not restore signed step after reset`() = test { loadSignedTx() val broadcastResult = CompletableDeferred>() - whenever(trezorRepo.broadcastRawTx(any())) + whenever(trezorRepo.broadcastRawTx(any(), any())) .doSuspendableAnswer { broadcastResult.await() } sut.broadcastSignedTx() @@ -293,7 +293,7 @@ class TrezorViewModelTest : BaseUnitTest() { val broadcastResults = ArrayDeque( listOf(firstBroadcastResult, secondBroadcastResult) ) - whenever(trezorRepo.broadcastRawTx(any())) + whenever(trezorRepo.broadcastRawTx(any(), any())) .doSuspendableAnswer { broadcastResults.removeFirst().await() } sut.broadcastSignedTx()