4 · Fund Alice
Alice needs coins before she can pay. An SPV wallet counts only coins it can prove, so you won’t just send to her address and wait. You’ll:
- Get a receive address for Alice.
- Send coins from the faucet through Arcade.
- Mine a block, once a miner holds the transaction.
- Wait for Alice’s headers to reach that block.
- Build a BEEF from Arcade’s merkle proof and import it.
Localnet helpers
Section titled “Localnet helpers”Add these to lib/localnet.dart. They drive the localnet scripts and Arcade’s API. They’re tutorial code, not libspiffy API.
import 'dart:convert';import 'dart:typed_data';
import 'package:http/http.dart' as http;
/// Sends [satoshis] from the localnet faucet to [address] through Arcade,/// so that Arcade follows the payment to its merkle proof.Future<({String txid, String rawHex})> faucetSend( String address, int satoshis) async { final result = await Process.run( './scripts/faucet.sh', ['send', address, '$satoshis', '--arcade'], workingDirectory: localnetDir, ); final out = '${result.stdout}'; final txid = RegExp(r'^txid\s+([0-9a-f]{64})$', multiLine: true).firstMatch(out); final hex = RegExp(r'^hex\s+([0-9a-f]+)$', multiLine: true).firstMatch(out); if (result.exitCode != 0 || txid == null || hex == null) { throw StateError('faucet send failed: $out ${result.stderr}'); } return (txid: txid.group(1)!, rawHex: hex.group(1)!);}
/// Mines one block on localnet.Future<void> mineBlock() async { final result = await Process.run('./scripts/mine.sh', ['1'], workingDirectory: localnetDir); if (result.exitCode != 0) { throw StateError('mine.sh failed: ${result.stdout} ${result.stderr}'); }}
/// Arcade's view of [txid]: its `txStatus`, and `merklePath` and/// `blockHeight` once it is mined.Future<Map<String, dynamic>> arcadeStatus(String txid) async { final response = await http.get(Uri.parse('$arcadeUrl/tx/$txid')); if (response.statusCode != 200) { throw StateError('Arcade has no record of $txid: ${response.body}'); } return jsonDecode(response.body) as Map<String, dynamic>;}
/// Waits until Arcade reports [txid] in one of [statuses].Future<Map<String, dynamic>> waitForArcade(String txid, Set<String> statuses, {Duration timeout = const Duration(seconds: 60)}) async { final deadline = DateTime.now().add(timeout); while (true) { final status = await arcadeStatus(txid); if (statuses.contains(status['txStatus'])) return status; if (DateTime.now().isAfter(deadline)) { throw StateError('$txid is ${status['txStatus']}, not one of $statuses'); } await Future<void>.delayed(const Duration(milliseconds: 500)); }}
/// A mined transaction as a BEEF: the raw transaction plus the merkle path/// (a BUMP) that Arcade built for it.Uint8List minedBeef(String rawHex, String merklePath) => BEEF.create( bumps: [BUMP.fromHex(merklePath)], txs: [Uint8List.fromList(hexDecode(rawHex))], hasMerkle: [true], bumpIndex: [0], ).serialize();
List<int> hexDecode(String hex) => [ for (var i = 0; i < hex.length; i += 2) int.parse(hex.substring(i, i + 2), radix: 16) ];
String hexEncode(List<int> bytes) => bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();BEEF and BUMP come from package:libspiffy/libspiffy.dart.
4.1 A receive address
Section titled “4.1 A receive address” // 4.1 A receive address. final addr = await coordinator .ask(GenerateAddressCommand(walletId: 'alice', label: 'faucet')); print('Alice receives at ${addr.address}');The reply is an AddressGeneratedEvent. The label is stored with the address, which is useful when you list addresses later.
4.2 Coins from the faucet
Section titled “4.2 Coins from the faucet” // 4.2 Coins from the faucet, through Arcade. final sent = await faucetSend(addr.address!, 1000000); print('Faucet sent 1000000 sats in ${sent.txid}');4.3 Mine it
Section titled “4.3 Mine it” // 4.3 Mine it once a miner holds it. await waitForArcade(sent.txid, {'SEEN_ON_NETWORK', 'SEEN_MULTIPLE_NODES'}); await mineBlock(); final mined = await waitForArcade(sent.txid, {'MINED', 'IMMUTABLE'}); final height = mined['blockHeight'] as int; print('Mined at height $height');Arcade moves a transaction through RECEIVED, ACCEPTED_BY_NETWORK and SEEN_ON_NETWORK to MINED. Wait for SEEN_ON_NETWORK before mining: a block mined before the transaction reaches a subtree doesn’t include it.
4.4 Wait for the header
Section titled “4.4 Wait for the header” // 4.4 Wait for Alice's headers to reach that block. while (libspiffy.headerChain.bestHeight < height) { await Future<void>.delayed(const Duration(milliseconds: 250)); }Alice’s wallet checks the proof against the block header it synced itself from port 18444. Until it holds that header, it has nothing to check against.
4.5 Import it and check the balance
Section titled “4.5 Import it and check the balance” // 4.5 Import it with its merkle proof, then check the balance. final beef = minedBeef(sent.rawHex, mined['merklePath'] as String); await coordinator.ask(ImportTransactionCommand(walletId: 'alice', beef: beef)); await printBalance(coordinator, 'alice');ImportTransactionCommand is answered with a TransactionImportedEvent once Alice’s read model holds the transaction. If the proof doesn’t check out, ask throws a CoordinatorFailure instead.
Add printBalance below main:
Future<void> printBalance(WalletCoordinator coordinator, String walletId) async { final balance = await coordinator.ask(GetBalanceQuery(walletId: walletId)); print('$walletId: ${balance.totalBalance} sats ' '(confirmed ${balance.confirmedBalance}, ' 'unconfirmed ${balance.unconfirmedBalance})');}Alice receives at mu343b2opCEdXpDRT4wXjnC8qHfee5SqkBFaucet sent 1000000 sats in a80c33b869c4ef6d0f33698bc48a0c3eb0bd9ed6d9d84786a59ac5c996e8a77cMined at height 486alice: 1000000 sats (confirmed 1000000, unconfirmed 0)Alice has 1,000,000 sats, confirmed against her own header chain.