cake_wallet/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
2024-11-07 13:01:32 -03:00

261 lines
8.7 KiB
Dart

import 'package:bitbox/bitbox.dart' as bitbox;
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_mnemonics_bip39.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/transaction_priority.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:mobx/mobx.dart';
import 'bitcoin_cash_base.dart';
part 'bitcoin_cash_wallet.g.dart';
class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet;
abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
BitcoinCashWalletBase({
required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
BitcoinAddressType? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
required bool mempoolAPIEnabled,
}) : super(
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
network: BitcoinCashNetwork.mainnet,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: seedBytes,
currency: CryptoCurrency.bch,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
hdWallets: {CWBitcoinDerivationType.bip39: bitcoinCashHDWallet(seedBytes)},
) {
walletAddresses = BitcoinCashWalletAddresses(
walletInfo,
initialAddresses: initialAddresses,
network: network,
initialAddressPageType: addressPageType,
isHardwareWallet: walletInfo.isHardwareWallet,
hdWallets: hdWallets,
);
autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
});
}
@override
BitcoinCashNetwork get network => BitcoinCashNetwork.mainnet;
static Future<BitcoinCashWallet> create({
required String mnemonic,
required String password,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
required bool mempoolAPIEnabled,
}) async {
return BitcoinCashWallet(
mnemonic: mnemonic,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses,
initialBalance: initialBalance,
seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
passphrase: passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
static Future<BitcoinCashWallet> open({
required String name,
required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool mempoolAPIEnabled,
}) async {
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
ElectrumWalletSnapshot? snp = null;
try {
snp = await ElectrumWalletSnapshot.load(
encryptionFileUtils,
name,
walletInfo.type,
password,
BitcoinCashNetwork.mainnet,
);
} catch (e) {
if (!hasKeysFile) rethrow;
}
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
keysData =
WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
return BitcoinCashWallet(
mnemonic: keysData.mnemonic!,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp?.addresses.map((addr) {
try {
BitcoinCashAddress(addr.address);
return BitcoinAddressRecord(
addr.address,
index: addr.index,
isChange: addr.isChange,
addressType: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh),
derivationType: CWBitcoinDerivationType.bip39,
);
} catch (_) {
return BitcoinAddressRecord(
AddressUtils.getCashAddrFormat(addr.address),
index: addr.index,
isChange: addr.isChange,
addressType: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh),
derivationType: CWBitcoinDerivationType.bip39,
);
}
}).toList(),
initialBalance: snp?.balance,
seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase),
encryptionFileUtils: encryptionFileUtils,
initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh,
passphrase: keysData.passphrase,
mempoolAPIEnabled: mempoolAPIEnabled,
);
}
bitbox.ECPair generateKeyPair({required Bip32Slip10Secp256k1 hd, required int index}) =>
bitbox.ECPair.fromPrivateKey(
Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw),
);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0;
int totalValue = 0;
for (final input in unspentCoins) {
if (input.isSending) {
inputsCount++;
totalValue += input.value;
}
if (amount != null && totalValue >= amount) {
break;
}
}
if (amount != null && totalValue < amount) return 0;
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount);
}
@override
int feeRate(TransactionPriority priority) {
if (priority is ElectrumTransactionPriority) {
switch (priority) {
case ElectrumTransactionPriority.slow:
return 1;
case ElectrumTransactionPriority.medium:
return 5;
case ElectrumTransactionPriority.fast:
return 10;
}
}
return 0;
}
@override
Future<String> signMessage(String message, {String? address = null}) async {
Bip32Slip10Secp256k1 HD = bip32;
final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address);
if (record.isChange) {
HD = HD.childKey(Bip32KeyIndex(1));
} else {
HD = HD.childKey(Bip32KeyIndex(0));
}
HD = HD.childKey(Bip32KeyIndex(record.index));
final priv = ECPrivate.fromWif(
WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer),
netVersion: network.wifNetVer,
);
return priv.signMessage(StringUtils.encode(message));
}
@override
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
}) async =>
feeRate *
ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
);
static Bip32Slip10Secp256k1 bitcoinCashHDWallet(List<int> seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
}