dev
dart 702 lines 22.6 KB
Raw
1 import 'dart:convert';
2 import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:async';
5 import 'dart:isolate';
6 import 'package:cw_core/utils/print_verbose.dart';
7 import 'package:cw_decred/api/libdcrwallet_bindings.dart';
8 import 'package:cw_decred/api/util.dart';
9
10 final int ErrCodeNotSynced = 1;
11
12 final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux.
13 ? 'libdcrwallet.so'
14 : 'cw_decred.framework/cw_decred';
15
16 class Libwallet {
17 final SendPort _commands;
18 final ReceivePort _responses;
19 final Map<int, Completer<Object?>> _activeRequests = {};
20 int _idCounter = 0;
21 bool _closed = false;
22
23 static Future<Libwallet> spawn() async {
24 // Create a receive port and add its initial message handler.
25 final initPort = RawReceivePort();
26 final connection = Completer<(ReceivePort, SendPort)>.sync();
27 initPort.handler = (initialMessage) {
28 final commandPort = initialMessage as SendPort;
29 connection.complete((
30 ReceivePort.fromRawReceivePort(initPort),
31 commandPort,
32 ));
33 };
34 // Spawn the isolate.
35 try {
36 await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
37 } on Object {
38 initPort.close();
39 rethrow;
40 }
41
42 final (ReceivePort receivePort, SendPort sendPort) = await connection.future;
43
44 return Libwallet._(receivePort, sendPort);
45 }
46
47 Libwallet._(this._responses, this._commands) {
48 _responses.listen(_handleResponsesFromIsolate);
49 }
50
51 void _handleResponsesFromIsolate(dynamic message) {
52 final (int id, Object? response) = message as (int, Object?);
53 final completer = _activeRequests.remove(id)!;
54
55 if (response is RemoteError) {
56 completer.completeError(response);
57 } else {
58 completer.complete(response);
59 }
60
61 if (_closed && _activeRequests.isEmpty) _responses.close();
62 }
63
64 static void _handleCommandsToIsolate(
65 ReceivePort receivePort,
66 SendPort sendPort,
67 ) {
68 final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName));
69 receivePort.listen((message) {
70 if (message == 'shutdown') {
71 receivePort.close();
72 return;
73 }
74 final (int id, Map<String, String> args) = message as (int, Map<String, String>);
75 var res = PayloadResult("", "", 0);
76 final method = args["method"] ?? "";
77 try {
78 switch (method) {
79 case "initlibdcrwallet":
80 final logDir = args["logdir"] ?? "";
81 final level = args["level"] ?? "";
82 final cLogDir = logDir.toCString();
83 final cLevel = level.toCString();
84 executePayloadFn(
85 fn: () => dcrwalletApi.initialize(cLogDir, cLevel),
86 ptrsToFree: [cLogDir, cLevel],
87 );
88 break;
89 case "createwallet":
90 final config = args["config"] ?? "";
91 final cConfig = config.toCString();
92 executePayloadFn(
93 fn: () => dcrwalletApi.createWallet(cConfig),
94 ptrsToFree: [cConfig],
95 );
96 break;
97 case "createwatchonlywallet":
98 final config = args["config"] ?? "";
99 final cConfig = config.toCString();
100 executePayloadFn(
101 fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig),
102 ptrsToFree: [cConfig],
103 );
104 break;
105 case "loadwallet":
106 final config = args["config"] ?? "";
107 final cConfig = config.toCString();
108 executePayloadFn(
109 fn: () => dcrwalletApi.loadWallet(cConfig),
110 ptrsToFree: [cConfig],
111 );
112 break;
113 case "startsync":
114 final name = args["name"] ?? "";
115 final peers = args["peers"] ?? "";
116 final cName = name.toCString();
117 final cPeers = peers.toCString();
118 executePayloadFn(
119 fn: () => dcrwalletApi.syncWallet(cName, cPeers),
120 ptrsToFree: [cName, cPeers],
121 );
122 break;
123 case "closewallet":
124 final name = args["name"] ?? "";
125 final cName = name.toCString();
126 executePayloadFn(
127 fn: () => dcrwalletApi.closeWallet(cName),
128 ptrsToFree: [cName],
129 );
130 break;
131 case "changewalletpassword":
132 final name = args["name"] ?? "";
133 final oldPass = args["oldpass"] ?? "";
134 final newPass = args["newpass"] ?? "";
135 final cName = name.toCString();
136 final cOldPass = oldPass.toCString();
137 final cNewPass = newPass.toCString();
138 res = executePayloadFn(
139 fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass),
140 ptrsToFree: [cName, cOldPass, cNewPass],
141 );
142 break;
143 case "walletseed":
144 final name = args["name"] ?? "";
145 final pass = args["pass"] ?? "";
146 final cName = name.toCString();
147 final cPass = pass.toCString();
148 res = executePayloadFn(
149 fn: () => dcrwalletApi.walletSeed(cName, cPass),
150 ptrsToFree: [cName, cPass],
151 );
152 break;
153 case "syncstatus":
154 final name = args["name"] ?? "";
155 final cName = name.toCString();
156 res = executePayloadFn(
157 fn: () => dcrwalletApi.syncWalletStatus(cName),
158 ptrsToFree: [cName],
159 );
160 break;
161 case "balance":
162 final name = args["name"] ?? "";
163 final cName = name.toCString();
164 res = executePayloadFn(
165 fn: () => dcrwalletApi.walletBalance(cName),
166 ptrsToFree: [cName],
167 );
168 break;
169 case "estimatefee":
170 final name = args["name"] ?? "";
171 final numBlocks = args["numblocks"] ?? "";
172 final cName = name.toCString();
173 final cNumBlocks = numBlocks.toCString();
174 res = executePayloadFn(
175 fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks),
176 ptrsToFree: [cName, cNumBlocks],
177 );
178 break;
179 case "createsignedtransaction":
180 final name = args["name"] ?? "";
181 final signReq = args["signreq"] ?? "";
182 final cName = name.toCString();
183 final cSignReq = signReq.toCString();
184 res = executePayloadFn(
185 fn: () => dcrwalletApi.createTransaction(cName, cSignReq),
186 ptrsToFree: [cName, cSignReq],
187 );
188 break;
189 case "sendrawtransaction":
190 final name = args["name"] ?? "";
191 final txHex = args["txhex"] ?? "";
192 final cName = name.toCString();
193 final cTxHex = txHex.toCString();
194 res = executePayloadFn(
195 fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex),
196 ptrsToFree: [cName, cTxHex],
197 );
198 break;
199 case "listtransactions":
200 final name = args["name"] ?? "";
201 final from = args["from"] ?? "";
202 final count = args["count"] ?? "";
203 final cName = name.toCString();
204 final cFrom = from.toCString();
205 final cCount = count.toCString();
206 res = executePayloadFn(
207 fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount),
208 ptrsToFree: [cName, cFrom, cCount],
209 );
210 break;
211 case "bestblock":
212 final name = args["name"] ?? "";
213 final cName = name.toCString();
214 res = executePayloadFn(
215 fn: () => dcrwalletApi.bestBlock(cName),
216 ptrsToFree: [cName],
217 );
218 break;
219 case "listunspents":
220 final name = args["name"] ?? "";
221 final cName = name.toCString();
222 res = executePayloadFn(
223 fn: () => dcrwalletApi.listUnspents(cName),
224 ptrsToFree: [cName],
225 );
226 break;
227 case "rescanfromheight":
228 final name = args["name"] ?? "";
229 final height = args["height"] ?? "";
230 final cName = name.toCString();
231 final cHeight = height.toCString();
232 res = executePayloadFn(
233 fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight),
234 ptrsToFree: [cName, cHeight],
235 );
236 break;
237 case "signmessage":
238 final name = args["name"] ?? "";
239 final message = args["message"] ?? "";
240 final address = args["address"] ?? "";
241 final pass = args["pass"] ?? "";
242 final cName = name.toCString();
243 final cMessage = message.toCString();
244 final cAddress = address.toCString();
245 final cPass = pass.toCString();
246 res = executePayloadFn(
247 fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass),
248 ptrsToFree: [cName, cMessage, cAddress, cPass],
249 );
250 break;
251 case "verifymessage":
252 final name = args["name"] ?? "";
253 final message = args["message"] ?? "";
254 final address = args["address"] ?? "";
255 final sig = args["sig"] ?? "";
256 final cName = name.toCString();
257 final cMessage = message.toCString();
258 final cAddress = address.toCString();
259 final cSig = sig.toCString();
260 res = executePayloadFn(
261 fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig),
262 ptrsToFree: [cName, cMessage, cAddress, cSig],
263 );
264 break;
265 case "newexternaladdress":
266 final name = args["name"] ?? "";
267 final cName = name.toCString();
268 res = executePayloadFn(
269 fn: () => dcrwalletApi.newExternalAddress(cName),
270 ptrsToFree: [cName],
271 skipErrorCheck: true,
272 );
273 break;
274 case "defaultpubkey":
275 final name = args["name"] ?? "";
276 final cName = name.toCString();
277 res = executePayloadFn(
278 fn: () => dcrwalletApi.defaultPubkey(cName),
279 ptrsToFree: [cName],
280 );
281 break;
282 case "addresses":
283 final name = args["name"] ?? "";
284 final nUsed = args["nused"] ?? "";
285 final nUnused = args["nunused"] ?? "";
286 final cName = name.toCString();
287 final cNUsed = nUsed.toCString();
288 final cNUnused = nUnused.toCString();
289 res = executePayloadFn(
290 fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused),
291 ptrsToFree: [cName, cNUsed, cNUnused],
292 );
293 break;
294 case "birthstate":
295 final name = args["name"] ?? "";
296 final cName = name.toCString();
297 res = executePayloadFn(
298 fn: () => dcrwalletApi.birthState(cName),
299 ptrsToFree: [cName],
300 );
301 break;
302 case "shutdown":
303 final name = args["name"] ?? "";
304 // final cName = name.toCString();
305 executePayloadFn(
306 fn: () => dcrwalletApi.shutdown(),
307 ptrsToFree: [],
308 );
309 break;
310 default:
311 res = PayloadResult("", "unknown libwallet method ${method}", 0);
312 }
313 sendPort.send((id, res));
314 } catch (e) {
315 final errMsg = e.toString();
316 printV("decred libwallet returned an error for method ${method}: ${errMsg}");
317 sendPort.send((id, PayloadResult("", errMsg, 0)));
318 }
319 });
320 }
321
322 static void _startRemoteIsolate(SendPort sendPort) {
323 final receivePort = ReceivePort();
324 sendPort.send(receivePort.sendPort);
325 _handleCommandsToIsolate(receivePort, sendPort);
326 }
327
328 // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets
329 // it ready for use. This must be done before attempting to create, load or use
330 // a wallet. An empty string can be used to log to stdout and create no log files.
331 Future<void> initLibdcrwallet(String logDir, String level) async {
332 if (_closed) throw StateError('Closed');
333 final completer = Completer<Object?>.sync();
334 final id = _idCounter++;
335 _activeRequests[id] = completer;
336 final req = {
337 "method": "initlibdcrwallet",
338 "logdir": logDir,
339 "level": level,
340 };
341 _commands.send((id, req));
342 await completer.future;
343 }
344
345 Future<void> createWallet(String config) async {
346 if (_closed) throw StateError('Closed');
347 final completer = Completer<Object?>.sync();
348 final id = _idCounter++;
349 _activeRequests[id] = completer;
350 final req = {
351 "method": "createwallet",
352 "config": config,
353 };
354 _commands.send((id, req));
355 await completer.future;
356 }
357
358 Future<void> createWatchOnlyWallet(String config) async {
359 if (_closed) throw StateError('Closed');
360 final completer = Completer<Object?>.sync();
361 final id = _idCounter++;
362 _activeRequests[id] = completer;
363 final req = {
364 "method": "createwatchonlywallet",
365 "config": config,
366 };
367 _commands.send((id, req));
368 await completer.future;
369 }
370
371 Future<void> loadWallet(String config) async {
372 if (_closed) throw StateError('Closed');
373 final completer = Completer<Object?>.sync();
374 final id = _idCounter++;
375 _activeRequests[id] = completer;
376 final req = {
377 "method": "loadwallet",
378 "config": config,
379 };
380 _commands.send((id, req));
381 await completer.future;
382 }
383
384 Future<void> startSync(String walletName, String peers) async {
385 if (_closed) throw StateError('Closed');
386 final completer = Completer<Object?>.sync();
387 final id = _idCounter++;
388 _activeRequests[id] = completer;
389 final req = {
390 "method": "startsync",
391 "name": walletName,
392 "peers": peers,
393 };
394 _commands.send((id, req));
395 await completer.future;
396 }
397
398 Future<void> closeWallet(String walletName) async {
399 if (_closed) throw StateError('Closed');
400 final completer = Completer<Object?>.sync();
401 final id = _idCounter++;
402 _activeRequests[id] = completer;
403 final req = {
404 "method": "closewallet",
405 "name": walletName,
406 };
407 _commands.send((id, req));
408 await completer.future;
409 }
410
411 Future<String> changeWalletPassword(
412 String walletName, String currentPassword, String newPassword) async {
413 if (_closed) throw StateError('Closed');
414 final completer = Completer<Object?>.sync();
415 final id = _idCounter++;
416 _activeRequests[id] = completer;
417 final req = {
418 "method": "changewalletpassword",
419 "name": walletName,
420 "oldpass": currentPassword,
421 "newpass": newPassword
422 };
423 _commands.send((id, req));
424 final res = await completer.future as PayloadResult;
425 return res.payload;
426 }
427
428 Future<String?> walletSeed(String walletName, String walletPassword) async {
429 if (_closed) throw StateError('Closed');
430 final completer = Completer<Object?>.sync();
431 final id = _idCounter++;
432 _activeRequests[id] = completer;
433 final req = {
434 "method": "walletseed",
435 "name": walletName,
436 "pass": walletPassword,
437 };
438 _commands.send((id, req));
439 final res = await completer.future as PayloadResult;
440 return res.payload;
441 }
442
443 Future<String> syncStatus(String walletName) async {
444 if (_closed) throw StateError('Closed');
445 final completer = Completer<Object?>.sync();
446 final id = _idCounter++;
447 _activeRequests[id] = completer;
448 final req = {
449 "method": "syncstatus",
450 "name": walletName,
451 };
452 _commands.send((id, req));
453 final res = await completer.future as PayloadResult;
454 return res.payload;
455 }
456
457 Future<Map> balance(String walletName, {bool throwOnError = false}) async {
458 if (_closed) throw StateError('Closed');
459 final completer = Completer<Object?>.sync();
460 final id = _idCounter++;
461 _activeRequests[id] = completer;
462 final req = {
463 "method": "balance",
464 "name": walletName,
465 };
466 _commands.send((id, req));
467 final res = await completer.future as PayloadResult;
468 try {
469 return jsonDecode(res.payload);
470 } catch (_) {
471 if (throwOnError) {
472 rethrow;
473 }
474 return {};
475 }
476 }
477
478 Future<String> estimateFee(String walletName, int numBlocks) async {
479 if (_closed) throw StateError('Closed');
480 final completer = Completer<Object?>.sync();
481 final id = _idCounter++;
482 _activeRequests[id] = completer;
483 final req = {
484 "method": "estimatefee",
485 "name": walletName,
486 "numblocks": numBlocks.toString(),
487 };
488 _commands.send((id, req));
489 final res = await completer.future as PayloadResult;
490 return res.payload;
491 }
492
493 Future<String> createSignedTransaction(
494 String walletName, String createSignedTransactionReq) async {
495 if (_closed) throw StateError('Closed');
496 final completer = Completer<Object?>.sync();
497 final id = _idCounter++;
498 _activeRequests[id] = completer;
499 final req = {
500 "method": "createsignedtransaction",
501 "name": walletName,
502 "signreq": createSignedTransactionReq,
503 };
504 _commands.send((id, req));
505 final res = await completer.future as PayloadResult;
506 return res.payload;
507 }
508
509 Future<String> sendRawTransaction(String walletName, String txHex) async {
510 if (_closed) throw StateError('Closed');
511 final completer = Completer<Object?>.sync();
512 final id = _idCounter++;
513 _activeRequests[id] = completer;
514 final req = {
515 "method": "sendrawtransaction",
516 "name": walletName,
517 "txhex": txHex,
518 };
519 _commands.send((id, req));
520 final res = await completer.future as PayloadResult;
521 return res.payload;
522 }
523
524 Future<String> listTransactions(String walletName, String from, String count) async {
525 if (_closed) throw StateError('Closed');
526 final completer = Completer<Object?>.sync();
527 final id = _idCounter++;
528 _activeRequests[id] = completer;
529 final req = {
530 "method": "listtransactions",
531 "name": walletName,
532 "from": from,
533 "count": count,
534 };
535 _commands.send((id, req));
536 final res = await completer.future as PayloadResult;
537 return res.payload;
538 }
539
540 Future<String> bestBlock(String walletName) async {
541 if (_closed) throw StateError('Closed');
542 final completer = Completer<Object?>.sync();
543 final id = _idCounter++;
544 _activeRequests[id] = completer;
545 final req = {
546 "method": "bestblock",
547 "name": walletName,
548 };
549 _commands.send((id, req));
550 final res = await completer.future as PayloadResult;
551 return res.payload;
552 }
553
554 Future<String> listUnspents(String walletName) async {
555 if (_closed) throw StateError('Closed');
556 final completer = Completer<Object?>.sync();
557 final id = _idCounter++;
558 _activeRequests[id] = completer;
559 final req = {
560 "method": "listunspents",
561 "name": walletName,
562 };
563 _commands.send((id, req));
564 final res = await completer.future as PayloadResult;
565 return res.payload;
566 }
567
568 Future<String> rescanFromHeight(String walletName, String height) async {
569 if (_closed) throw StateError('Closed');
570 final completer = Completer<Object?>.sync();
571 final id = _idCounter++;
572 _activeRequests[id] = completer;
573 final req = {
574 "method": "rescanfromheight",
575 "name": walletName,
576 "height": height,
577 };
578 _commands.send((id, req));
579 final res = await completer.future as PayloadResult;
580 return res.payload;
581 }
582
583 Future<String> signMessage(
584 String walletName, String message, String address, String walletPass) async {
585 if (_closed) throw StateError('Closed');
586 final completer = Completer<Object?>.sync();
587 final id = _idCounter++;
588 _activeRequests[id] = completer;
589 final req = {
590 "method": "signmessage",
591 "name": walletName,
592 "message": message,
593 "address": address,
594 "pass": walletPass,
595 };
596 _commands.send((id, req));
597 final res = await completer.future as PayloadResult;
598 return res.payload;
599 }
600
601 Future<String> verifyMessage(
602 String walletName, String message, String address, String sig) async {
603 if (_closed) throw StateError('Closed');
604 final completer = Completer<Object?>.sync();
605 final id = _idCounter++;
606 _activeRequests[id] = completer;
607 final req = {
608 "method": "verifymessage",
609 "name": walletName,
610 "message": message,
611 "address": address,
612 "sig": sig,
613 };
614 _commands.send((id, req));
615 final res = await completer.future as PayloadResult;
616 return res.payload;
617 }
618
619 Future<String?> newExternalAddress(String walletName) async {
620 if (_closed) throw StateError('Closed');
621 final completer = Completer<Object?>.sync();
622 final id = _idCounter++;
623 _activeRequests[id] = completer;
624 final req = {
625 "method": "newexternaladdress",
626 "name": walletName,
627 };
628 _commands.send((id, req));
629 final res = await completer.future as PayloadResult;
630 if (res.errCode == ErrCodeNotSynced) {
631 // Wallet is not synced. We do not want to give out a used address so give
632 // nothing.
633 return null;
634 }
635 checkErr(res.err);
636 return res.payload;
637 }
638
639 Future<String> defaultPubkey(String walletName) async {
640 if (_closed) throw StateError('Closed');
641 final completer = Completer<Object?>.sync();
642 final id = _idCounter++;
643 _activeRequests[id] = completer;
644 final req = {
645 "method": "defaultpubkey",
646 "name": walletName,
647 };
648 _commands.send((id, req));
649 final res = await completer.future as PayloadResult;
650 return res.payload;
651 }
652
653 Future<String> addresses(String walletName, String nUsed, String nUnused) async {
654 if (_closed) throw StateError('Closed');
655 final completer = Completer<Object?>.sync();
656 final id = _idCounter++;
657 _activeRequests[id] = completer;
658 final req = {
659 "method": "addresses",
660 "name": walletName,
661 "nused": nUsed,
662 "nunused": nUnused,
663 };
664 _commands.send((id, req));
665 final res = await completer.future as PayloadResult;
666 return res.payload;
667 }
668
669 Future<String> birthState(String walletName) async {
670 if (_closed) throw StateError('Closed');
671 final completer = Completer<Object?>.sync();
672 final id = _idCounter++;
673 _activeRequests[id] = completer;
674 final req = {
675 "method": "birthstate",
676 "name": walletName,
677 };
678 _commands.send((id, req));
679 final res = await completer.future as PayloadResult;
680 return res.payload;
681 }
682
683 Future<void> shutdown() async {
684 if (_closed) throw StateError('Closed');
685 final completer = Completer<Object?>.sync();
686 final id = _idCounter++;
687 _activeRequests[id] = completer;
688 final req = {
689 "method": "shutdown",
690 };
691 _commands.send((id, req));
692 await completer.future as PayloadResult;
693 }
694
695 void close() {
696 if (!_closed) {
697 _closed = true;
698 _commands.send('shutdown');
699 if (_activeRequests.isEmpty) _responses.close();
700 }
701 }
702 }