feat: enhance hardware wallet compatibility and improve transaction handling (#3517)

* feat: enhance hardware wallet compatibility and improve transaction handling

- Updated hardware wallet services for Bitcoin and Litecoin (Ledger & Trezor).
- Added `LitecoinTrezorService` for signing and managing Litecoin transactions.
- Improved `PSBTTransaction` builder to handle change outputs and derivation paths.
- Fixed exception handling in `wallet_hardware_restore_view_model`.
- Refactored output and input structures to ensure consistency and extend compatibility.

* refactor: address comments

* refactor: address comments

* refactor: address comments
This commit is contained in:
Konstantin Ullrich
2026-09-01 19:42:33 +08:00
committed by GitHub
parent df87bab836
commit 0d353a0932
17 changed files with 521 additions and 305 deletions
+48 -16
View File
@@ -430,7 +430,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
.isNotEmpty;
Future<PsbtV2> buildPsbt({
required List<BitcoinBaseOutput> outputs,
required List<BitcoinOutput> outputs,
required List<OutputInfo> cwOutputs,
required BigInt fee,
required BasedUtxoNetwork network,
@@ -447,33 +447,65 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
));
psbtReadyInputs.add(
PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
),
);
}
final psbtReadyOutputs = outputs.map((o) {
final cwOutput = cwOutputs
.where(
(e) => [e.address, e.extractedAddress]
.map((e) => e?.toLowerCase())
.contains(o.address.toAddress().toLowerCase()),
)
.firstOrNull;
if (o.isChange && publicKeys.containsKey(o.address.pubKeyHash())) {
final changeKey = publicKeys[o.address.pubKeyHash()]!;
return PSBTReadyBitcoinOutput(
address: o.address,
value: o.value,
isSilentPayment: o.isSilentPayment,
isChange: o.isChange,
changeMasterFingerprint: masterFingerprint,
changeDerivationPath: changeKey.derivationPath,
changePublicKey: changeKey.publicKey,
outputInfo: cwOutput,
);
}
return PSBTReadyBitcoinOutput(
address: o.address,
value: o.value,
isSilentPayment: o.isSilentPayment,
isChange: o.isChange,
outputInfo: cwOutput,
);
}).toList();
final locktime = antiFeeSnipingLocktime(
chainTip: await getCurrentChainTip(),
synced: syncStatus is SyncedSyncStatus,
);
return PSBTTransactionBuild(
inputs: psbtReadyInputs,
outputs: outputs,
enableRBF: enableRBF,
cwOutputs: cwOutputs,
locktime: locktime)
.psbt;
inputs: psbtReadyInputs,
outputs: psbtReadyOutputs,
enableRBF: enableRBF,
locktime: locktime,
).psbt;
}
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required List<BitcoinOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
+5 -2
View File
@@ -1283,8 +1283,11 @@ abstract class ElectrumWalletBase
final changeDerivationPath = "${_hardenedDerivationPath(changeBaseDerivationPath)}"
"/${changeAddress.isHidden ? "1" : "0"}"
"/${changeAddress.index}";
final changeHd = _hdFor(record: changeAddress).childKey(Bip32KeyIndex(changeAddress.index));
final changePubKeyHex = changeHd.publicKey.toHex();
utxoDetails.publicKeys[address.pubKeyHash()] =
PublicKeyWithDerivationPath('', changeDerivationPath);
PublicKeyWithDerivationPath(changePubKeyHex, changeDerivationPath);
// calcFee updates the silent payment outputs to calculate the tx size accounting
// for taproot addresses, but if more inputs are needed to make up for fees,
@@ -1737,7 +1740,7 @@ abstract class ElectrumWalletBase
HardwareWalletService? hardwareWalletService;
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required List<BitcoinOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
+2 -2
View File
@@ -106,11 +106,11 @@ class LitecoinBitboxService extends HardwareWalletService
@override
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<PSBTReadyBitcoinOutput> outputs,
required List<PSBTReadyUtxoWithAddress> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
}) async {
final psbt = PSBTTransactionBuild(inputs: inputs, outputs: outputs, cwOutputs: []).psbt;
final psbt = PSBTTransactionBuild(inputs: inputs, outputs: outputs).psbt;
final signedPsbt = await manager.signBTCPsbt(bitboxCoinType, base64Encode(psbt.asPsbtV0()));
final transactionRes = PsbtV2()
@@ -1,8 +1,7 @@
import 'dart:typed_data';
import "dart:typed_data";
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import "package:cw_bitcoin/electrum_wallet.dart";
import "package:cw_bitcoin/psbt/transaction_builder.dart";
mixin BitcoinHardwareWalletService {
Future<Uint8List> getMasterFingerprint() async => Uint8List.fromList([0, 0, 0, 0]);
@@ -10,7 +9,7 @@ mixin BitcoinHardwareWalletService {
mixin LitecoinHardwareWalletService on BitcoinHardwareWalletService {
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<PSBTReadyBitcoinOutput> outputs,
required List<PSBTReadyUtxoWithAddress> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
});
@@ -1,16 +1,16 @@
import 'dart:async';
import 'dart:typed_data';
import "dart:async";
import "dart:typed_data";
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:ledger_litecoin/ledger_litecoin.dart';
import "package:bitcoin_base/bitcoin_base.dart";
import "package:blockchain_utils/blockchain_utils.dart";
import "package:cw_bitcoin/electrum_wallet.dart";
import "package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart";
import "package:cw_bitcoin/psbt/transaction_builder.dart";
import "package:cw_bitcoin/utils.dart";
import "package:cw_core/hardware/hardware_account_data.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:ledger_flutter_plus/ledger_flutter_plus.dart";
import "package:ledger_litecoin/ledger_litecoin.dart";
class LitecoinLedgerService extends HardwareWalletService
with BitcoinHardwareWalletService, LitecoinHardwareWalletService {
@@ -31,18 +31,21 @@ class LitecoinLedgerService extends HardwareWalletService
for (final i in indexRange) {
final derivationPath = "m/84'/2'/$i'";
final xpub = await litecoinLedgerApp.getXPubKey(
accountsDerivationPath: derivationPath,
xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16));
accountsDerivationPath: derivationPath,
xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16),
);
final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: LitecoinNetwork.mainnet);
accounts.add(HardwareAccountData(
address: address,
accountIndex: i,
derivationPath: derivationPath,
xpub: xpub,
));
accounts.add(
HardwareAccountData(
address: address,
accountIndex: i,
derivationPath: derivationPath,
xpub: xpub,
),
);
}
return accounts;
@@ -50,7 +53,7 @@ class LitecoinLedgerService extends HardwareWalletService
@override
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<PSBTReadyBitcoinOutput> outputs,
required List<PSBTReadyUtxoWithAddress> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
}) {
@@ -58,31 +61,39 @@ class LitecoinLedgerService extends HardwareWalletService
for (final utxo in inputs) {
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
readyInputs.add(LedgerTransaction(
rawTx: utxo.rawTx,
outputIndex: utxo.utxo.vout,
ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)),
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
));
readyInputs.add(
LedgerTransaction(
rawTx: utxo.rawTx,
outputIndex: utxo.utxo.vout,
ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)),
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
),
);
}
// Check if we have the key to one of the output addresses to hide change on the device
String? changePath;
for (final output in outputs) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
if (output.isChange) {
changePath ??= output.changeDerivationPath;
}
}
return litecoinLedgerApp.createTransaction(
inputs: readyInputs,
outputs: outputs
.map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value,
Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
.toList(),
changePath: changePath,
sigHashType: 0x01,
additionals: ["bech32"],
isSegWit: true,
useTrustedInputForSegwit: true);
inputs: readyInputs,
outputs: outputs
.map(
(e) => TransactionOutput.fromBigInt(
(e as BitcoinOutput).value,
Uint8List.fromList(e.address.toScriptPubKey().toBytes()),
),
)
.toList(),
changePath: changePath,
sigHashType: 0x01,
additionals: ["bech32"],
isSegWit: true,
useTrustedInputForSegwit: true,
);
}
}
@@ -0,0 +1,100 @@
import 'dart:async';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:trezor_connect/trezor_connect.dart';
class LitecoinTrezorService extends HardwareWalletService
with BitcoinHardwareWalletService, LitecoinHardwareWalletService {
LitecoinTrezorService(this.connect);
final TrezorConnect connect;
@override
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final indexRange = List.generate(limit, (i) => i + index);
final requestParams = <TrezorGetPublicKeyParams>[];
final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer;
for (final i in indexRange) {
final derivationPath = "m/84'/2'/$i'";
requestParams.add(TrezorGetPublicKeyParams(path: derivationPath, coin: "LTC"));
}
final accounts = await connect.getPublicKeyBundle(requestParams);
return accounts?.map((account) {
final hd = Bip32Slip10Secp256k1.fromExtendedKey(account.xpub, xpubVersion)
.childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: LitecoinNetwork.mainnet);
return HardwareAccountData(
address: address,
xpub: account.xpub,
accountIndex: account.path[2] - 0x80000000, // unharden the path to get the index
derivationPath: account.serializedPath,
);
}).toList() ??
[];
}
@override
Future<String> signLitecoinTransaction({
required List<PSBTReadyBitcoinOutput> outputs,
required List<PSBTReadyUtxoWithAddress> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
}) async {
final readyInputs = inputs
.map((input) => TrezorTxInput(
prevHash: input.utxo.txHash,
prevIndex: input.utxo.vout,
amount: input.utxo.value.toInt(),
addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
scriptType: "SPENDWITNESS",
))
.toList();
final readyOutputs = outputs.map((output) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
return TrezorTxOutput(
amount: output.toOutput.amount.toInt(),
address: maybeChangePath != null
? null
: output.toOutput.scriptPubKey.toAddress(network: LitecoinNetwork.mainnet),
scriptType: _getScriptType(output.toOutput.scriptPubKey.getAddressType()!),
addressPath: maybeChangePath != null
? Bip32PathParser.parse(maybeChangePath.derivationPath).toList()
: null,
);
}).toList();
final signedTx =
await connect.signTransaction(coin: 'LTC', inputs: readyInputs, outputs: readyOutputs);
return signedTx!.serializedTx;
}
}
String _getScriptType(BitcoinAddressType addressType) {
switch (addressType) {
case P2pkhAddressType.p2pkh:
return "PAYTOADDRESS";
case P2shAddressType.p2wpkhInP2sh:
return "PAYTOSCRIPTHASH";
case SegwitAddresType.p2tr:
return "PAYTOTAPROOT";
case SegwitAddresType.p2wsh:
return "PAYTOP2SHWITNESS";
case SegwitAddresType.p2wpkh:
return "PAYTOWITNESS";
default:
throw Exception("Unknown Address Type");
}
}
+128 -140
View File
@@ -1,190 +1,178 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:flutter/foundation.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:trezor_connect/trezor_connect.dart';
import 'package:ledger_bitcoin/src/utils/uint8list_extension.dart' as ext;
import 'package:trezor_flutter/trezor_flutter.dart' as sdk;
class BitcoinTrezorService extends HardwareWalletService with BitcoinHardwareWalletService {
BitcoinTrezorService(this.connect);
BitcoinTrezorService(this.client) : _trezorBitcoin = sdk.TrezorBitcoin(client);
final TrezorConnect connect;
final sdk.TrezorClient client;
final sdk.TrezorBitcoin _trezorBitcoin;
@override
Future<Uint8List> getMasterFingerprint() async {
final publicKey = await _trezorBitcoin.getPublicKey(derivationPath: "m/84'/0'/0'");
final fingerprintBuffer = ByteData(4)..setUint32(0, publicKey.$2);
return fingerprintBuffer.buffer.asUint8List();
}
@override
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final accounts = <HardwareAccountData>[];
final indexRange = List.generate(limit, (i) => i + index);
final requestParams = <TrezorGetPublicKeyParams>[];
for (final i in indexRange) {
requestParams.add(TrezorGetPublicKeyParams(path: "m/84'/0'/$i'"));
final derivationPath = "m/84'/0'/$i'";
final xPub =
await _trezorBitcoin.getPublicKey(derivationPath: derivationPath, ignoreXpubMagic: true);
final hd = Bip32Slip10Secp256k1.fromExtendedKey(xPub.$1).childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
accounts.add(
HardwareAccountData(
address: address,
accountIndex: i,
derivationPath: derivationPath,
xpub: xPub.$1,
),
);
}
final accounts = await connect.getPublicKeyBundle(requestParams);
return accounts?.map((account) {
final hd = Bip32Slip10Secp256k1.fromExtendedKey(account.xpub).childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
return HardwareAccountData(
address: address,
xpub: account.xpub,
accountIndex: account.path[2] - 0x80000000, // unharden the path to get the index
derivationPath: account.serializedPath,
);
}).toList() ??
[];
return accounts;
}
@override
Future<Uint8List> signTransaction({required String transaction}) async {
final psbt = PsbtV2()..deserialize(base64Decode(transaction));
final masterFingerprint = await getMasterFingerprint();
final inputs = <sdk.TrezorTxInput>[];
final prevTxs = <String, sdk.TrezorPrevTx>{};
final inputs = <TrezorTxInput>[];
final inputCount = psbt.getGlobalInputCount();
for (var i = 0; i < inputCount; i++) {
final inputTxRaw = psbt.getInputNonWitnessUtxo(i);
final inputTx = BtcTransaction.fromRaw(hex.encode(inputTxRaw!));
final inputOutputIndex = psbt.getInputOutputIndex(i);
final rawPrevTx = psbt.getInputNonWitnessUtxo(i);
if (rawPrevTx == null) {
throw StateError("PSBT input $i is missing its previous transaction");
}
final prevTx = BtcTransaction.fromRaw(hex.encode(rawPrevTx));
final vout = psbt.getInputOutputIndex(i);
final publicKeys = psbt.inputMaps[i].keys.where((e) => e.startsWith("06"));
final pubkey = Uint8List.fromList(hex.decode(publicKeys.first.substring(2)));
final txidBytes = psbt.getInputPreviousTxid(i).reversed.toList();
inputs.add(TrezorTxInput(
prevHash: hex.encode(psbt.getInputPreviousTxid(i).reversed.toList()),
prevIndex: inputOutputIndex,
amount: inputTx.outputs[inputOutputIndex].amount.toInt(),
addressPath: psbt.getInputBip32Derivation(i, pubkey)!.$2,
final pubkey = _firstDerivationPubkey(psbt.inputMaps[i], "06");
if (pubkey == null) {
throw StateError("PSBT input $i is missing its BIP32 derivation");
}
final derivation = psbt.getInputBip32Derivation(i, pubkey)!;
final path = derivation.$2;
if (!listEquals(derivation.$1, masterFingerprint)) {
throw Exception("Fingerprint missmatch with the PSBT");
}
inputs.add(
sdk.TrezorTxInput(
addressPath: path,
prevHash: txidBytes,
prevIndex: vout,
amount: prevTx.outputs[vout].amount.toInt(),
sequence: psbt.getInputSequence(i),
scriptType: "SPENDWITNESS"));
scriptType: "SPENDWITNESS",
),
);
prevTxs[hex.encode(txidBytes)] = sdk.TrezorPrevTx(
meta: sdk.TrezorPrevTxMeta(
version: Uint8List.fromList(prevTx.version).readUint32LE(0),
lockTime: Uint8List.fromList(prevTx.locktime).readUint32LE(0),
inputsCount: prevTx.inputs.length,
outputsCount: prevTx.outputs.length,
),
inputs: prevTx.inputs
.map(
(e) => sdk.TrezorPrevInput(
prevHash: hex.decode(e.txId),
prevIndex: e.txIndex,
scriptSig: e.scriptSig.toBytes(),
sequence: Uint8List.fromList(e.sequence).readUint32LE(0),
),
)
.toList(),
outputs: prevTx.outputs
.map(
(e) => sdk.TrezorPrevOutput(
amount: e.amount.toInt(),
scriptPubkey: e.scriptPubKey.toBytes(),
),
)
.toList(),
);
}
final outputs = <TrezorTxOutput>[];
final outputs = <sdk.TrezorTxOutput>[];
final outputCount = psbt.getGlobalOutputCount();
for (var i = 0; i < outputCount; i++) {
final amount = psbt.getOutputAmount(i);
// An output carrying our own BIP32 derivation is change: identify it
// by path so the device verifies it internally instead of displaying
// it as a payment.
final changePubkey = _firstDerivationPubkey(psbt.outputMaps[i], "02");
if (changePubkey != null) {
final (fingerprint, path) = psbt.getOutputBip32Derivation(i, changePubkey);
if (listEquals(fingerprint, masterFingerprint)) {
outputs.add(sdk.TrezorTxOutput(
addressPath: path,
amount: amount,
scriptType: "PAYTOWITNESS",
));
continue;
}
}
final script = Script.fromRaw(byteData: psbt.getOutputScript(i));
// Trezor's protocol expects script_type: PAYTOADDRESS whenever the
// output is identified by `address` (vs. own-change `addressPath`).
// Suite parses the address string itself to determine the actual
// on-chain script type (P2WPKH, P2TR, P2SH, etc.); script_type is
// only semantically meaningful when addressPath is set. Passing
// PAYTOWITNESS / PAYTOP2SHWITNESS / PAYTOTAPROOT here together with
// `address` causes Suite to reject the deeplink as "Invalid
// parameters from calling app" because those values specifically
// mean "own change paid to that wallet type."
outputs.add(TrezorTxOutput(
amount: psbt.getOutputAmount(i),
outputs.add(sdk.TrezorTxOutput(
address: script.toAddress(),
amount: amount,
scriptType: "PAYTOADDRESS",
// ToDo: when change-output detection lands, set addressPath + _getScriptType(...) for own-change outputs.
// ToDo: addressPath: psbt.getOutputBip32Derivation(i, pubkey).$2, // To highlight change outputs
));
}
final signedTx = await connect.signTransaction(coin: 'btc', inputs: inputs, outputs: outputs);
final signed = await _trezorBitcoin.signTransaction(
inputs: inputs,
outputs: outputs,
prevTxs: prevTxs,
version: psbt.getGlobalTxVersion(),
lockTime: psbt.getGlobalFallbackLocktime() ?? 0,
);
return Uint8List.fromList(BytesUtils.fromHexString(signedTx!.serializedTx));
return signed.serializedTx;
}
@override
Future<Uint8List> signMessage({required Uint8List message, String? derivationPath}) async {
final sig = await connect.signMessage(derivationPath ?? "m/84'/0'/0'/0/0",
message: hex.encode(message), hex: true);
return base64Decode(sig!.signature);
}
}
class LitecoinTrezorService extends HardwareWalletService
with BitcoinHardwareWalletService, LitecoinHardwareWalletService {
LitecoinTrezorService(this.connect);
final TrezorConnect connect;
@override
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final indexRange = List.generate(limit, (i) => i + index);
final requestParams = <TrezorGetPublicKeyParams>[];
final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer;
for (final i in indexRange) {
final derivationPath = "m/84'/2'/$i'";
requestParams.add(TrezorGetPublicKeyParams(path: derivationPath, coin: "LTC"));
}
final accounts = await connect.getPublicKeyBundle(requestParams);
return accounts?.map((account) {
final hd = Bip32Slip10Secp256k1.fromExtendedKey(account.xpub, xpubVersion)
.childKey(Bip32KeyIndex(0));
final address = generateP2WPKHAddress(hd: hd, index: 0, network: LitecoinNetwork.mainnet);
return HardwareAccountData(
address: address,
xpub: account.xpub,
accountIndex: account.path[2] - 0x80000000, // unharden the path to get the index
derivationPath: account.serializedPath,
);
}).toList() ??
[];
}
@override
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<PSBTReadyUtxoWithAddress> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
}) async {
final readyInputs = inputs
.map((input) => TrezorTxInput(
prevHash: input.utxo.txHash,
prevIndex: input.utxo.vout,
amount: input.utxo.value.toInt(),
addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
scriptType: "SPENDWITNESS",
))
.toList();
final readyOutputs = outputs.map((output) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
return TrezorTxOutput(
amount: output.toOutput.amount.toInt(),
address: maybeChangePath != null
? null
: output.toOutput.scriptPubKey.toAddress(network: LitecoinNetwork.mainnet),
scriptType: _getScriptType(output.toOutput.scriptPubKey.getAddressType()!),
addressPath: maybeChangePath != null
? Bip32PathParser.parse(maybeChangePath.derivationPath).toList()
: null,
Future<Uint8List> signMessage({required Uint8List message, String? derivationPath}) =>
_trezorBitcoin.signMessage(
derivationPath: derivationPath ?? "m/84'/0'/0'/0/0",
message: message,
);
}).toList();
final signedTx =
await connect.signTransaction(coin: 'LTC', inputs: readyInputs, outputs: readyOutputs);
return signedTx!.serializedTx;
}
}
String _getScriptType(BitcoinAddressType addressType) {
switch (addressType) {
case P2pkhAddressType.p2pkh:
return "PAYTOADDRESS";
case P2shAddressType.p2wpkhInP2sh:
return "PAYTOSCRIPTHASH";
case SegwitAddresType.p2tr:
return "PAYTOTAPROOT";
case SegwitAddresType.p2wsh:
return "PAYTOP2SHWITNESS";
case SegwitAddresType.p2wpkh:
return "PAYTOWITNESS";
default:
throw Exception("Unknown Address Type");
/// Extracts the pubkey from the first BIP32-derivation record
/// ([keyTypeHex] `06` for inputs, `02` for outputs) of a PSBT key-value map.
Uint8List? _firstDerivationPubkey(Map<String, Uint8List> map, String keyTypeHex) {
final key = map.keys.where((mapKey) => mapKey.startsWith(keyTypeHex)).firstOrNull;
if (key == null) return null;
return Uint8List.fromList(hex.decode(key.substring(2)));
}
}
+34 -14
View File
@@ -1556,7 +1556,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required List<BitcoinOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
@@ -1568,28 +1568,48 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint =
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
await (hardwareWalletService! as BitcoinHardwareWalletService).getMasterFingerprint();
final readyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
readyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
));
readyInputs.add(
PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
),
);
}
final orderedOutputs = orderOutputs(outputs, outputOrdering);
final orderedOutputs =
orderOutputs(outputs, outputOrdering).map((o) {
if (o.isChange && publicKeys.containsKey(o.address.pubKeyHash())) {
final changeKey = publicKeys[o.address.pubKeyHash()]!;
return PSBTReadyBitcoinOutput(
address: o.address,
value: o.value,
isSilentPayment: o.isSilentPayment,
isChange: o.isChange,
changeMasterFingerprint: masterFingerprint,
changeDerivationPath: changeKey.derivationPath,
changePublicKey: changeKey.publicKey,
);
}
return PSBTReadyBitcoinOutput.fromOutput(o);
});
final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService)
.signLitecoinTransaction(
outputs: orderedOutputs, inputs: readyInputs, publicKeys: publicKeys);
final rawHex =
await (hardwareWalletService! as LitecoinHardwareWalletService).signLitecoinTransaction(
outputs: orderedOutputs.toList(),
inputs: readyInputs,
publicKeys: publicKeys,
);
return BtcTransaction.fromRaw(rawHex);
}
+112 -63
View File
@@ -1,21 +1,19 @@
import 'dart:convert';
import 'dart:typed_data';
import "dart:convert";
import "dart:typed_data";
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:convert/convert.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:ledger_bitcoin/psbt.dart';
import "package:bitcoin_base/bitcoin_base.dart";
import "package:convert/convert.dart";
import "package:cw_core/output_info.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:ledger_bitcoin/psbt.dart";
class PSBTTransactionBuild {
final PsbtV2 psbt = PsbtV2();
PSBTTransactionBuild(
{required List<PSBTReadyUtxoWithAddress> inputs,
required List<BitcoinBaseOutput> outputs,
required List<OutputInfo> cwOutputs,
bool enableRBF = true,
int locktime = 0}) {
PSBTTransactionBuild({
required List<PSBTReadyUtxoWithAddress> inputs,
required List<PSBTReadyBitcoinOutput> outputs,
bool enableRBF = true,
int locktime = 0,
}) {
psbt.setGlobalTxVersion(2);
psbt.setGlobalFallbackLocktime(locktime);
psbt.setGlobalInputCount(inputs.length);
@@ -24,12 +22,10 @@ class PSBTTransactionBuild {
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
printV(input.utxo.isP2tr());
printV(input.utxo.isSegwit());
printV(input.utxo.isP2shSegwit());
psbt.setInputPreviousTxId(
i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()));
i,
Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()),
);
psbt.setInputOutputIndex(i, input.utxo.vout);
psbt.setInputSequence(i, enableRBF ? 0xfffffffd : 0xffffffff);
@@ -47,75 +43,98 @@ class PSBTTransactionBuild {
for (var i = 0; i < outputs.length; i++) {
final output = outputs[i];
if (output is BitcoinOutput) {
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
psbt.setOutputAmount(i, output.value.toInt());
if (cwOutputs.isNotEmpty) {
try {
final cwOutput = cwOutputs
.where((e) => [e.address, e.extractedAddress]
.map((e) => e?.toLowerCase())
.contains(output.address.toAddress().toLowerCase()))
.firstOrNull;
if (cwOutput != null &&
cwOutput.extra.containsKey('bip353_name') &&
cwOutput.extra.containsKey('bip353_proof')) {
final bip353Name = utf8.encode(cwOutput.extra['bip353_name'] as String);
final bip353Proof = base64.decode(cwOutput.extra['bip353_proof'] as String);
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
psbt.setOutputAmount(i, output.value.toInt());
if (bip353Name.length > 255) {
printV('BIP353 name is too long, skipping');
continue;
}
final proof = Uint8List.fromList([
bip353Name.length,
...bip353Name,
...bip353Proof,
]);
if (output.isChange &&
output.changeDerivationPath != null &&
output.changePublicKey != null &&
output.changeMasterFingerprint != null) {
psbt.setOutputBip32Derivation(
i,
Uint8List.fromList(hex.decode(output.changePublicKey!)),
output.changeMasterFingerprint!,
BIPPath.fromString(output.changeDerivationPath!).toPathArray(),
);
}
psbt.setOutputDNSSECProof(i, proof);
if (output.outputInfo != null) {
try {
final cwOutput = output.outputInfo!;
if (cwOutput.extra.containsKey("bip353_name") &&
cwOutput.extra.containsKey("bip353_proof")) {
final bip353Name = utf8.encode(cwOutput.extra["bip353_name"] as String);
final bip353Proof = base64.decode(cwOutput.extra["bip353_proof"] as String);
if (bip353Name.length > 255) {
printV("BIP353 name is too long, skipping");
continue;
}
} catch (e) {
printV('Error setting DNSSEC proof: $e');
final proof = Uint8List.fromList([
bip353Name.length,
...bip353Name,
...bip353Proof,
]);
psbt.setOutputDNSSECProof(i, proof);
}
} catch (e) {
printV("Error setting DNSSEC proof: $e");
}
}
}
}
final PsbtV2 psbt = PsbtV2();
void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
}
void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
psbt.setInputWitnessUtxo(
i,
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
}
void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
);
psbt.setInputRedeemScript(
i, Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
i,
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
psbt.setInputWitnessUtxo(
i,
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
);
}
}
class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
final String rawTx;
final String ownerDerivationPath;
final Uint8List ownerMasterFingerprint;
final String ownerPublicKey;
PSBTReadyUtxoWithAddress({
required super.utxo,
required this.rawTx,
@@ -124,4 +143,34 @@ class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
required this.ownerMasterFingerprint,
required this.ownerPublicKey,
});
final String rawTx;
final String ownerDerivationPath;
final Uint8List ownerMasterFingerprint;
final String ownerPublicKey;
}
class PSBTReadyBitcoinOutput extends BitcoinOutput {
PSBTReadyBitcoinOutput({
required super.address,
required super.value,
super.isSilentPayment = false,
super.isChange = false,
this.changeDerivationPath,
this.changeMasterFingerprint,
this.changePublicKey,
this.outputInfo,
});
factory PSBTReadyBitcoinOutput.fromOutput(BitcoinOutput output) => PSBTReadyBitcoinOutput(
address: output.address,
value: output.value,
isSilentPayment: output.isSilentPayment,
isChange: output.isChange,
);
final String? changeDerivationPath;
final Uint8List? changeMasterFingerprint;
final String? changePublicKey;
final OutputInfo? outputInfo;
}
+5
View File
@@ -55,6 +55,11 @@ dependencies:
git:
url: https://github.com/cake-tech/trezor_connect
ref: d1242cea90f84b00200e7bcab914a5af750e23fb
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter
ref: f5121b1f3a06000ff836da8e6e6c83f53a9fa487
path: trezor-flutter
bitbox_flutter:
path: ../scripts/bitbox_flutter
socks_socket:
+2 -2
View File
@@ -32,8 +32,8 @@ dependencies:
ledger_flutter_plus: ^1.4.1
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
url: https://github.com/cake-tech/trezor-flutter
ref: f5121b1f3a06000ff836da8e6e6c83f53a9fa487
path: trezor-flutter
dev_dependencies:
+3 -3
View File
@@ -558,9 +558,9 @@ class CWBitcoin extends Bitcoin {
@override
HardwareWalletService getTrezorHardwareWalletService(
trezor.TrezorConnect connect, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(connect);
return LitecoinTrezorService(connect);
trezor.TrezorConnect? connect, trezor.TrezorClient? client, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(client!);
return LitecoinTrezorService(connect!);
}
@override
+15 -10
View File
@@ -361,9 +361,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
ConnectDevicePageParams(
walletType: availableWalletTypes.first,
hardwareWalletType: hardwareWalletType,
onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [availableWalletTypes.first, hardwareWalletType]),
onConnectDevice: (context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [availableWalletTypes.first, hardwareWalletType],
),
isReconnect: false,
),
getIt.get<HardwareWalletViewModel>(param1: hardwareWalletType),
@@ -373,19 +374,23 @@ Route<dynamic> createRoute(RouteSettings settings) {
return handleRouteWithPlatformAwareness(
(_) => getIt.get<NewWalletTypePage>(
param1: NewWalletTypeArguments(
onTypeSelected: (BuildContext context, WalletType type) {
if (hardwareWalletType == HardwareWalletType.trezor && type != WalletType.monero) {
Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]);
onTypeSelected: (context, type) {
if (hardwareWalletType == HardwareWalletType.trezor &&
!trezorUseNative.contains(type)) {
Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType],
);
return;
}
final arguments = ConnectDevicePageParams(
walletType: type,
hardwareWalletType: hardwareWalletType,
onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]),
onConnectDevice: (context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType],
),
isReconnect: false,
);
@@ -29,6 +29,8 @@ import "package:trezor_flutter/trezor_flutter.dart" as sdk;
part "trezor_connect_view_model.g.dart";
const trezorUseNative = [WalletType.bitcoin, WalletType.monero];
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
@@ -242,7 +244,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
}
@override
bool isConnected(WalletType type) => type == WalletType.monero
bool isConnected(WalletType type) => trezorUseNative.contains(type)
? _client != null && _client?.connection.isDisconnected == false
: true;
@@ -252,9 +254,9 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
case WalletType.monero:
return monero!.getTrezorHardwareWalletService(_client!);
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
return bitcoin!.getTrezorHardwareWalletService(null, _client!, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, null, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
@@ -56,7 +56,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
availableAccounts.addAll(accounts);
_nextIndex += limit;
} catch (e) {
} on Exception catch (e) {
printV(e);
error =
hardwareWalletVM.interpretErrorCode(e.toString()) ?? S.current.ledger_connection_error;
+4 -4
View File
@@ -1480,13 +1480,13 @@ dependency_overrides:
ref: d1242cea90f84b00200e7bcab914a5af750e23fb
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
url: https://github.com/cake-tech/trezor-flutter
ref: f5121b1f3a06000ff836da8e6e6c83f53a9fa487
path: ./trezor-flutter
trezor_usb_transport:
git:
url: https://github.com/cake-tech/trezor-flutter.git
ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
url: https://github.com/cake-tech/trezor-flutter
ref: f5121b1f3a06000ff836da8e6e6c83f53a9fa487
path: ./trezor_usb_transport
tuple:
git:
+3 -1
View File
@@ -127,6 +127,7 @@ import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:bitbox_flutter/bitbox_flutter.dart' as bitbox;
import 'package:trezor_connect/trezor_connect.dart' as trezor;
import 'package:trezor_flutter/trezor_flutter.dart' as trezor;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:collection/collection.dart';
@@ -156,6 +157,7 @@ import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
import 'package:cw_bitcoin/hardware/litecoin_ledger_service.dart';
import 'package:cw_bitcoin/hardware/bitbox_service.dart';
import 'package:cw_bitcoin/hardware/litecoin_trezor_service.dart';
import 'package:cw_bitcoin/hardware/trezor_service.dart';
import 'package:mobx/mobx.dart';
import "package:breez_sdk_spark_flutter/src/rust/errors.dart";
@@ -275,7 +277,7 @@ abstract class Bitcoin {
Future<void> setHardwareWalletService(WalletBase wallet, HardwareWalletService service);
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection, bool isBitcoin);
HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager, bool isBitcoin);
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect, bool isBitcoin);
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect? connect, trezor.TrezorClient? client, bool isBitcoin);
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs);
bool txIsReceivedSilentPayment(TransactionInfo txInfo);
bool txIsMweb(TransactionInfo txInfo);