mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user