Skip to content

Commit

Permalink
Cw 679 add ledger litecoin support (#1565)
Browse files Browse the repository at this point in the history
* Add Litecoin Hardware Wallet Creation

* Add Litecoin Hardware Wallet Creation

* Fix Bitcoin not sending on Ledger

* Fixes to sending LTC using Ledger

* CW-679 Fix merge conflicts

* CW-679 Fix merge conflicts

* CW-679 Minor fixes

* CW-679 Add derivation Path of change address

* ledger flutter plus refactoring

* ledger flutter plus refactoring

* ledger flutter plus refactoring

* Ups :|

* Ups :| I forgot USB

* Handle BT Off

* Fix Issue with A14 and USB

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Small Ledger Quality of life improvements

* Pls work

* Pls work

* Pls work

* Pls work

* Fix overpopulation

* Fix ble device detection and support for Stax and Flex

* clean up pubspec

* clean up

* MWeb merge fix

* MWeb merge fix

* Fix Merge conflicts

* Fix Requested changes
  • Loading branch information
konstantinullrich authored Oct 23, 2024
1 parent e04185a commit 68926c0
Show file tree
Hide file tree
Showing 45 changed files with 840 additions and 403 deletions.
Binary file added assets/images/hardware_wallet/ledger_flex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/hardware_wallet/ledger_nano_s.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/hardware_wallet/ledger_nano_x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/hardware_wallet/ledger_stax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/images/ledger_nano.png
Binary file not shown.
21 changes: 11 additions & 10 deletions cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,31 @@ import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';

class BitcoinHardwareWalletService {
BitcoinHardwareWalletService(this.ledger, this.device);
BitcoinHardwareWalletService(this.ledgerConnection);

final Ledger ledger;
final LedgerDevice device;
final LedgerConnection ledgerConnection;

Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final bitcoinLedgerApp = BitcoinLedgerApp(ledger);
Future<List<HardwareAccountData>> getAvailableAccounts(
{int index = 0, int limit = 5}) async {
final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection);

final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device);
print(masterFp);
final masterFp = await bitcoinLedgerApp.getMasterFingerprint();

final accounts = <HardwareAccountData>[];
final indexRange = List.generate(limit, (i) => i + index);

for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'";
final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath);
final xpub =
await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath);
Bip32Slip10Secp256k1 hd =
Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0));

final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
final address = generateP2WPKHAddress(
hd: hd, index: 0, network: BitcoinNetwork.mainnet);

accounts.add(HardwareAccountData(
address: address,
Expand Down
57 changes: 33 additions & 24 deletions cw_bitcoin/lib/bitcoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart';

part 'bitcoin_wallet.g.dart';
Expand Down Expand Up @@ -61,8 +61,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialBalance: initialBalance,
seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
currency: networkParam == BitcoinNetwork.testnet
? CryptoCurrency.tbtc
: CryptoCurrency.btc,
alwaysScan: alwaysScan,
) {
// in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
Expand All @@ -80,11 +81,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
mainHd: hd,
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: networkParam ?? network,
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
masterHd:
seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
isHardwareWallet: walletInfo.isHardwareWallet,
);

autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
this.walletAddresses.isEnabledAutoGenerateSubaddress =
this.isEnabledAutoGenerateSubaddress;
});
}

Expand Down Expand Up @@ -185,8 +189,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo.derivationInfo ??= DerivationInfo();

// set the default if not present:
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
walletInfo.derivationInfo!.derivationPath ??=
snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType ??=
snp?.derivationType ?? DerivationType.electrum;

Uint8List? seedBytes = null;
final mnemonic = keysData.mnemonic;
Expand Down Expand Up @@ -228,15 +234,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
);
}

Ledger? _ledger;
LedgerDevice? _ledgerDevice;
LedgerConnection? _ledgerConnection;
BitcoinLedgerApp? _bitcoinLedgerApp;

void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) {
_ledger = setLedger;
_ledgerDevice = setLedgerDevice;
_bitcoinLedgerApp =
BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
@override
void setLedgerConnection(LedgerConnection connection) {
_ledgerConnection = connection;
_bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!,
derivationPath: walletInfo.derivationInfo!.derivationPath!);
}

@override
Expand All @@ -251,12 +256,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!);
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();

final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
final rawTx =
await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath =
publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;

psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
Expand All @@ -268,25 +275,27 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
));
}

final psbt =
PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
final psbt = PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);

final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
}

@override
Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) {
final addressEntry = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address)
? walletAddresses.allAddresses
.firstWhere((element) => element.address == address)
: null;
final index = addressEntry?.index ?? 0;
final isChange = addressEntry?.isHidden == true ? 1 : 0;
final accountPath = walletInfo.derivationInfo?.derivationPath;
final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
final derivationPath =
accountPath != null ? "$accountPath/$isChange/$index" : null;

