diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76f07edf18..aa66fdc6ab 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -24,7 +24,8 @@ enum DeviceConnectionType { WalletType.bitcoin, WalletType.litecoin, WalletType.ethereum, - WalletType.polygon + WalletType.polygon, + WalletType.decred ].contains(walletType); break; case HardwareWalletType.trezor: diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index a6d1e10179..9e1b246332 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -176,14 +176,14 @@ class Libwallet { ptrsToFree: [cName, cNumBlocks], ); break; - case "createsignedtransaction": + case "createtransaction": final name = args["name"] ?? ""; - final signReq = args["signreq"] ?? ""; + final signReq = args["req"] ?? ""; final cName = name.toCString(); - final cSignReq = signReq.toCString(); + final cReq = signReq.toCString(); res = executePayloadFn( - fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), - ptrsToFree: [cName, cSignReq], + fn: () => dcrwalletApi.createTransaction(cName, cReq), + ptrsToFree: [cName, cReq], ); break; case "sendrawtransaction": @@ -299,9 +299,65 @@ class Libwallet { ptrsToFree: [cName], ); break; - case "shutdown": + case "addrfromextendedkey": + final req = args["req"] ?? ""; + final cReq = req.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addrFromExtendedKey(cReq), + ptrsToFree: [cReq], + ); + break; + case "createextendedkey": + final req = args["req"] ?? ""; + final cReq = req.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.createExtendedKey(cReq), + ptrsToFree: [cReq], + ); + break; + case "decodetx": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final txHex = args["txhex"] ?? ""; + final cTxHex = txHex.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.decodeTx(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + break; + case "gettxn": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final hashes = args["hashes"] ?? ""; + final cHashes = hashes.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.getTxn(cName, cHashes), + ptrsToFree: [cName, cHashes], + ); + break; + case "validateaddr": final name = args["name"] ?? ""; - // final cName = name.toCString(); + final cName = name.toCString(); + final addr = args["addr"] ?? ""; + final cAddr = addr.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.validateAddr(cName, cAddr), + ptrsToFree: [cName, cAddr], + ); + break; + case "addsigs": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final txHex = args["txhex"] ?? ""; + final cTxHex = txHex.toCString(); + final sigScripts = args["sigscripts"] ?? ""; + final cSigScripts = sigScripts.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addSigs(cName, cTxHex, cSigScripts), + ptrsToFree: [cName, cTxHex, cSigScripts], + ); + break; + case "shutdown": executePayloadFn( fn: () => dcrwalletApi.shutdown(), ptrsToFree: [], @@ -490,16 +546,15 @@ class Libwallet { return res.payload; } - Future createSignedTransaction( - String walletName, String createSignedTransactionReq) async { + Future createTransaction(String walletName, String createTransactionReq) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { - "method": "createsignedtransaction", + "method": "createtransaction", "name": walletName, - "signreq": createSignedTransactionReq, + "req": createTransactionReq, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; @@ -680,6 +735,95 @@ class Libwallet { return res.payload; } + Future addrFromExtendedKey(String afekReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addrfromextendedkey", + "req": afekReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future createExtendedKey(String createReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createextendedkey", + "req": createReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future decodeTx(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "decodetx", + "name": walletName, + "txhex": txHex, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future getTxn(String walletName, String hashes) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "gettxn", + "name": walletName, + "hashes": hashes, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future validateAddr(String walletName, String addr) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "validateaddr", + "name": walletName, + "addr": addr, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future addSigs(String walletName, String txHex, String sigScripts) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addsigs", + "name": walletName, + "txhex": txHex, + "sigscripts": sigScripts, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + Future shutdown() async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); diff --git a/cw_decred/lib/ledger.dart b/cw_decred/lib/ledger.dart new file mode 100644 index 0000000000..79575bc475 --- /dev/null +++ b/cw_decred/lib/ledger.dart @@ -0,0 +1,713 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:dart_varuint_bitcoin/dart_varuint_bitcoin.dart' as varint; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/src/operations/litecoin_sign_msg_operation.dart'; +import 'package:ledger_litecoin/src/tx_utils/transaction.dart'; +import 'package:ledger_litecoin/src/litecoin_transformer.dart'; +import 'package:ledger_litecoin/src/tx_utils/finalize_input.dart'; +import 'package:ledger_litecoin/src/tx_utils/constants.dart'; +import 'package:ledger_litecoin/src/ledger/ledger_input_operation.dart'; +import 'package:ledger_litecoin/src/ledger/litecoin_instructions.dart'; +import 'package:ledger_litecoin/src/utils/bip32_path_helper.dart'; +import 'package:ledger_litecoin/src/utils/bip32_path_to_buffer.dart'; + +/// This command is used to sign a given secure hash using a private key (after +/// re-hashing it following the standard Bitcoin signing process) to finalize a +/// transaction input signing process. +/// +/// This command will be rejected if the transaction signing state is not +/// consistent or if a user validation is required and the provided user +/// validation code is not correct. +class UntrustedHashSignOperation extends LedgerInputOperation { + final String derivationPath; + + final int lockTime; + + final int sigHashType; + + final int expiryHeight; + + UntrustedHashSignOperation( + this.derivationPath, this.lockTime, this.expiryHeight, this.sigHashType) + : super(btcCLA, untrustedHashSignINS); + + @override + int get p1 => 0x00; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async { + final result = reader.read(reader.remainingLength); + + if (result.isNotEmpty) { + result[0] = 0x30; + return result.sublist(0, result.length - 2); + } + + return result; + } + + @override + Future writeInputData() async { + final writer = ByteDataWriter(); + + final path = BIPPath.fromString(derivationPath).toPathArray(); + writer.write(packDerivationPath(path)); + writer.writeUint32(lockTime); + writer.writeUint32(expiryHeight); + writer.write([sigHashType]); + + return writer.toBytes(); + } +} + +class HashOutputFull extends LedgerInputOperation { + final Uint8List outputScript; + + HashOutputFull(this.outputScript) : super(btcCLA, untrustedHashTransactionInputFinalizeINS); + + @override + int get p1 => 0x80; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async => reader.read(reader.remainingLength); + + @override + Future writeInputData() async => outputScript; +} + +class TrustedInput { + final bool trustedInput; + final Uint8List value; + final Uint8List tree; + final Uint8List sequence; + + const TrustedInput({ + required this.trustedInput, + required this.value, + required this.tree, + required this.sequence, + }); +} + +/// This command is used to sign a given secure hash using a private key (after +/// re-hashing it following the standard Bitcoin signing process) to finalize a +/// transaction input signing process. +/// +/// This command will be rejected if the transaction signing state is not +/// consistent or if a user validation is required and the provided user +/// validation code is not correct. +class UntrustedHashTxInputStartOperation extends LedgerInputOperation { + final bool firstRound; + final bool isNewTransaction; + + final Uint8List transactionData; + + UntrustedHashTxInputStartOperation(this.isNewTransaction, this.firstRound, this.transactionData) + : super(btcCLA, untrustedHashTransactionInputStartINS); + + @override + int get p1 => firstRound ? 0x00 : 0x80; + + @override + int get p2 => isNewTransaction ? 0x00 : 0x80; + + @override + Future read(ByteDataReader reader) async => reader.read(reader.remainingLength); + + @override + Future writeInputData() async => transactionData; +} + +Future startUntrustedHashTransactionInput( + LedgerConnection connection, LedgerTransformer transformer, + {required bool isNewTransaction, + required Transaction transaction, + required List inputs}) async { + var data = ByteDataWriter() + ..write(transaction.version) + ..write(varint.encode(transaction.inputs.length).buffer); + + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + true, + data.toBytes(), + ), + transformer: transformer); + + var i = 0; + + for (final input in transaction.inputs) { + late final Uint8List prefix; + if (inputs[i].trustedInput) { + prefix = Uint8List.fromList([0x01, inputs[i].value.length]); + } else { + prefix = Uint8List.fromList([0x00]); + } + + final data = Uint8List.fromList([ + ...prefix, + ...inputs[i].value, + ...inputs[i].tree, + ...varint.encode(input.script.length).buffer, + ]); + + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + false, + data, + ), + transformer: transformer); + + final scriptBlocks = []; + var offset = 0; + + if (input.script.isEmpty) { + scriptBlocks.add(input.sequence); + } else { + while (offset != input.script.length) { + final blockSize = input.script.length - offset > MAX_SCRIPT_BLOCK + ? MAX_SCRIPT_BLOCK + : input.script.length - offset; + + if (offset + blockSize != input.script.length) { + scriptBlocks.add(input.script.sublist(offset, offset + blockSize)); + } else { + scriptBlocks.add( + Uint8List.fromList( + [...input.script.sublist(offset, offset + blockSize), ...input.sequence]), + ); + } + + offset += blockSize; + } + } + + for (final scriptBlock in scriptBlocks) { + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + false, + scriptBlock, + ), + transformer: transformer); + } + + i++; + } +} + +/// This command is used to extract a Trusted Input (encrypted transaction hash, +/// output index, output amount) from a transaction. +/// +/// The transaction data to be provided should be encoded using bitcoin standard +/// raw transaction encoding. Scripts can be sent over several APDUs. +/// Other individual transaction elements split over different APDUs will be +/// rejected. 64 bits varints are rejected. +class GetTrustedInputOperation extends LedgerInputOperation { + final int? indexLookup; + + final Uint8List inputData; + + GetTrustedInputOperation(this.inputData, [this.indexLookup]) : super(btcCLA, getTrustedInputINS); + + @override + int get p1 => indexLookup != null ? 0x00 : 0x80; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async { + final result = reader.read(reader.remainingLength); + + return result.isNotEmpty ? result.sublist(0, result.length - 2) : result; + } + + @override + Future writeInputData() async { + final writer = ByteDataWriter(); + if (indexLookup != null) writer.writeUint32(indexLookup!, Endian.big); + + writer.write(inputData); + + return writer.toBytes(); + } +} + +Future getTrustedInput(LedgerConnection connection, LedgerTransformer transformer, + {required int indexLookup, required Transaction transaction}) async { + Future processScriptBlocks(Uint8List script, Uint8List? sequence) async { + final seq = sequence ?? Uint8List(0); + final scriptBlocks = []; + var offset = 0; + + while (offset != script.length) { + final blockSize = + script.length - offset > MAX_SCRIPT_BLOCK ? MAX_SCRIPT_BLOCK : script.length - offset; + + if (offset + blockSize != script.length) { + scriptBlocks.add(script.sublist(offset, offset + blockSize)); + } else { + scriptBlocks + .add(Uint8List.fromList([...script.sublist(offset, offset + blockSize), ...seq])); + } + + offset += blockSize; + } + + // Handle case when no script length: we still want to pass the sequence + // relatable: https://github.com/LedgerHQ/ledger-live-desktop/issues/1386 + if (script.isEmpty) scriptBlocks.add(seq); + + Uint8List res = Uint8List(0); + + for (final scriptBlock in scriptBlocks) { + res = await connection.sendOperation(GetTrustedInputOperation(scriptBlock), + transformer: transformer); + } + + return res; + } + + await connection.sendOperation( + GetTrustedInputOperation( + Uint8List.fromList( + [...transaction.version, ...varint.encode(transaction.inputs.length).buffer]), + indexLookup, + ), + transformer: transformer); + + for (final input in transaction.inputs) { + var data = Uint8List.fromList( + [...input.prevout, ...input.tree!, ...varint.encode(input.script.length).buffer]); + + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + + data = Uint8List.fromList([...input.script, ...input.sequence]); + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + } + + await connection.sendOperation( + GetTrustedInputOperation(varint.encode(transaction.outputs.length).buffer), + transformer: transformer); + + for (final output in transaction.outputs) { + final data = Uint8List.fromList([ + ...output.amount, + ...[0, 0], + ...varint.encode(output.script.length).buffer, + ...output.script, + ]); + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + } + + final res = await processScriptBlocks(Uint8List.fromList([0, 0, 0, 0, 0, 0, 0, 0]), null); + + return hex.encode(res); +} + +class PubkeyResp { + String pubkey; + String address; + String chainCode; + + PubkeyResp(this.pubkey, this.address, this.chainCode); +} + +/// Returns an extended public key at the given derivation path, serialized as per BIP-32 +class ExtendedPublicKeyOperation extends LedgerInputOperation { + /// If [displayPublicKey] is set to true the Public Key will be shown to the user on the ledger device + final bool displayPublicKey; + + /// The [derivationPath] is a Bip32-path used to derive the public key/Address + /// If the path is not standard, an error is returned + final String derivationPath; + + ExtendedPublicKeyOperation({ + required this.displayPublicKey, + required this.derivationPath, + }) : super(0xE0, 0x40); + + @override + Future read(ByteDataReader reader) async { + final b = reader.read(reader.remainingLength); + final pubkeyLength = b[0]; + final addressLength = b[1 + pubkeyLength]; + final pubkeyB = b.sublist(1, 1 + pubkeyLength); + final addressB = b.sublist(1 + pubkeyLength + 1, 1 + pubkeyLength + 1 + addressLength); + final chainCodeB = + b.sublist(1 + pubkeyLength + 1 + addressLength, 1 + pubkeyLength + 1 + addressLength + 32); + final pubkey = uint8ListToHex(pubkeyB); + final address = ascii.decode(addressB); + final chainCode = uint8ListToHex(chainCodeB); + return PubkeyResp(pubkey, address, chainCode); + } + + @override + int get p1 => displayPublicKey ? 0x01 : 0x00; + + @override + int get p2 => 0x00; // legacy key type + + @override + Future writeInputData() async { + final path = BIPPath.fromString(derivationPath).toPathArray(); + + final writer = ByteDataWriter()..writeUint8(path.length); // Write length of the derivation path + + for (final element in path) { + writer.writeUint32(element); // Add each part of the path + } + final b = writer.toBytes(); + + return b; + } +} + +String uint8ListToHex(Uint8List data) { + final StringBuffer hexBuffer = StringBuffer(); + for (int byte in data) { + hexBuffer.write(byte.toRadixString(16).padLeft(2, '0')); + } + return hexBuffer.toString(); +} + +Uint8List hexToUint8List(String hexString) { + if (hexString.length % 2 != 0) { + throw ArgumentError('Hex string must have an even number of characters'); + } + + final List bytes = []; + for (int i = 0; i < hexString.length; i += 2) { + String hexPair = hexString.substring(i, i + 2); + int byte = int.parse(hexPair, radix: 16); + bytes.add(byte); + } + return Uint8List.fromList(bytes); +} + +Uint8List intToLittleEndianBytes(int value, int byteCount) { + final buffer = Uint8List(byteCount).buffer; + final byteData = ByteData.view(buffer); + + switch (byteCount) { + case 1: + byteData.setUint8(0, value); + break; + case 2: + byteData.setUint16(0, value, Endian.little); + break; + case 4: + byteData.setUint32(0, value, Endian.little); + break; + case 8: + byteData.setUint64(0, value, Endian.little); + break; + default: + throw ArgumentError('Unsupported byteCount: $byteCount'); + } + + return buffer.asUint8List(); +} + +Uint8List strHashToUint8List(String txid) { + if (txid.length != 64) { + throw ArgumentError('txid should be 32 bytes long'); + } + + final List bytes = []; + for (int i = 64; i > 0; i -= 2) { + String hexPair = txid.substring(i - 2, i); + int byte = int.parse(hexPair, radix: 16); + bytes.add(byte); + } + return Uint8List.fromList(bytes); +} + +class InputTx { + Transaction tx; + int vout; + String path; + String pubkey; + Uint8List sequence; + Uint8List tree; + Uint8List redeemScript; + + InputTx(this.tx, this.vout, this.path, this.pubkey, this.sequence, this.tree, this.redeemScript); +} + +Future createInputTx(Map inp, int n, String walletName, + Uint8List sequence, Uint8List tree, Libwallet libwallet) async { + List ins = []; + final vins = inp["vin"]; + for (var vin in vins) { + final sequence = intToLittleEndianBytes(vin["sequence"], 4); + final tree = intToLittleEndianBytes(vin["tree"], 1); + final prevout = strHashToUint8List(vin["txid"]); + final vout = intToLittleEndianBytes(vin["vout"], 4); + BytesBuilder bb = BytesBuilder(); + bb.add(prevout); + bb.add(vout); + final inp = TransactionInput(bb.toBytes(), Uint8List.new(25), sequence, tree); + ins.add(inp); + } + List outs = []; + final vouts = inp["vout"]; + String addr = ""; + Uint8List redeemScript = Uint8List(0); + for (var vout in vouts) { + final atoms = (vout["value"] * 100000000).toInt(); + final amount = intToLittleEndianBytes(atoms, 8); + final script = hexToUint8List(vout["scriptPubKey"]["hex"]); + final out = TransactionOutput(amount, script); + outs.add(out); + if (vout["n"] == n) { + addr = vout["scriptPubKey"]["addresses"][0]; + redeemScript = hexToUint8List(vout["scriptPubKey"]["hex"]); + } + } + final vaJSON = await libwallet.validateAddr(walletName, addr); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final accountn = va["accountn"] ?? 0; + final branch = va["branch"] ?? 0; + final index = va["index"] ?? 0; + final path = "44'/42'/" + accountn.toString() + "'/" + branch.toString() + "/" + index.toString(); + final tx = Transaction( + version: intToLittleEndianBytes(inp["version"], 4), + inputs: ins, + outputs: outs, + locktime: intToLittleEndianBytes(inp["locktime"], 4)); + return InputTx(tx, n, path, va["pubkey"], sequence, tree, redeemScript); +} + +Uint8List serializeTransactionOutputs(List outs) { + var outputBuffer = outs.isNotEmpty ? varint.encode(outs.length).buffer : Uint8List(0); + + for (final out in outs) { + final atoms = (out["value"] * 100000000).toInt(); + outputBuffer = Uint8List.fromList([ + ...outputBuffer, + ...intToLittleEndianBytes(atoms, 8), + ...intToLittleEndianBytes(out["version"], 2), + ...varint.encode(out["scriptPubKey"]["hex"].length).buffer, + ...hexToUint8List(out["scriptPubKey"]["hex"]), + ]); + } + + return outputBuffer; +} + +Future signTransaction( + String unsignedTx, String walletName, LedgerConnection connection, Libwallet libwallet) async { + final verboseTxJSON = await libwallet.decodeTx(walletName, unsignedTx); + final verboseTx = json.decode(verboseTxJSON.isEmpty ? "{}" : verboseTxJSON); + final targetTx = Transaction(version: intToLittleEndianBytes(verboseTx["version"], 4)); + targetTx.locktime = intToLittleEndianBytes(verboseTx["locktime"], 4); + final vins = verboseTx["vin"]; + final nullScript = Uint8List(0); + final nullPrevout = Uint8List(0); + var ins = []; + List ttIns = []; + for (var vin in vins) { + final txIds = jsonEncode([vin["txid"]]); + final txHexesJSON = await libwallet.getTxn(walletName, txIds); + final txHexes = json.decode(txHexesJSON); + final verboseInJSON = await libwallet.decodeTx(walletName, txHexes[0]); + final verboseIn = json.decode(verboseInJSON.isEmpty ? "{}" : verboseInJSON); + final sequence = intToLittleEndianBytes(vin["sequence"], 4); + final tree = intToLittleEndianBytes(vin["tree"], 1); + ins.add(await createInputTx(verboseIn, vin["vout"], walletName, sequence, tree, libwallet)); + ttIns.add(TransactionInput(nullPrevout, nullScript, sequence, tree)); + } + targetTx.inputs = ttIns; + String changePath = ""; + final vouts = verboseTx["vout"]; + final prefixWriter = ByteDataWriter(); + prefixWriter.write(varint.encode(vouts.length).buffer); + for (var vout in vouts) { + final addr = vout["scriptPubKey"]["addresses"][0]; + final vaJSON = await libwallet.validateAddr(walletName, addr); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final atoms = (vout["value"] * 100000000).toInt(); + final script = vout["scriptPubKey"]["hex"]; + prefixWriter.write(intToLittleEndianBytes(atoms, 8)); + prefixWriter.write(intToLittleEndianBytes(vout["version"], 2)); + prefixWriter.write(varint.encode((script.length / 2).toInt()).buffer); + prefixWriter.write(hexToUint8List(script)); + final branch = va["branch"] ?? 0; + // Assume the internal branch is change. + if (branch == 1) { + final accountn = va["accountn"] ?? 0; + final index = va["index"] ?? 0; + changePath = "44'/42'/" + accountn.toString() + "'/1/" + index.toString(); + } + } + final version = intToLittleEndianBytes(verboseTx["version"], 4); + final List trustedInputs = []; + final List regularOutputs = []; + final List signatures = []; + final LitecoinTransformer transformer = LitecoinTransformer(); + final outputScript = serializeTransactionOutputs(vouts); + + for (final inp in ins) { + final trustedInput = + await getTrustedInput(connection, transformer, indexLookup: inp.vout, transaction: inp.tx); + + trustedInputs.add(TrustedInput( + trustedInput: true, + value: hexToUint8List(trustedInput), + tree: inp.tree, + sequence: inp.sequence, + )); + + regularOutputs.add(inp.tx.outputs[inp.vout]); + } + var isNewTx = true; + for (var i = 0; i < ins.length; i++) { + final script = ins[i].redeemScript; + targetTx.inputs[i].script = script; + await startUntrustedHashTransactionInput( + connection, + transformer, + isNewTransaction: isNewTx, + transaction: targetTx, + inputs: trustedInputs, + ); + + await provideOutputFullChangePath(connection, transformer, path: changePath); + + await connection.sendOperation(HashOutputFull(prefixWriter.toBytes()), + transformer: transformer); + + const SIGHASH_ALL = 1; + final signature = await connection.sendOperation( + UntrustedHashSignOperation( + ins[i].path, verboseTx["locktime"], verboseTx["expiry"], SIGHASH_ALL), + transformer: transformer); + + isNewTx = false; + signatures.add(signature); + targetTx.inputs[i].script = nullScript; + } + + var sigScripts = []; + for (int i = 0; i < signatures.length; i++) { + final pk = hexToUint8List(ins[i].pubkey); + final sigBuffer = Uint8List.fromList([ + ...varint.encode(signatures[i].length).buffer, + ...signatures[i], + ...varint.encode(pk.length).buffer, + ...pk, + ]); + sigScripts.add(hex.encode(sigBuffer)); + } + final sigScriptsJSON = jsonEncode(sigScripts); + return await libwallet.addSigs(walletName, unsignedTx, sigScriptsJSON); +} + +class LedgerWalletService extends HardwareWalletService { + LedgerWalletService( + this.ledgerConnection, { + this.transformer = const LitecoinTransformer(), + this.derivPath = "m/44'/42'/0'/0/0", + }); + final LedgerConnection ledgerConnection; + final LitecoinTransformer transformer; + final String derivPath; + + // NOTE: Seems unused. We need the wallet name and libwallet for signing so + // not adding to this class. + @override + Future signTransaction({required String transaction}) => throw UnimplementedError(); + + @override + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + await DecredWalletService.initLibwallet(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final parentPkRes = await ledgerConnection.sendOperation( + ExtendedPublicKeyOperation(displayPublicKey: false, derivationPath: "44'/42'"), + transformer: transformer); + + for (final i in indexRange) { + final derivationPath = "44'/42'/$i'"; + final pkRes = await ledgerConnection.sendOperation( + ExtendedPublicKeyOperation(displayPublicKey: false, derivationPath: derivationPath), + transformer: transformer); + + final createReq = { + "key": pkRes.pubkey, + "parentkey": parentPkRes.pubkey, + "chaincode": pkRes.chainCode, + "network": "mainnet", // TODO: Change with network. + "depth": 2, + "childn": i, + "isprivate": false, + }; + + final xpub = await DecredWalletService.libwallet!.createExtendedKey(jsonEncode(createReq)); + + // The xpub is at the account level. 0/0 will take the address from the + // external branch with index 0. + final addrReq = { + "key": xpub, + "path": "0/0", + "addrtype": "p2pkh", + }; + + final address = await DecredWalletService.libwallet!.addrFromExtendedKey(jsonEncode(addrReq)); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } + + /// This command is used to sign message using a private key. + /// + /// The signature is performed as follows: + /// The [message] to sign is the magic "\x19Decred Signed Message:\n" - + /// followed by the length of the message to sign on 1 byte (if requested) followed by the binary content of the message + /// The signature is performed on a double SHA-256 hash of the data to sign using the selected private key + /// + /// + /// The signature is returned using the standard ASN-1 encoding. + /// To convert it to the proprietary Bitcoin-QT format, the host has to : + /// + /// Get the parity of the first byte (sequence) : P + /// Add 27 to P if the public key is not compressed, otherwise add 31 to P + /// Return the Base64 encoded version of P || r || s + @override + Future signMessage({required Uint8List message, String? derivationPath}) async { + var dp = derivationPath; + if (dp == null) { + dp = derivPath; + } + return await ledgerConnection.sendOperation( + LitecoinSignMsgOperation(message, dp), + transformer: transformer, + ); + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index fb177f4ca2..67cbbaa905 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -7,13 +7,22 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_decred/amount_format.dart'; -import 'package:cw_decred/pending_transaction.dart'; -import 'package:cw_decred/transaction_credentials.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:hive/hive.dart'; +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; import 'package:cw_decred/transaction_history.dart'; import 'package:cw_decred/wallet_addresses.dart'; @@ -21,15 +30,7 @@ import 'package:cw_decred/transaction_priority.dart'; import 'package:cw_decred/wallet_service.dart'; import 'package:cw_decred/balance.dart'; import 'package:cw_decred/transaction_info.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_decred/ledger.dart'; part 'wallet.g.dart'; @@ -44,10 +45,11 @@ abstract class DecredWalletBase _closeLibwallet = closeLibwallet, this.syncStatus = NotConnectedSyncStatus(), this.unspentCoinsInfo = unspentCoinsInfo, - this.watchingOnly = + this.isWatchingOnly = walletInfo.derivationInfo?.derivationPath == DecredWalletService.pubkeyRestorePath || walletInfo.derivationInfo?.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, + this.isHardware = walletInfo.hardwareWalletType != null, this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), this.isTestnet = walletInfo.derivationInfo?.derivationPath == DecredWalletService.seedRestorePathTestnet || @@ -75,6 +77,7 @@ abstract class DecredWalletBase final Libwallet _libwallet; final Function() _closeLibwallet; final idPrefix = "decred_"; + LedgerWalletService? ledgerWalletService; // TODO: Encrypt this. var _seed = ""; @@ -83,7 +86,8 @@ abstract class DecredWalletBase // synced is used to set the syncTimer interval. bool synced = false; - bool watchingOnly; + bool isWatchingOnly; + bool isHardware; bool connecting = false; String persistantPeer = "default-spv-nodes"; FeeCache feeRateFast = FeeCache(defaultFeeRate); @@ -109,7 +113,7 @@ abstract class DecredWalletBase @override String? get seed { - if (watchingOnly) { + if (isWatchingOnly) { return null; } return _seed; @@ -130,7 +134,7 @@ abstract class DecredWalletBase Future init() async { final getSeed = () async { - if (!watchingOnly) { + if (!isWatchingOnly) { _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? ""; } _pubkey = await _libwallet.defaultPubkey(walletInfo.name); @@ -352,7 +356,7 @@ abstract class DecredWalletBase @override Future createTransaction(Object credentials) async { - if (watchingOnly) { + if (isWatchingOnly && !isHardware) { return DecredPendingTransaction( txid: "", amount: 0, @@ -405,17 +409,26 @@ abstract class DecredWalletBase // The inputs are always used. Currently we don't have use for this // argument. sendall ingores output value and sends everything. - final signReq = { + final req = { // "inputs": inputs, "ignoreInputs": ignoreInputs, "outputs": outputs, "feerate": creds.feeRate ?? defaultFeeRate, - "password": _password, "sendall": sendAll, }; - final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); + if (!isHardware) { + req["password"] = _password; + req["sign"] = true; + } + final res = await _libwallet.createTransaction(walletInfo.name, jsonEncode(req)); final decoded = json.decode(res); - final signedHex = decoded["signedhex"]; + var signedHex; + if (isHardware) { + signedHex = await signTransaction( + decoded["hex"], walletInfo.name, ledgerWalletService!.ledgerConnection, _libwallet); + } else { + signedHex = decoded["hex"]; + } final send = () async { await _libwallet.sendRawTransaction(walletInfo.name, signedHex); await updateBalance(); @@ -536,7 +549,7 @@ abstract class DecredWalletBase // mnemonic. As long as not private data is imported into the wallet, we // can always rescan from there. var rescanHeight = 0; - if (!watchingOnly) { + if (!isWatchingOnly) { rescanHeight = await walletBirthdayBlockHeight(); // Sync has not yet reached the birthday block. if (rescanHeight == -1) { @@ -561,7 +574,7 @@ abstract class DecredWalletBase @override Future changePassword(String password) async { - if (watchingOnly) { + if (isWatchingOnly) { return; } return () async { @@ -631,7 +644,21 @@ abstract class DecredWalletBase @override Future signMessage(String message, {String? address = null}) async { - if (watchingOnly) { + if (isHardware) { + var path = "m/44'/42'/0'/0/0"; + if (address != null) { + final vaJSON = await _libwallet.validateAddr(walletInfo.name, address); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final accountn = va["accountn"] ?? 0; + final branch = va["branch"] ?? 0; + final index = va["index"] ?? 0; + path = "44'/42'/" + accountn.toString() + "'/" + branch.toString() + "/" + index.toString(); + } + final signature = await ledgerWalletService! + .signMessage(message: ascii.encode(message), derivationPath: path); + return base64Encode(signature); + } + if (isWatchingOnly) { throw "a watching only wallet cannot sign"; } var addr = address; @@ -766,5 +793,5 @@ abstract class DecredWalletBase String get password => _password; @override - bool canSend() => seed != null; + bool canSend() => seed != null || isHardware; } diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index ca04514479..2c7b2810cb 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -32,9 +32,7 @@ class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { DecredRestoreWalletFromHardwareCredentials( {required String name, required this.hwAccountData, WalletInfo? walletInfo}) - : t = throw UnimplementedError(), - super(name: name, walletInfo: walletInfo); + : super(name: name, walletInfo: walletInfo); final HardwareAccountData hwAccountData; - final void t; } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 93c708886c..a1863109ee 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -31,6 +31,10 @@ class DecredWalletService extends WalletService< static Libwallet? libwallet; Future init() async { + return initLibwallet(); + } + + static Future initLibwallet() async { if (libwallet != null) { return; } @@ -245,6 +249,29 @@ class DecredWalletService extends WalletService< @override Future restoreFromHardwareWallet( - DecredRestoreWalletFromHardwareCredentials credentials) async => - throw UnimplementedError(); + DecredRestoreWalletFromHardwareCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + final config = { + "name": credentials.walletInfo!.name, + "datadir": dirPath, + "pubkey": credentials.hwAccountData.xpub, + "net": network, + "unsyncedaddrs": true, + }; + await libwallet!.createWatchOnlyWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); + credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; + credentials.hardwareWalletType = HardwareWalletType.ledger; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } } diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 83fc4d2148..3934bd240b 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -47,7 +47,7 @@ packages: source: hosted version: "2.13.0" blockchain_utils: - dependency: transitive + dependency: "direct main" description: path: "." ref: cake-update-v2 @@ -55,6 +55,14 @@ packages: url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -177,7 +185,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -185,7 +193,7 @@ packages: source: hosted version: "1.19.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 @@ -231,6 +239,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dart_varuint_bitcoin: + dependency: "direct main" + description: + name: dart_varuint_bitcoin + sha256: "4f0ccc9733fb54148b9d3688eea822b7aaabf5cc00025998f8c09a1d45b31b4b" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" decimal: dependency: transitive description: @@ -256,7 +280,7 @@ packages: source: hosted version: "1.3.3" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" @@ -305,6 +329,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 + url: "https://pub.dev" + source: hosted + version: "1.1.0" frontend_server_client: dependency: transitive description: @@ -329,8 +361,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hive: + hex: dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + hive: + dependency: "direct main" description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" @@ -370,7 +410,7 @@ packages: source: hosted version: "4.0.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf @@ -425,6 +465,31 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + ledger_flutter_plus: + dependency: "direct main" + description: + name: ledger_flutter_plus + sha256: "531da5daba5731d9eca2732881ef2f039b97bf8aa3564e7098dfa99a9b07a8e6" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + ledger_litecoin: + dependency: "direct main" + description: + path: "packages/ledger-litecoin" + ref: HEAD + resolved-ref: "6e6ce17c2ba5cfe3d16a60d53f930db8e827b8f3" + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" + source: git + version: "0.0.2" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -474,7 +539,7 @@ packages: source: hosted version: "2.0.0" mobx: - dependency: transitive + dependency: "direct main" description: name: mobx sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 @@ -515,7 +580,7 @@ packages: source: hosted version: "2.1.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" @@ -570,6 +635,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -642,6 +715,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -784,6 +865,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "35d210e93a5938c6a6d1fd3c710cf4ac90b1bdd1b11c8eb2beeb32600672e6e6" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -848,6 +945,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 989831a891..e3fc4516ae 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -16,6 +16,24 @@ dependencies: cw_core: path: ../cw_core + ledger_flutter_plus: ^1.4.1 + ledger_litecoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-litecoin + mobx: any + bitcoin_base: any + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v2 + ffi: any + path: any + hive: any + collection: any + intl: any + dart_varuint_bitcoin: any + convert: any dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index 2838959364..dda61ebb46 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -18,6 +18,14 @@ class CWDecred extends Decred { {required String name, required String pubkey, required String password}) => DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); + @override + WalletCredentials createDecredHardwareWalletCredentials( + {required String name, + required HardwareAccountData accountData, + WalletInfo? walletInfo}) => + DecredRestoreWalletFromHardwareCredentials( + name: name, hwAccountData: accountData, walletInfo: walletInfo); + @override WalletService createDecredWalletService( Box walletInfoSource, Box unspentCoinSource) { @@ -111,4 +119,16 @@ class CWDecred extends Decred { final decredWallet = wallet as DecredWallet; return decredWallet.pubkey; } + + @override + void setHardwareWalletService(WalletBase wallet, HardwareWalletService service) { + final decredWallet = wallet as DecredWallet; + final ledgerService = service as LedgerWalletService; + decredWallet.ledgerWalletService = ledgerService; + } + + @override + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) { + return LedgerWalletService(connection); + } } diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index a14aa6bc91..ac9a7966e1 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -326,7 +326,7 @@ class CryptoBalanceWidget extends StatelessWidget { child: InfoCard( title: S.of(context).synchronizing, description: S.of(context).decred_info_card_details, - image: 'assets/images/dcr_icon.png', + image: 'assets/images/crypto/decred.webp', leftButtonTitle: S.of(context).litecoin_mweb_dismiss, rightButtonTitle: S.of(context).learn_more, leftButtonAction: () => dashboardViewModel.dismissDecredInfoCard(), diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 2ac7644681..582af62908 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; @@ -190,6 +191,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return ethereum!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.polygon: return polygon!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); + case WalletType.decred: + return decred!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -206,6 +209,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return ethereum!.getLedgerHardwareWalletService(connection); case WalletType.polygon: return polygon!.getLedgerHardwareWalletService(connection); + case WalletType.decred: + return decred!.getLedgerHardwareWalletService(connection); default: throw UnimplementedError(); } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 9c56ce08a6..1be6b0ab19 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; @@ -98,6 +99,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with password: password, height: _options['height'] as int? ?? 0, ); + case WalletType.decred: + credentials = + decred!.createDecredHardwareWalletCredentials(name: name, accountData: selectedAccount!); + break; default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 81b884ff1e..58140af8a1 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" CW_DECRED_DIR=$(realpath ../..)/cw_decred LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="f4d456d44b817a954a2cb599ecc64c560bab39c6" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH || true diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 37384f4e12..963bec2c04 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="f4d456d44b817a954a2cb599ecc64c560bab39c6" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh index b1bbfbc438..60fb94443a 100755 --- a/scripts/macos/build_decred.sh +++ b/scripts/macos/build_decred.sh @@ -4,7 +4,7 @@ LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="f4d456d44b817a954a2cb599ecc64c560bab39c6" echo "======================= DECRED LIBWALLET =========================" diff --git a/tool/configure.dart b/tool/configure.dart index 50930c5913..317497b155 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1379,7 +1379,13 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; +import 'package:cw_core/wallet_base.dart'; import 'package:hive/hive.dart'; """; const decredCWHeaders = """ @@ -1390,6 +1396,7 @@ import 'package:cw_decred/wallet_creation_credentials.dart'; import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/transaction_credentials.dart'; import 'package:cw_decred/mnemonic.dart'; +import 'package:cw_decred/ledger.dart'; """; const decredCwPart = "part 'cw_decred.dart';"; const decredContent = """ @@ -1401,6 +1408,8 @@ abstract class Decred { {required String name, required String mnemonic, required String password}); WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( {required String name, required String pubkey, required String password}); + WalletCredentials createDecredHardwareWalletCredentials( + {required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); WalletService createDecredWalletService( Box walletInfoSource, Box unspentCoinSource); @@ -1427,6 +1436,9 @@ abstract class Decred { List getDecredWordList(); String pubkey(Object wallet); + + void setHardwareWalletService(WalletBase wallet, HardwareWalletService service); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); } """;