Skip to content

Commit 577b782

Browse files
committed
test: add integration wallet workflow surface canary
1 parent c5b0fd9 commit 577b782

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// @Tags(['integration'])
2+
3+
import 'dart:async';
4+
import 'dart:io';
5+
6+
import 'package:bdk_dart/bdk.dart';
7+
import 'package:test/test.dart';
8+
9+
import '../test_constants.dart';
10+
import 'integration_helpers.dart';
11+
12+
Future<void> _deleteDirectoryWithRetry(Directory dir) async {
13+
for (var attempt = 0; attempt < 10; attempt++) {
14+
try {
15+
if (dir.existsSync()) {
16+
await dir.delete(recursive: true);
17+
}
18+
return;
19+
} on PathAccessException {
20+
await Future<void>.delayed(const Duration(milliseconds: 100));
21+
}
22+
}
23+
if (dir.existsSync()) {
24+
await dir.delete(recursive: true);
25+
}
26+
}
27+
28+
String _createTempSqlitePath(String prefix) {
29+
final tempDir = Directory.systemTemp.createTempSync(prefix);
30+
addTearDown(() => _deleteDirectoryWithRetry(tempDir));
31+
return '${tempDir.path}/wallet.sqlite';
32+
}
33+
34+
void _exerciseWalletEventSurface(List<WalletEvent> events) {
35+
for (final event in events) {
36+
if (event is ChainTipChangedWalletEvent) {
37+
event.oldTip.height;
38+
event.newTip.height;
39+
event.oldTip.hash.toString();
40+
event.newTip.hash.toString();
41+
} else if (event is TxConfirmedWalletEvent) {
42+
expect(event.tx.computeTxid().toString(), equals(event.txid.toString()));
43+
event.blockTime.confirmationTime;
44+
event.oldBlockTime?.confirmationTime;
45+
} else if (event is TxUnconfirmedWalletEvent) {
46+
expect(event.tx.computeTxid().toString(), equals(event.txid.toString()));
47+
event.oldBlockTime?.confirmationTime;
48+
} else if (event is TxReplacedWalletEvent) {
49+
expect(event.tx.computeTxid().toString(), equals(event.txid.toString()));
50+
for (final conflict in event.conflicts) {
51+
conflict.vin;
52+
conflict.txid.toString();
53+
}
54+
} else if (event is TxDroppedWalletEvent) {
55+
expect(event.tx.computeTxid().toString(), equals(event.txid.toString()));
56+
} else {
57+
fail('Unhandled wallet event variant: ${event.runtimeType}');
58+
}
59+
}
60+
}
61+
62+
void _exerciseWalletReadSurface(Wallet wallet) {
63+
final checkpoint = wallet.latestCheckpoint();
64+
expect(checkpoint.height, greaterThanOrEqualTo(0));
65+
checkpoint.hash.toString();
66+
67+
final balance = wallet.balance();
68+
expect(balance.total.toSat(), greaterThanOrEqualTo(0));
69+
70+
final network = wallet.network();
71+
expect(network, isNotNull);
72+
73+
final nextExternalIndex = wallet.nextDerivationIndex(KeychainKind.external_);
74+
expect(nextExternalIndex, greaterThanOrEqualTo(0));
75+
76+
final peeked = wallet.peekAddress(KeychainKind.external_, nextExternalIndex);
77+
expect(peeked.index, equals(nextExternalIndex));
78+
expect(peeked.address.scriptPubkey().toBytes(), isNotEmpty);
79+
80+
final nextUnused = wallet.nextUnusedAddress(KeychainKind.external_);
81+
expect(nextUnused.address.scriptPubkey().toBytes(), isNotEmpty);
82+
83+
final unusedAddresses = wallet.listUnusedAddresses(KeychainKind.external_);
84+
for (final info in unusedAddresses.take(3)) {
85+
expect(info.address.scriptPubkey().toBytes(), isNotEmpty);
86+
}
87+
88+
final outputs = wallet.listOutput();
89+
final unspent = wallet.listUnspent();
90+
expect(outputs.length, greaterThanOrEqualTo(unspent.length));
91+
92+
final txs = wallet.transactions();
93+
for (final tx in txs.take(30)) {
94+
final txid = tx.transaction.computeTxid();
95+
final canonical = wallet.getTx(txid);
96+
if (canonical != null) {
97+
expect(canonical.transaction.computeTxid().toString(), txid.toString());
98+
}
99+
100+
final details = wallet.txDetails(txid);
101+
if (details != null) {
102+
expect(details.txid.toString(), equals(txid.toString()));
103+
expect(details.tx.computeTxid().toString(), equals(txid.toString()));
104+
}
105+
}
106+
}
107+
108+
void main() {
109+
group('Workflow surface canary', () {
110+
test(
111+
'electrum sync path exercises broad wallet surface',
112+
() {
113+
final disposers = <Disposer>[];
114+
final sqlitePath = _createTempSqlitePath(
115+
'bdk_dart_surface_canary_electrum_',
116+
);
117+
118+
try {
119+
final descriptor = buildBip84Descriptor(Network.testnet);
120+
addDisposer(disposers, descriptor.dispose);
121+
122+
final changeDescriptor = buildBip84ChangeDescriptor(Network.testnet);
123+
addDisposer(disposers, changeDescriptor.dispose);
124+
125+
final persister = Persister.newSqlite(sqlitePath);
126+
addDisposer(disposers, persister.dispose);
127+
128+
final wallet = Wallet(
129+
descriptor,
130+
changeDescriptor,
131+
Network.testnet,
132+
persister,
133+
defaultLookahead,
134+
);
135+
addDisposer(disposers, wallet.dispose);
136+
137+
wallet.revealNextAddress(KeychainKind.external_);
138+
wallet.persist(persister);
139+
final checkpointBeforeSync = wallet.latestCheckpoint().height;
140+
141+
final requestBuilder = wallet.startSyncWithRevealedSpks();
142+
addDisposer(disposers, requestBuilder.dispose);
143+
final request = requestBuilder.build();
144+
addDisposer(disposers, request.dispose);
145+
146+
final client = buildElectrumClientFromEnv();
147+
addDisposer(disposers, client.dispose);
148+
client.ping();
149+
150+
final update = client.sync_(request, 100, true);
151+
addDisposer(disposers, update.dispose);
152+
153+
final events = wallet.applyUpdateEvents(update);
154+
_exerciseWalletEventSurface(events);
155+
_exerciseWalletReadSurface(wallet);
156+
157+
wallet.persist(persister);
158+
final txCountBeforeReload = wallet.transactions().length;
159+
160+
final reloadedPersister = Persister.newSqlite(sqlitePath);
161+
addDisposer(disposers, reloadedPersister.dispose);
162+
final reloadedWallet = Wallet.load(
163+
descriptor,
164+
changeDescriptor,
165+
reloadedPersister,
166+
defaultLookahead,
167+
);
168+
addDisposer(disposers, reloadedWallet.dispose);
169+
170+
expect(
171+
reloadedWallet.latestCheckpoint().height,
172+
greaterThanOrEqualTo(checkpointBeforeSync),
173+
);
174+
_exerciseWalletReadSurface(reloadedWallet);
175+
expect(
176+
reloadedWallet.transactions().length,
177+
equals(txCountBeforeReload),
178+
);
179+
} finally {
180+
disposeAll(disposers);
181+
}
182+
},
183+
skip: integrationSkipReason(requiredEnv: [electrumUrlEnv]),
184+
);
185+
186+
test(
187+
'esplora sync path exercises broad wallet surface',
188+
() {
189+
final disposers = <Disposer>[];
190+
final sqlitePath = _createTempSqlitePath(
191+
'bdk_dart_surface_canary_esplora_',
192+
);
193+
194+
try {
195+
final descriptor = buildBip84Descriptor(Network.testnet);
196+
addDisposer(disposers, descriptor.dispose);
197+
198+
final changeDescriptor = buildBip84ChangeDescriptor(Network.testnet);
199+
addDisposer(disposers, changeDescriptor.dispose);
200+
201+
final persister = Persister.newSqlite(sqlitePath);
202+
addDisposer(disposers, persister.dispose);
203+
204+
final wallet = Wallet(
205+
descriptor,
206+
changeDescriptor,
207+
Network.testnet,
208+
persister,
209+
defaultLookahead,
210+
);
211+
addDisposer(disposers, wallet.dispose);
212+
213+
wallet.revealNextAddress(KeychainKind.external_);
214+
wallet.persist(persister);
215+
final checkpointBeforeSync = wallet.latestCheckpoint().height;
216+
217+
final requestBuilder = wallet.startSyncWithRevealedSpks();
218+
addDisposer(disposers, requestBuilder.dispose);
219+
final request = requestBuilder.build();
220+
addDisposer(disposers, request.dispose);
221+
222+
final client = buildEsploraClientFromEnv();
223+
addDisposer(disposers, client.dispose);
224+
expect(client.getHeight(), greaterThan(0));
225+
226+
final update = client.sync_(request, 4);
227+
addDisposer(disposers, update.dispose);
228+
229+
final events = wallet.applyUpdateEvents(update);
230+
_exerciseWalletEventSurface(events);
231+
_exerciseWalletReadSurface(wallet);
232+
233+
wallet.persist(persister);
234+
final txCountBeforeReload = wallet.transactions().length;
235+
236+
final reloadedPersister = Persister.newSqlite(sqlitePath);
237+
addDisposer(disposers, reloadedPersister.dispose);
238+
final reloadedWallet = Wallet.load(
239+
descriptor,
240+
changeDescriptor,
241+
reloadedPersister,
242+
defaultLookahead,
243+
);
244+
addDisposer(disposers, reloadedWallet.dispose);
245+
246+
expect(
247+
reloadedWallet.latestCheckpoint().height,
248+
greaterThanOrEqualTo(checkpointBeforeSync),
249+
);
250+
_exerciseWalletReadSurface(reloadedWallet);
251+
expect(
252+
reloadedWallet.transactions().length,
253+
equals(txCountBeforeReload),
254+
);
255+
} finally {
256+
disposeAll(disposers);
257+
}
258+
},
259+
skip: integrationSkipReason(requiredEnv: [esploraUrlEnv]),
260+
);
261+
});
262+
}

0 commit comments

Comments
 (0)