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