dev
dart 190 lines 7.43 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:typed_data';
4
5 import 'package:bitcoin_base/bitcoin_base.dart';
6 import 'package:blockchain_utils/blockchain_utils.dart';
7 import 'package:cw_bitcoin/electrum_wallet.dart';
8 import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
9 import 'package:cw_bitcoin/psbt/transaction_builder.dart';
10 import 'package:cw_bitcoin/utils.dart';
11 import 'package:cw_core/hardware/hardware_account_data.dart';
12 import 'package:cw_core/hardware/hardware_wallet_service.dart';
13 import 'package:ledger_bitcoin/psbt.dart';
14 import 'package:trezor_connect/trezor_connect.dart';
15
16 class BitcoinTrezorService extends HardwareWalletService with BitcoinHardwareWalletService {
17 BitcoinTrezorService(this.connect);
18
19 final TrezorConnect connect;
20
21 @override
22 Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
23 final indexRange = List.generate(limit, (i) => i + index);
24 final requestParams = <TrezorGetPublicKeyParams>[];
25
26 for (final i in indexRange) {
27 requestParams.add(TrezorGetPublicKeyParams(path: "m/84'/0'/$i'"));
28 }
29
30 final accounts = await connect.getPublicKeyBundle(requestParams);
31
32 return accounts?.map((account) {
33 final hd = Bip32Slip10Secp256k1.fromExtendedKey(account.xpub).childKey(Bip32KeyIndex(0));
34 final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
35 return HardwareAccountData(
36 address: address,
37 xpub: account.xpub,
38 accountIndex: account.path[2] - 0x80000000, // unharden the path to get the index
39 derivationPath: account.serializedPath,
40 );
41 }).toList() ??
42 [];
43 }
44
45 @override
46 Future<Uint8List> signTransaction({required String transaction}) async {
47 final psbt = PsbtV2()..deserialize(base64Decode(transaction));
48
49 final inputs = <TrezorTxInput>[];
50 final inputCount = psbt.getGlobalInputCount();
51 for (var i = 0; i < inputCount; i++) {
52 final inputTxRaw = psbt.getInputNonWitnessUtxo(i);
53 final inputTx = BtcTransaction.fromRaw(hex.encode(inputTxRaw!));
54 final inputOutputIndex = psbt.getInputOutputIndex(i);
55
56 final publicKeys = psbt.inputMaps[i].keys.where((e) => e.startsWith("06"));
57 final pubkey = Uint8List.fromList(hex.decode(publicKeys.first.substring(2)));
58
59 inputs.add(TrezorTxInput(
60 prevHash: hex.encode(psbt.getInputPreviousTxid(i).reversed.toList()),
61 prevIndex: inputOutputIndex,
62 amount: inputTx.outputs[inputOutputIndex].amount.toInt(),
63 addressPath: psbt.getInputBip32Derivation(i, pubkey)!.$2,
64 sequence: psbt.getInputSequence(i),
65 scriptType: "SPENDWITNESS"));
66 }
67
68 final outputs = <TrezorTxOutput>[];
69 final outputCount = psbt.getGlobalOutputCount();
70 for (var i = 0; i < outputCount; i++) {
71 final script = Script.fromRaw(byteData: psbt.getOutputScript(i));
72 // Trezor's protocol expects script_type: PAYTOADDRESS whenever the
73 // output is identified by `address` (vs. own-change `addressPath`).
74 // Suite parses the address string itself to determine the actual
75 // on-chain script type (P2WPKH, P2TR, P2SH, etc.); script_type is
76 // only semantically meaningful when addressPath is set. Passing
77 // PAYTOWITNESS / PAYTOP2SHWITNESS / PAYTOTAPROOT here together with
78 // `address` causes Suite to reject the deeplink as "Invalid
79 // parameters from calling app" because those values specifically
80 // mean "own change paid to that wallet type."
81 outputs.add(TrezorTxOutput(
82 amount: psbt.getOutputAmount(i),
83 address: script.toAddress(),
84 scriptType: "PAYTOADDRESS",
85 // ToDo: when change-output detection lands, set addressPath + _getScriptType(...) for own-change outputs.
86 // ToDo: addressPath: psbt.getOutputBip32Derivation(i, pubkey).$2, // To highlight change outputs
87 ));
88 }
89
90 final signedTx = await connect.signTransaction(coin: 'btc', inputs: inputs, outputs: outputs);
91
92 return Uint8List.fromList(BytesUtils.fromHexString(signedTx!.serializedTx));
93 }
94
95 @override
96 Future<Uint8List> signMessage({required Uint8List message, String? derivationPath}) async {
97 final sig = await connect.signMessage(derivationPath ?? "m/84'/0'/0'/0/0",
98 message: hex.encode(message), hex: true);
99 return base64Decode(sig!.signature);
100 }
101 }
102
103 class LitecoinTrezorService extends HardwareWalletService
104 with BitcoinHardwareWalletService, LitecoinHardwareWalletService {
105 LitecoinTrezorService(this.connect);
106
107 final TrezorConnect connect;
108
109 @override
110 Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
111 final indexRange = List.generate(limit, (i) => i + index);
112 final requestParams = <TrezorGetPublicKeyParams>[];
113 final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer;
114
115 for (final i in indexRange) {
116 final derivationPath = "m/84'/2'/$i'";
117 requestParams.add(TrezorGetPublicKeyParams(path: derivationPath, coin: "LTC"));
118 }
119
120 final accounts = await connect.getPublicKeyBundle(requestParams);
121
122 return accounts?.map((account) {
123 final hd = Bip32Slip10Secp256k1.fromExtendedKey(account.xpub, xpubVersion)
124 .childKey(Bip32KeyIndex(0));
125
126 final address = generateP2WPKHAddress(hd: hd, index: 0, network: LitecoinNetwork.mainnet);
127 return HardwareAccountData(
128 address: address,
129 xpub: account.xpub,
130 accountIndex: account.path[2] - 0x80000000, // unharden the path to get the index
131 derivationPath: account.serializedPath,
132 );
133 }).toList() ??
134 [];
135 }
136
137 @override
138 Future<String> signLitecoinTransaction({
139 required List<BitcoinBaseOutput> outputs,
140 required List<PSBTReadyUtxoWithAddress> inputs,
141 required Map<String, PublicKeyWithDerivationPath> publicKeys,
142 }) async {
143 final readyInputs = inputs
144 .map((input) => TrezorTxInput(
145 prevHash: input.utxo.txHash,
146 prevIndex: input.utxo.vout,
147 amount: input.utxo.value.toInt(),
148 addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
149 scriptType: "SPENDWITNESS",
150 ))
151 .toList();
152
153 final readyOutputs = outputs.map((output) {
154 final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
155
156 return TrezorTxOutput(
157 amount: output.toOutput.amount.toInt(),
158 address: maybeChangePath != null
159 ? null
160 : output.toOutput.scriptPubKey.toAddress(network: LitecoinNetwork.mainnet),
161 scriptType: _getScriptType(output.toOutput.scriptPubKey.getAddressType()!),
162 addressPath: maybeChangePath != null
163 ? Bip32PathParser.parse(maybeChangePath.derivationPath).toList()
164 : null,
165 );
166 }).toList();
167
168 final signedTx =
169 await connect.signTransaction(coin: 'LTC', inputs: readyInputs, outputs: readyOutputs);
170
171 return signedTx!.serializedTx;
172 }
173 }
174
175 String _getScriptType(BitcoinAddressType addressType) {
176 switch (addressType) {
177 case P2pkhAddressType.p2pkh:
178 return "PAYTOADDRESS";
179 case P2shAddressType.p2wpkhInP2sh:
180 return "PAYTOSCRIPTHASH";
181 case SegwitAddresType.p2tr:
182 return "PAYTOTAPROOT";
183 case SegwitAddresType.p2wsh:
184 return "PAYTOP2SHWITNESS";
185 case SegwitAddresType.p2wpkh:
186 return "PAYTOWITNESS";
187 default:
188 throw Exception("Unknown Address Type");
189 }
190 }