diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml
index dc231df42..acaa12fe0 100644
--- a/.github/workflows/pr_test_build.yml
+++ b/.github/workflows/pr_test_build.yml
@@ -42,7 +42,7 @@ jobs:
- name: Flutter action
uses: subosito/flutter-action@v1
with:
- flutter-version: "3.10.x"
+ flutter-version: "3.19.5"
channel: stable
- name: Install package dependencies
@@ -113,6 +113,7 @@ jobs:
touch lib/.secrets.g.dart
touch cw_evm/lib/.secrets.g.dart
touch cw_solana/lib/.secrets.g.dart
+ touch cw_tron/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@@ -150,6 +151,9 @@ jobs:
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
+ echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
+ echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
+ echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
- name: Rename app
run: |
diff --git a/.gitignore b/.gitignore
index 6f2d0a182..d0952ca98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,9 +94,12 @@ android/app/key.jks
**/tool/.evm-secrets-config.json
**/tool/.ethereum-secrets-config.json
**/tool/.solana-secrets-config.json
+**/tool/.nano-secrets-config.json
+**/tool/.tron-secrets-config.json
**/lib/.secrets.g.dart
**/cw_evm/lib/.secrets.g.dart
**/cw_solana/lib/.secrets.g.dart
+**/cw_tron/lib/.secrets.g.dart
vendor/
@@ -132,6 +135,7 @@ lib/bitcoin_cash/bitcoin_cash.dart
lib/nano/nano.dart
lib/polygon/polygon.dart
lib/solana/solana.dart
+lib/tron/tron.dart
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
@@ -152,6 +156,7 @@ assets/images/app_logo.png
macos/Runner/Info.plist
macos/Runner/DebugProfile.entitlements
macos/Runner/Release.entitlements
+lib/core/secure_storage.dart
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index eea9b5521..57462099c 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -9,6 +9,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
> getAvailableAccounts({int index = 0, int limit = 5}) async {
+ final bitcoinLedgerApp = BitcoinLedgerApp(ledger);
+
+ final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device);
+ print(masterFp);
+
+ final accounts = [];
+ 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);
+ HDWallet hd = HDWallet.fromBase58(xpub).derive(0);
+
+ final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
+
+ accounts.add(HardwareAccountData(
+ address: address,
+ accountIndex: i,
+ derivationPath: derivationPath,
+ masterFingerprint: masterFp,
+ xpub: xpub,
+ ));
+ }
+
+ return accounts;
+ }
+}
diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart
index 9163fcb11..4a01d6ddc 100644
--- a/cw_bitcoin/lib/bitcoin_mnemonic.dart
+++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart
@@ -90,8 +90,7 @@ List prefixMatches(String source, List prefixes) {
return prefixes.map((prefix) => hx.startsWith(prefix.toLowerCase())).toList();
}
-Future generateMnemonic(
- {int strength = 264, String prefix = segwit}) async {
+Future generateElectrumMnemonic({int strength = 264, String prefix = segwit}) async {
final wordBitlen = logBase(wordlist.length, 2).ceil();
final wordCount = strength / wordBitlen;
final byteCount = ((wordCount * wordBitlen).ceil() / 8).ceil();
@@ -106,22 +105,29 @@ Future generateMnemonic(
return result;
}
+Future checkIfMnemonicIsElectrum2(String mnemonic) async {
+ return prefixMatches(mnemonic, [segwit]).first;
+}
+
+Future getMnemonicHash(String mnemonic) async {
+ final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
+ final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
+ final hx = digest.toString();
+ return hx;
+}
+
Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async {
- final pbkdf2 = cryptography.Pbkdf2(
- macAlgorithm: cryptography.Hmac.sha512(),
- iterations: 2048,
- bits: 512);
+ final pbkdf2 =
+ cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
final text = normalizeText(mnemonic);
// pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce)
final key = await pbkdf2.deriveKey(
- secretKey: cryptography.SecretKey(text.codeUnits),
- nonce: 'electrum'.codeUnits);
+ secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits);
final bytes = await key.extractBytes();
return Uint8List.fromList(bytes);
}
-bool matchesAnyPrefix(String mnemonic) =>
- prefixMatches(mnemonic, [segwit]).any((el) => el);
+bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el);
bool validateMnemonic(String mnemonic, {String prefix = segwit}) {
try {
@@ -208,10 +214,8 @@ String removeCJKSpaces(String source) {
}
String normalizeText(String source) {
- final res = removeCombiningCharacters(unorm.nfkd(source).toLowerCase())
- .trim()
- .split('/\s+/')
- .join(' ');
+ final res =
+ removeCombiningCharacters(unorm.nfkd(source).toLowerCase()).trim().split('/\s+/').join(' ');
return removeCJKSpaces(res);
}
diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
index d51775368..7c4dcfd5f 100644
--- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart
+++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart
@@ -37,7 +37,7 @@ class BitcoinTransactionPriority extends TransactionPriority {
switch (this) {
case BitcoinTransactionPriority.slow:
- label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs';
+ label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
case BitcoinTransactionPriority.medium:
label = 'Medium'; // S.current.transaction_priority_medium;
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index bf59e8637..f96b0e4da 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -1,8 +1,13 @@
import 'package:bitcoin_base/bitcoin_base.dart';
+import 'package:convert/convert.dart';
+
import 'package:cw_bitcoin/bitcoin_mnemonic.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:hive/hive.dart';
+import 'package:ledger_bitcoin/ledger_bitcoin.dart';
+import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
@@ -12,6 +17,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
+import 'package:bip39/bip39.dart' as bip39;
part 'bitcoin_wallet.g.dart';
@@ -19,31 +25,40 @@ class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinWalletBase({
- required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box unspentCoinsInfo,
- required Uint8List seedBytes,
+ Uint8List? seedBytes,
+ String? mnemonic,
+ String? xpub,
String? addressPageType,
BasedUtxoNetwork? networkParam,
List? initialAddresses,
ElectrumBalance? initialBalance,
Map? initialRegularAddressIndex,
Map? initialChangeAddressIndex,
+ String? passphrase,
}) : super(
- mnemonic: mnemonic,
- password: password,
- walletInfo: walletInfo,
- unspentCoinsInfo: unspentCoinsInfo,
- networkType: networkParam == null
- ? bitcoin.bitcoin
- : networkParam == BitcoinNetwork.mainnet
- ? bitcoin.bitcoin
- : bitcoin.testnet,
- initialAddresses: initialAddresses,
- initialBalance: initialBalance,
- seedBytes: seedBytes,
- currency: CryptoCurrency.btc) {
+ mnemonic: mnemonic,
+ passphrase: passphrase,
+ xpub: xpub,
+ password: password,
+ walletInfo: walletInfo,
+ unspentCoinsInfo: unspentCoinsInfo,
+ networkType: networkParam == null
+ ? bitcoin.bitcoin
+ : networkParam == BitcoinNetwork.mainnet
+ ? bitcoin.bitcoin
+ : bitcoin.testnet,
+ initialAddresses: initialAddresses,
+ initialBalance: initialBalance,
+ seedBytes: seedBytes,
+ currency: CryptoCurrency.btc) {
+ // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
+ // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
+ // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
+ // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
+ // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses(
walletInfo,
electrumClient: electrumClient,
@@ -51,7 +66,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd,
- sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
+ sideHd: accountHD.derive(1),
network: networkParam ?? network,
);
autorun((_) {
@@ -64,6 +79,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password,
required WalletInfo walletInfo,
required Box unspentCoinsInfo,
+ String? passphrase,
String? addressPageType,
BasedUtxoNetwork? network,
List? initialAddresses,
@@ -71,14 +87,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Map? initialRegularAddressIndex,
Map? initialChangeAddressIndex,
}) async {
+ late Uint8List seedBytes;
+
+ switch (walletInfo.derivationInfo?.derivationType) {
+ case DerivationType.bip39:
+ seedBytes = await bip39.mnemonicToSeed(
+ mnemonic,
+ passphrase: passphrase ?? "",
+ );
+ break;
+ case DerivationType.electrum:
+ default:
+ seedBytes = await mnemonicToSeedBytes(mnemonic);
+ break;
+ }
return BitcoinWallet(
mnemonic: mnemonic,
+ passphrase: passphrase ?? "",
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
- seedBytes: await mnemonicToSeedBytes(mnemonic),
+ seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
@@ -97,18 +128,91 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
: BitcoinNetwork.mainnet;
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
+ walletInfo.derivationInfo ??= DerivationInfo(
+ derivationType: snp.derivationType ?? DerivationType.electrum,
+ derivationPath: snp.derivationPath,
+ );
+
+ // set the default if not present:
+ walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/0";
+ walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum;
+
+ Uint8List? seedBytes = null;
+
+ if (snp.mnemonic != null) {
+ switch (walletInfo.derivationInfo!.derivationType) {
+ case DerivationType.electrum:
+ seedBytes = await mnemonicToSeedBytes(snp.mnemonic!);
+ break;
+ case DerivationType.bip39:
+ default:
+ seedBytes = await bip39.mnemonicToSeed(
+ snp.mnemonic!,
+ passphrase: snp.passphrase ?? '',
+ );
+ break;
+ }
+ }
+
return BitcoinWallet(
mnemonic: snp.mnemonic,
+ xpub: snp.xpub,
password: password,
+ passphrase: snp.passphrase,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses,
initialBalance: snp.balance,
- seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
+ seedBytes: seedBytes,
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
networkParam: network,
);
}
+
+ Ledger? _ledger;
+ LedgerDevice? _ledgerDevice;
+ BitcoinLedgerApp? _bitcoinLedgerApp;
+
+ void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) {
+ _ledger = setLedger;
+ _ledgerDevice = setLedgerDevice;
+ _bitcoinLedgerApp = BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
+ }
+
+ @override
+ Future buildHardwareWalletTransaction({
+ required List outputs,
+ required BigInt fee,
+ required BasedUtxoNetwork network,
+ required List utxos,
+ required Map publicKeys,
+ String? memo,
+ bool enableRBF = false,
+ BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
+ BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
+ }) async {
+ final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!);
+
+ final psbtReadyInputs = [];
+ for (final utxo in utxos) {
+ 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,
+ ));
+ }
+
+ final psbt = PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
+
+ final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
+ return BtcTransaction.fromRaw(hex.encode(rawHex));
+ }
}
diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart
index 37b272a1b..915d7cc10 100644
--- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart
@@ -1,23 +1,58 @@
+import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class BitcoinNewWalletCredentials extends WalletCredentials {
- BitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo})
- : super(name: name, walletInfo: walletInfo);
+ BitcoinNewWalletCredentials(
+ {required String name,
+ WalletInfo? walletInfo,
+ DerivationType? derivationType,
+ String? derivationPath})
+ : super(
+ name: name,
+ walletInfo: walletInfo,
+ );
}
class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials {
- BitcoinRestoreWalletFromSeedCredentials(
- {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo})
- : super(name: name, password: password, walletInfo: walletInfo);
+ BitcoinRestoreWalletFromSeedCredentials({
+ required String name,
+ required String password,
+ required this.mnemonic,
+ WalletInfo? walletInfo,
+ required DerivationType derivationType,
+ required String derivationPath,
+ String? passphrase,
+ }) : super(
+ name: name,
+ password: password,
+ passphrase: passphrase,
+ walletInfo: walletInfo,
+ derivationInfo: DerivationInfo(
+ derivationType: derivationType,
+ derivationPath: derivationPath,
+ ));
final String mnemonic;
}
class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
- BitcoinRestoreWalletFromWIFCredentials(
- {required String name, required String password, required this.wif, WalletInfo? walletInfo})
- : super(name: name, password: password, walletInfo: walletInfo);
+ BitcoinRestoreWalletFromWIFCredentials({
+ required String name,
+ required String password,
+ required this.wif,
+ WalletInfo? walletInfo,
+ }) : super(name: name, password: password, walletInfo: walletInfo);
final String wif;
-}
\ No newline at end of file
+}
+
+class BitcoinRestoreWalletFromHardware extends WalletCredentials {
+ BitcoinRestoreWalletFromHardware({
+ required String name,
+ required this.hwAccountData,
+ WalletInfo? walletInfo,
+ }) : super(name: name, walletInfo: walletInfo);
+
+ final HardwareAccountData hwAccountData;
+}
diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart
index 38e769d15..cf99324da 100644
--- a/cw_bitcoin/lib/bitcoin_wallet_service.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart
@@ -12,9 +12,13 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
+import 'package:bip39/bip39.dart' as bip39;
-class BitcoinWalletService extends WalletService {
+class BitcoinWalletService extends WalletService<
+ BitcoinNewWalletCredentials,
+ BitcoinRestoreWalletFromSeedCredentials,
+ BitcoinRestoreWalletFromWIFCredentials,
+ BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
final Box walletInfoSource;
@@ -29,8 +33,9 @@ class BitcoinWalletService extends WalletService restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
+ {bool? isTestnet}) async {
+
+ final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
+ credentials.walletInfo?.network = network.value;
+ credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath;
+
+ final wallet = await BitcoinWallet(password: credentials.password!,
+ xpub: credentials.hwAccountData.xpub,
+ walletInfo: credentials.walletInfo!,
+ unspentCoinsInfo: unspentCoinsInfoSource,
+ networkParam: network,
+ );
+ await wallet.save();
+ await wallet.init();
+ return wallet;
+ }
+
@override
Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
- {bool? isTestnet}) async =>
+ {bool? isTestnet}) async =>
throw UnimplementedError();
@override
Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
- if (!validateMnemonic(credentials.mnemonic)) {
+ if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw BitcoinMnemonicIsIncorrectException();
}
@@ -114,6 +138,7 @@ class BitcoinWalletService extends WalletService> electrum_derivations = {
+ DerivationType.electrum: [
+ DerivationInfo(
+ derivationType: DerivationType.electrum,
+ derivationPath: "m/0'",
+ description: "Electrum",
+ scriptType: "p2wpkh",
+ ),
+ ],
+ DerivationType.bip39: [
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/44'/0'/0'",
+ description: "Standard BIP44",
+ scriptType: "p2pkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/49'/0'/0'",
+ description: "Standard BIP49 compatibility segwit",
+ scriptType: "p2wpkh-p2sh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/0'/0'",
+ description: "Standard BIP84 native segwit",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/0'",
+ description: "Non-standard legacy",
+ scriptType: "p2pkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/0'",
+ description: "Non-standard compatibility segwit",
+ scriptType: "p2wpkh-p2sh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/0'",
+ description: "Non-standard native segwit",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/44'/0'/0'",
+ description: "Samourai Deposit",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/49'/0'/0'",
+ description: "Samourai Deposit",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/0'/2147483644'",
+ description: "Samourai Bad Bank (toxic change)",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/0'/2147483645'",
+ description: "Samourai Whirlpool Pre Mix",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/0'/2147483646'",
+ description: "Samourai Whirlpool Post Mix",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/44'/0'/2147483647'",
+ description: "Samourai Ricochet legacy",
+ scriptType: "p2pkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/49'/0'/2147483647'",
+ description: "Samourai Ricochet compatibility segwit",
+ scriptType: "p2wpkh-p2sh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/0'/2147483647'",
+ description: "Samourai Ricochet native segwit",
+ scriptType: "p2wpkh",
+ ),
+ DerivationInfo(
+ derivationType: DerivationType.bip39,
+ derivationPath: "m/84'/2'/0'",
+ description: "Default Litecoin",
+ scriptType: "p2wpkh",
+ ),
+ ],
+};
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 5bed6a449..783eb10d7 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -4,8 +4,8 @@ import 'dart:io';
import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
-import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
+import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
@@ -37,9 +37,9 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
+import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
-import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart';
@@ -53,15 +53,16 @@ abstract class ElectrumWalletBase
required WalletInfo walletInfo,
required Box unspentCoinsInfo,
required this.networkType,
- required this.mnemonic,
- required Uint8List seedBytes,
+ String? xpub,
+ String? mnemonic,
+ Uint8List? seedBytes,
+ this.passphrase,
List? initialAddresses,
ElectrumClient? electrumClient,
ElectrumBalance? initialBalance,
CryptoCurrency? currency})
- : hd = currency == CryptoCurrency.bch
- ? bitcoinCashHDWallet(seedBytes)
- : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"),
+ : accountHD =
+ getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_feeRates = [],
@@ -78,20 +79,45 @@ abstract class ElectrumWalletBase
this.unspentCoinsInfo = unspentCoinsInfo,
this.network = _getNetwork(networkType, currency),
this.isTestnet = networkType == bitcoin.testnet,
+ this._mnemonic = mnemonic,
super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
}
+ static bitcoin.HDWallet getAccountHDWallet(
+ CryptoCurrency? currency,
+ bitcoin.NetworkType networkType,
+ 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");
+ }
+
+ if (seedBytes != null) {
+ return currency == CryptoCurrency.bch
+ ? bitcoinCashHDWallet(seedBytes)
+ : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
+ .derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? "m/0'"));
+ }
+
+ return bitcoin.HDWallet.fromBase58(xpub!);
+ }
+
static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) =>
- bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0");
+ bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'");
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;
- final bitcoin.HDWallet hd;
- final String mnemonic;
+ final bitcoin.HDWallet accountHD;
+ final String? _mnemonic;
+
+ bitcoin.HDWallet get hd => accountHD.derive(0);
+ final String? passphrase;
@override
@observable
@@ -120,10 +146,10 @@ abstract class ElectrumWalletBase
.map((addr) => scriptHash(addr.address, network: network))
.toList();
- String get xpub => hd.base58!;
+ String get xpub => accountHD.base58!;
@override
- String get seed => mnemonic;
+ String? get seed => _mnemonic;
bitcoin.NetworkType networkType;
BasedUtxoNetwork network;
@@ -200,22 +226,38 @@ abstract class ElectrumWalletBase
int credentialsAmount = 0,
}) async {
final utxos = [];
- List privateKeys = [];
+ final privateKeys = [];
+ final publicKeys = {};
+
int allInputsAmount = 0;
+ bool spendsUnconfirmedTX = false;
+
for (int i = 0; i < unspentCoins.length; i++) {
final utx = unspentCoins[i];
- if (utx.isSending) {
+ if (utx.isSending && !utx.isFrozen) {
+ if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
+
allInputsAmount += utx.value;
final address = addressTypeFromStr(utx.address, network);
- final privkey = generateECPrivate(
- hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
- index: utx.bitcoinAddressRecord.index,
- network: network);
+ final hd =
+ utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
+ final derivationPath =
+ "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
+ "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
+ "/${utx.bitcoinAddressRecord.index}";
+ final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
- privateKeys.add(privkey);
+ publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
+
+ if (!walletInfo.isHardwareWallet) {
+ final privkey =
+ generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
+
+ privateKeys.add(privkey);
+ }
utxos.add(
UtxoWithAddress(
@@ -226,7 +268,7 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(address),
),
ownerDetails: UtxoAddressDetails(
- publicKey: privkey.getPublic().toHex(),
+ publicKey: pubKeyHex,
address: address,
),
),
@@ -264,6 +306,10 @@ abstract class ElectrumWalletBase
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = allInputsAmount - fee;
+ if (amount <= 0) {
+ throw BitcoinTransactionWrongBalanceException();
+ }
+
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
@@ -283,11 +329,13 @@ abstract class ElectrumWalletBase
return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
+ publicKeys: publicKeys,
fee: fee,
amount: amount,
isSendAll: true,
hasChange: false,
memo: memo,
+ spendsUnconfirmedTX: spendsUnconfirmedTX,
);
}
@@ -297,27 +345,48 @@ abstract class ElectrumWalletBase
int feeRate, {
int? inputsCount,
String? memo,
+ bool? useUnconfirmed,
}) async {
final utxos = [];
- List privateKeys = [];
+ final privateKeys = [];
+ final publicKeys = {};
+
int allInputsAmount = 0;
+ bool spendsUnconfirmedTX = false;
int leftAmount = credentialsAmount;
- final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
+ final sendingCoins = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList();
+ final unconfirmedCoins = sendingCoins.where((utx) => utx.confirmations == 0).toList();
for (int i = 0; i < sendingCoins.length; i++) {
final utx = sendingCoins[i];
+ final isUncormirmed = utx.confirmations == 0;
+ if (useUnconfirmed != true && isUncormirmed) continue;
+
+ if (!spendsUnconfirmedTX) spendsUnconfirmedTX = isUncormirmed;
+
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network);
- final privkey = generateECPrivate(
- hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
- index: utx.bitcoinAddressRecord.index,
- network: network);
- privateKeys.add(privkey);
+ final hd =
+ utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
+ final derivationPath =
+ "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
+ "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
+ "/${utx.bitcoinAddressRecord.index}";
+ final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
+
+ publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
+
+ if (!walletInfo.isHardwareWallet) {
+ final privkey =
+ generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
+
+ privateKeys.add(privkey);
+ }
utxos.add(
UtxoWithAddress(
@@ -328,7 +397,7 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(address),
),
ownerDetails: UtxoAddressDetails(
- publicKey: privkey.getPublic().toHex(),
+ publicKey: pubKeyHex,
address: address,
),
),
@@ -345,11 +414,23 @@ abstract class ElectrumWalletBase
}
final spendingAllCoins = sendingCoins.length == utxos.length;
+ final spendingAllConfirmedCoins =
+ !spendsUnconfirmedTX && utxos.length == sendingCoins.length - unconfirmedCoins.length;
// How much is being spent - how much is being sent
int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
+ if (!spendingAllCoins) {
+ return estimateTxForAmount(
+ credentialsAmount,
+ outputs,
+ feeRate,
+ inputsCount: utxos.length + 1,
+ memo: memo,
+ useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
+ );
+ }
throw BitcoinTransactionWrongBalanceException();
}
@@ -403,6 +484,7 @@ abstract class ElectrumWalletBase
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
+ useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
);
}
@@ -449,6 +531,7 @@ abstract class ElectrumWalletBase
feeRate,
inputsCount: utxos.length + 1,
memo: memo,
+ useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
);
}
}
@@ -456,11 +539,13 @@ abstract class ElectrumWalletBase
return EstimatedTxResult(
utxos: utxos,
privateKeys: privateKeys,
+ publicKeys: publicKeys,
fee: fee,
amount: amount,
hasChange: true,
isSendAll: false,
memo: memo,
+ spendsUnconfirmedTX: spendsUnconfirmedTX,
);
}
@@ -522,6 +607,35 @@ abstract class ElectrumWalletBase
);
}
+ if (walletInfo.isHardwareWallet) {
+ final transaction = await buildHardwareWalletTransaction(
+ utxos: estimatedTx.utxos,
+ outputs: outputs,
+ publicKeys: estimatedTx.publicKeys,
+ fee: BigInt.from(estimatedTx.fee),
+ network: network,
+ memo: estimatedTx.memo,
+ outputOrdering: BitcoinOrdering.none,
+ enableRBF: true,
+ );
+
+ return PendingBitcoinTransaction(
+ transaction,
+ type,
+ electrumClient: electrumClient,
+ amount: estimatedTx.amount,
+ fee: estimatedTx.fee,
+ feeRate: feeRateInt.toString(),
+ network: network,
+ hasChange: estimatedTx.hasChange,
+ isSendAll: estimatedTx.isSendAll,
+ hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
+ )..addListener((transaction) async {
+ transactionHistory.addOne(transaction);
+ await updateBalance();
+ });
+ }
+
BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
@@ -531,7 +645,7 @@ abstract class ElectrumWalletBase
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
- enableRBF: true,
+ enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
} else {
txb = BitcoinTransactionBuilder(
@@ -541,7 +655,7 @@ abstract class ElectrumWalletBase
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
- enableRBF: true,
+ enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
}
@@ -583,8 +697,23 @@ abstract class ElectrumWalletBase
}
}
+ Future buildHardwareWalletTransaction({
+ required List outputs,
+ required BigInt fee,
+ required BasedUtxoNetwork network,
+ required List utxos,
+ required Map publicKeys,
+ String? memo,
+ bool enableRBF = false,
+ BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
+ BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
+ }) async =>
+ throw UnimplementedError();
+
String toJSON() => json.encode({
- 'mnemonic': mnemonic,
+ 'mnemonic': _mnemonic,
+ 'xpub': xpub,
+ 'passphrase': passphrase ?? '',
'account_index': walletAddresses.currentReceiveAddressIndexByType,
'change_address_index': walletAddresses.currentChangeAddressIndexByType,
'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
@@ -592,6 +721,8 @@ abstract class ElectrumWalletBase
? SegwitAddresType.p2wpkh.toString()
: walletInfo.addressPageType.toString(),
'balance': balance[currency]?.toJSON(),
+ 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index,
+ 'derivationPath': walletInfo.derivationInfo?.derivationPath,
});
int feeRate(TransactionPriority priority) {
@@ -721,6 +852,7 @@ abstract class ElectrumWalletBase
final tx = await fetchTransactionInfo(
hash: coin.hash, height: 0, myAddresses: addressesSet);
coin.isChange = tx?.direction == TransactionDirection.outgoing;
+ coin.confirmations = tx?.confirmations;
updatedUnspentCoins.add(coin);
} catch (_) {}
}))));
@@ -745,6 +877,7 @@ abstract class ElectrumWalletBase
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
+ coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
_addCoinInfo(coin);
}
@@ -1037,9 +1170,11 @@ abstract class ElectrumWalletBase
return Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight);
+ final balance = await electrumClient.getBalance(addressRecord.scriptHash!);
if (history.isNotEmpty) {
addressRecord.txCount = history.length;
+ addressRecord.balance = balance['confirmed'] as int? ?? 0;
historiesWithDetails.addAll(history);
final matchedAddresses =
@@ -1221,7 +1356,7 @@ abstract class ElectrumWalletBase
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
@override
- String signMessage(String message, {String? address = null}) {
+ Future signMessage(String message, {String? address = null}) async {
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
@@ -1244,6 +1379,9 @@ abstract class ElectrumWalletBase
return BitcoinNetwork.mainnet;
}
+
+ static String _hardenedDerivationPath(String derivationPath) =>
+ derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
}
class EstimateTxParams {
@@ -1265,20 +1403,31 @@ class EstimatedTxResult {
EstimatedTxResult({
required this.utxos,
required this.privateKeys,
+ required this.publicKeys,
required this.fee,
required this.amount,
required this.hasChange,
required this.isSendAll,
this.memo,
+ required this.spendsUnconfirmedTX,
});
final List utxos;
final List privateKeys;
+ final Map publicKeys; // PubKey to derivationPath
final int fee;
final int amount;
final bool hasChange;
final bool isSendAll;
final String? memo;
+ final bool spendsUnconfirmedTX;
+}
+
+class PublicKeyWithDerivationPath {
+ const PublicKeyWithDerivationPath(this.publicKey, this.derivationPath);
+
+ final String derivationPath;
+ final String publicKey;
}
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart
index 6f76ab312..340b17cfb 100644
--- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart
+++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart
@@ -3,6 +3,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_core/pathForWallet.dart';
+import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_type.dart';
@@ -12,11 +13,15 @@ class ElectrumWalletSnapshot {
required this.type,
required this.password,
required this.mnemonic,
+ required this.xpub,
required this.addresses,
required this.balance,
required this.regularAddressIndex,
required this.changeAddressIndex,
required this.addressPageType,
+ this.passphrase,
+ this.derivationType,
+ this.derivationPath,
});
final String name;
@@ -24,11 +29,15 @@ class ElectrumWalletSnapshot {
final WalletType type;
final String? addressPageType;
- String mnemonic;
+ String? mnemonic;
+ String? xpub;
List addresses;
ElectrumBalance balance;
Map regularAddressIndex;
Map changeAddressIndex;
+ String? passphrase;
+ DerivationType? derivationType;
+ String? derivationPath;
static Future load(
String name, WalletType type, String password, BasedUtxoNetwork network) async {
@@ -36,7 +45,9 @@ class ElectrumWalletSnapshot {
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ??