| 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 | } |