From 8578fe277dc424e16f7f339b5c63c2e8d3d35531 Mon Sep 17 00:00:00 2001 From: Usman Umar Garba Date: Sat, 3 Oct 2026 21:28:59 +0100 Subject: [PATCH 1/4] test(integration): migrate to local regtest and replace sync with fullScan - Adds an isolated CI job using the infinity-pro Regtest container. - Uses dart:io HttpClient to implement deterministic Bitcoin Core Regtest funding. - Adds local funded fullScan() integration tests for Electrum and Esplora. - Updates dart_test.yaml and test tags to isolate regtest execution. --- .github/workflows/ci.yml | 47 ++++++ dart_test.yaml | 3 + test/integration/electrum_full_scan_test.dart | 130 ++++++++++++++++ test/integration/esplora_full_scan_test.dart | 139 ++++++++++++++++++ test/integration/regtest_funding_helper.dart | 84 +++++++++++ 5 files changed, 403 insertions(+) create mode 100644 dart_test.yaml create mode 100644 test/integration/electrum_full_scan_test.dart create mode 100644 test/integration/esplora_full_scan_test.dart create mode 100644 test/integration/regtest_funding_helper.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fd1ff5..05f7f66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,53 @@ jobs: working-directory: bdk_demo run: flutter test + integration-tests: + runs-on: ubuntu-latest + services: + regtest: + image: ghcr.io/thunderbiscuit/podman-regtest-infinity-pro:0.4.0 + ports: + - 18443:18443 + - 18444:18444 + - 3002:3002 + - 60401:60401 + options: >- + --health-cmd "test $(bitcoin-cli -regtest -rpcuser=regtest -rpcpassword=password getblockcount) -gt 100" + --health-interval 3s + --health-timeout 5s + --health-retries 20 + env: + BDK_DART_RUN_INTEGRATION: "1" + BDK_DART_ELECTRUM_URL: "tcp://127.0.0.1:60401" + BDK_DART_ESPLORA_URL: "http://127.0.0.1:3002" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: "3.10.0" + + - name: Generate bindings and native library + run: | + chmod +x scripts/generate_bindings.sh + ./scripts/generate_bindings.sh + + - name: Pub get + run: dart pub get + + - name: Run integration tests + run: dart test --tags regtest + windows-smoke: runs-on: windows-latest diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..fef4f89 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + integration: + regtest: diff --git a/test/integration/electrum_full_scan_test.dart b/test/integration/electrum_full_scan_test.dart new file mode 100644 index 0000000..88f22cd --- /dev/null +++ b/test/integration/electrum_full_scan_test.dart @@ -0,0 +1,130 @@ +@Tags(['integration', 'regtest']) +import 'dart:async'; +import 'dart:io'; + +import 'package:bdk_dart/bdk.dart'; +import 'package:test/test.dart'; + +import '../test_constants.dart'; +import 'integration_helpers.dart'; +import 'regtest_funding_helper.dart'; + +Future _deleteDirectoryWithRetry(Directory dir) async { + for (var attempt = 0; attempt < 10; attempt++) { + try { + if (dir.existsSync()) await dir.delete(recursive: true); + return; + } on PathAccessException { + await Future.delayed(const Duration(milliseconds: 100)); + } + } + if (dir.existsSync()) await dir.delete(recursive: true); +} + +String _createTempSqlitePath() { + final tempDir = Directory.systemTemp.createTempSync( + 'bdk_dart_electrum_fullscan_', + ); + addTearDown(() => _deleteDirectoryWithRetry(tempDir)); + return '${tempDir.path}/wallet.sqlite'; +} + +void main() { + group('Electrum fullScan integration', () { + test( + 'fund wallet and fullScan successfully', + () async { + final disposers = []; + final sqlitePath = _createTempSqlitePath(); + + try { + final descriptor = buildBip84Descriptor(Network.regtest); + addDisposer(disposers, descriptor.dispose); + + final changeDescriptor = buildBip84ChangeDescriptor(Network.regtest); + addDisposer(disposers, changeDescriptor.dispose); + + final persister = Persister.newSqlite(path: sqlitePath); + addDisposer(disposers, persister.dispose); + + final wallet = Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: Network.regtest, + persister: persister, + lookahead: defaultLookahead, + ); + addDisposer(disposers, wallet.dispose); + + final revealed = wallet.revealNextAddress( + keychain: KeychainKind.external_, + ); + addDisposer(disposers, revealed.address.dispose); + wallet.persist(persister: persister); + + final client = buildElectrumClientFromEnv(); + addDisposer(disposers, client.dispose); + + final addressString = revealed.address.toString(); + printOnFailure('Funding Regtest address: $addressString'); + + final txid = await fundRegtestAddress(addressString, amount: 1.0); + printOnFailure('Funding transaction ID: $txid'); + + var balance = wallet.balance().total; + Object? lastError; + + for (var attempt = 0; attempt < 15; attempt++) { + try { + final requestBuilder = wallet.startFullScan(); + addDisposer(disposers, requestBuilder.dispose); + + final request = requestBuilder.build(); + addDisposer(disposers, request.dispose); + + final update = client.fullScan( + request: request, + stopGap: 20, + batchSize: 100, + fetchPrevTxouts: true, + ); + addDisposer(disposers, update.dispose); + + wallet.applyUpdate(update: update); + balance = wallet.balance().total; + + if (balance.toSat() > 0) { + break; + } + } catch (error) { + lastError = error; + printOnFailure( + 'Electrum fullScan attempt ${attempt + 1} failed: $error', + ); + } + + if (balance.toSat() == 0 && attempt < 14) { + await Future.delayed(const Duration(seconds: 1)); + } + } + + wallet.persist(persister: persister); + + printOnFailure('Final Wallet Balance: ${balance.toSat()} sats'); + + expect( + balance.toSat(), + greaterThan(0), + reason: + 'Wallet should have a positive balance after funding. ' + 'Last error: $lastError', + ); + } finally { + disposeAll(disposers); + } + }, + skip: integrationSkipReason(requiredEnv: [electrumUrlEnv]), + timeout: const Timeout(Duration(minutes: 2)), + ); + }); +} diff --git a/test/integration/esplora_full_scan_test.dart b/test/integration/esplora_full_scan_test.dart new file mode 100644 index 0000000..a9a3f7e --- /dev/null +++ b/test/integration/esplora_full_scan_test.dart @@ -0,0 +1,139 @@ +@Tags(['integration', 'regtest']) +import 'dart:async'; +import 'dart:io'; + +import 'package:bdk_dart/bdk.dart'; +import 'package:test/test.dart'; + +import '../test_constants.dart'; +import 'integration_helpers.dart'; +import 'regtest_funding_helper.dart'; + +Future _deleteDirectoryWithRetry(Directory dir) async { + for (var attempt = 0; attempt < 10; attempt++) { + try { + if (dir.existsSync()) await dir.delete(recursive: true); + return; + } on PathAccessException { + await Future.delayed(const Duration(milliseconds: 100)); + } + } + if (dir.existsSync()) await dir.delete(recursive: true); +} + +String _createTempSqlitePath() { + final tempDir = Directory.systemTemp.createTempSync( + 'bdk_dart_esplora_fullscan_', + ); + addTearDown(() => _deleteDirectoryWithRetry(tempDir)); + return '${tempDir.path}/wallet.sqlite'; +} + +void main() { + group('Esplora fullScan integration', () { + test( + 'fund wallet and fullScan successfully', + () async { + final disposers = []; + final sqlitePath = _createTempSqlitePath(); + + try { + final descriptor = buildBip84Descriptor(Network.regtest); + addDisposer(disposers, descriptor.dispose); + + final changeDescriptor = buildBip84ChangeDescriptor(Network.regtest); + addDisposer(disposers, changeDescriptor.dispose); + + final persister = Persister.newSqlite(path: sqlitePath); + addDisposer(disposers, persister.dispose); + + final wallet = Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: Network.regtest, + persister: persister, + lookahead: defaultLookahead, + ); + addDisposer(disposers, wallet.dispose); + + final revealed = wallet.revealNextAddress( + keychain: KeychainKind.external_, + ); + addDisposer(disposers, revealed.address.dispose); + wallet.persist(persister: persister); + + final client = buildEsploraClientFromEnv(); + // note: esplora client doesn't need explicit dispose + + final addressString = revealed.address.toString(); + printOnFailure('Funding Regtest address: $addressString'); + + final txid = await fundRegtestAddress(addressString, amount: 1.0); + printOnFailure('Funding transaction ID: $txid'); + + // Deterministic wait: Poll Esplora for the transaction + Object? lastError; + var txFound = false; + + for (var attempt = 0; attempt < 15; attempt++) { + try { + final tx = client.getTx(txid: Txid.fromString(hex: txid)); + if (tx != null) { + txFound = true; + break; + } + } catch (error) { + lastError = error; + printOnFailure( + 'Esplora getTx attempt ${attempt + 1} failed: $error', + ); + } + + if (!txFound && attempt < 14) { + await Future.delayed(const Duration(seconds: 1)); + } + } + + expect( + txFound, + isTrue, + reason: + 'Esplora should index the funding transaction within the timeout. ' + 'Last error: $lastError', + ); + + // Now perform the single fullScan since we know it's indexed + final requestBuilder = wallet.startFullScan(); + addDisposer(disposers, requestBuilder.dispose); + + final request = requestBuilder.build(); + addDisposer(disposers, request.dispose); + + final update = client.fullScan( + request: request, + stopGap: 20, + parallelRequests: 1, + ); + addDisposer(disposers, update.dispose); + + wallet.applyUpdate(update: update); + final balance = wallet.balance().total; + + wallet.persist(persister: persister); + + printOnFailure('Final Wallet Balance: ${balance.toSat()} sats'); + + expect( + balance.toSat(), + greaterThan(0), + reason: 'Wallet should have a positive balance after funding', + ); + } finally { + disposeAll(disposers); + } + }, + skip: integrationSkipReason(requiredEnv: [esploraUrlEnv]), + timeout: const Timeout(Duration(minutes: 2)), + ); + }); +} diff --git a/test/integration/regtest_funding_helper.dart b/test/integration/regtest_funding_helper.dart new file mode 100644 index 0000000..60a8b49 --- /dev/null +++ b/test/integration/regtest_funding_helper.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'dart:io'; + +String _envOrDefault(String key, String defaultValue) { + final value = Platform.environment[key]; + return (value != null && value.trim().isNotEmpty) + ? value.trim() + : defaultValue; +} + +/// Makes a JSON-RPC call to the local Bitcoin Core Regtest node. +Future _rpcCall(String method, [dynamic params = const []]) async { + // Note the /wallet/faucet path for wallet-specific commands + final rpcUrl = _envOrDefault( + 'BDK_DART_BITCOIN_RPC_URL', + 'http://127.0.0.1:18443/wallet/faucet', + ); + final rpcUser = _envOrDefault('BDK_DART_BITCOIN_RPC_USER', 'regtest'); + final rpcPassword = _envOrDefault( + 'BDK_DART_BITCOIN_RPC_PASSWORD', + 'password', + ); + + final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 10); + + try { + final request = await client + .postUrl(Uri.parse(rpcUrl)) + .timeout(const Duration(seconds: 10)); + + final auth = base64Encode(utf8.encode('$rpcUser:$rpcPassword')); + request.headers.set(HttpHeaders.authorizationHeader, 'Basic $auth'); + request.headers.contentType = ContentType.json; + + final body = jsonEncode({ + 'jsonrpc': '1.0', + 'id': 'bdk_dart_integration', + 'method': method, + 'params': params, + }); + + request.write(body); + + final response = await request.close().timeout(const Duration(seconds: 10)); + final responseBody = await response.transform(utf8.decoder).join(); + + if (response.statusCode != HttpStatus.ok) { + throw Exception('RPC Error (HTTP ${response.statusCode}): $responseBody'); + } + + final decoded = jsonDecode(responseBody) as Map; + if (decoded['error'] != null) { + throw Exception('RPC JSON Error: ${decoded['error']}'); + } + + return decoded['result']; + } finally { + client.close(); + } +} + +/// Funds a Regtest address with the given [amount] (in BTC) and mines 2 blocks +/// to confirm it. +/// +/// Returns the transaction ID as a String. +Future fundRegtestAddress(String address, {double amount = 1.0}) async { + // 1. Send BTC to the provided address with a fixed fee rate to avoid estimation errors + final txid = + await _rpcCall('sendtoaddress', { + 'address': address, + 'amount': amount, + 'fee_rate': 1.0, // 1 sat/vB + }) + as String; + + // 2. Get a new address to receive the block rewards + final minerAddress = await _rpcCall('getnewaddress') as String; + + // 3. Mine 2 blocks to confirm the transaction (2 confirmations) + await _rpcCall('generatetoaddress', [2, minerAddress]); + + return txid; +} From 2815be3b5ae5aa50d1486c7aa420e0863512ec82 Mon Sep 17 00:00:00 2001 From: Usman Umar Garba Date: Sun, 18 Oct 2026 20:37:13 +0100 Subject: [PATCH 2/4] test(integration): verify fullScan confirmation on regtest --- test/integration/electrum_full_scan_test.dart | 146 +++++++++++++- test/integration/esplora_full_scan_test.dart | 189 ++++++++++++++++-- test/integration/regtest_funding_helper.dart | 39 ++-- 3 files changed, 339 insertions(+), 35 deletions(-) diff --git a/test/integration/electrum_full_scan_test.dart b/test/integration/electrum_full_scan_test.dart index 88f22cd..5c47aef 100644 --- a/test/integration/electrum_full_scan_test.dart +++ b/test/integration/electrum_full_scan_test.dart @@ -68,11 +68,106 @@ void main() { final addressString = revealed.address.toString(); printOnFailure('Funding Regtest address: $addressString'); - final txid = await fundRegtestAddress(addressString, amount: 1.0); + final txid = await broadcastRegtestAddress( + addressString, + amount: 1.0, + ); printOnFailure('Funding transaction ID: $txid'); + // Phase 1: Scan while the funding transaction is still unconfirmed. + TxUnconfirmedWalletEvent? unconfirmedEvent; + var unconfirmedPositionSeen = false; + Object? lastUnconfirmedError; + + for (var attempt = 0; attempt < 15; attempt++) { + try { + final requestBuilder = wallet.startFullScan(); + addDisposer(disposers, requestBuilder.dispose); + + final request = requestBuilder.build(); + addDisposer(disposers, request.dispose); + + final update = client.fullScan( + request: request, + stopGap: 20, + batchSize: 100, + fetchPrevTxouts: true, + ); + addDisposer(disposers, update.dispose); + + final events = wallet.applyUpdateEvents(update: update); + + for (final event in events.whereType()) { + if (event.txid.toString() == txid) { + unconfirmedEvent = event; + } + } + + final details = wallet.txDetails( + txid: Txid.fromString(hex: txid), + ); + + if (details != null && + details.chainPosition is UnconfirmedChainPosition) { + unconfirmedPositionSeen = true; + } + + if (unconfirmedEvent != null && unconfirmedPositionSeen) { + break; + } + } catch (error) { + lastUnconfirmedError = error; + printOnFailure( + 'Electrum unconfirmed scan attempt ${attempt + 1} failed: $error', + ); + } + + if (attempt < 14) { + await Future.delayed(const Duration(seconds: 1)); + } + } + + // Assert that the Dart API exposed the unconfirmed transaction state. + expect( + unconfirmedEvent, + isNotNull, + reason: + 'The funded transaction should produce a TxUnconfirmedWalletEvent. ' + 'Last error: $lastUnconfirmedError', + ); + + final pendingEvent = unconfirmedEvent!; + + expect(pendingEvent.tx.computeTxid().toString(), equals(txid)); + expect( + pendingEvent.oldBlockTime, + isNull, + reason: 'The transaction is first observed in the mempool.', + ); + + expect( + unconfirmedPositionSeen, + isTrue, + reason: + 'The wallet should expose UnconfirmedChainPosition before mining.', + ); + + final pendingDetails = wallet.txDetails( + txid: Txid.fromString(hex: txid), + ); + + expect(pendingDetails, isNotNull); + expect( + pendingDetails!.chainPosition, + isA(), + ); + + // Phase 2: Confirm the transaction, then run the existing confirmed scan loop. + await mineRegtestBlocks(); + var balance = wallet.balance().total; Object? lastError; + TxConfirmedWalletEvent? confirmedEvent; for (var attempt = 0; attempt < 15; attempt++) { try { @@ -90,10 +185,17 @@ void main() { ); addDisposer(disposers, update.dispose); - wallet.applyUpdate(update: update); + final events = wallet.applyUpdateEvents(update: update); + + for (final event in events.whereType()) { + if (event.txid.toString() == txid) { + confirmedEvent = event; + } + } + balance = wallet.balance().total; - if (balance.toSat() > 0) { + if (confirmedEvent != null && balance.toSat() > 0) { break; } } catch (error) { @@ -103,7 +205,7 @@ void main() { ); } - if (balance.toSat() == 0 && attempt < 14) { + if (confirmedEvent == null && attempt < 14) { await Future.delayed(const Duration(seconds: 1)); } } @@ -119,6 +221,42 @@ void main() { 'Wallet should have a positive balance after funding. ' 'Last error: $lastError', ); + + expect( + confirmedEvent, + isNotNull, + reason: + 'The funded transaction should produce a confirmed wallet event.', + ); + + final event = confirmedEvent!; + + expect(event.tx.computeTxid().toString(), equals(txid)); + expect(event.blockTime.blockId.height, greaterThan(0)); + expect(event.blockTime.blockId.hash.toString(), isNotEmpty); + expect(event.blockTime.confirmationTime, greaterThan(0)); + + final details = wallet.txDetails(txid: Txid.fromString(hex: txid)); + + expect(details, isNotNull); + expect(details!.txid.toString(), equals(txid)); + expect(details.tx.computeTxid().toString(), equals(txid)); + expect(details.chainPosition, isA()); + + final position = details.chainPosition as ConfirmedChainPosition; + + expect( + position.confirmationBlockTime.blockId.height, + equals(event.blockTime.blockId.height), + ); + expect( + position.confirmationBlockTime.blockId.hash.toString(), + equals(event.blockTime.blockId.hash.toString()), + ); + expect( + position.confirmationBlockTime.confirmationTime, + equals(event.blockTime.confirmationTime), + ); } finally { disposeAll(disposers); } diff --git a/test/integration/esplora_full_scan_test.dart b/test/integration/esplora_full_scan_test.dart index a9a3f7e..3a512f7 100644 --- a/test/integration/esplora_full_scan_test.dart +++ b/test/integration/esplora_full_scan_test.dart @@ -68,10 +68,13 @@ void main() { final addressString = revealed.address.toString(); printOnFailure('Funding Regtest address: $addressString'); - final txid = await fundRegtestAddress(addressString, amount: 1.0); + final txid = await broadcastRegtestAddress( + addressString, + amount: 1.0, + ); printOnFailure('Funding transaction ID: $txid'); - // Deterministic wait: Poll Esplora for the transaction + // Deterministic wait: Poll Esplora for the transaction in mempool Object? lastError; var txFound = false; @@ -102,22 +105,140 @@ void main() { 'Last error: $lastError', ); - // Now perform the single fullScan since we know it's indexed - final requestBuilder = wallet.startFullScan(); - addDisposer(disposers, requestBuilder.dispose); + // Phase 1: Scan while the transaction is unconfirmed. + TxUnconfirmedWalletEvent? unconfirmedEvent; + var unconfirmedPositionSeen = false; + Object? lastUnconfirmedError; + + for (var attempt = 0; attempt < 15; attempt++) { + try { + final requestBuilder = wallet.startFullScan(); + addDisposer(disposers, requestBuilder.dispose); + + final request = requestBuilder.build(); + addDisposer(disposers, request.dispose); + + final update = client.fullScan( + request: request, + stopGap: 20, + parallelRequests: 1, + ); + addDisposer(disposers, update.dispose); + + final events = wallet.applyUpdateEvents(update: update); + + for (final event + in events.whereType()) { + if (event.txid.toString() == txid) { + unconfirmedEvent = event; + } + } + + final details = wallet.txDetails( + txid: Txid.fromString(hex: txid), + ); + + if (details != null && + details.chainPosition is UnconfirmedChainPosition) { + unconfirmedPositionSeen = true; + } + + if (unconfirmedEvent != null && unconfirmedPositionSeen) { + break; + } + } catch (error) { + lastUnconfirmedError = error; + printOnFailure( + 'Esplora unconfirmed scan attempt ${attempt + 1} failed: $error', + ); + } + + if (attempt < 14) { + await Future.delayed(const Duration(seconds: 1)); + } + } + + expect( + unconfirmedEvent, + isNotNull, + reason: + 'The funded transaction should produce a TxUnconfirmedWalletEvent. ' + 'Last error: $lastUnconfirmedError', + ); + + final pendingEvent = unconfirmedEvent!; - final request = requestBuilder.build(); - addDisposer(disposers, request.dispose); + expect(pendingEvent.tx.computeTxid().toString(), equals(txid)); + expect( + pendingEvent.oldBlockTime, + isNull, + reason: 'The transaction is first observed in the mempool.', + ); + + expect( + unconfirmedPositionSeen, + isTrue, + reason: + 'The wallet should expose UnconfirmedChainPosition before mining.', + ); - final update = client.fullScan( - request: request, - stopGap: 20, - parallelRequests: 1, + final pendingDetails = wallet.txDetails( + txid: Txid.fromString(hex: txid), + ); + + expect(pendingDetails, isNotNull); + expect( + pendingDetails!.chainPosition, + isA(), ); - addDisposer(disposers, update.dispose); - wallet.applyUpdate(update: update); - final balance = wallet.balance().total; + // Phase 2: Mine the transaction before running the confirmed scan. + await mineRegtestBlocks(); + + // Phase 3: Run the confirmed scan loop until confirmedEvent is captured + var balance = wallet.balance().total; + TxConfirmedWalletEvent? confirmedEvent; + Object? lastConfirmedError; + + for (var attempt = 0; attempt < 15; attempt++) { + try { + final requestBuilder = wallet.startFullScan(); + addDisposer(disposers, requestBuilder.dispose); + + final request = requestBuilder.build(); + addDisposer(disposers, request.dispose); + + final update = client.fullScan( + request: request, + stopGap: 20, + parallelRequests: 1, + ); + addDisposer(disposers, update.dispose); + + final events = wallet.applyUpdateEvents(update: update); + + for (final event in events.whereType()) { + if (event.txid.toString() == txid) { + confirmedEvent = event; + } + } + + balance = wallet.balance().total; + + if (confirmedEvent != null && balance.toSat() > 0) { + break; + } + } catch (error) { + lastConfirmedError = error; + printOnFailure( + 'Esplora confirmed scan attempt ${attempt + 1} failed: $error', + ); + } + + if (confirmedEvent == null && attempt < 14) { + await Future.delayed(const Duration(seconds: 1)); + } + } wallet.persist(persister: persister); @@ -126,7 +247,45 @@ void main() { expect( balance.toSat(), greaterThan(0), - reason: 'Wallet should have a positive balance after funding', + reason: + 'Wallet should have a positive balance after funding. ' + 'Last error: $lastConfirmedError', + ); + + expect( + confirmedEvent, + isNotNull, + reason: + 'The funded transaction should produce a confirmed wallet event.', + ); + + final event = confirmedEvent!; + + expect(event.tx.computeTxid().toString(), equals(txid)); + expect(event.blockTime.blockId.height, greaterThan(0)); + expect(event.blockTime.blockId.hash.toString(), isNotEmpty); + expect(event.blockTime.confirmationTime, greaterThan(0)); + + final details = wallet.txDetails(txid: Txid.fromString(hex: txid)); + + expect(details, isNotNull); + expect(details!.txid.toString(), equals(txid)); + expect(details.tx.computeTxid().toString(), equals(txid)); + expect(details.chainPosition, isA()); + + final position = details.chainPosition as ConfirmedChainPosition; + + expect( + position.confirmationBlockTime.blockId.height, + equals(event.blockTime.blockId.height), + ); + expect( + position.confirmationBlockTime.blockId.hash.toString(), + equals(event.blockTime.blockId.hash.toString()), + ); + expect( + position.confirmationBlockTime.confirmationTime, + equals(event.blockTime.confirmationTime), ); } finally { disposeAll(disposers); diff --git a/test/integration/regtest_funding_helper.dart b/test/integration/regtest_funding_helper.dart index 60a8b49..8555248 100644 --- a/test/integration/regtest_funding_helper.dart +++ b/test/integration/regtest_funding_helper.dart @@ -60,25 +60,32 @@ Future _rpcCall(String method, [dynamic params = const []]) async { } } -/// Funds a Regtest address with the given [amount] (in BTC) and mines 2 blocks -/// to confirm it. +/// Broadcasts a transaction to a Regtest address without mining it. /// -/// Returns the transaction ID as a String. -Future fundRegtestAddress(String address, {double amount = 1.0}) async { - // 1. Send BTC to the provided address with a fixed fee rate to avoid estimation errors - final txid = - await _rpcCall('sendtoaddress', { - 'address': address, - 'amount': amount, - 'fee_rate': 1.0, // 1 sat/vB - }) - as String; +/// Returns the transaction ID. +Future broadcastRegtestAddress( + String address, { + double amount = 1.0, +}) async { + return await _rpcCall('sendtoaddress', { + 'address': address, + 'amount': amount, + 'fee_rate': 1.0, + }) + as String; +} - // 2. Get a new address to receive the block rewards +/// Mines [blocks] Regtest blocks. +Future mineRegtestBlocks({int blocks = 2}) async { final minerAddress = await _rpcCall('getnewaddress') as String; + await _rpcCall('generatetoaddress', [blocks, minerAddress]); +} - // 3. Mine 2 blocks to confirm the transaction (2 confirmations) - await _rpcCall('generatetoaddress', [2, minerAddress]); - +/// Funds a Regtest address and mines 2 blocks to confirm the transaction. +/// +/// Returns the transaction ID. +Future fundRegtestAddress(String address, {double amount = 1.0}) async { + final txid = await broadcastRegtestAddress(address, amount: amount); + await mineRegtestBlocks(); return txid; } From f608bc3c7cc945bc225044aa193c0912d467d3dc Mon Sep 17 00:00:00 2001 From: Usman Umar Garba Date: Sun, 18 Oct 2026 21:28:36 +0100 Subject: [PATCH 3/4] ci: fix Android SDK setup and format integration test --- .github/workflows/ci.yml | 2 ++ test/integration/electrum_full_scan_test.dart | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05f7f66..f8481fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,8 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 + with: + packages: 'platform-tools' - name: Setup Dart uses: dart-lang/setup-dart@v1 diff --git a/test/integration/electrum_full_scan_test.dart b/test/integration/electrum_full_scan_test.dart index 5c47aef..6ea247f 100644 --- a/test/integration/electrum_full_scan_test.dart +++ b/test/integration/electrum_full_scan_test.dart @@ -97,7 +97,8 @@ void main() { final events = wallet.applyUpdateEvents(update: update); - for (final event in events.whereType()) { + for (final event + in events.whereType()) { if (event.txid.toString() == txid) { unconfirmedEvent = event; } From 5c64fd0246a48fa766153b4ee226938496e45a26 Mon Sep 17 00:00:00 2001 From: Usman Umar Garba <110467626+Ugarba202@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:19:38 +0100 Subject: [PATCH 4/4] Fix Android SDK setup by removing empty packages Removed empty packages entry for Android SDK setup. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c17ec5d..0a6060f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,7 +202,7 @@ jobs: uses: android-actions/setup-android@v3 with: packages: 'platform-tools' - packages: "" + - name: Setup Dart uses: dart-lang/setup-dart@v1