mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-11-16 17:27:37 +00:00
3a391f10a3
* fix: enhance regex, fix multiline * feat: improve scan msg, fix missing txs, use date api * feat: node fixes, enhance send modal, TX list tag & filter, refactors * fix: continuous scanning * fix: missing close * fix: resubscribe tweaks * feat: use mempool api setting toggle * handle any failure of height API and fallback to the old method [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
329 lines
11 KiB
Dart
329 lines
11 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
|
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
|
|
import 'package:bip39/bip39.dart' as bip39;
|
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
|
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
|
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
|
import 'package:cw_bitcoin/electrum_derivations.dart';
|
|
import 'package:cw_core/encryption_file_utils.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/unspent_coins_info.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/litecoin_wallet_addresses.dart';
|
|
import 'package:cw_core/transaction_priority.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 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
|
import 'package:pointycastle/ecc/api.dart';
|
|
import 'package:pointycastle/ecc/curves/secp256k1.dart';
|
|
|
|
part 'litecoin_wallet.g.dart';
|
|
|
|
class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet;
|
|
|
|
abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|
LitecoinWalletBase({
|
|
required String mnemonic,
|
|
required String password,
|
|
required WalletInfo walletInfo,
|
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
|
required Uint8List seedBytes,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
String? passphrase,
|
|
String? addressPageType,
|
|
List<BitcoinAddressRecord>? initialAddresses,
|
|
ElectrumBalance? initialBalance,
|
|
Map<String, int>? initialRegularAddressIndex,
|
|
Map<String, int>? initialChangeAddressIndex,
|
|
}) : super(
|
|
mnemonic: mnemonic,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
network: LitecoinNetwork.mainnet,
|
|
initialAddresses: initialAddresses,
|
|
initialBalance: initialBalance,
|
|
seedBytes: seedBytes,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
passphrase: passphrase,
|
|
currency: CryptoCurrency.ltc) {
|
|
walletAddresses = LitecoinWalletAddresses(
|
|
walletInfo,
|
|
initialAddresses: initialAddresses,
|
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
|
mainHd: hd,
|
|
sideHd: accountHD.childKey(Bip32KeyIndex(1)),
|
|
network: network,
|
|
);
|
|
autorun((_) {
|
|
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
|
});
|
|
}
|
|
|
|
static Future<LitecoinWallet> 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}) 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, passphrase: passphrase ?? "");
|
|
break;
|
|
}
|
|
return LitecoinWallet(
|
|
mnemonic: mnemonic,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
initialAddresses: initialAddresses,
|
|
initialBalance: initialBalance,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
passphrase: passphrase,
|
|
seedBytes: seedBytes,
|
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
|
addressPageType: addressPageType,
|
|
);
|
|
}
|
|
|
|
static Future<LitecoinWallet> open(
|
|
{required String name,
|
|
required WalletInfo walletInfo,
|
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
|
required String password,
|
|
required EncryptionFileUtils encryptionFileUtils}) async {
|
|
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
|
|
|
|
ElectrumWalletSnapshot? snp = null;
|
|
|
|
try {
|
|
snp = await ElectrumWalletSnapshot.load(
|
|
encryptionFileUtils,
|
|
name,
|
|
walletInfo.type,
|
|
password,
|
|
LitecoinNetwork.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,
|
|
);
|
|
}
|
|
|
|
walletInfo.derivationInfo ??= DerivationInfo();
|
|
|
|
// set the default if not present:
|
|
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
|
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
|
|
|
Uint8List? seedBytes = null;
|
|
final mnemonic = keysData.mnemonic;
|
|
final passphrase = keysData.passphrase;
|
|
|
|
if (mnemonic != null) {
|
|
switch (walletInfo.derivationInfo?.derivationType) {
|
|
case DerivationType.bip39:
|
|
seedBytes = await bip39.mnemonicToSeed(
|
|
mnemonic,
|
|
passphrase: passphrase ?? "",
|
|
);
|
|
break;
|
|
case DerivationType.electrum:
|
|
default:
|
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return LitecoinWallet(
|
|
mnemonic: keysData.mnemonic!,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
initialAddresses: snp?.addresses,
|
|
initialBalance: snp?.balance,
|
|
seedBytes: seedBytes!,
|
|
passphrase: passphrase,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
initialRegularAddressIndex: snp?.regularAddressIndex,
|
|
initialChangeAddressIndex: snp?.changeAddressIndex,
|
|
addressPageType: snp?.addressPageType,
|
|
);
|
|
}
|
|
|
|
@override
|
|
int feeRate(TransactionPriority priority) {
|
|
if (priority is LitecoinTransactionPriority) {
|
|
switch (priority) {
|
|
case LitecoinTransactionPriority.slow:
|
|
return 1;
|
|
case LitecoinTransactionPriority.medium:
|
|
return 2;
|
|
case LitecoinTransactionPriority.fast:
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
@override
|
|
Future<String> signMessage(String message, {String? address = null}) async {
|
|
final index = address != null
|
|
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
|
|
: null;
|
|
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
|
|
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
|
|
|
|
final privateKey = ECDSAPrivateKey.fromBytes(
|
|
priv.toBytes(),
|
|
Curves.generatorSecp256k1,
|
|
);
|
|
|
|
final signature =
|
|
signLitecoinMessage(utf8.encode(message), privateKey: privateKey, bipPrive: priv.prive);
|
|
|
|
return base64Encode(signature);
|
|
}
|
|
|
|
List<int> _magicPrefix(List<int> message, List<int> messagePrefix) {
|
|
final encodeLength = IntUtils.encodeVarint(message.length);
|
|
|
|
return [...messagePrefix, ...encodeLength, ...message];
|
|
}
|
|
|
|
List<int> signLitecoinMessage(List<int> message,
|
|
{required ECDSAPrivateKey privateKey, required Bip32PrivateKey bipPrive}) {
|
|
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
|
final messageHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix));
|
|
final signingKey = EcdsaSigningKey(privateKey);
|
|
ECDSASignature ecdsaSign =
|
|
signingKey.signDigestDeterminstic(digest: messageHash, hashFunc: () => SHA256());
|
|
final n = Curves.generatorSecp256k1.order! >> 1;
|
|
BigInt newS;
|
|
if (ecdsaSign.s.compareTo(n) > 0) {
|
|
newS = Curves.generatorSecp256k1.order! - ecdsaSign.s;
|
|
} else {
|
|
newS = ecdsaSign.s;
|
|
}
|
|
final rawSig = ECDSASignature(ecdsaSign.r, newS);
|
|
final rawSigBytes = rawSig.toBytes(BitcoinSignerUtils.baselen);
|
|
|
|
final pub = bipPrive.publicKey;
|
|
final ECDomainParameters curve = ECCurve_secp256k1();
|
|
final point = curve.curve.decodePoint(pub.point.toBytes());
|
|
|
|
final rawSigEc = ECSignature(rawSig.r, rawSig.s);
|
|
|
|
final recId = SignUtils.findRecoveryId(
|
|
SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length),
|
|
rawSigEc,
|
|
Uint8List.fromList(pub.uncompressed),
|
|
);
|
|
|
|
final v = recId + 27 + (point!.isCompressed ? 4 : 0);
|
|
|
|
final combined = Uint8List.fromList([v, ...rawSigBytes]);
|
|
|
|
return combined;
|
|
}
|
|
|
|
List<int> magicMessage(List<int> message, String messagePrefix) {
|
|
final prefixBytes = StringUtils.encode(messagePrefix);
|
|
final magic = _magicPrefix(message, prefixBytes);
|
|
return QuickCrypto.sha256Hash(magic);
|
|
}
|
|
|
|
@override
|
|
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
|
|
if (address == null) {
|
|
return false;
|
|
}
|
|
|
|
List<int> sigDecodedBytes = [];
|
|
|
|
if (signature.endsWith('=')) {
|
|
sigDecodedBytes = base64.decode(signature);
|
|
} else {
|
|
sigDecodedBytes = hex.decode(signature);
|
|
}
|
|
|
|
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
|
|
throw ArgumentException(
|
|
"litecoin signature must be 64 bytes without recover-id or 65 bytes with recover-id");
|
|
}
|
|
|
|
String messagePrefix = '\x19Litecoin Signed Message:\n';
|
|
final messageHash = QuickCrypto.sha256Hash(magicMessage(utf8.encode(message), messagePrefix));
|
|
|
|
List<int> correctSignature =
|
|
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
|
|
List<int> rBytes = correctSignature.sublist(0, 32);
|
|
List<int> sBytes = correctSignature.sublist(32);
|
|
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
|
|
|
|
List<int> possibleRecoverIds = [0, 1];
|
|
|
|
final baseAddress = RegexUtils.addressTypeFromStr(address, network);
|
|
|
|
for (int recoveryId in possibleRecoverIds) {
|
|
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
|
|
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
|
|
|
|
String? recoveredAddress;
|
|
|
|
if (baseAddress is P2pkAddress) {
|
|
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
|
|
} else if (baseAddress is P2pkhAddress) {
|
|
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
|
|
} else if (baseAddress is P2wshAddress) {
|
|
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
|
|
} else if (baseAddress is P2wpkhAddress) {
|
|
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
|
|
}
|
|
|
|
if (recoveredAddress == address) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|