Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/images/hardware_wallet/device_trezor_safe_5.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/output_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
import 'package:sp_scanner/sp_scanner.dart';
Expand Down Expand Up @@ -163,7 +162,7 @@ abstract class ElectrumWalletBase
[HardwareWalletType? hardwareWalletType]) {
switch (network) {
case LitecoinNetwork.mainnet:
if (hardwareWalletType == HardwareWalletType.ledger)
if ([HardwareWalletType.ledger, HardwareWalletType.trezor].contains(hardwareWalletType))
return Bip44Conf.litecoinMainNet.altKeyNetVer;
return null;
default:
Expand Down Expand Up @@ -1423,8 +1422,6 @@ abstract class ElectrumWalletBase

HardwareWalletService? hardwareWalletService;

void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError();

Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
Expand Down
12 changes: 12 additions & 0 deletions cw_bitcoin/lib/hardware/bitcoin_hardware_wallet_service.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import 'dart:typed_data';

import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:ledger_litecoin/ledger_litecoin.dart';

mixin BitcoinHardwareWalletService {
Future<Uint8List> getMasterFingerprint();
}

mixin LitecoinHardwareWalletService {
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<LedgerTransaction> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
});
}
36 changes: 32 additions & 4 deletions cw_bitcoin/lib/hardware/litecoin_ledger_service.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
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/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 {
LitecoinLedgerService(this.ledgerConnection);
class LitecoinLedgerService extends HardwareWalletService with LitecoinHardwareWalletService {
LitecoinLedgerService(this.ledgerConnection)
: litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection);

final LedgerConnection ledgerConnection;
final LitecoinLedgerApp litecoinLedgerApp;

@override
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection);

await litecoinLedgerApp.getVersion();

final accounts = <HardwareAccountData>[];
Expand All @@ -42,4 +45,29 @@ class LitecoinLedgerService extends HardwareWalletService {

return accounts;
}

@override
Future<String> signLitecoinTransaction({
required List<BitcoinBaseOutput> outputs,
required List<LedgerTransaction> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
}) {
String? changePath;
for (final output in outputs) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
}

return litecoinLedgerApp.createTransaction(
inputs: inputs,
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);
}
}
187 changes: 187 additions & 0 deletions cw_bitcoin/lib/hardware/trezor_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_litecoin/src/tx_utils/transaction.dart';
import 'package:trezor_connect/trezor_connect.dart';

class BitcoinTrezorService extends HardwareWalletService with BitcoinHardwareWalletService {
BitcoinTrezorService(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>[];

for (final i in indexRange) {
requestParams.add(TrezorGetPublicKeyParams(path: "m/84'/0'/$i'"));
}

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() ??
[];
}

@override
Future<Uint8List> signTransaction({required String transaction}) async {
final psbt = PsbtV2()..deserialize(base64Decode(transaction));

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 publicKeys = psbt.inputMaps[i].keys.where((e) => e.startsWith("06"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's 06? and also this part can use a little bit of code documentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

06 is the psbt key for a public key. So here we are selecting all public keys of that input

final pubkey = Uint8List.fromList(hex.decode(publicKeys.first.substring(2)));

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,
sequence: psbt.getInputSequence(i),
scriptType: "SPENDWITNESS"));
}

final outputs = <TrezorTxOutput>[];
final outputCount = psbt.getGlobalOutputCount();
for (var i = 0; i < outputCount; i++) {
final script = Script.fromRaw(byteData: psbt.getOutputScript(i));
outputs.add(TrezorTxOutput(
amount: psbt.getOutputAmount(i),
address: script.toAddress(),
scriptType: _getScriptType(script.getAddressType()!)
// ToDo: addressPath: psbt.getOutputBip32Derivation(i, pubkey).$2, // To highlight change outputs
));
}

final signedTx = await connect.signTransaction(coin: 'btc', inputs: inputs, outputs: outputs);

return Uint8List.fromList(BytesUtils.fromHexString(signedTx!.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);
}

@override
Future<Uint8List> getMasterFingerprint() async => Uint8List.fromList([0, 0, 0, 0]);
}

class LitecoinTrezorService extends HardwareWalletService with 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<LedgerTransaction> inputs,
required Map<String, PublicKeyWithDerivationPath> publicKeys}) async {
final readyInputs = <TrezorTxInput>[];
for (final input in inputs) {
final inputTx = BtcTransaction.fromRaw(input.rawTx);
final inputOutputIndex = input.outputIndex;

readyInputs.add(TrezorTxInput(
prevHash: inputTx.txId(),
prevIndex: inputOutputIndex,
amount: inputTx.outputs[inputOutputIndex].amount.toInt(),
addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
sequence: input.sequence,
scriptType: "SPENDWITNESS",
));
}

final readyOutputs = <TrezorTxOutput>[];
for (final output in outputs) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];

readyOutputs.add(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,
));
}

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");
}
}
31 changes: 3 additions & 28 deletions cw_bitcoin/lib/litecoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/unspent_coin_type.dart';
Expand Down Expand Up @@ -42,7 +43,6 @@ import 'package:cw_core/output_info.dart';
import 'package:flutter/foundation.dart';
import 'package:grpc/grpc.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:ledger_litecoin/ledger_litecoin.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_type.dart';
Expand Down Expand Up @@ -1368,16 +1368,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return false;
}

LedgerConnection? _ledgerConnection;
LitecoinLedgerApp? _litecoinLedgerApp;

@override
void setLedgerConnection(LedgerConnection connection) {
_ledgerConnection = connection;
_litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!,
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}

@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
Expand Down Expand Up @@ -1406,23 +1396,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
));
}

String? changePath;
for (final output in outputs) {
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
}

final rawHex = await _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);
final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService)
.signLitecoinTransaction(outputs: outputs, inputs: readyInputs, publicKeys: publicKeys);

return BtcTransaction.fromRaw(rawHex);
}
Expand Down
Loading