final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!,
final signature = await _bitcoinLedgerApp!.signMessage(
message: ascii.encode(message), signDerivationPath: derivationPath);
return base64Encode(signature);
}
Expand Down
1 change: 1 addition & 0 deletions cw_bitcoin/lib/bitcoin_wallet_addresses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
required super.mainHd,
required super.sideHd,
required super.network,
required super.isHardwareWallet,
super.initialAddresses,
super.initialRegularAddressIndex,
super.initialChangeAddressIndex,
Expand Down
69 changes: 51 additions & 18 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart';
Expand All @@ -26,6 +25,8 @@ import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
Expand All @@ -37,10 +38,10 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/unspent_coin_type.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 All @@ -51,9 +52,10 @@ part 'electrum_wallet.g.dart';

class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet;

abstract class ElectrumWalletBase
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store, WalletKeysFile {
abstract class ElectrumWalletBase extends WalletBase<
ElectrumBalance,
ElectrumTransactionHistory,
ElectrumTransactionInfo> with Store, WalletKeysFile {
ElectrumWalletBase({
required String password,
required WalletInfo walletInfo,
Expand All @@ -69,8 +71,8 @@ abstract class ElectrumWalletBase
ElectrumBalance? initialBalance,
CryptoCurrency? currency,
this.alwaysScan,
}) : accountHD =
getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
}) : accountHD = getAccountHDWallet(
currency, network, seedBytes, xpub, walletInfo.derivationInfo),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_feeRates = <int>[],
Expand Down Expand Up @@ -105,8 +107,12 @@ abstract class ElectrumWalletBase
sharedPrefs.complete(SharedPreferences.getInstance());
}

static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
static Bip32Slip10Secp256k1 getAccountHDWallet(
CryptoCurrency? currency,
BasedUtxoNetwork network,
Uint8List? seedBytes,
String? xpub,
DerivationInfo? derivationInfo) {
if (seedBytes == null && xpub == null) {
throw Exception(
"To create a Wallet you need either a seed or an xpub. This should not happen");
Expand All @@ -117,8 +123,9 @@ abstract class ElectrumWalletBase
case CryptoCurrency.btc:
case CryptoCurrency.ltc:
case CryptoCurrency.tbtc:
return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network))
.derivePath(_hardenedDerivationPath(
derivationInfo?.derivationPath ?? electrum_path))
as Bip32Slip10Secp256k1;
case CryptoCurrency.bch:
return bitcoinCashHDWallet(seedBytes);
Expand All @@ -127,15 +134,26 @@ abstract class ElectrumWalletBase
}
}

return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
return Bip32Slip10Secp256k1.fromExtendedKey(
xpub!, getKeyNetVersion(network));
}

static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'")
as Bip32Slip10Secp256k1;

static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;

static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) {
switch (network) {
case LitecoinNetwork.mainnet:
return Bip44Conf.litecoinMainNet.altKeyNetVer;
default:
return null;
}
}

bool? alwaysScan;

final Bip32Slip10Secp256k1 accountHD;
Expand Down Expand Up @@ -634,8 +652,9 @@ abstract class ElectrumWalletBase
ECPrivate? privkey;
bool? isSilentPayment = false;

final hd =
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
final hd = utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd;

if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
Expand Down Expand Up @@ -833,7 +852,7 @@ abstract class ElectrumWalletBase
inputs: utxoDetails.availableInputs,
outputs: updatedOutputs,
);
final address = RegexUtils.addressTypeFromStr(changeAddress, network);
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
updatedOutputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
Expand All @@ -845,6 +864,14 @@ abstract class ElectrumWalletBase
isChange: true,
));

// Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
final changeDerivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
"/${changeAddress.isHidden ? "1" : "0"}"
"/${changeAddress.index}";
utxoDetails.publicKeys[address.pubKeyHash()] =
PublicKeyWithDerivationPath('', 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,
// the silent payment outputs need to be recalculated for the new inputs
Expand Down Expand Up @@ -1206,6 +1233,9 @@ abstract class ElectrumWalletBase
}
}

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

Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
Expand Down Expand Up @@ -1563,7 +1593,9 @@ abstract class ElectrumWalletBase

final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
hd: addressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: addressRecord.index,
network: network);

Expand Down Expand Up @@ -1745,7 +1777,8 @@ abstract class ElectrumWalletBase

if (height != null) {
if (time == null && height > 0) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000)
.round();
}

if (confirmations == null) {
Expand Down
Loading

0 comments on commit 68926c0

Please sign in to comment.