mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-01 07:04:08 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
exclude:
|
||||
- "**/.secrets.g.dart"
|
||||
@@ -9,6 +9,7 @@ abstract class BaseBitcoinAddressRecord {
|
||||
BaseBitcoinAddressRecord(
|
||||
this.address, {
|
||||
required this.index,
|
||||
this.accountIndex = 0,
|
||||
this.isHidden = false,
|
||||
this.isLegacyDerivation = false,
|
||||
int txCount = 0,
|
||||
@@ -29,6 +30,7 @@ abstract class BaseBitcoinAddressRecord {
|
||||
bool isHidden;
|
||||
bool isLegacyDerivation;
|
||||
final int index;
|
||||
final int accountIndex;
|
||||
int _txCount;
|
||||
int _balance;
|
||||
String _name;
|
||||
@@ -63,7 +65,9 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||
BitcoinAddressRecord(
|
||||
super.address, {
|
||||
required super.index,
|
||||
super.accountIndex = 0,
|
||||
super.isHidden = false,
|
||||
this.isHiddenChecked = false,
|
||||
super.isLegacyDerivation = false,
|
||||
super.txCount = 0,
|
||||
super.balance = 0,
|
||||
@@ -105,7 +109,9 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||
return BitcoinAddressRecord(
|
||||
decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
accountIndex: decoded['accountIndex'] as int? ?? 0,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isHiddenChecked: decoded['isHiddenChecked'] as bool? ?? false,
|
||||
isLegacyDerivation: parsedIsLegacy,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
@@ -117,6 +123,10 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether [isHidden] has been checked against the wallet's keys. Records created from the
|
||||
/// wallet's own keys are correct by construction; older ones are checked once on wallet open.
|
||||
bool isHiddenChecked;
|
||||
|
||||
String? scriptHash;
|
||||
|
||||
static int _purposeForType(BitcoinAddressType type) {
|
||||
@@ -162,7 +172,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||
|
||||
final coinType = _coinTypeForNetwork();
|
||||
final purpose = _purposeForType(type);
|
||||
final accountPath = isLegacyDerivation ? electrum_path : "m/$purpose'/$coinType'/0'";
|
||||
final accountPath =
|
||||
isLegacyDerivation ? electrum_path : "m/$purpose'/$coinType'/$accountIndex'";
|
||||
|
||||
final chain = isHidden ? 1 : 0;
|
||||
return "$accountPath/$chain/$index";
|
||||
@@ -172,7 +183,9 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||
String toJSON() => json.encode({
|
||||
'address': address,
|
||||
'index': index,
|
||||
'accountIndex': accountIndex,
|
||||
'isHidden': isHidden,
|
||||
'isHiddenChecked': isHiddenChecked,
|
||||
'isLegacyDerivation': isLegacyDerivation,
|
||||
'isUsed': isUsed,
|
||||
'txCount': txCount,
|
||||
@@ -187,6 +200,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
||||
BitcoinSilentPaymentAddressRecord(
|
||||
super.address, {
|
||||
required super.index,
|
||||
super.accountIndex = 0,
|
||||
super.isHidden = false,
|
||||
super.txCount = 0,
|
||||
super.balance = 0,
|
||||
@@ -205,6 +219,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
||||
return BitcoinSilentPaymentAddressRecord(
|
||||
decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
accountIndex: decoded['accountIndex'] as int? ?? 0,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
@@ -233,6 +248,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
||||
String toJSON() => json.encode({
|
||||
'address': address,
|
||||
'index': index,
|
||||
'accountIndex': accountIndex,
|
||||
'isHidden': isHidden,
|
||||
'isUsed': isUsed,
|
||||
'txCount': txCount,
|
||||
|
||||
@@ -7,16 +7,20 @@ import 'package:cw_bitcoin/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import "package:cw_bitcoin/bitcoin_receive_page_option.dart";
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_derivations.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.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_bitcoin/locktime.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
|
||||
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:cw_bitcoin/payjoin/manager.dart';
|
||||
import 'package:cw_bitcoin/payjoin/storage.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
@@ -24,12 +28,14 @@ import 'package:cw_bitcoin/psbt/signer.dart';
|
||||
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
|
||||
import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
|
||||
import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import "package:cw_core/receive_page_option.dart";
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
@@ -123,8 +129,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
initialSilentAddresses: initialSilentAddresses,
|
||||
initialSilentAddressIndex: initialSilentAddressIndex,
|
||||
mainHdByType: mainHdByType,
|
||||
sideHdByType: sideHdByType,
|
||||
mainHdByTypeAndAccount: mainHdByTypeAndAccount,
|
||||
sideHdByTypeAndAccount: sideHdByTypeAndAccount,
|
||||
accountIndexes: [currentAccountIndex],
|
||||
currentAccountIndex: currentAccountIndex,
|
||||
legacyMainHd: mainHd,
|
||||
legacySideHd: sideHd,
|
||||
network: networkParam ?? network,
|
||||
@@ -152,9 +160,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
lnurlDomain: "cake.cash",
|
||||
cachedAddress: cachedLightningAddress,
|
||||
);
|
||||
walletAddresses.lightningWallet = lightningWallet;
|
||||
walletAddresses.setLightningAddress(walletInfo.name);
|
||||
}
|
||||
} else {
|
||||
lightningWallet?.close();
|
||||
lightningWallet = null;
|
||||
}
|
||||
});
|
||||
@@ -345,15 +355,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
return balance;
|
||||
}
|
||||
|
||||
final lBalance = await lightningWallet!.getBalance();
|
||||
try {
|
||||
final lBalance = await lightningWallet!.getBalance();
|
||||
|
||||
this.balance[CryptoCurrency.btcln] =
|
||||
ElectrumBalance(confirmed: lBalance.toInt(), unconfirmed: 0, frozen: 0);
|
||||
this.balance[CryptoCurrency.btcln] = ElectrumBalance(
|
||||
confirmed: lBalance,
|
||||
unconfirmed: Money.zero(CryptoCurrency.btcln),
|
||||
frozen: Money.zero(CryptoCurrency.btcln));
|
||||
} catch (e) {
|
||||
printV("Error fetching lightning balance: $e");
|
||||
}
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: balance.confirmed,
|
||||
unconfirmed: balance.unconfirmed,
|
||||
frozen: balance.frozen.toInt(),
|
||||
frozen: balance.frozen ?? Money.zero(currency),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,12 +424,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
|
||||
late final PayjoinManager payjoinManager;
|
||||
|
||||
@override
|
||||
bool get hasPayjoinSupport => keys.privateKey.isNotEmpty;
|
||||
|
||||
@override
|
||||
bool get hasLightningSupport => seed != null && LightningWallet.isAvailable;
|
||||
|
||||
bool get isPayjoinAvailable => unspentCoinsInfo.values
|
||||
.where((element) => element.walletId == id && element.isSending && !element.isFrozen)
|
||||
.isNotEmpty;
|
||||
|
||||
Future<PsbtV2> buildPsbt({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<BitcoinOutput> outputs,
|
||||
required List<OutputInfo> cwOutputs,
|
||||
required BigInt fee,
|
||||
required BasedUtxoNetwork network,
|
||||
@@ -430,24 +452,65 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
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,
|
||||
));
|
||||
psbtReadyInputs.add(
|
||||
PSBTReadyUtxoWithAddress(
|
||||
utxo: utxo.utxo,
|
||||
rawTx: rawTx,
|
||||
ownerDetails: utxo.ownerDetails,
|
||||
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
||||
ownerMasterFingerprint: masterFingerprint,
|
||||
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final psbtReadyOutputs = outputs.map((o) {
|
||||
final cwOutput = cwOutputs
|
||||
.where(
|
||||
(e) => [e.address, e.extractedAddress]
|
||||
.map((e) => e?.toLowerCase())
|
||||
.contains(o.address.toAddress().toLowerCase()),
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (o.isChange && publicKeys.containsKey(o.address.pubKeyHash())) {
|
||||
final changeKey = publicKeys[o.address.pubKeyHash()]!;
|
||||
return PSBTReadyBitcoinOutput(
|
||||
address: o.address,
|
||||
value: o.value,
|
||||
isSilentPayment: o.isSilentPayment,
|
||||
isChange: o.isChange,
|
||||
changeMasterFingerprint: masterFingerprint,
|
||||
changeDerivationPath: changeKey.derivationPath,
|
||||
changePublicKey: changeKey.publicKey,
|
||||
outputInfo: cwOutput,
|
||||
);
|
||||
}
|
||||
return PSBTReadyBitcoinOutput(
|
||||
address: o.address,
|
||||
value: o.value,
|
||||
isSilentPayment: o.isSilentPayment,
|
||||
isChange: o.isChange,
|
||||
outputInfo: cwOutput,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final locktime = antiFeeSnipingLocktime(
|
||||
chainTip: await getCurrentChainTip(),
|
||||
synced: syncStatus is SyncedSyncStatus,
|
||||
);
|
||||
|
||||
return PSBTTransactionBuild(
|
||||
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF, cwOutputs: cwOutputs)
|
||||
.psbt;
|
||||
inputs: psbtReadyInputs,
|
||||
outputs: psbtReadyOutputs,
|
||||
enableRBF: enableRBF,
|
||||
locktime: locktime,
|
||||
).psbt;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BtcTransaction> buildHardwareWalletTransaction({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<BitcoinOutput> outputs,
|
||||
required BigInt fee,
|
||||
required BasedUtxoNetwork network,
|
||||
required List<UtxoWithAddress> utxos,
|
||||
@@ -461,8 +524,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
final masterFingerprint =
|
||||
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
|
||||
|
||||
final orderedOutputs = orderOutputs(outputs, outputOrdering);
|
||||
|
||||
final psbt = await buildPsbt(
|
||||
outputs: outputs,
|
||||
outputs: orderedOutputs,
|
||||
fee: fee,
|
||||
network: network,
|
||||
utxos: utxos,
|
||||
@@ -472,7 +537,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
memo: memo,
|
||||
enableRBF: enableRBF,
|
||||
inputOrdering: inputOrdering,
|
||||
outputOrdering: outputOrdering,
|
||||
// Already applied above; don't reorder again.
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
);
|
||||
|
||||
final psbtStr = base64Encode(psbt.serialize());
|
||||
@@ -495,19 +561,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
|
||||
if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
|
||||
isLNCompatible == true) {
|
||||
BigInt amount;
|
||||
Money amount;
|
||||
if (credentials.outputs.first.sendAll) {
|
||||
amount = await lightningWallet!.getBalance();
|
||||
} else {
|
||||
amount = parseFixed(
|
||||
credentials.outputs.first.cryptoAmount?.isNotEmpty == true
|
||||
? credentials.outputs.first.cryptoAmount!
|
||||
: "0",
|
||||
8);
|
||||
amount = credentials.outputs.first.cryptoAmount;
|
||||
}
|
||||
|
||||
return lightningWallet!.createTransaction(lnAddr, amount > BigInt.zero ? amount : null,
|
||||
credentials.priority, credentials.outputs.first.sendAll);
|
||||
return lightningWallet!.createTransaction(
|
||||
lnAddr,
|
||||
amount.amount > BigInt.zero ? amount.amount : null,
|
||||
credentials.priority,
|
||||
credentials.outputs.first.sendAll,
|
||||
);
|
||||
}
|
||||
|
||||
final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
|
||||
@@ -526,7 +592,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
))
|
||||
.toList(),
|
||||
cwOutputs: credentials.outputs,
|
||||
fee: BigInt.from(tx.fee),
|
||||
fee: tx.fee.amount,
|
||||
network: network,
|
||||
memo: credentials.outputs.first.memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
@@ -545,17 +611,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
tx.commitOverride = () async {
|
||||
final sender =
|
||||
await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
|
||||
payjoinManager.spawnNewSender(
|
||||
sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
|
||||
payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri, amount: tx.amount.amount);
|
||||
};
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
|
||||
.where((e) => (e.isSending && !e.isFrozen))
|
||||
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
|
||||
.toList();
|
||||
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) =>
|
||||
unspentCoinsForCurrentAccount
|
||||
.where(
|
||||
(e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
|
||||
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
|
||||
.toList();
|
||||
|
||||
Future<void> commitPsbt(String finalizedPsbt) {
|
||||
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
|
||||
@@ -565,9 +632,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
return PendingBitcoinTransaction(
|
||||
btcTx,
|
||||
type,
|
||||
accountIndex: currentAccountIndex,
|
||||
electrumClient: electrumClient,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
amount: Money.zero(currency),
|
||||
fee: Money.zero(currency),
|
||||
feeRate: "",
|
||||
network: network,
|
||||
hasChange: true,
|
||||
@@ -623,9 +691,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
return PendingBitcoinTransaction(
|
||||
btcTx,
|
||||
type,
|
||||
accountIndex: currentAccountIndex,
|
||||
electrumClient: electrumClient,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
amount: Money.zero(currency),
|
||||
fee: Money.zero(currency),
|
||||
feeRate: "",
|
||||
network: network,
|
||||
hasChange: true,
|
||||
@@ -653,4 +722,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
|
||||
return super.signMessage(message, address: address);
|
||||
}
|
||||
|
||||
@override
|
||||
bool receiveOptionAvailable(ReceivePageOption option) {
|
||||
final isExtraAccount = currentAccountIndex > 0;
|
||||
|
||||
if (option == BitcoinReceivePageOption.lightning) {
|
||||
// Lightning is tied to the primary account only
|
||||
return !isExtraAccount && hasLightningSupport && isLightningInitialized;
|
||||
}
|
||||
|
||||
if (option == BitcoinReceivePageOption.silent_payments) {
|
||||
// Silent payments are tied to the primary account only
|
||||
return !isExtraAccount && hasSilentPaymentsScanning;
|
||||
}
|
||||
|
||||
// Restrict extra Bitcoin accounts (accountIndex > 0) to the allowed address types
|
||||
if (isExtraAccount && option is BitcoinReceivePageOption) {
|
||||
final addressType = option.toType();
|
||||
if (!EXTRA_ACCOUNT_ADDRESS_TYPES.contains(addressType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd
|
||||
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
BitcoinWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHdByType,
|
||||
required super.sideHdByType,
|
||||
required super.mainHdByTypeAndAccount,
|
||||
required super.sideHdByTypeAndAccount,
|
||||
required super.accountIndexes,
|
||||
required super.currentAccountIndex,
|
||||
required super.legacySideHd,
|
||||
required super.legacyMainHd,
|
||||
required super.network,
|
||||
@@ -124,7 +126,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
|
||||
|
||||
Future<PaymentURI> getPaymentRequestUri(String amount) async {
|
||||
if (addressPageType is LightningAddressType && lightningWallet != null) {
|
||||
final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null;
|
||||
final amountSats = amount.isNotEmpty ? tryParseFixed(amount, 8) : null;
|
||||
final lnUrl = getLnurlOfLightningAddress(address);
|
||||
if (amountSats == null) {
|
||||
return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount);
|
||||
@@ -145,7 +147,11 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
|
||||
try {
|
||||
return super
|
||||
.allAddresses
|
||||
.firstWhere((e) => e.type == SegwitAddresType.p2wpkh && !e.isHidden && !e.isUsed)
|
||||
.firstWhere((e) =>
|
||||
e.accountIndex == currentAccountIndex &&
|
||||
e.type == SegwitAddresType.p2wpkh &&
|
||||
!e.isHidden &&
|
||||
!e.isUsed)
|
||||
.address;
|
||||
} catch (_) {
|
||||
return super.addressForBuy;
|
||||
@@ -154,11 +160,21 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
|
||||
|
||||
@override
|
||||
String get addressForExchange {
|
||||
if (addressPageType == LightningAddressType.p2l) {
|
||||
final addresses = receiveAddresses
|
||||
.where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed);
|
||||
return addresses.first.address;
|
||||
final current = getFreshAddress();
|
||||
final availableReceiveAddresses = receiveAddresses.where((element) =>
|
||||
!element.isUsed && !element.isHidden && !hiddenAddresses.contains(element.address));
|
||||
|
||||
final bool isSilentPaymentsPage = addressPageType == SilentPaymentsAddresType.p2sp;
|
||||
final bool isLightningPage = addressPageType == LightningAddressType.p2l;
|
||||
|
||||
if (isSilentPaymentsPage || isLightningPage) {
|
||||
final segwit = availableReceiveAddresses
|
||||
.where((e) => e.type == SegwitAddresType.p2wpkh)
|
||||
.map((e) => e.address)
|
||||
.toList();
|
||||
|
||||
return segwit.isNotEmpty ? segwit.first : current;
|
||||
}
|
||||
return address;
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
class BitcoinWalletKeys {
|
||||
const BitcoinWalletKeys(
|
||||
{required this.wif, required this.privateKey, required this.publicKey, required this.xpub});
|
||||
const BitcoinWalletKeys({
|
||||
required this.wif,
|
||||
required this.privateKey,
|
||||
required this.publicKey,
|
||||
required this.xpub,
|
||||
this.masterFingerprint = '',
|
||||
});
|
||||
|
||||
final String wif;
|
||||
final String privateKey;
|
||||
final String publicKey;
|
||||
final String xpub;
|
||||
final String masterFingerprint;
|
||||
|
||||
Map<String, String> toJson() =>
|
||||
{'wif': wif, 'privateKey': privateKey, 'publicKey': publicKey, 'xpub': xpub};
|
||||
Map<String, String> toJson() => {
|
||||
'wif': wif,
|
||||
'privateKey': privateKey,
|
||||
'publicKey': publicKey,
|
||||
'xpub': xpub,
|
||||
'masterFingerprint': masterFingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/utils/zpub.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_wallet.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
@@ -133,31 +132,6 @@ class BitcoinWalletService extends WalletService<
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = await WalletInfo.get(currentName, getType());
|
||||
if (currentWalletInfo == null) {
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
final currentWallet = await BitcoinWalletBase.open(
|
||||
password: password,
|
||||
name: currentName,
|
||||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
payjoinBox: payjoinSessionSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
newWalletInfo.name = newName;
|
||||
|
||||
await newWalletInfo.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
|
||||
{bool? isTestnet}) async {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'dart:math';
|
||||
|
||||
int effectiveValue(int value, int inputCost) => value - inputCost;
|
||||
|
||||
class SelectedCoins {
|
||||
final List<int> indices;
|
||||
final bool hasChange;
|
||||
const SelectedCoins(this.indices, this.hasChange);
|
||||
}
|
||||
|
||||
SelectedCoins? branchAndBound(List<int> effValues, int target, int costOfChange,
|
||||
{int maxTries = 100000}) {
|
||||
final n = effValues.length;
|
||||
final order = List<int>.generate(n, (i) => i)
|
||||
..sort((a, b) => effValues[b].compareTo(effValues[a]));
|
||||
final sorted = [for (final i in order) effValues[i]];
|
||||
final suffix = List<int>.filled(n + 1, 0);
|
||||
for (var i = n - 1; i >= 0; i--) suffix[i] = suffix[i + 1] + sorted[i];
|
||||
final upper = target + costOfChange;
|
||||
List<int>? best;
|
||||
final picked = <int>[];
|
||||
var tries = 0;
|
||||
void dfs(int i, int sum) {
|
||||
if (best != null || tries++ >= maxTries) return;
|
||||
if (sum > upper) return;
|
||||
if (sum >= target) { best = List.of(picked); return; }
|
||||
if (i >= n || sum + suffix[i] < target) return;
|
||||
picked.add(order[i]); dfs(i + 1, sum + sorted[i]); picked.removeLast();
|
||||
dfs(i + 1, sum);
|
||||
}
|
||||
dfs(0, 0);
|
||||
return best == null ? null : SelectedCoins(best!, false);
|
||||
}
|
||||
|
||||
SelectedCoins? singleRandomDraw(List<int> effValues, int target, int minChange, Random rng) {
|
||||
final order = List<int>.generate(effValues.length, (i) => i)..shuffle(rng);
|
||||
final picked = <int>[]; var sum = 0;
|
||||
for (final i in order) {
|
||||
picked.add(i); sum += effValues[i];
|
||||
if (sum >= target + minChange) return SelectedCoins(picked, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Branch-and-bound over effective values (value minus the cost of spending the
|
||||
/// input at the current fee rate). [inputCosts] is parallel to [values] so each
|
||||
/// input pays for its own script type's size. A match means the excess over
|
||||
/// [target] stays within [window], so the remainder can be absorbed into the fee
|
||||
/// instead of creating a change output. Returns null when no such subset exists.
|
||||
SelectedCoins? changelessMatch({
|
||||
required List<int> values,
|
||||
required int target,
|
||||
required List<int> inputCosts,
|
||||
required int window,
|
||||
int maxTries = 100000,
|
||||
}) {
|
||||
assert(values.length == inputCosts.length);
|
||||
final keep = <int>[];
|
||||
final eff = <int>[];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
final e = effectiveValue(values[i], inputCosts[i]);
|
||||
if (e > 0) {
|
||||
keep.add(i);
|
||||
eff.add(e);
|
||||
}
|
||||
}
|
||||
final match = branchAndBound(eff, target, window, maxTries: maxTries);
|
||||
if (match == null) return null;
|
||||
return SelectedCoins([for (final i in match.indices) keep[i]], false);
|
||||
}
|
||||
|
||||
class InsufficientFundsException implements Exception { const InsufficientFundsException(); }
|
||||
|
||||
SelectedCoins selectCoins({required List<int> values, required int target,
|
||||
required int inputCost, required int costOfChange, required int minChange, Random? rng}) {
|
||||
final keep = <int>[]; final eff = <int>[];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
final e = effectiveValue(values[i], inputCost);
|
||||
if (e > 0) { keep.add(i); eff.add(e); }
|
||||
}
|
||||
SelectedCoins? map(SelectedCoins? s) =>
|
||||
s == null ? null : SelectedCoins([for (final i in s.indices) keep[i]], s.hasChange);
|
||||
final bnb = map(branchAndBound(eff, target, costOfChange));
|
||||
if (bnb != null) return bnb;
|
||||
final srd = map(singleRandomDraw(eff, target, minChange, rng ?? Random.secure()));
|
||||
if (srd != null) return srd;
|
||||
throw const InsufficientFundsException();
|
||||
}
|
||||
@@ -173,6 +173,7 @@ class ElectrumClient {
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response = json.decode(unterminatedString);
|
||||
_handleResponse(response);
|
||||
// unterminatedString = null;
|
||||
unterminatedString = '';
|
||||
}
|
||||
@@ -340,6 +341,80 @@ class ElectrumClient {
|
||||
return historyMap;
|
||||
}
|
||||
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getBatchUnspent(
|
||||
List<String> scriptHashes, {
|
||||
int timeout = 10000,
|
||||
}) async {
|
||||
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
|
||||
|
||||
final batchResults = await callBatchWithTimeout(
|
||||
method: 'blockchain.scripthash.listunspent',
|
||||
paramsList: paramsList,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
final unspentMap = <String, List<Map<String, dynamic>>>{};
|
||||
|
||||
for (int i = 0; i < scriptHashes.length; i++) {
|
||||
final sh = scriptHashes[i];
|
||||
|
||||
if (i >= batchResults.length) {
|
||||
unspentMap[sh] = const [];
|
||||
continue;
|
||||
}
|
||||
|
||||
final result = batchResults[i];
|
||||
|
||||
if (result is List) {
|
||||
unspentMap[sh] = result
|
||||
.whereType<Map<dynamic, dynamic>>()
|
||||
.map((m) => m.map((k, v) => MapEntry(k.toString(), v)))
|
||||
.cast<Map<String, dynamic>>()
|
||||
.toList();
|
||||
} else {
|
||||
unspentMap[sh] = const [];
|
||||
}
|
||||
}
|
||||
|
||||
return unspentMap;
|
||||
}
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> getBatchBalance(
|
||||
List<String> scriptHashes, {
|
||||
int timeout = 10000,
|
||||
}) async {
|
||||
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
|
||||
|
||||
final batchResults = await callBatchWithTimeout(
|
||||
method: 'blockchain.scripthash.get_balance',
|
||||
paramsList: paramsList,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
final balanceMap = <String, Map<String, dynamic>>{};
|
||||
|
||||
for (int i = 0; i < scriptHashes.length; i++) {
|
||||
final sh = scriptHashes[i];
|
||||
|
||||
if (i >= batchResults.length) {
|
||||
balanceMap[sh] = <String, dynamic>{};
|
||||
continue;
|
||||
}
|
||||
|
||||
final result = batchResults[i];
|
||||
|
||||
if (result is Map<String, dynamic>) {
|
||||
balanceMap[sh] = result;
|
||||
} else if (result is Map) {
|
||||
balanceMap[sh] = Map<String, dynamic>.from(result);
|
||||
} else {
|
||||
balanceMap[sh] = <String, dynamic>{};
|
||||
}
|
||||
}
|
||||
|
||||
return balanceMap;
|
||||
}
|
||||
|
||||
Future<Map<String, Map<String, dynamic>>> getBatchTransactionVerbose(
|
||||
List<String> hashes, {
|
||||
int timeout = 10000,
|
||||
@@ -504,14 +579,29 @@ class ElectrumClient {
|
||||
return [];
|
||||
});
|
||||
|
||||
// Floor at 0 so unavailable/-1 estimates never become negative rates;
|
||||
// cap at 2000 sat/vB per CW-1597.
|
||||
static const int _maxFeeRate = 2000;
|
||||
|
||||
static int _sanitizeFeeRate(double feeRate) {
|
||||
final rate = (stringDoubleToBitcoinAmount(feeRate.toString()) / 1000).round();
|
||||
if (rate < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (rate > _maxFeeRate) {
|
||||
return _maxFeeRate;
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
|
||||
try {
|
||||
final topDoubleString = await estimatefee(p: 1);
|
||||
final middleDoubleString = await estimatefee(p: 5);
|
||||
final bottomDoubleString = await estimatefee(p: 10);
|
||||
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
|
||||
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
|
||||
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
|
||||
final topDouble = await estimatefee(p: 1);
|
||||
final middleDouble = await estimatefee(p: 5);
|
||||
final bottomDouble = await estimatefee(p: 10);
|
||||
final top = _sanitizeFeeRate(topDouble);
|
||||
final middle = _sanitizeFeeRate(middleDouble);
|
||||
final bottom = _sanitizeFeeRate(bottomDouble);
|
||||
|
||||
return [bottom, middle, top];
|
||||
} catch (_) {
|
||||
|
||||
@@ -1,51 +1,90 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class ElectrumBalance extends Balance {
|
||||
|
||||
factory ElectrumBalance.zero(CryptoCurrency currency) => ElectrumBalance(
|
||||
confirmed: Money.zero(currency),
|
||||
unconfirmed: Money.zero(currency),
|
||||
frozen: Money.zero(currency),
|
||||
);
|
||||
|
||||
ElectrumBalance({
|
||||
required this.confirmed,
|
||||
required this.unconfirmed,
|
||||
required int frozen,
|
||||
this.secondConfirmed = 0,
|
||||
this.secondUnconfirmed = 0,
|
||||
}) : this._frozen = frozen,
|
||||
super.fromInt(confirmed, unconfirmed,
|
||||
secondAvailable: secondConfirmed, secondAdditional: secondUnconfirmed, frozen: frozen);
|
||||
required this.frozen,
|
||||
this.secondConfirmed,
|
||||
this.secondUnconfirmed,
|
||||
}) : super(confirmed, unconfirmed,
|
||||
secondAvailable: secondConfirmed, secondUnavailable: secondUnconfirmed);
|
||||
|
||||
static ElectrumBalance? fromJSON(String? jsonSource) {
|
||||
Money confirmed;
|
||||
Money unconfirmed;
|
||||
Money? secondConfirmed;
|
||||
Money? secondUnconfirmed;
|
||||
|
||||
@override
|
||||
Money get available => (confirmed + unconfirmed) - frozen;
|
||||
|
||||
@override
|
||||
Money get unavailable => unconfirmed;
|
||||
|
||||
@override
|
||||
Money frozen;
|
||||
|
||||
ElectrumBalance copy() => ElectrumBalance(
|
||||
confirmed: confirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
frozen: frozen,
|
||||
secondConfirmed: secondConfirmed,
|
||||
secondUnconfirmed: secondUnconfirmed,
|
||||
);
|
||||
|
||||
static ElectrumBalance? fromJSON(String? jsonSource, Currency currency) {
|
||||
if (jsonSource == null) return null;
|
||||
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: decoded['confirmed'] as int? ?? 0,
|
||||
unconfirmed: decoded['unconfirmed'] as int? ?? 0,
|
||||
frozen: decoded['frozen'] as int? ?? 0,
|
||||
secondConfirmed: decoded['secondConfirmed'] as int? ?? 0,
|
||||
secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0,
|
||||
confirmed: Money.fromInt(decoded['confirmed'] as int? ?? 0, currency),
|
||||
unconfirmed: Money.fromInt(decoded['unconfirmed'] as int? ?? 0, currency),
|
||||
frozen: Money.fromInt(decoded['frozen'] as int? ?? 0, currency),
|
||||
secondConfirmed: Money.fromInt(decoded['secondConfirmed'] as int? ?? 0, currency),
|
||||
secondUnconfirmed: Money.fromInt(decoded['secondUnconfirmed'] as int? ?? 0, currency),
|
||||
);
|
||||
}
|
||||
|
||||
int confirmed;
|
||||
int unconfirmed;
|
||||
final int _frozen;
|
||||
|
||||
@override
|
||||
BigInt get frozen => BigInt.from(_frozen);
|
||||
|
||||
int secondConfirmed = 0;
|
||||
int secondUnconfirmed = 0;
|
||||
|
||||
@override
|
||||
BigInt get fullAvailableBalance =>
|
||||
BigInt.from((confirmed + unconfirmed) + secondConfirmed - _frozen);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'confirmed': confirmed,
|
||||
'unconfirmed': unconfirmed,
|
||||
'frozen': _frozen,
|
||||
'secondConfirmed': secondConfirmed,
|
||||
'secondUnconfirmed': secondUnconfirmed,
|
||||
'confirmed': confirmed.amount.toInt(),
|
||||
'unconfirmed': unavailable.amount.toInt(),
|
||||
'frozen': frozen.amount.toInt(),
|
||||
'secondConfirmed': secondAvailable?.amount.toInt() ?? 0,
|
||||
'secondUnconfirmed': secondUnavailable?.amount.toInt() ?? 0,
|
||||
});
|
||||
|
||||
// Value equality so instances can be compared/deduped (e.g. by MobX reactions via MapEquality)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is ElectrumBalance &&
|
||||
other.confirmed == confirmed &&
|
||||
other.unconfirmed == unconfirmed &&
|
||||
other.frozen == frozen &&
|
||||
other.secondConfirmed == secondConfirmed &&
|
||||
other.secondUnconfirmed == secondUnconfirmed);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
frozen,
|
||||
secondConfirmed,
|
||||
secondUnconfirmed,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
|
||||
@@ -17,12 +18,13 @@ class ElectrumTransactionBundle {
|
||||
{required this.ins, required this.confirmations, this.time});
|
||||
|
||||
final BtcTransaction originalTransaction;
|
||||
final List<BtcTransaction> ins;
|
||||
final List<BtcTransaction?> ins;
|
||||
final int? time;
|
||||
final int confirmations;
|
||||
}
|
||||
|
||||
class ElectrumTransactionInfo extends TransactionInfo {
|
||||
int? accountIndex;
|
||||
List<BitcoinSilentPaymentsUnspent>? unspents;
|
||||
bool isReceivedSilentPayment;
|
||||
bool isHogEx;
|
||||
@@ -31,8 +33,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
this.type, {
|
||||
required String id,
|
||||
int? height,
|
||||
required int amount,
|
||||
int? fee,
|
||||
required Money amount,
|
||||
Money? fee,
|
||||
List<String>? inputAddresses,
|
||||
List<String>? outputAddresses,
|
||||
required TransactionDirection direction,
|
||||
@@ -40,6 +42,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
bool isReplaced = false,
|
||||
required DateTime date,
|
||||
required int confirmations,
|
||||
this.accountIndex,
|
||||
String? to,
|
||||
this.unspents,
|
||||
this.isReceivedSilentPayment = false,
|
||||
@@ -106,9 +109,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
height: height,
|
||||
isPending: false,
|
||||
isReplaced: false,
|
||||
fee: fee,
|
||||
fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
|
||||
direction: direction,
|
||||
amount: amount,
|
||||
amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
|
||||
date: date,
|
||||
confirmations: confirmations);
|
||||
}
|
||||
@@ -126,9 +129,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
List<String> inputAddresses = [];
|
||||
List<String> outputAddresses = [];
|
||||
|
||||
var hasMissingInputTx = false;
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
final inputTransaction = bundle.ins[i];
|
||||
final inputTransaction = i < bundle.ins.length ? bundle.ins[i] : null;
|
||||
if (inputTransaction == null || input.txIndex >= inputTransaction.outputs.length) {
|
||||
hasMissingInputTx = true;
|
||||
continue;
|
||||
}
|
||||
final outTransaction = inputTransaction.outputs[input.txIndex];
|
||||
inputAmount += outTransaction.amount.toInt();
|
||||
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
|
||||
@@ -182,9 +190,13 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
final b = tx.outputs.first.scriptPubKey.toBytes();
|
||||
return b.length == 34 && b[0] == 88 && b[1] == 32;
|
||||
};
|
||||
final isHogEx = isHogExTx(bundle.originalTransaction) && isHogExTx(bundle.ins.first);
|
||||
final firstInput = bundle.ins.isNotEmpty ? bundle.ins.first : null;
|
||||
final isHogEx =
|
||||
firstInput != null && isHogExTx(bundle.originalTransaction) && isHogExTx(firstInput);
|
||||
|
||||
final fee = inputAmount - totalOutAmount;
|
||||
final fee = hasMissingInputTx ? null : inputAmount - totalOutAmount;
|
||||
final walletCurrency = walletTypeToCryptoCurrency(type);
|
||||
final feeMoney = fee != null ? Money.fromInt(fee, walletCurrency) : null;
|
||||
return ElectrumTransactionInfo(type,
|
||||
id: bundle.originalTransaction.txId(),
|
||||
height: height,
|
||||
@@ -192,11 +204,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
isReplaced: false,
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee,
|
||||
fee: feeMoney,
|
||||
direction: direction,
|
||||
amount: amount,
|
||||
amount: Money.fromInt(amount, walletCurrency),
|
||||
date: date,
|
||||
isHogEx: isHogEx,
|
||||
additionalInfo: {'hasMissingInputTx': hasMissingInputTx},
|
||||
confirmations: bundle.confirmations);
|
||||
}
|
||||
|
||||
@@ -204,18 +217,23 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
final inputAddresses = data['inputAddresses'] as List<dynamic>? ?? [];
|
||||
final outputAddresses = data['outputAddresses'] as List<dynamic>? ?? [];
|
||||
final unspents = data['unspents'] as List<dynamic>? ?? [];
|
||||
final additionalInfo = data['additionalInfo'] as Map<String, dynamic>?;
|
||||
final isLightning = (additionalInfo?['isLightning'] as bool?) == true;
|
||||
|
||||
final currency = isLightning ? CryptoCurrency.btcln : walletTypeToCryptoCurrency(type);
|
||||
|
||||
return ElectrumTransactionInfo(
|
||||
type,
|
||||
id: data['id'] as String,
|
||||
height: data['height'] as int?,
|
||||
amount: data['amount'] as int,
|
||||
fee: data['fee'] as int,
|
||||
amount: Money.fromInt(data['amount'] as int, currency),
|
||||
fee: data['fee'] != null ? Money.fromInt(data['fee'] as int, currency) : null,
|
||||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
isReplaced: data['isReplaced'] as bool? ?? false,
|
||||
confirmations: data['confirmations'] as int,
|
||||
accountIndex: data['accountIndex'] as int?,
|
||||
inputAddresses:
|
||||
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
|
||||
outputAddresses:
|
||||
@@ -226,29 +244,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
|
||||
.toList(),
|
||||
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
|
||||
additionalInfo: data['additionalInfo'] as Map<String, dynamic>?,
|
||||
additionalInfo: additionalInfo,
|
||||
);
|
||||
}
|
||||
|
||||
final WalletType type;
|
||||
|
||||
String? _fiatAmount;
|
||||
|
||||
@override
|
||||
String amountFormatted() =>
|
||||
'${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(type).title}';
|
||||
|
||||
@override
|
||||
String? feeFormatted() => fee != null
|
||||
? '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(type).title}'
|
||||
: '';
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
ElectrumTransactionInfo updated(ElectrumTransactionInfo info) {
|
||||
return ElectrumTransactionInfo(info.type,
|
||||
id: id,
|
||||
@@ -262,6 +263,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
inputAddresses: inputAddresses,
|
||||
outputAddresses: outputAddresses,
|
||||
confirmations: info.confirmations,
|
||||
accountIndex: accountIndex,
|
||||
additionalInfo: additionalInfo);
|
||||
}
|
||||
|
||||
@@ -269,13 +271,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
final m = <String, dynamic>{};
|
||||
m['id'] = id;
|
||||
m['height'] = height;
|
||||
m['amount'] = amount;
|
||||
m['amount'] = amount.amount.toInt();
|
||||
m['direction'] = direction.index;
|
||||
m['date'] = date.millisecondsSinceEpoch;
|
||||
m['isPending'] = isPending;
|
||||
m['isReplaced'] = isReplaced;
|
||||
m['confirmations'] = confirmations;
|
||||
m['fee'] = fee;
|
||||
m['accountIndex'] = accountIndex;
|
||||
m['fee'] = fee?.amount.toInt();
|
||||
m['to'] = to;
|
||||
m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? [];
|
||||
m['inputAddresses'] = inputAddresses;
|
||||
@@ -286,6 +289,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||
}
|
||||
|
||||
String toString() {
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)';
|
||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, accountIndex: $accountIndex, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)';
|
||||
}
|
||||
}
|
||||
|
||||
+1449
-552
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import 'dart:async' show Zone;
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math';
|
||||
import "package:collection/collection.dart";
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
@@ -20,6 +22,15 @@ import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'electrum_wallet_addresses.g.dart';
|
||||
|
||||
class UnsupportedAddressTypeForAccountException implements Exception {
|
||||
UnsupportedAddressTypeForAccountException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
abstract class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||
|
||||
const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
|
||||
@@ -30,6 +41,11 @@ const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
|
||||
P2shAddressType.p2wpkhInP2sh,
|
||||
];
|
||||
|
||||
const List<BitcoinAddressType> LEGACY_DUPLICATE_ADDRESS_TYPES = [
|
||||
SegwitAddresType.p2wpkh,
|
||||
SegwitAddresType.p2wsh,
|
||||
];
|
||||
|
||||
const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
|
||||
SegwitAddresType.p2wpkh,
|
||||
SegwitAddresType.mweb,
|
||||
@@ -43,11 +59,15 @@ const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
|
||||
P2pkhAddressType.p2pkh,
|
||||
];
|
||||
|
||||
const List<BitcoinAddressType> EXTRA_ACCOUNT_ADDRESS_TYPES = [SegwitAddresType.p2wpkh];
|
||||
|
||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
ElectrumWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required this.mainHdByType,
|
||||
required this.sideHdByType,
|
||||
required this.mainHdByTypeAndAccount,
|
||||
required this.sideHdByTypeAndAccount,
|
||||
required this.accountIndexes,
|
||||
required this.currentAccountIndex,
|
||||
required this.legacyMainHd,
|
||||
required this.legacySideHd,
|
||||
required this.network,
|
||||
@@ -72,12 +92,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
.toSet()),
|
||||
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
|
||||
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
|
||||
_addressPageType = initialAddressPageType ??
|
||||
(walletInfo.addressPageType != null
|
||||
? walletInfo.addressPageType == LightningAddressType.p2l.value
|
||||
? LightningAddressType.p2l
|
||||
: BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh),
|
||||
_addressPageType = _resolveInitialAddressPageType(
|
||||
initialAddressPageType: initialAddressPageType,
|
||||
walletInfo: walletInfo,
|
||||
mainHdByTypeAndAccount: mainHdByTypeAndAccount,
|
||||
currentAccountIndex: currentAccountIndex,
|
||||
),
|
||||
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
|
||||
(initialSilentAddresses ?? []).toSet()),
|
||||
currentSilentAddressIndex = initialSilentAddressIndex,
|
||||
@@ -142,12 +162,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
// TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
|
||||
final ObservableList<BitcoinAddressRecord> mwebAddresses;
|
||||
final BasedUtxoNetwork network;
|
||||
Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
|
||||
Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
|
||||
final Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> mainHdByTypeAndAccount;
|
||||
final Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> sideHdByTypeAndAccount;
|
||||
List<int> accountIndexes;
|
||||
|
||||
@observable
|
||||
int currentAccountIndex;
|
||||
final Bip32Slip10Secp256k1 legacyMainHd;
|
||||
final Bip32Slip10Secp256k1 legacySideHd;
|
||||
final bool isHardwareWallet;
|
||||
final LightningWallet? lightningWallet;
|
||||
LightningWallet? lightningWallet;
|
||||
|
||||
@observable
|
||||
ObservableMap<BitcoinAddressType, String> lockedReceiveAddressByType;
|
||||
@@ -170,9 +194,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
@computed
|
||||
List<BitcoinAddressRecord> get allAddresses => _addresses;
|
||||
|
||||
@observable
|
||||
bool addressRefreshToggle = false;
|
||||
|
||||
@action
|
||||
String getFreshAddress() {
|
||||
addressRefreshToggle = !addressRefreshToggle;
|
||||
return address;
|
||||
}
|
||||
|
||||
@override
|
||||
String get addressForExchange => getFreshAddress();
|
||||
|
||||
@override
|
||||
@computed
|
||||
String get address {
|
||||
final _ = addressRefreshToggle;
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp) {
|
||||
if (activeSilentAddress != null) {
|
||||
return activeSilentAddress!;
|
||||
@@ -186,8 +223,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
"Error: Unable to fetch your Lightning address, please check your network connection.";
|
||||
}
|
||||
|
||||
final typeMatchingAddressesAll =
|
||||
_addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
|
||||
final accountIndexForCheck = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
|
||||
if (!_isAddressTypeSupportedForAccount(addressPageType, accountIndexForCheck)) {
|
||||
printV("addressPageType $addressPageType is not supported for account $accountIndexForCheck");
|
||||
return "";
|
||||
}
|
||||
|
||||
final typeMatchingAddressesAll = _addresses
|
||||
.where((addr) =>
|
||||
_isCurrentAccountAddress(addr) && !addr.isHidden && _isAddressPageTypeMatch(addr))
|
||||
.toList();
|
||||
|
||||
// Prefer standard derivation addresses for the current/active address,
|
||||
// but keep legacy addresses present in the overall address lists.
|
||||
@@ -196,8 +241,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
...typeMatchingAddressesAll.where((a) => a.isLegacyDerivation),
|
||||
];
|
||||
|
||||
final typeMatchingReceiveAddressesAll =
|
||||
typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList();
|
||||
final typeMatchingReceiveAddressesAll = typeMatchingAddressesAll
|
||||
.where((addr) => !addr.isUsed && !hiddenAddresses.contains(addr.address))
|
||||
.toList();
|
||||
final typeMatchingReceiveAddresses = <BitcoinAddressRecord>[
|
||||
...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation),
|
||||
...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation),
|
||||
@@ -220,10 +266,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
}
|
||||
|
||||
final locked = lockedReceiveAddressByType[addressPageType];
|
||||
if (locked != null) return locked;
|
||||
if (locked != null && !hiddenAddresses.contains(locked)) return locked;
|
||||
|
||||
final prev = previousAddressRecord;
|
||||
if (prev != null && prev.type == addressPageType && !prev.isUsed && !prev.isLegacyDerivation) {
|
||||
if (prev != null &&
|
||||
_isCurrentAccountAddress(prev) &&
|
||||
prev.type == addressPageType &&
|
||||
!prev.isUsed &&
|
||||
!prev.isLegacyDerivation &&
|
||||
!hiddenAddresses.contains(prev.address)) {
|
||||
return prev.address;
|
||||
}
|
||||
|
||||
@@ -285,8 +336,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
return silentAddress?.toString() ?? '';
|
||||
}
|
||||
|
||||
final mainHd = mainHdByType[addressPageType] ?? mainHdByType.values.first;
|
||||
return getAddress(index: 0, hd: mainHd, addressType: addressPageType);
|
||||
return _firstAddressForType(addressPageType);
|
||||
}
|
||||
|
||||
String get payjoinCompatibleAddress {
|
||||
@@ -295,7 +345,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
? SegwitAddresType.p2wpkh
|
||||
: addressPageType;
|
||||
|
||||
final mainHd = mainHdByType[addrType] ?? mainHdByType.values.first;
|
||||
return _firstAddressForType(addrType);
|
||||
}
|
||||
|
||||
String _firstAddressForType(BitcoinAddressType addrType) {
|
||||
final mainMap = walletInfo.type == WalletType.bitcoin
|
||||
? mainHdByTypeAndAccount[currentAccountIndex]
|
||||
: mainHdByTypeAndAccount[0];
|
||||
|
||||
final mainHd = mainMap?[addrType] ?? mainMap?.values.first;
|
||||
|
||||
if (mainHd == null) return '';
|
||||
|
||||
return getAddress(index: 0, hd: mainHd, addressType: addrType);
|
||||
}
|
||||
|
||||
@@ -337,7 +398,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
Future<void> init({List<int> accountIndexes = const []}) async {
|
||||
final effectiveAccountIndexes =
|
||||
accountIndexes.isNotEmpty ? accountIndexes : this.accountIndexes;
|
||||
|
||||
if (accountIndexes.isNotEmpty) this.accountIndexes = accountIndexes;
|
||||
if (walletInfo.type == WalletType.bitcoinCash) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
} else if (walletInfo.type == WalletType.litecoin) {
|
||||
@@ -348,28 +413,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
} else if (walletInfo.type == WalletType.dogecoin) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
} else if (walletInfo.type == WalletType.bitcoin) {
|
||||
await _generateInitialAddresses(isLegacyDerivation: true);
|
||||
await _generateInitialAddresses();
|
||||
if (!isHardwareWallet) {
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh, isLegacyDerivation: true);
|
||||
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
|
||||
for (final accountIndex in effectiveAccountIndexes) {
|
||||
|
||||
await _generateInitialAddresses(
|
||||
type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
|
||||
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
|
||||
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2tr, isLegacyDerivation: true);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2tr);
|
||||
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh, isLegacyDerivation: true);
|
||||
await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
|
||||
await prepareAccountAddresses(
|
||||
accountIndex,
|
||||
types: accountIndex == 0 ? BITCOIN_ADDRESS_TYPES : EXTRA_ACCOUNT_ADDRESS_TYPES,
|
||||
includeLegacy: accountIndex == 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
_validateAddresses();
|
||||
await _validateAddresses();
|
||||
await updateAddressesInBox();
|
||||
|
||||
if (currentReceiveAddressIndex >= receiveAddresses.length) {
|
||||
@@ -389,9 +446,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
updateChangeAddresses();
|
||||
|
||||
if (changeAddresses.isEmpty) {
|
||||
final newAddresses = await _createNewAddresses(gap,
|
||||
startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
|
||||
isHidden: true);
|
||||
final accountChangeCount = _addresses
|
||||
.where((addressRecord) =>
|
||||
_isCurrentAccountAddress(addressRecord) &&
|
||||
addressRecord.isHidden &&
|
||||
addressRecord.type == addressPageType)
|
||||
.length;
|
||||
|
||||
final newAddresses = await _createNewAddresses(
|
||||
gap,
|
||||
startIndex: accountChangeCount,
|
||||
isHidden: true,
|
||||
type: addressPageType,
|
||||
accountIndex: currentAccountIndex,
|
||||
);
|
||||
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
|
||||
@@ -405,6 +474,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
return address;
|
||||
}
|
||||
|
||||
bool _isCurrentAccountAddress(BitcoinAddressRecord addressRecord) {
|
||||
return walletInfo.type != WalletType.bitcoin ||
|
||||
addressRecord.accountIndex == currentAccountIndex;
|
||||
}
|
||||
|
||||
Map<String, String> get labels {
|
||||
final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32));
|
||||
final labels = <String, String>{};
|
||||
@@ -422,8 +496,35 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
return labels;
|
||||
}
|
||||
|
||||
Future<void> prepareAccountAddresses(
|
||||
int accountIndex, {
|
||||
List<BitcoinAddressType> types = BITCOIN_ADDRESS_TYPES,
|
||||
bool includeLegacy = false,
|
||||
}) async {
|
||||
for (final type in types) {
|
||||
final shouldSkipHardwareWalletType = isHardwareWallet && type != SegwitAddresType.p2wpkh;
|
||||
|
||||
if (shouldSkipHardwareWalletType) continue;
|
||||
|
||||
await _generateInitialAddresses(accountIndex: accountIndex, type: type);
|
||||
|
||||
// Legacy derivation for these types is identical to the standard one.
|
||||
if (includeLegacy && !LEGACY_DUPLICATE_ADDRESS_TYPES.contains(type)) {
|
||||
await _generateInitialAddresses(
|
||||
accountIndex: accountIndex,
|
||||
type: type,
|
||||
isLegacyDerivation: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
BaseBitcoinAddressRecord generateNewAddress({String label = ''}) {
|
||||
if (addressPageType is LightningAddressType) {
|
||||
throw Exception("Lightning addresses cannot be rotated");
|
||||
}
|
||||
|
||||
if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) {
|
||||
final currentSilentAddressIndex = silentAddresses
|
||||
.where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr)
|
||||
@@ -449,14 +550,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
return address;
|
||||
}
|
||||
|
||||
final newAddressIndex = addressesByReceiveType.fold(
|
||||
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
|
||||
final accountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
|
||||
|
||||
final hd = _hdFor(isHidden: false, type: addressPageType, isLegacyDerivation: false);
|
||||
final newAddressIndex = addressesByReceiveType.fold(
|
||||
0,
|
||||
(int acc, addressRecord) => addressRecord is BitcoinAddressRecord &&
|
||||
addressRecord.accountIndex == accountIndex &&
|
||||
addressRecord.isHidden == false
|
||||
? acc + 1
|
||||
: acc);
|
||||
|
||||
final hd = _hdForAddressGeneration(
|
||||
isHidden: false,
|
||||
type: addressPageType,
|
||||
isLegacyDerivation: false,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: newAddressIndex, hd: hd, addressType: addressPageType),
|
||||
index: newAddressIndex,
|
||||
accountIndex: accountIndex,
|
||||
isHidden: false,
|
||||
isHiddenChecked: true,
|
||||
isLegacyDerivation: false,
|
||||
name: label,
|
||||
type: addressPageType,
|
||||
@@ -486,45 +601,54 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
void addBitcoinAddressTypes() {
|
||||
final lastP2wpkh = _addresses
|
||||
.where((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
|
||||
.toList()
|
||||
.last;
|
||||
if (lastP2wpkh.address != address) {
|
||||
addressesMap[lastP2wpkh.address] = 'P2WPKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WPKH';
|
||||
_isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
|
||||
.lastOrNull;
|
||||
if (lastP2wpkh != null) {
|
||||
if (lastP2wpkh.address != address) {
|
||||
addressesMap[lastP2wpkh.address] = 'P2WPKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WPKH';
|
||||
}
|
||||
}
|
||||
|
||||
final lastP2pkh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
|
||||
if (lastP2pkh.address != address) {
|
||||
addressesMap[lastP2pkh.address] = 'P2PKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2PKH';
|
||||
final lastP2pkh = _addresses.firstWhereOrNull(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
|
||||
if (lastP2pkh != null) {
|
||||
if (lastP2pkh.address != address) {
|
||||
addressesMap[lastP2pkh.address] = 'P2PKH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2PKH';
|
||||
}
|
||||
}
|
||||
|
||||
final lastP2sh = _addresses.firstWhere((addressRecord) =>
|
||||
final lastP2sh = _addresses.firstWhereOrNull((addressRecord) =>
|
||||
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
|
||||
if (lastP2sh.address != address) {
|
||||
addressesMap[lastP2sh.address] = 'P2SH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2SH';
|
||||
if (lastP2sh != null) {
|
||||
if (lastP2sh.address != address) {
|
||||
addressesMap[lastP2sh.address] = 'P2SH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2SH';
|
||||
}
|
||||
}
|
||||
|
||||
final lastP2tr = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
|
||||
if (lastP2tr.address != address) {
|
||||
addressesMap[lastP2tr.address] = 'P2TR';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2TR';
|
||||
final lastP2tr = _addresses.firstWhereOrNull(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
|
||||
if (lastP2tr != null) {
|
||||
if (lastP2tr.address != address) {
|
||||
addressesMap[lastP2tr.address] = 'P2TR';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2TR';
|
||||
}
|
||||
}
|
||||
|
||||
final lastP2wsh = _addresses.firstWhere(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
|
||||
if (lastP2wsh.address != address) {
|
||||
addressesMap[lastP2wsh.address] = 'P2WSH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WSH';
|
||||
final lastP2wsh = _addresses.firstWhereOrNull(
|
||||
(addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
|
||||
if (lastP2wsh != null) {
|
||||
if (lastP2wsh.address != address) {
|
||||
addressesMap[lastP2wsh.address] = 'P2WSH';
|
||||
} else {
|
||||
addressesMap[address] = 'Active - P2WSH';
|
||||
}
|
||||
}
|
||||
|
||||
final firstSilentAddressRecord = silentAddresses.firstOrNull;
|
||||
@@ -656,15 +780,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
|
||||
addressesByReceiveType.clear();
|
||||
addressesByReceiveType.addAll(
|
||||
_addresses.where(_isAddressPageTypeMatch).toList(),
|
||||
_addresses
|
||||
.where((addressRecord) =>
|
||||
_isCurrentAccountAddress(addressRecord) && _isAddressPageTypeMatch(addressRecord))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
void updateReceiveAddresses() {
|
||||
receiveAddresses.removeRange(0, receiveAddresses.length);
|
||||
final newAddresses =
|
||||
_addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
|
||||
final newAddresses = _addresses.where((addressRecord) =>
|
||||
_isCurrentAccountAddress(addressRecord) &&
|
||||
!addressRecord.isHidden &&
|
||||
!addressRecord.isUsed);
|
||||
receiveAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
@@ -672,6 +801,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
void updateChangeAddresses() {
|
||||
changeAddresses.removeRange(0, changeAddresses.length);
|
||||
final newAddresses = _addresses.where((addressRecord) =>
|
||||
_isCurrentAccountAddress(addressRecord) &&
|
||||
addressRecord.isHidden &&
|
||||
!addressRecord.isUsed &&
|
||||
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
|
||||
@@ -686,6 +816,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
Future<String?> Function(BitcoinAddressRecord) getAddressHistory, {
|
||||
BitcoinAddressType type = SegwitAddresType.p2wpkh,
|
||||
required bool isLegacyDerivation,
|
||||
int accountIndex = 0,
|
||||
}) async {
|
||||
final newAddresses = await _createNewAddresses(
|
||||
gap,
|
||||
@@ -693,6 +824,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
isHidden: isHidden,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
type: type,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
|
||||
addAddresses(newAddresses);
|
||||
@@ -708,58 +840,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
getAddressHistory,
|
||||
type: type,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<bool> discoverAddressesBatch(
|
||||
Future<List<BitcoinAddressRecord>> discoverAddressesBatch(
|
||||
List<BitcoinAddressRecord> addressList,
|
||||
bool isHidden,
|
||||
Future<Set<String>> Function(List<BitcoinAddressRecord>) getUsedAddresses, {
|
||||
BitcoinAddressType type = SegwitAddresType.p2wpkh,
|
||||
required bool isLegacyDerivation,
|
||||
int accountIndex = 0,
|
||||
}) async {
|
||||
final newAddresses = await _createNewAddresses(
|
||||
gap,
|
||||
startIndex: addressList.length,
|
||||
isHidden: isHidden,
|
||||
type: type,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
addAddresses(newAddresses);
|
||||
|
||||
final usedAddresses = await getUsedAddresses(newAddresses);
|
||||
final isLastAddressUsed =
|
||||
newAddresses.isNotEmpty && usedAddresses.contains(newAddresses.last.address);
|
||||
|
||||
if (!isLastAddressUsed) {
|
||||
return false;
|
||||
final hasUsedAddressInGap =
|
||||
newAddresses.any((addressRecord) => usedAddresses.contains(addressRecord.address));
|
||||
|
||||
if (!hasUsedAddressInGap) {
|
||||
return newAddresses;
|
||||
}
|
||||
|
||||
final updatedAddressList = [...addressList, ...newAddresses];
|
||||
|
||||
await discoverAddressesBatch(
|
||||
final moreNewAddresses = await discoverAddressesBatch(
|
||||
updatedAddressList,
|
||||
isHidden,
|
||||
getUsedAddresses,
|
||||
type: type,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
|
||||
return true;
|
||||
return [...newAddresses, ...moreNewAddresses];
|
||||
}
|
||||
|
||||
Future<void> _generateInitialAddresses(
|
||||
{BitcoinAddressType type = SegwitAddresType.p2wpkh, bool isLegacyDerivation = false}) async {
|
||||
// Legacy derivation produces the same addresses as standard for these types.
|
||||
// Don't generate a legacy set to avoid duplicates.
|
||||
if (isLegacyDerivation && (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh)) {
|
||||
return;
|
||||
}
|
||||
Future<void> _generateInitialAddresses({
|
||||
BitcoinAddressType type = SegwitAddresType.p2wpkh,
|
||||
bool isLegacyDerivation = false,
|
||||
int accountIndex = 0,
|
||||
}) async {
|
||||
if (isLegacyDerivation && accountIndex != 0) return;
|
||||
|
||||
var countOfReceiveAddresses = 0;
|
||||
var countOfHiddenAddresses = 0;
|
||||
|
||||
_addresses.forEach((addr) {
|
||||
if (addr.type == type && addr.isLegacyDerivation == isLegacyDerivation) {
|
||||
if (addr.accountIndex == accountIndex &&
|
||||
addr.type == type &&
|
||||
addr.isLegacyDerivation == isLegacyDerivation) {
|
||||
if (addr.isHidden) {
|
||||
countOfHiddenAddresses += 1;
|
||||
} else {
|
||||
@@ -774,7 +915,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
startIndex: countOfReceiveAddresses,
|
||||
isHidden: false,
|
||||
type: type,
|
||||
isLegacyDerivation: isLegacyDerivation);
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex);
|
||||
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
|
||||
@@ -784,31 +927,49 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
startIndex: countOfHiddenAddresses,
|
||||
isHidden: true,
|
||||
type: type,
|
||||
isLegacyDerivation: isLegacyDerivation);
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex);
|
||||
addAddresses(newAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
|
||||
{int startIndex = 0,
|
||||
bool isHidden = false,
|
||||
BitcoinAddressType? type,
|
||||
bool isLegacyDerivation = false}) async {
|
||||
Future<List<BitcoinAddressRecord>> _createNewAddresses(
|
||||
int count, {
|
||||
int startIndex = 0,
|
||||
bool isHidden = false,
|
||||
BitcoinAddressType? type,
|
||||
bool isLegacyDerivation = false,
|
||||
int accountIndex = 0,
|
||||
}) async {
|
||||
final list = <BitcoinAddressRecord>[];
|
||||
|
||||
for (var i = startIndex; i < count + startIndex; i++) {
|
||||
final addrType = type ?? addressPageType;
|
||||
final hd = _hdFor(isHidden: isHidden, type: addrType, isLegacyDerivation: isLegacyDerivation);
|
||||
|
||||
final address = BitcoinAddressRecord(
|
||||
await getAddressAsync(index: i, hd: hd, addressType: addrType),
|
||||
index: i,
|
||||
isHidden: isHidden,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
type: addrType,
|
||||
network: network,
|
||||
);
|
||||
list.add(address);
|
||||
try {
|
||||
final hd = _hdForAddressGeneration(
|
||||
isHidden: isHidden,
|
||||
type: addrType,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
|
||||
final address = BitcoinAddressRecord(
|
||||
await getAddressAsync(index: i, hd: hd, addressType: addrType),
|
||||
index: i,
|
||||
isHidden: isHidden,
|
||||
isHiddenChecked: true,
|
||||
isLegacyDerivation: isLegacyDerivation,
|
||||
type: addrType,
|
||||
network: network,
|
||||
accountIndex: accountIndex,
|
||||
);
|
||||
list.add(address);
|
||||
} on UnsupportedAddressTypeForAccountException catch (e) {
|
||||
printV("_createNewAddresses: skipping index $i, type $addrType, "
|
||||
"account $accountIndex: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
@@ -821,6 +982,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
this._addresses.clear();
|
||||
this._addresses.addAll(addressesSet);
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -841,27 +1004,52 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
void _validateAddresses() {
|
||||
_addresses.forEach((element) async {
|
||||
if (element.type == SegwitAddresType.mweb) {
|
||||
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
|
||||
return;
|
||||
static const _validationTimeSlice = Duration(milliseconds: 16);
|
||||
|
||||
Future<void> _validateAddresses() async {
|
||||
final addresses = _addresses.toList();
|
||||
final slice = Stopwatch()..start();
|
||||
|
||||
for (final element in addresses) {
|
||||
try {
|
||||
await _validateAddress(element);
|
||||
} catch (e, s) {
|
||||
Zone.current.handleUncaughtError(e, s);
|
||||
}
|
||||
|
||||
final mainHd = _hdFor(
|
||||
isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
|
||||
final sideHd = _hdFor(
|
||||
isHidden: true, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
|
||||
if (!element.isHidden &&
|
||||
element.address !=
|
||||
await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
|
||||
element.isHidden = true;
|
||||
} else if (element.isHidden &&
|
||||
element.address !=
|
||||
await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
|
||||
element.isHidden = false;
|
||||
if (slice.elapsed >= _validationTimeSlice) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
slice.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _validateAddress(BitcoinAddressRecord element) async {
|
||||
if (element.isHiddenChecked) return;
|
||||
|
||||
if (element.type == SegwitAddresType.mweb) {
|
||||
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Relabel only when the address re-derives from the other chain. A record from a path these
|
||||
// keys don't produce matches neither chain, so it keeps its label instead of flipping.
|
||||
final otherChainHd = _hdForAddressGeneration(
|
||||
isHidden: !element.isHidden,
|
||||
type: element.type,
|
||||
isLegacyDerivation: element.isLegacyDerivation,
|
||||
accountIndex: element.accountIndex,
|
||||
);
|
||||
final otherChainAddress =
|
||||
await getAddressAsync(index: element.index, hd: otherChainHd, addressType: element.type);
|
||||
if (element.address == otherChainAddress) {
|
||||
element.isHidden = !element.isHidden;
|
||||
}
|
||||
element.isHiddenChecked = true;
|
||||
} on UnsupportedAddressTypeForAccountException catch (e) {
|
||||
printV("_validateAddresses: skipping ${element.address}: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -869,7 +1057,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
|
||||
@action
|
||||
Future<void> setAddressType(BitcoinAddressType type) async {
|
||||
_addressPageType = type;
|
||||
|
||||
final needsAccountScopedHd =
|
||||
type is! LightningAddressType && type != SilentPaymentsAddresType.p2sp;
|
||||
|
||||
BitcoinAddressType resolvedType = type;
|
||||
if (needsAccountScopedHd) {
|
||||
final accountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
|
||||
if (!_isAddressTypeSupportedForAccount(type, accountIndex)) {
|
||||
resolvedType = EXTRA_ACCOUNT_ADDRESS_TYPES.first;
|
||||
}
|
||||
}
|
||||
|
||||
_addressPageType = resolvedType;
|
||||
updateAddressesByMatch();
|
||||
walletInfo.addressPageType = addressPageType.toString();
|
||||
await walletInfo.save();
|
||||
@@ -881,8 +1081,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
|
||||
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
|
||||
|
||||
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
|
||||
!addr.isHidden && !addr.isUsed && addr.type == type;
|
||||
bool _isUnusedReceiveAddressByType(
|
||||
BitcoinAddressRecord addressRecord,
|
||||
BitcoinAddressType type,
|
||||
) {
|
||||
return _isCurrentAccountAddress(addressRecord) &&
|
||||
!addressRecord.isHidden &&
|
||||
!addressRecord.isUsed &&
|
||||
addressRecord.type == type;
|
||||
}
|
||||
|
||||
@action
|
||||
void deleteSilentPaymentAddress(String address) {
|
||||
@@ -893,16 +1100,84 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
updateAddressesByMatch();
|
||||
}
|
||||
|
||||
Bip32Slip10Secp256k1 _hdFor({
|
||||
// Remove all addresses associated with a specific account index.
|
||||
@action
|
||||
Future<void> removeAddressesForAccount(int accountIndex) async {
|
||||
_addresses.removeWhere((addr) => addr.accountIndex == accountIndex);
|
||||
updateAddressesByMatch();
|
||||
updateReceiveAddresses();
|
||||
updateChangeAddresses();
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
|
||||
|
||||
bool _isAddressTypeSupportedForAccount(BitcoinAddressType type, int accountIndex) {
|
||||
final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? accountIndex : 0;
|
||||
return mainHdByTypeAndAccount[effectiveAccountIndex]?.containsKey(type) ?? false;
|
||||
}
|
||||
|
||||
static BitcoinAddressType _resolveInitialAddressPageType({
|
||||
required BitcoinAddressType? initialAddressPageType,
|
||||
required WalletInfo walletInfo,
|
||||
required Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> mainHdByTypeAndAccount,
|
||||
required int currentAccountIndex,
|
||||
}) {
|
||||
final resolved = initialAddressPageType ??
|
||||
(walletInfo.addressPageType != null
|
||||
? walletInfo.addressPageType == LightningAddressType.p2l.value
|
||||
? LightningAddressType.p2l
|
||||
: BitcoinAddressType.fromValue(walletInfo.addressPageType!)
|
||||
: SegwitAddresType.p2wpkh);
|
||||
|
||||
// Non-account-scoped address types (Lightning, Silent Payments) don't need
|
||||
// an HD map entry.
|
||||
if (resolved is LightningAddressType || resolved == SilentPaymentsAddresType.p2sp) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
|
||||
|
||||
final isValid = mainHdByTypeAndAccount[effectiveAccountIndex]?.containsKey(resolved) ?? false;
|
||||
|
||||
if (!isValid) {
|
||||
return EXTRA_ACCOUNT_ADDRESS_TYPES.first;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
Bip32Slip10Secp256k1 _hdForAddressGeneration({
|
||||
required bool isHidden,
|
||||
required BitcoinAddressType type,
|
||||
required bool isLegacyDerivation,
|
||||
int accountIndex = 0,
|
||||
}) {
|
||||
if (isLegacyDerivation) return isHidden ? legacySideHd : legacyMainHd;
|
||||
if (isLegacyDerivation) {
|
||||
if (accountIndex != 0) {
|
||||
throw UnsupportedAddressTypeForAccountException(
|
||||
"Legacy derivation is only supported for the first account");
|
||||
}
|
||||
|
||||
return isHidden ? legacySideHd : legacyMainHd;
|
||||
}
|
||||
|
||||
final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? accountIndex : 0;
|
||||
|
||||
final map = isHidden
|
||||
? sideHdByTypeAndAccount[effectiveAccountIndex]
|
||||
: mainHdByTypeAndAccount[effectiveAccountIndex];
|
||||
|
||||
if (map == null) {
|
||||
throw UnsupportedAddressTypeForAccountException(
|
||||
"HD map not found for account $effectiveAccountIndex");
|
||||
}
|
||||
|
||||
final map = isHidden ? sideHdByType : mainHdByType;
|
||||
final hd = map[type];
|
||||
if (hd == null) throw Exception("HD not found for type $type");
|
||||
if (hd == null) {
|
||||
throw UnsupportedAddressTypeForAccountException(
|
||||
"HD not found for account $accountIndex type $type");
|
||||
}
|
||||
|
||||
return hd;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:convert';
|
||||
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/amount/money.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_bitcoin/electrum_derivations.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
@@ -92,9 +94,15 @@ class ElectrumWalletSnapshot {
|
||||
final useLightning = data['useLightning'] as bool? ?? true;
|
||||
final cachedLightningAddress = data['cachedLightningAddress'] as String?;
|
||||
|
||||
final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
|
||||
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
|
||||
final lightningBalance = ElectrumBalance.fromJSON(data['lightningBalance'] as String?);
|
||||
final currency = walletTypeToCryptoCurrency(type);
|
||||
final balance = ElectrumBalance.fromJSON(data['balance'] as String?, currency) ??
|
||||
ElectrumBalance(
|
||||
confirmed: Money.zero(currency),
|
||||
unconfirmed: Money.zero(currency),
|
||||
frozen: Money.zero(currency),
|
||||
);
|
||||
final lightningBalance =
|
||||
ElectrumBalance.fromJSON(data['lightningBalance'] as String?, currency);
|
||||
|
||||
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
|
||||
|
||||
@@ -106,11 +106,11 @@ class LitecoinBitboxService extends HardwareWalletService
|
||||
|
||||
@override
|
||||
Future<String> signLitecoinTransaction({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<PSBTReadyBitcoinOutput> outputs,
|
||||
required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
||||
}) async {
|
||||
final psbt = PSBTTransactionBuild(inputs: inputs, outputs: outputs, cwOutputs: []).psbt;
|
||||
final psbt = PSBTTransactionBuild(inputs: inputs, outputs: outputs).psbt;
|
||||
|
||||
final signedPsbt = await manager.signBTCPsbt(bitboxCoinType, base64Encode(psbt.asPsbtV0()));
|
||||
final transactionRes = PsbtV2()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:typed_data';
|
||||
import "dart:typed_data";
|
||||
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
|
||||
import "package:cw_bitcoin/electrum_wallet.dart";
|
||||
import "package:cw_bitcoin/psbt/transaction_builder.dart";
|
||||
|
||||
mixin BitcoinHardwareWalletService {
|
||||
Future<Uint8List> getMasterFingerprint() async => Uint8List.fromList([0, 0, 0, 0]);
|
||||
@@ -10,7 +9,7 @@ mixin BitcoinHardwareWalletService {
|
||||
|
||||
mixin LitecoinHardwareWalletService on BitcoinHardwareWalletService {
|
||||
Future<String> signLitecoinTransaction({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<PSBTReadyBitcoinOutput> outputs,
|
||||
required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
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/psbt/transaction_builder.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';
|
||||
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/psbt/transaction_builder.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
|
||||
with BitcoinHardwareWalletService, LitecoinHardwareWalletService {
|
||||
@@ -31,18 +31,21 @@ class LitecoinLedgerService extends HardwareWalletService
|
||||
for (final i in indexRange) {
|
||||
final derivationPath = "m/84'/2'/$i'";
|
||||
final xpub = await litecoinLedgerApp.getXPubKey(
|
||||
accountsDerivationPath: derivationPath,
|
||||
xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16));
|
||||
accountsDerivationPath: derivationPath,
|
||||
xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16),
|
||||
);
|
||||
final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0));
|
||||
|
||||
final address = generateP2WPKHAddress(hd: hd, index: 0, network: LitecoinNetwork.mainnet);
|
||||
|
||||
accounts.add(HardwareAccountData(
|
||||
address: address,
|
||||
accountIndex: i,
|
||||
derivationPath: derivationPath,
|
||||
xpub: xpub,
|
||||
));
|
||||
accounts.add(
|
||||
HardwareAccountData(
|
||||
address: address,
|
||||
accountIndex: i,
|
||||
derivationPath: derivationPath,
|
||||
xpub: xpub,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return accounts;
|
||||
@@ -50,7 +53,7 @@ class LitecoinLedgerService extends HardwareWalletService
|
||||
|
||||
@override
|
||||
Future<String> signLitecoinTransaction({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<PSBTReadyBitcoinOutput> outputs,
|
||||
required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
||||
}) {
|
||||
@@ -58,31 +61,39 @@ class LitecoinLedgerService extends HardwareWalletService
|
||||
for (final utxo in inputs) {
|
||||
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
|
||||
|
||||
readyInputs.add(LedgerTransaction(
|
||||
rawTx: utxo.rawTx,
|
||||
outputIndex: utxo.utxo.vout,
|
||||
ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)),
|
||||
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
||||
));
|
||||
readyInputs.add(
|
||||
LedgerTransaction(
|
||||
rawTx: utxo.rawTx,
|
||||
outputIndex: utxo.utxo.vout,
|
||||
ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)),
|
||||
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we have the key to one of the output addresses to hide change on the device
|
||||
String? changePath;
|
||||
for (final output in outputs) {
|
||||
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
|
||||
if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath;
|
||||
if (output.isChange) {
|
||||
changePath ??= output.changeDerivationPath;
|
||||
}
|
||||
}
|
||||
|
||||
return 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);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/psbt/transaction_builder.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:trezor_connect/trezor_connect.dart';
|
||||
|
||||
class LitecoinTrezorService extends HardwareWalletService
|
||||
with BitcoinHardwareWalletService, 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<PSBTReadyBitcoinOutput> outputs,
|
||||
required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
||||
}) async {
|
||||
final readyInputs = inputs
|
||||
.map((input) => TrezorTxInput(
|
||||
prevHash: input.utxo.txHash,
|
||||
prevIndex: input.utxo.vout,
|
||||
amount: input.utxo.value.toInt(),
|
||||
addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
|
||||
scriptType: "SPENDWITNESS",
|
||||
))
|
||||
.toList();
|
||||
|
||||
final readyOutputs = outputs.map((output) {
|
||||
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
|
||||
|
||||
return 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,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,178 @@
|
||||
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/psbt/transaction_builder.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:flutter/foundation.dart';
|
||||
import 'package:ledger_bitcoin/psbt.dart';
|
||||
import 'package:trezor_connect/trezor_connect.dart';
|
||||
import 'package:ledger_bitcoin/src/utils/uint8list_extension.dart' as ext;
|
||||
import 'package:trezor_flutter/trezor_flutter.dart' as sdk;
|
||||
|
||||
class BitcoinTrezorService extends HardwareWalletService with BitcoinHardwareWalletService {
|
||||
BitcoinTrezorService(this.connect);
|
||||
BitcoinTrezorService(this.client) : _trezorBitcoin = sdk.TrezorBitcoin(client);
|
||||
|
||||
final TrezorConnect connect;
|
||||
final sdk.TrezorClient client;
|
||||
final sdk.TrezorBitcoin _trezorBitcoin;
|
||||
|
||||
@override
|
||||
Future<Uint8List> getMasterFingerprint() async {
|
||||
final publicKey = await _trezorBitcoin.getPublicKey(derivationPath: "m/84'/0'/0'");
|
||||
final fingerprintBuffer = ByteData(4)..setUint32(0, publicKey.$2);
|
||||
return fingerprintBuffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
|
||||
final accounts = <HardwareAccountData>[];
|
||||
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 derivationPath = "m/84'/0'/$i'";
|
||||
final xPub =
|
||||
await _trezorBitcoin.getPublicKey(derivationPath: derivationPath, ignoreXpubMagic: true);
|
||||
final hd = Bip32Slip10Secp256k1.fromExtendedKey(xPub.$1).childKey(Bip32KeyIndex(0));
|
||||
|
||||
final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
|
||||
|
||||
accounts.add(
|
||||
HardwareAccountData(
|
||||
address: address,
|
||||
accountIndex: i,
|
||||
derivationPath: derivationPath,
|
||||
xpub: xPub.$1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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() ??
|
||||
[];
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> signTransaction({required String transaction}) async {
|
||||
final psbt = PsbtV2()..deserialize(base64Decode(transaction));
|
||||
final masterFingerprint = await getMasterFingerprint();
|
||||
|
||||
final inputs = <sdk.TrezorTxInput>[];
|
||||
final prevTxs = <String, sdk.TrezorPrevTx>{};
|
||||
|
||||
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 rawPrevTx = psbt.getInputNonWitnessUtxo(i);
|
||||
if (rawPrevTx == null) {
|
||||
throw StateError("PSBT input $i is missing its previous transaction");
|
||||
}
|
||||
final prevTx = BtcTransaction.fromRaw(hex.encode(rawPrevTx));
|
||||
final vout = psbt.getInputOutputIndex(i);
|
||||
|
||||
final publicKeys = psbt.inputMaps[i].keys.where((e) => e.startsWith("06"));
|
||||
final pubkey = Uint8List.fromList(hex.decode(publicKeys.first.substring(2)));
|
||||
final txidBytes = psbt.getInputPreviousTxid(i).reversed.toList();
|
||||
|
||||
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,
|
||||
final pubkey = _firstDerivationPubkey(psbt.inputMaps[i], "06");
|
||||
if (pubkey == null) {
|
||||
throw StateError("PSBT input $i is missing its BIP32 derivation");
|
||||
}
|
||||
|
||||
final derivation = psbt.getInputBip32Derivation(i, pubkey)!;
|
||||
final path = derivation.$2;
|
||||
|
||||
if (!listEquals(derivation.$1, masterFingerprint)) {
|
||||
throw Exception("Fingerprint missmatch with the PSBT");
|
||||
}
|
||||
|
||||
inputs.add(
|
||||
sdk.TrezorTxInput(
|
||||
addressPath: path,
|
||||
prevHash: txidBytes,
|
||||
prevIndex: vout,
|
||||
amount: prevTx.outputs[vout].amount.toInt(),
|
||||
sequence: psbt.getInputSequence(i),
|
||||
scriptType: "SPENDWITNESS"));
|
||||
scriptType: "SPENDWITNESS",
|
||||
),
|
||||
);
|
||||
|
||||
prevTxs[hex.encode(txidBytes)] = sdk.TrezorPrevTx(
|
||||
meta: sdk.TrezorPrevTxMeta(
|
||||
version: Uint8List.fromList(prevTx.version).readUint32LE(0),
|
||||
lockTime: Uint8List.fromList(prevTx.locktime).readUint32LE(0),
|
||||
inputsCount: prevTx.inputs.length,
|
||||
outputsCount: prevTx.outputs.length,
|
||||
),
|
||||
inputs: prevTx.inputs
|
||||
.map(
|
||||
(e) => sdk.TrezorPrevInput(
|
||||
prevHash: hex.decode(e.txId),
|
||||
prevIndex: e.txIndex,
|
||||
scriptSig: e.scriptSig.toBytes(),
|
||||
sequence: Uint8List.fromList(e.sequence).readUint32LE(0),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
outputs: prevTx.outputs
|
||||
.map(
|
||||
(e) => sdk.TrezorPrevOutput(
|
||||
amount: e.amount.toInt(),
|
||||
scriptPubkey: e.scriptPubKey.toBytes(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
final outputs = <TrezorTxOutput>[];
|
||||
final outputs = <sdk.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 amount = psbt.getOutputAmount(i);
|
||||
|
||||
// An output carrying our own BIP32 derivation is change: identify it
|
||||
// by path so the device verifies it internally instead of displaying
|
||||
// it as a payment.
|
||||
final changePubkey = _firstDerivationPubkey(psbt.outputMaps[i], "02");
|
||||
if (changePubkey != null) {
|
||||
final (fingerprint, path) = psbt.getOutputBip32Derivation(i, changePubkey);
|
||||
if (listEquals(fingerprint, masterFingerprint)) {
|
||||
outputs.add(sdk.TrezorTxOutput(
|
||||
addressPath: path,
|
||||
amount: amount,
|
||||
scriptType: "PAYTOWITNESS",
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final script = Script.fromRaw(byteData: psbt.getOutputScript(i));
|
||||
outputs.add(sdk.TrezorTxOutput(
|
||||
address: script.toAddress(),
|
||||
amount: amount,
|
||||
scriptType: "PAYTOADDRESS",
|
||||
));
|
||||
}
|
||||
|
||||
final signedTx = await connect.signTransaction(coin: 'btc', inputs: inputs, outputs: outputs);
|
||||
final signed = await _trezorBitcoin.signTransaction(
|
||||
inputs: inputs,
|
||||
outputs: outputs,
|
||||
prevTxs: prevTxs,
|
||||
version: psbt.getGlobalTxVersion(),
|
||||
lockTime: psbt.getGlobalFallbackLocktime() ?? 0,
|
||||
);
|
||||
|
||||
return Uint8List.fromList(BytesUtils.fromHexString(signedTx!.serializedTx));
|
||||
return signed.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);
|
||||
}
|
||||
}
|
||||
|
||||
class LitecoinTrezorService extends HardwareWalletService
|
||||
with BitcoinHardwareWalletService, 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<PSBTReadyUtxoWithAddress> inputs,
|
||||
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
||||
}) async {
|
||||
final readyInputs = inputs
|
||||
.map((input) => TrezorTxInput(
|
||||
prevHash: input.utxo.txHash,
|
||||
prevIndex: input.utxo.vout,
|
||||
amount: input.utxo.value.toInt(),
|
||||
addressPath: Bip32PathParser.parse(input.ownerDerivationPath).toList(),
|
||||
scriptType: "SPENDWITNESS",
|
||||
))
|
||||
.toList();
|
||||
|
||||
final readyOutputs = outputs.map((output) {
|
||||
final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()];
|
||||
|
||||
return 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,
|
||||
Future<Uint8List> signMessage({required Uint8List message, String? derivationPath}) =>
|
||||
_trezorBitcoin.signMessage(
|
||||
derivationPath: derivationPath ?? "m/84'/0'/0'/0/0",
|
||||
message: message,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
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");
|
||||
/// Extracts the pubkey from the first BIP32-derivation record
|
||||
/// ([keyTypeHex] `06` for inputs, `02` for outputs) of a PSBT key-value map.
|
||||
Uint8List? _firstDerivationPubkey(Map<String, Uint8List> map, String keyTypeHex) {
|
||||
final key = map.keys.where((mapKey) => mapKey.startsWith(keyTypeHex)).firstOrNull;
|
||||
if (key == null) return null;
|
||||
return Uint8List.fromList(hex.decode(key.substring(2)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
@@ -14,30 +17,34 @@ bool _breezSdkSparkLibUninitialized = true;
|
||||
Stream<LogEntry>? _logStream;
|
||||
|
||||
class LightningWallet {
|
||||
LightningWallet({
|
||||
required this.mnemonic,
|
||||
required this.apiKey,
|
||||
required this.lnurlDomain,
|
||||
this.network = Network.mainnet,
|
||||
this.passphrase,
|
||||
this.seedBytes,
|
||||
this.cachedAddress,
|
||||
});
|
||||
|
||||
final String mnemonic;
|
||||
final String? passphrase;
|
||||
final Uint8List? seedBytes;
|
||||
final String apiKey;
|
||||
final String lnurlDomain;
|
||||
final Network network;
|
||||
late BreezSdk sdk;
|
||||
BreezSdk? _sdk;
|
||||
|
||||
String? cachedAddress;
|
||||
|
||||
static int MAX_RETRIES = 10;
|
||||
|
||||
LightningWallet({
|
||||
required this.mnemonic,
|
||||
this.passphrase,
|
||||
this.seedBytes,
|
||||
required this.apiKey,
|
||||
required this.lnurlDomain,
|
||||
this.network = Network.mainnet,
|
||||
this.cachedAddress,
|
||||
});
|
||||
|
||||
static bool get isAvailable => Platform.isIOS || Platform.isAndroid || Platform.isMacOS;
|
||||
|
||||
Currency get currency => CryptoCurrency.btcln;
|
||||
|
||||
BreezSdk get sdk => _sdk!;
|
||||
|
||||
StreamSubscription<SdkEvent>? _eventSubscription;
|
||||
Stream<SdkEvent>? _eventStream;
|
||||
|
||||
@@ -47,9 +54,25 @@ class LightningWallet {
|
||||
|
||||
void _subscribeToLogStream(File logFile) {
|
||||
_logSubscription = _logStream?.listen((logEntry) {
|
||||
logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append);
|
||||
try {
|
||||
// Check if file exists before writing (optional, but safer)
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// Silently fail or use printV(e) so it doesn't crash the app
|
||||
printV("Failed to write to log: $e");
|
||||
}
|
||||
}, onError: (e) {
|
||||
logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append);
|
||||
try {
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append);
|
||||
} catch (err) {
|
||||
printV("Failed to write error to log: $err");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,10 +87,11 @@ class LightningWallet {
|
||||
? Seed.entropy(seedBytes!)
|
||||
: Seed.mnemonic(mnemonic: mnemonic, passphrase: passphrase);
|
||||
final config = defaultConfig(network: Network.mainnet).copyWith(
|
||||
lnurlDomain: lnurlDomain,
|
||||
apiKey: apiKey,
|
||||
privateEnabledDefault: true,
|
||||
maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5)));
|
||||
lnurlDomain: lnurlDomain,
|
||||
apiKey: apiKey,
|
||||
privateEnabledDefault: true,
|
||||
maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5)),
|
||||
);
|
||||
|
||||
final connectRequest = ConnectRequest(
|
||||
config: config,
|
||||
@@ -75,7 +99,7 @@ class LightningWallet {
|
||||
storageDir: "$appPath/.breez/",
|
||||
);
|
||||
|
||||
sdk = await connect(request: connectRequest);
|
||||
_sdk = await connect(request: connectRequest);
|
||||
|
||||
_eventStream ??= sdk.addEventListener().asBroadcastStream();
|
||||
_logStream ??= initLogging().asBroadcastStream();
|
||||
@@ -87,7 +111,7 @@ class LightningWallet {
|
||||
printV(e);
|
||||
}
|
||||
|
||||
await sdk.syncWallet(request: SyncWalletRequest());
|
||||
await sdk.syncWallet(request: const SyncWalletRequest());
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -98,7 +122,9 @@ class LightningWallet {
|
||||
|
||||
Future<void> close() async {
|
||||
_eventSubscription?.cancel();
|
||||
await sdk.disconnect();
|
||||
try {
|
||||
await _sdk?.disconnect();
|
||||
} catch (_) {}
|
||||
_logSubscription?.cancel();
|
||||
}
|
||||
|
||||
@@ -114,30 +140,33 @@ class LightningWallet {
|
||||
}
|
||||
} catch (_) {} // No need to log here since it should be in the lightning log
|
||||
retries++;
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
return cachedAddress;
|
||||
}
|
||||
|
||||
Future<String> getDepositAddress() async => (await sdk.receivePayment(
|
||||
request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress())))
|
||||
.paymentRequest;
|
||||
request: const ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()),
|
||||
))
|
||||
.paymentRequest;
|
||||
|
||||
Future<BigInt> getBalance() async {
|
||||
Future<Money> getBalance() async {
|
||||
try {
|
||||
return (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats;
|
||||
return Money(
|
||||
(await sdk.getInfo(request: const GetInfoRequest(ensureSynced: true))).balanceSats,
|
||||
CryptoCurrency.btcln,
|
||||
);
|
||||
} on SdkError_Generic catch (_) {
|
||||
} on SdkError_NetworkError catch (_) {}
|
||||
|
||||
return BigInt.zero;
|
||||
return Money.zero(CryptoCurrency.btcln);
|
||||
}
|
||||
|
||||
Future<String> registerAddress(String username) async {
|
||||
return (await sdk.registerLightningAddress(
|
||||
request: RegisterLightningAddressRequest(username: username)))
|
||||
.lightningAddress;
|
||||
}
|
||||
Future<String> registerAddress(String username) async => (await sdk.registerLightningAddress(
|
||||
request: RegisterLightningAddressRequest(username: username),
|
||||
))
|
||||
.lightningAddress;
|
||||
|
||||
Future<String?> getBolt11Invoice(BigInt? amount, String description) async {
|
||||
try {
|
||||
@@ -154,7 +183,9 @@ class LightningWallet {
|
||||
} on SdkError_NetworkError catch (_) {
|
||||
return null;
|
||||
} on SdkError_SparkError catch (e) {
|
||||
if (!e.field0.contains("dns") && !e.field0.contains("TimedOut")) rethrow;
|
||||
if (!e.field0.contains("dns") && !e.field0.contains("TimedOut")) {
|
||||
rethrow;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -170,17 +201,22 @@ class LightningWallet {
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingLightningTransaction> createTransaction(String address, BigInt? amountSats,
|
||||
BitcoinTransactionPriority? priority, bool feesIncluded) async {
|
||||
Future<PendingLightningTransaction> createTransaction(
|
||||
String address,
|
||||
BigInt? amountSats,
|
||||
BitcoinTransactionPriority? priority,
|
||||
bool feesIncluded,
|
||||
) async {
|
||||
final inputType = await sdk.parse(input: address);
|
||||
|
||||
final feePolicy = feesIncluded ? FeePolicy.feesIncluded : FeePolicy.feesExcluded;
|
||||
|
||||
if (inputType is InputType_Bolt11Invoice) {
|
||||
final request = PrepareSendPaymentRequest(
|
||||
paymentRequest: inputType.field0.invoice.bolt11,
|
||||
amount: amountSats,
|
||||
feePolicy: feePolicy);
|
||||
paymentRequest: PaymentRequest.input(input: inputType.field0.invoice.bolt11),
|
||||
amount: amountSats,
|
||||
feePolicy: feePolicy,
|
||||
);
|
||||
final prepareResponse = await sdk.prepareSendPayment(request: request);
|
||||
|
||||
final paymentMethod = prepareResponse.paymentMethod;
|
||||
@@ -188,17 +224,23 @@ class LightningWallet {
|
||||
final lightningFeeSats = paymentMethod.lightningFeeSats;
|
||||
final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats;
|
||||
|
||||
final baseAmount = request.amount ?? amountSats;
|
||||
final amount = baseAmount != null
|
||||
? Money(baseAmount, currency)
|
||||
: Money(paymentMethod.invoiceDetails.amountMsat ?? BigInt.zero, currency) /
|
||||
BigInt.from(1000);
|
||||
|
||||
return PendingLightningTransaction(
|
||||
id: paymentMethod.invoiceDetails.paymentHash,
|
||||
amount: request.amount?.toInt() ??
|
||||
amountSats?.toInt() ??
|
||||
((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
|
||||
fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0),
|
||||
amount: amount,
|
||||
fee: Money(lightningFeeSats + (sparkTransferFeeSats ?? BigInt.zero), currency),
|
||||
commitOverride: () async {
|
||||
try {
|
||||
final res = await sdk.sendPayment(
|
||||
request: SendPaymentRequest(prepareResponse: prepareResponse));
|
||||
request: SendPaymentRequest(prepareResponse: prepareResponse),
|
||||
);
|
||||
printV(res.payment.status.name);
|
||||
return res.payment.id;
|
||||
} on SdkError_SparkError catch (e) {
|
||||
if (e.field0.contains("AlreadyExists")) {
|
||||
throw Exception("Invoice already paid");
|
||||
@@ -209,19 +251,19 @@ class LightningWallet {
|
||||
);
|
||||
}
|
||||
} else if (inputType is InputType_LightningAddress || inputType is InputType_LnurlPay) {
|
||||
final optionalValidateSuccessActionUrl = true;
|
||||
const optionalValidateSuccessActionUrl = true;
|
||||
|
||||
PrepareLnurlPayRequest request;
|
||||
if (inputType is InputType_LightningAddress) {
|
||||
request = PrepareLnurlPayRequest(
|
||||
amountSats: amountSats!,
|
||||
amount: amountSats!,
|
||||
payRequest: inputType.field0.payRequest,
|
||||
validateSuccessActionUrl: optionalValidateSuccessActionUrl,
|
||||
feePolicy: feePolicy,
|
||||
);
|
||||
} else {
|
||||
request = PrepareLnurlPayRequest(
|
||||
amountSats: amountSats!,
|
||||
amount: amountSats!,
|
||||
payRequest: (inputType as InputType_LnurlPay).field0,
|
||||
validateSuccessActionUrl: optionalValidateSuccessActionUrl,
|
||||
feePolicy: feePolicy,
|
||||
@@ -230,21 +272,20 @@ class LightningWallet {
|
||||
|
||||
final prepareResponse = await sdk.prepareLnurlPay(request: request);
|
||||
|
||||
final feeSats = prepareResponse.feeSats;
|
||||
|
||||
return PendingLightningTransaction(
|
||||
id: prepareResponse.invoiceDetails.paymentHash,
|
||||
amount: prepareResponse.amountSats.toInt(),
|
||||
fee: feeSats.toInt(),
|
||||
amount: Money(prepareResponse.amountSats, currency),
|
||||
fee: Money(prepareResponse.feeSats, currency),
|
||||
commitOverride: () async {
|
||||
final res =
|
||||
await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse));
|
||||
printV(res.payment.status.name);
|
||||
return res.payment.id;
|
||||
},
|
||||
);
|
||||
} else if (inputType is InputType_BitcoinAddress) {
|
||||
final request = PrepareSendPaymentRequest(
|
||||
paymentRequest: inputType.field0.address,
|
||||
paymentRequest: PaymentRequest.input(input: inputType.field0.address),
|
||||
amount: amountSats,
|
||||
feePolicy: feePolicy,
|
||||
);
|
||||
@@ -255,32 +296,33 @@ class LightningWallet {
|
||||
final feeQuote = paymentMethod.feeQuote;
|
||||
|
||||
OnchainConfirmationSpeed onchainConfirmationSpeed;
|
||||
int fee;
|
||||
BigInt fee;
|
||||
switch (priority) {
|
||||
case BitcoinTransactionPriority.fast:
|
||||
fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt();
|
||||
fee = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat;
|
||||
onchainConfirmationSpeed = OnchainConfirmationSpeed.fast;
|
||||
break;
|
||||
case BitcoinTransactionPriority.medium:
|
||||
fee =
|
||||
(feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt();
|
||||
fee = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat;
|
||||
onchainConfirmationSpeed = OnchainConfirmationSpeed.medium;
|
||||
break;
|
||||
case BitcoinTransactionPriority.slow:
|
||||
default:
|
||||
fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt();
|
||||
fee = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat;
|
||||
onchainConfirmationSpeed = OnchainConfirmationSpeed.slow;
|
||||
}
|
||||
|
||||
return PendingLightningTransaction(
|
||||
id: "", // ToDo: Find out where to get it
|
||||
amount: prepareResponse.amount.toInt(),
|
||||
fee: fee,
|
||||
amount: Money(prepareResponse.amount, currency),
|
||||
fee: Money(fee, currency),
|
||||
commitOverride: () async {
|
||||
final options =
|
||||
SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed);
|
||||
await sdk.sendPayment(
|
||||
request: SendPaymentRequest(prepareResponse: prepareResponse, options: options));
|
||||
final res = await sdk.sendPayment(
|
||||
request: SendPaymentRequest(prepareResponse: prepareResponse, options: options),
|
||||
);
|
||||
return res.payment.id;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -296,7 +338,7 @@ class LightningWallet {
|
||||
// statusFilter: [PaymentStatus.completed],
|
||||
fromTimestamp:
|
||||
fromDate != null ? BigInt.from((fromDate.millisecondsSinceEpoch / 1000).round()) : null,
|
||||
assetFilter: AssetFilter.bitcoin(),
|
||||
assetFilter: const AssetFilter.bitcoin(),
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
sortAscending: false, // Sort order (true = oldest first, false = newest first)
|
||||
@@ -327,7 +369,7 @@ class LightningWallet {
|
||||
///
|
||||
Future<List<Map<String, dynamic>>> getUnclaimedDeposits() async {
|
||||
final unclaimedDeposits = <Map<String, dynamic>>[];
|
||||
final response = await sdk.listUnclaimedDeposits(request: ListUnclaimedDepositsRequest());
|
||||
final response = await sdk.listUnclaimedDeposits(request: const ListUnclaimedDepositsRequest());
|
||||
for (final deposit in response.deposits) {
|
||||
final unclaimedDeposit = {
|
||||
"txId": deposit.txid,
|
||||
@@ -350,7 +392,7 @@ class LightningWallet {
|
||||
return unclaimedDeposits;
|
||||
}
|
||||
|
||||
Future<ElectrumTransactionInfo> claimDeposit(String txId, int vout, BigInt newFee) async {
|
||||
Future<ElectrumTransactionInfo?> claimDeposit(String txId, int vout, BigInt newFee) async {
|
||||
final response = await sdk.claimDeposit(
|
||||
request: ClaimDepositRequest(
|
||||
txid: txId,
|
||||
@@ -359,11 +401,18 @@ class LightningWallet {
|
||||
),
|
||||
);
|
||||
|
||||
return _getElectrumTransactionInfoFromPayment(response.payment);
|
||||
if (response.payment == null) {
|
||||
return null;
|
||||
}
|
||||
return _getElectrumTransactionInfoFromPayment(response.payment!);
|
||||
}
|
||||
|
||||
Future<String> refundDeposit(
|
||||
String txId, int vout, String destinationAddress, BigInt feeRate) async {
|
||||
String txId,
|
||||
int vout,
|
||||
String destinationAddress,
|
||||
BigInt feeRate,
|
||||
) async {
|
||||
final response = await sdk.refundDeposit(
|
||||
request: RefundDepositRequest(
|
||||
txid: txId,
|
||||
@@ -404,7 +453,7 @@ class LightningWallet {
|
||||
}
|
||||
|
||||
ElectrumTransactionInfo _getElectrumTransactionInfoFromPayment(Payment payment) {
|
||||
TransactionDirection direction = TransactionDirection.outgoing;
|
||||
var direction = TransactionDirection.outgoing;
|
||||
|
||||
if (payment.paymentType == PaymentType.receive) {
|
||||
direction = TransactionDirection.incoming;
|
||||
@@ -413,30 +462,37 @@ class LightningWallet {
|
||||
direction = TransactionDirection.incoming;
|
||||
}
|
||||
|
||||
String? preimage;
|
||||
if (payment.details != null && payment.details is PaymentDetails_Lightning) {
|
||||
preimage = (payment.details as PaymentDetails_Lightning).htlcDetails.preimage;
|
||||
}
|
||||
|
||||
return ElectrumTransactionInfo(
|
||||
WalletType.bitcoin,
|
||||
id: payment.id,
|
||||
amount: payment.amount.toInt(),
|
||||
amount: Money(payment.amount, currency),
|
||||
direction: direction,
|
||||
isPending: payment.status == PaymentStatus.pending,
|
||||
fee: payment.fees.toInt(),
|
||||
fee: Money(payment.fees, currency),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000),
|
||||
confirmations: payment.status == PaymentStatus.pending ? 0 : 10,
|
||||
additionalInfo: {"isLightning": true},
|
||||
additionalInfo: {
|
||||
"isLightning": true,
|
||||
if (preimage != null) "preimage": preimage,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) {
|
||||
return ElectrumTransactionInfo(
|
||||
WalletType.bitcoin,
|
||||
id: deposit.txid,
|
||||
amount: deposit.amountSats.toInt(),
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: true,
|
||||
fee: 0,
|
||||
date: DateTime.now(),
|
||||
confirmations: 0,
|
||||
additionalInfo: {"isLightning": true, "isSparkDeposit": true},
|
||||
);
|
||||
}
|
||||
ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) =>
|
||||
ElectrumTransactionInfo(
|
||||
WalletType.bitcoin,
|
||||
id: deposit.txid,
|
||||
amount: Money(deposit.amountSats, currency),
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: true,
|
||||
fee: Money.zero(currency),
|
||||
date: DateTime.now(),
|
||||
confirmations: 0,
|
||||
additionalInfo: {"isLightning": true, "isSparkDeposit": true},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
|
||||
class PendingLightningTransaction with PendingTransaction {
|
||||
@@ -10,33 +10,31 @@ class PendingLightningTransaction with PendingTransaction {
|
||||
required this.commitOverride,
|
||||
});
|
||||
|
||||
final int amount;
|
||||
final int fee;
|
||||
final bool isSendAll;
|
||||
Future<void> Function() commitOverride;
|
||||
Future<String> Function() commitOverride;
|
||||
final List<void Function()> _listeners = [];
|
||||
|
||||
@override
|
||||
final String id;
|
||||
String id;
|
||||
|
||||
@override
|
||||
final Money amount;
|
||||
|
||||
@override
|
||||
final Money fee;
|
||||
|
||||
@override
|
||||
String get hex => "";
|
||||
|
||||
@override
|
||||
String get amountFormatted => bitcoinAmountToString(amount: amount);
|
||||
|
||||
@override
|
||||
String get feeFormatted => "$feeFormattedValue BTC";
|
||||
|
||||
@override
|
||||
String get feeFormattedValue => bitcoinAmountToString(amount: fee);
|
||||
String get amountFormatted => amount.toString();
|
||||
|
||||
@override
|
||||
int? get outputCount => 1;
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
await commitOverride.call();
|
||||
id = await commitOverride.call();
|
||||
_listeners.forEach((e) => e.call());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/mweb_utxo.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
@@ -17,12 +18,9 @@ import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:convert/convert.dart' as convert;
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
@@ -32,28 +30,22 @@ import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
|
||||
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/mweb_utxo.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.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_mweb/cw_mweb.dart';
|
||||
import 'package:cw_mweb/mwebd.pbgrpc.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
@@ -123,8 +115,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
initialRegularAddressIndex: initialRegularAddressIndex,
|
||||
initialChangeAddressIndex: initialChangeAddressIndex,
|
||||
initialMwebAddresses: initialMwebAddresses,
|
||||
mainHdByType: mainHdByType,
|
||||
sideHdByType: sideHdByType,
|
||||
mainHdByTypeAndAccount: mainHdByTypeAndAccount,
|
||||
sideHdByTypeAndAccount: sideHdByTypeAndAccount,
|
||||
accountIndexes: [currentAccountIndex],
|
||||
currentAccountIndex: currentAccountIndex,
|
||||
legacyMainHd: mainHd,
|
||||
legacySideHd: sideHd,
|
||||
network: network,
|
||||
@@ -384,6 +378,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
} catch (e) {
|
||||
printV("failed to subscribe for updates: $e");
|
||||
}
|
||||
await checkIfBatchSupported();
|
||||
updateFeeRates();
|
||||
_feeRatesTimer?.cancel();
|
||||
_feeRatesTimer =
|
||||
@@ -508,22 +503,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
// rename the hive box:
|
||||
final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
static Future<void> copyMwebBox({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
}) async {
|
||||
final oldBoxName = "${fromName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final newBoxName = "${toName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
if (oldBoxName == newBoxName) return;
|
||||
|
||||
final oldBox = await CakeHive.openBox<MwebUtxo>(oldBoxName);
|
||||
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
|
||||
final newBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
|
||||
for (final key in oldBox.keys) {
|
||||
final value = oldBox.get(key);
|
||||
await oldBox.delete(key);
|
||||
await mwebUtxosBox.put(key, value!);
|
||||
await newBox.put(key, oldBox.get(key)!);
|
||||
}
|
||||
oldBox.deleteFromDisk();
|
||||
}
|
||||
|
||||
await super.renameWalletFiles(newWalletName);
|
||||
static Future<void> deleteMwebBox(String name) async {
|
||||
final boxName = "${name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
|
||||
final box = await CakeHive.openBox<MwebUtxo>(boxName);
|
||||
await box.deleteFromDisk();
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -589,8 +587,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
WalletType.litecoin,
|
||||
id: utxo.outputId,
|
||||
height: utxo.height,
|
||||
amount: utxo.value.toInt(),
|
||||
fee: 0,
|
||||
amount: Money.fromInt(utxo.value, currency),
|
||||
fee: Money.zero(currency),
|
||||
direction: TransactionDirection.incoming,
|
||||
isPending: utxo.height == 0,
|
||||
date: date,
|
||||
@@ -736,9 +734,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
|
||||
Future<void> checkMwebUtxosSpent() async {
|
||||
printV("checkMwebUtxosSpent() called!");
|
||||
if (!mwebEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!mwebEnabled) return;
|
||||
|
||||
final pendingOutgoingTransactions = transactionHistory.transactions.values
|
||||
.where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending);
|
||||
@@ -765,8 +761,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
final height = await electrumClient.getCurrentBlockChainTip();
|
||||
if (height == null || status.blockHeaderHeight != height) return;
|
||||
if (status.mwebUtxosHeight != height) return; // we aren't synced
|
||||
int amount = 0;
|
||||
Set<String> inputAddresses = {};
|
||||
var amount = 0;
|
||||
var inputAddresses = <String>{};
|
||||
var output = convert.AccumulatorSink<Digest>();
|
||||
var input = sha256.startChunkedConversion(output);
|
||||
|
||||
@@ -779,8 +775,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
if (!inputAddresses.contains(utxo.address)) {
|
||||
addressRecord.txCount++;
|
||||
}
|
||||
addressRecord.balance -= utxo.value.toInt();
|
||||
amount += utxo.value.toInt();
|
||||
addressRecord.balance -= utxo.value;
|
||||
amount += utxo.value;
|
||||
inputAddresses.add(utxo.address);
|
||||
input.add(hex.decode(outputId));
|
||||
}
|
||||
@@ -792,8 +788,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
WalletType.litecoin,
|
||||
id: digest.toString(),
|
||||
height: height,
|
||||
amount: amount,
|
||||
fee: 0,
|
||||
amount: Money.fromInt(amount, currency),
|
||||
fee: Money.zero(currency),
|
||||
direction: TransactionDirection.outgoing,
|
||||
isPending: false,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
|
||||
@@ -923,10 +919,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
// update unspent balances:
|
||||
await updateUnspent();
|
||||
|
||||
int confirmed = balance.confirmed;
|
||||
int unconfirmed = balance.unconfirmed;
|
||||
int confirmedMweb = 0;
|
||||
int unconfirmedMweb = 0;
|
||||
var confirmedMweb = 0;
|
||||
var unconfirmedMweb = 0;
|
||||
try {
|
||||
mwebUtxosBox.values.forEach((utxo) {
|
||||
bool isConfirmed = utxo.height > 0;
|
||||
@@ -935,20 +929,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
"utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}");
|
||||
|
||||
if (isConfirmed) {
|
||||
confirmedMweb += utxo.value.toInt();
|
||||
confirmedMweb += utxo.value;
|
||||
}
|
||||
|
||||
if (isConfirmed && utxo.spent) {
|
||||
unconfirmedMweb -= utxo.value.toInt();
|
||||
unconfirmedMweb -= utxo.value;
|
||||
}
|
||||
|
||||
if (!isConfirmed && !utxo.spent) {
|
||||
unconfirmedMweb += utxo.value.toInt();
|
||||
unconfirmedMweb += utxo.value;
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
for (var addressRecord in walletAddresses.allAddresses) {
|
||||
for (final addressRecord in walletAddresses.allAddresses) {
|
||||
addressRecord.balance = 0;
|
||||
addressRecord.txCount = 0;
|
||||
}
|
||||
@@ -992,11 +986,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
}
|
||||
|
||||
return ElectrumBalance(
|
||||
confirmed: confirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
frozen: balance.frozen.toInt(),
|
||||
secondConfirmed: confirmedMweb,
|
||||
secondUnconfirmed: unconfirmedMweb,
|
||||
confirmed: balance.confirmed,
|
||||
unconfirmed: balance.unconfirmed,
|
||||
frozen: balance.frozen,
|
||||
secondConfirmed: Money.fromInt(confirmedMweb, currency),
|
||||
secondUnconfirmed: Money.fromInt(unconfirmedMweb, currency),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1373,9 +1367,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
final tx = PendingBitcoinTransaction(
|
||||
btcTx,
|
||||
type,
|
||||
accountIndex: 0,
|
||||
electrumClient: electrumClient,
|
||||
amount: 0,
|
||||
fee: resp.fee.toInt(),
|
||||
amount: Money.zero(currency),
|
||||
fee: Money.fromInt(resp.fee.toInt(), currency),
|
||||
feeRate: "",
|
||||
network: network,
|
||||
hasChange: resp.recipient.length > 1,
|
||||
@@ -1564,7 +1559,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
|
||||
@override
|
||||
Future<BtcTransaction> buildHardwareWalletTransaction({
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<BitcoinOutput> outputs,
|
||||
required BigInt fee,
|
||||
required BasedUtxoNetwork network,
|
||||
required List<UtxoWithAddress> utxos,
|
||||
@@ -1576,25 +1571,48 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
|
||||
}) async {
|
||||
final masterFingerprint =
|
||||
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
|
||||
await (hardwareWalletService! as BitcoinHardwareWalletService).getMasterFingerprint();
|
||||
|
||||
final readyInputs = <PSBTReadyUtxoWithAddress>[];
|
||||
for (final utxo in utxos) {
|
||||
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
|
||||
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
|
||||
|
||||
readyInputs.add(PSBTReadyUtxoWithAddress(
|
||||
utxo: utxo.utxo,
|
||||
rawTx: rawTx,
|
||||
ownerDetails: utxo.ownerDetails,
|
||||
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
||||
ownerMasterFingerprint: masterFingerprint,
|
||||
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
|
||||
));
|
||||
readyInputs.add(
|
||||
PSBTReadyUtxoWithAddress(
|
||||
utxo: utxo.utxo,
|
||||
rawTx: rawTx,
|
||||
ownerDetails: utxo.ownerDetails,
|
||||
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
||||
ownerMasterFingerprint: masterFingerprint,
|
||||
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService)
|
||||
.signLitecoinTransaction(outputs: outputs, inputs: readyInputs, publicKeys: publicKeys);
|
||||
final orderedOutputs =
|
||||
orderOutputs(outputs, outputOrdering).map((o) {
|
||||
if (o.isChange && publicKeys.containsKey(o.address.pubKeyHash())) {
|
||||
final changeKey = publicKeys[o.address.pubKeyHash()]!;
|
||||
return PSBTReadyBitcoinOutput(
|
||||
address: o.address,
|
||||
value: o.value,
|
||||
isSilentPayment: o.isSilentPayment,
|
||||
isChange: o.isChange,
|
||||
changeMasterFingerprint: masterFingerprint,
|
||||
changeDerivationPath: changeKey.derivationPath,
|
||||
changePublicKey: changeKey.publicKey,
|
||||
);
|
||||
}
|
||||
return PSBTReadyBitcoinOutput.fromOutput(o);
|
||||
});
|
||||
|
||||
final rawHex =
|
||||
await (hardwareWalletService! as LitecoinHardwareWalletService).signLitecoinTransaction(
|
||||
outputs: orderedOutputs.toList(),
|
||||
inputs: readyInputs,
|
||||
publicKeys: publicKeys,
|
||||
);
|
||||
|
||||
return BtcTransaction.fromRaw(rawHex);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle
|
||||
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
|
||||
LitecoinWalletAddressesBase(
|
||||
WalletInfo walletInfo, {
|
||||
required super.mainHdByType,
|
||||
required super.sideHdByType,
|
||||
required super.mainHdByTypeAndAccount,
|
||||
required super.sideHdByTypeAndAccount,
|
||||
required super.accountIndexes,
|
||||
required super.currentAccountIndex,
|
||||
required super.legacyMainHd,
|
||||
required super.legacySideHd,
|
||||
required super.network,
|
||||
@@ -66,9 +68,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
|
||||
List.filled(32, 0);
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
Future<void> init({List<int> accountIndexes = const []}) async {
|
||||
if (!isHardwareWallet) await initMwebAddresses();
|
||||
await super.init();
|
||||
await super.init(accountIndexes: accountIndexes);
|
||||
}
|
||||
|
||||
@computed
|
||||
@@ -230,13 +232,25 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
|
||||
@override
|
||||
String get addressForExchange {
|
||||
// don't use mweb addresses for exchange refund address:
|
||||
try {
|
||||
final addresses = receiveAddresses
|
||||
.where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed);
|
||||
return addresses.first.address;
|
||||
} catch (_) {
|
||||
return receiveAddresses.first.address;
|
||||
|
||||
final current = getFreshAddress();
|
||||
final bool isMweb =
|
||||
receiveAddresses.any((e) => e.address == current && e.type == SegwitAddresType.mweb);
|
||||
|
||||
if (isMweb) {
|
||||
final segwit = receiveAddresses
|
||||
.where((e) =>
|
||||
e.type == SegwitAddresType.p2wpkh &&
|
||||
!e.isUsed &&
|
||||
!e.isHidden &&
|
||||
!hiddenAddresses.contains(e.address))
|
||||
.map((e) => e.address)
|
||||
.toList();
|
||||
|
||||
return segwit.isNotEmpty ? segwit.first : current;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -12,8 +12,6 @@ import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@@ -140,26 +138,11 @@ class LitecoinWalletService extends WalletService<
|
||||
|
||||
@override
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = await WalletInfo.get(currentName, getType());
|
||||
if (currentWalletInfo == null) {
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
final currentWallet = await LitecoinWalletBase.open(
|
||||
password: password,
|
||||
name: currentName,
|
||||
walletInfo: currentWalletInfo,
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
|
||||
);
|
||||
if (currentName == newName) return;
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
await saveBackup(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
newWalletInfo.name = newName;
|
||||
|
||||
await newWalletInfo.save();
|
||||
await LitecoinWalletBase.copyMwebBox(fromName: currentName, toName: newName);
|
||||
await super.rename(currentName, password, newName);
|
||||
await LitecoinWalletBase.deleteMwebBox(currentName);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import "dart:typed_data";
|
||||
|
||||
/// Anti-fee-sniping locktime, set to the current chain tip (exact-tip).
|
||||
///
|
||||
/// Matches the exact-tip cluster (payjoin-cli, ldk-node, Bull Bitcoin) and Core's
|
||||
/// default for externally-constructed transactions, and takes Cake off its unique
|
||||
/// `nLockTime = 0` fingerprint. It intentionally skips the ~10% backdate
|
||||
/// Core/Electrum apply for delayed-broadcast privacy, which does not apply here.
|
||||
/// Returns 0 when not synced or the tip is unknown, to avoid emitting a stale
|
||||
/// height.
|
||||
int antiFeeSnipingLocktime({
|
||||
required int chainTip,
|
||||
required bool synced,
|
||||
}) {
|
||||
if (!synced || chainTip <= 0) return 0;
|
||||
return chainTip;
|
||||
}
|
||||
|
||||
/// Little-endian 4-byte encoding for `BtcTransaction.locktime`.
|
||||
List<int> locktimeToBytes(int locktime) => (ByteData(4)..setUint32(0, locktime, Endian.little)).buffer.asUint8List();
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
|
||||
/// Applies [ordering] to a transaction's [outputs] before building.
|
||||
///
|
||||
/// Returns a new list; the input is never mutated. Only [BitcoinOrdering.shuffle]
|
||||
/// reorders (using a secure RNG by default) so the change output is no longer
|
||||
/// placed deterministically last. Any other value preserves the given order,
|
||||
/// matching the pre-existing hardware-wallet behavior.
|
||||
List<T> orderOutputs<T>(
|
||||
List<T> outputs,
|
||||
BitcoinOrdering ordering, {
|
||||
Random? rng,
|
||||
}) {
|
||||
final result = List<T>.of(outputs);
|
||||
if (ordering == BitcoinOrdering.shuffle) {
|
||||
result.shuffle(rng ?? Random.secure());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -290,11 +290,14 @@ class PayjoinManager {
|
||||
break;
|
||||
|
||||
case PayjoinReceiverRequestTypes.getCandidateInputs:
|
||||
utxos = _wallet.getUtxoWithPrivateKeys();
|
||||
utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
|
||||
if (utxos.isEmpty) {
|
||||
await _wallet.updateAllUnspents();
|
||||
utxos = _wallet.getUtxoWithPrivateKeys();
|
||||
utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
|
||||
}
|
||||
// Candidates arrive in wallet scan order (address, then age), which is
|
||||
// predictable; shuffle so the receiver's input choice can't mirror it.
|
||||
utxos.shuffle(Random.secure());
|
||||
mainToIsolateSendPort?.send({
|
||||
'requestId': message['requestId'],
|
||||
'result': utxos,
|
||||
|
||||
@@ -175,7 +175,15 @@ class PayjoinReceiverWorker {
|
||||
final unspent = listUnspent as List<UtxoWithPrivateKey>;
|
||||
if (unspent.isEmpty) throw RecoverableError('No unspent outputs available');
|
||||
|
||||
final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
|
||||
final candidateInputs = await Future.wait(unspent.map(_inputPairFromUtxo));
|
||||
|
||||
// Prefer a UTXO that avoids the Unnecessary Input Heuristic (UIH2);
|
||||
// fall back to the first candidate if none preserves privacy.
|
||||
InputPair selectedUtxo = candidateInputs.first;
|
||||
try {
|
||||
selectedUtxo = await pj5.tryPreservingPrivacy(candidateInputs: candidateInputs);
|
||||
} catch (_) {}
|
||||
|
||||
final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
|
||||
final pj7 = await pj6.commitInputs();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bbqrdart/bbqrdart.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
@@ -9,7 +8,6 @@ import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
@@ -23,6 +21,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
PendingBitcoinTransaction(
|
||||
this._tx,
|
||||
this.type, {
|
||||
required this.accountIndex,
|
||||
required this.electrumClient,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
@@ -33,6 +32,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
this.hasTaprootInputs = false,
|
||||
this.isMweb = false,
|
||||
this.utxos = const [],
|
||||
this.derivedOutputs = const [],
|
||||
this.publicKeys,
|
||||
this.commitOverride,
|
||||
this.unsignedPsbt,
|
||||
@@ -41,9 +41,8 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
|
||||
final WalletType type;
|
||||
final BtcTransaction _tx;
|
||||
final int? accountIndex;
|
||||
final ElectrumClient electrumClient;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final String feeRate;
|
||||
final BasedUtxoNetwork? network;
|
||||
final bool isSendAll;
|
||||
@@ -51,6 +50,9 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
final bool hasTaprootInputs;
|
||||
final bool isViewOnly;
|
||||
List<UtxoWithAddress> utxos;
|
||||
|
||||
final List<BitcoinOutput> derivedOutputs;
|
||||
|
||||
bool isMweb;
|
||||
String? changeAddressOverride;
|
||||
String? idOverride;
|
||||
@@ -61,6 +63,12 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
|
||||
Uint8List? unsignedPsbt;
|
||||
|
||||
@override
|
||||
final Money amount;
|
||||
|
||||
@override
|
||||
final Money fee;
|
||||
|
||||
@override
|
||||
String get id => idOverride ?? _tx.txId();
|
||||
|
||||
@@ -68,28 +76,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
String get hex => hexOverride ?? _tx.serialize();
|
||||
|
||||
@override
|
||||
String get amountFormatted => bitcoinAmountToString(amount: amount);
|
||||
|
||||
@override
|
||||
String get feeFormatted => "$feeFormattedValue ${_feeCurrency}";
|
||||
|
||||
String get _feeCurrency {
|
||||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
return "BTC";
|
||||
case WalletType.litecoin:
|
||||
return "LTC";
|
||||
case WalletType.bitcoinCash:
|
||||
return "BCH";
|
||||
case WalletType.dogecoin:
|
||||
return "DOGE";
|
||||
default:
|
||||
return type.name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get feeFormattedValue => bitcoinAmountToString(amount: fee);
|
||||
String get amountFormatted => amount.toString();
|
||||
|
||||
@override
|
||||
int? get outputCount => _tx.outputs.length;
|
||||
@@ -98,6 +85,13 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
|
||||
bool get hasSilentPayment => _tx.hasSilentPayment;
|
||||
|
||||
List<String?> get stealthAddresses => derivedOutputs
|
||||
.where((out) => !out.isChange)
|
||||
.map((out) => out.isSilentPayment
|
||||
? out.address.toAddress(network ?? BitcoinNetwork.mainnet)
|
||||
: null)
|
||||
.toList();
|
||||
|
||||
PendingChange? get change {
|
||||
try {
|
||||
final change = _tx.outputs.firstWhere((out) => out.isChange);
|
||||
@@ -180,18 +174,21 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
_listeners.add(listener);
|
||||
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
|
||||
id: id,
|
||||
height: 0,
|
||||
amount: amount,
|
||||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
isReplaced: false,
|
||||
confirmations: 0,
|
||||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee);
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(
|
||||
type,
|
||||
id: id,
|
||||
height: 0,
|
||||
amount: amount,
|
||||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
isReplaced: false,
|
||||
confirmations: 0,
|
||||
accountIndex: accountIndex,
|
||||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee,
|
||||
);
|
||||
|
||||
@override
|
||||
bool shouldCommitUR() => isViewOnly;
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import "dart:convert";
|
||||
import "dart:typed_data";
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:ledger_bitcoin/psbt.dart';
|
||||
import "package:bitcoin_base/bitcoin_base.dart";
|
||||
import "package:convert/convert.dart";
|
||||
import "package:cw_core/output_info.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:ledger_bitcoin/psbt.dart";
|
||||
|
||||
class PSBTTransactionBuild {
|
||||
final PsbtV2 psbt = PsbtV2();
|
||||
|
||||
PSBTTransactionBuild(
|
||||
{required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
required List<OutputInfo> cwOutputs,
|
||||
bool enableRBF = true}) {
|
||||
PSBTTransactionBuild({
|
||||
required List<PSBTReadyUtxoWithAddress> inputs,
|
||||
required List<PSBTReadyBitcoinOutput> outputs,
|
||||
bool enableRBF = true,
|
||||
int locktime = 0,
|
||||
}) {
|
||||
psbt.setGlobalTxVersion(2);
|
||||
psbt.setGlobalFallbackLocktime(locktime);
|
||||
psbt.setGlobalInputCount(inputs.length);
|
||||
psbt.setGlobalOutputCount(outputs.length);
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
final input = inputs[i];
|
||||
|
||||
printV(input.utxo.isP2tr());
|
||||
printV(input.utxo.isSegwit());
|
||||
printV(input.utxo.isP2shSegwit());
|
||||
|
||||
psbt.setInputPreviousTxId(
|
||||
i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()));
|
||||
i,
|
||||
Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()),
|
||||
);
|
||||
psbt.setInputOutputIndex(i, input.utxo.vout);
|
||||
psbt.setInputSequence(i, enableRBF ? 0xfffffffd : 0xffffffff);
|
||||
|
||||
@@ -45,75 +43,98 @@ class PSBTTransactionBuild {
|
||||
for (var i = 0; i < outputs.length; i++) {
|
||||
final output = outputs[i];
|
||||
|
||||
if (output is BitcoinOutput) {
|
||||
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
|
||||
psbt.setOutputAmount(i, output.value.toInt());
|
||||
if (cwOutputs.isNotEmpty) {
|
||||
try {
|
||||
final cwOutput = cwOutputs
|
||||
.where((e) => [e.address, e.extractedAddress]
|
||||
.map((e) => e?.toLowerCase())
|
||||
.contains(output.address.toAddress().toLowerCase()))
|
||||
.firstOrNull;
|
||||
if (cwOutput != null &&
|
||||
cwOutput.extra.containsKey('bip353_name') &&
|
||||
cwOutput.extra.containsKey('bip353_proof')) {
|
||||
final bip353Name = utf8.encode(cwOutput.extra['bip353_name'] as String);
|
||||
final bip353Proof = base64.decode(cwOutput.extra['bip353_proof'] as String);
|
||||
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
|
||||
psbt.setOutputAmount(i, output.value.toInt());
|
||||
|
||||
if (bip353Name.length > 255) {
|
||||
printV('BIP353 name is too long, skipping');
|
||||
continue;
|
||||
}
|
||||
final proof = Uint8List.fromList([
|
||||
bip353Name.length,
|
||||
...bip353Name,
|
||||
...bip353Proof,
|
||||
]);
|
||||
if (output.isChange &&
|
||||
output.changeDerivationPath != null &&
|
||||
output.changePublicKey != null &&
|
||||
output.changeMasterFingerprint != null) {
|
||||
psbt.setOutputBip32Derivation(
|
||||
i,
|
||||
Uint8List.fromList(hex.decode(output.changePublicKey!)),
|
||||
output.changeMasterFingerprint!,
|
||||
BIPPath.fromString(output.changeDerivationPath!).toPathArray(),
|
||||
);
|
||||
}
|
||||
|
||||
psbt.setOutputDNSSECProof(i, proof);
|
||||
if (output.outputInfo != null) {
|
||||
try {
|
||||
final cwOutput = output.outputInfo!;
|
||||
if (cwOutput.extra.containsKey("bip353_name") &&
|
||||
cwOutput.extra.containsKey("bip353_proof")) {
|
||||
final bip353Name = utf8.encode(cwOutput.extra["bip353_name"] as String);
|
||||
final bip353Proof = base64.decode(cwOutput.extra["bip353_proof"] as String);
|
||||
|
||||
if (bip353Name.length > 255) {
|
||||
printV("BIP353 name is too long, skipping");
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error setting DNSSEC proof: $e');
|
||||
final proof = Uint8List.fromList([
|
||||
bip353Name.length,
|
||||
...bip353Name,
|
||||
...bip353Proof,
|
||||
]);
|
||||
|
||||
psbt.setOutputDNSSECProof(i, proof);
|
||||
}
|
||||
} catch (e) {
|
||||
printV("Error setting DNSSEC proof: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final PsbtV2 psbt = PsbtV2();
|
||||
|
||||
void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) {
|
||||
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
|
||||
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
|
||||
psbt.setInputBip32Derivation(
|
||||
i,
|
||||
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint,
|
||||
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
|
||||
);
|
||||
}
|
||||
|
||||
void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) {
|
||||
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
|
||||
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
|
||||
psbt.setInputBip32Derivation(
|
||||
i,
|
||||
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint,
|
||||
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
|
||||
);
|
||||
|
||||
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
|
||||
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
|
||||
psbt.setInputWitnessUtxo(
|
||||
i,
|
||||
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
|
||||
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
|
||||
);
|
||||
}
|
||||
|
||||
void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) {
|
||||
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
|
||||
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
|
||||
psbt.setInputBip32Derivation(
|
||||
i,
|
||||
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
|
||||
input.ownerMasterFingerprint,
|
||||
BIPPath.fromString(input.ownerDerivationPath).toPathArray(),
|
||||
);
|
||||
|
||||
psbt.setInputRedeemScript(
|
||||
i, Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
|
||||
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
|
||||
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
|
||||
i,
|
||||
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
|
||||
);
|
||||
psbt.setInputWitnessUtxo(
|
||||
i,
|
||||
Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
|
||||
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
|
||||
final String rawTx;
|
||||
final String ownerDerivationPath;
|
||||
final Uint8List ownerMasterFingerprint;
|
||||
final String ownerPublicKey;
|
||||
|
||||
PSBTReadyUtxoWithAddress({
|
||||
required super.utxo,
|
||||
required this.rawTx,
|
||||
@@ -122,4 +143,34 @@ class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
|
||||
required this.ownerMasterFingerprint,
|
||||
required this.ownerPublicKey,
|
||||
});
|
||||
|
||||
final String rawTx;
|
||||
final String ownerDerivationPath;
|
||||
final Uint8List ownerMasterFingerprint;
|
||||
final String ownerPublicKey;
|
||||
}
|
||||
|
||||
class PSBTReadyBitcoinOutput extends BitcoinOutput {
|
||||
PSBTReadyBitcoinOutput({
|
||||
required super.address,
|
||||
required super.value,
|
||||
super.isSilentPayment = false,
|
||||
super.isChange = false,
|
||||
this.changeDerivationPath,
|
||||
this.changeMasterFingerprint,
|
||||
this.changePublicKey,
|
||||
this.outputInfo,
|
||||
});
|
||||
|
||||
factory PSBTReadyBitcoinOutput.fromOutput(BitcoinOutput output) => PSBTReadyBitcoinOutput(
|
||||
address: output.address,
|
||||
value: output.value,
|
||||
isSilentPayment: output.isSilentPayment,
|
||||
isChange: output.isChange,
|
||||
);
|
||||
|
||||
final String? changeDerivationPath;
|
||||
final Uint8List? changeMasterFingerprint;
|
||||
final String? changePublicKey;
|
||||
final OutputInfo? outputInfo;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ extension PsbtSigner on PsbtV2 {
|
||||
setGlobalInputCount(tx.inputs.length);
|
||||
setGlobalOutputCount(tx.outputs.length);
|
||||
setGlobalTxVersion(Uint8List.fromList(tx.version).readUint32LE(0));
|
||||
setGlobalFallbackLocktime(Uint8List.fromList(tx.locktime).readUint32LE(0));
|
||||
|
||||
for (var i = 0; i < getGlobalInputCount(); i++) {
|
||||
inputMaps.insert(i, <String, Uint8List>{});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+15
-10
@@ -6,7 +6,7 @@ author: Hash Bags
|
||||
homepage: https://github.com/Such-Software/hash-wallet
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.5 <3.0.0"
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=1.20.0"
|
||||
|
||||
dependencies:
|
||||
@@ -14,8 +14,8 @@ dependencies:
|
||||
sdk: flutter
|
||||
path_provider: ^2.0.11
|
||||
http: ^1.1.0
|
||||
mobx: ^2.0.7+4
|
||||
flutter_mobx: ^2.0.6+1
|
||||
mobx: ^2.6.0
|
||||
flutter_mobx: ^2.3.0
|
||||
intl: any
|
||||
shared_preferences: ^2.0.15
|
||||
cw_core:
|
||||
@@ -29,7 +29,7 @@ dependencies:
|
||||
blockchain_utils:
|
||||
git:
|
||||
url: https://github.com/cake-tech/blockchain_utils
|
||||
ref: cake-update-v2
|
||||
ref: 59fdf29d72068e0522a96a8953ed7272833a9f57
|
||||
cw_mweb:
|
||||
path: ../cw_mweb
|
||||
grpc: 4.0.1
|
||||
@@ -55,6 +55,11 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor_connect
|
||||
ref: d1242cea90f84b00200e7bcab914a5af750e23fb
|
||||
trezor_flutter:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor-flutter
|
||||
ref: 591d4b8c821b9c10561f848b866324a9a8dcb489
|
||||
path: trezor-flutter
|
||||
bitbox_flutter:
|
||||
path: ../scripts/bitbox_flutter
|
||||
socks_socket:
|
||||
@@ -72,15 +77,15 @@ dependencies:
|
||||
breez_sdk_spark_flutter:
|
||||
git:
|
||||
url: https://github.com/breez/breez-sdk-spark-flutter
|
||||
ref: v0.11.0
|
||||
ref: v0.23.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.15
|
||||
build_resolvers: ^2.4.4
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^2.0.1
|
||||
build_runner: ^2.15.0
|
||||
mobx_codegen: ^2.7.6
|
||||
# hive_generator: ^2.0.1
|
||||
hive: 2.0.1
|
||||
|
||||
dependency_overrides:
|
||||
watcher: ^1.1.0
|
||||
@@ -88,7 +93,7 @@ dependency_overrides:
|
||||
bitcoin_base:
|
||||
git:
|
||||
url: https://github.com/cake-tech/bitcoin_base
|
||||
ref: cake-update-v11
|
||||
ref: 4e41f96f4838139895c65c3f49109d05af5d46aa
|
||||
pointycastle: 3.7.4
|
||||
ffi: 2.1.0
|
||||
intl: any
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
../pubspec_overrides.yaml
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:math';
|
||||
import "package:cw_bitcoin/coin_selection.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
void main() {
|
||||
test('effectiveValue', () {
|
||||
expect(effectiveValue(10000, 136), 9864);
|
||||
expect(effectiveValue(100, 136), -36);
|
||||
});
|
||||
test('BnB finds changeless match in window', () {
|
||||
final r = branchAndBound([200,100,90,80], 300, 50);
|
||||
expect(r, isNotNull); expect(r!.hasChange, isFalse);
|
||||
final vals=[200,100,90,80]; final sum=r.indices.map((i)=>vals[i]).reduce((a,b)=>a+b);
|
||||
expect(sum>=300 && sum<=350, isTrue);
|
||||
});
|
||||
test('BnB null when no subset in window', () {
|
||||
expect(branchAndBound([1000,900], 300, 5), isNull);
|
||||
});
|
||||
test('SRD with change', () {
|
||||
final r = singleRandomDraw([500,500,500,500], 700, 50, Random(1));
|
||||
expect(r, isNotNull); expect(r!.hasChange, isTrue);
|
||||
});
|
||||
test('SRD randomizes across seeds', () {
|
||||
final a = (singleRandomDraw([100,101,102,103,104,105],150,10,Random(1))!.indices..sort());
|
||||
final b = (singleRandomDraw([100,101,102,103,104,105],150,10,Random(9))!.indices..sort());
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
test('SRD null on insufficient funds', () {
|
||||
expect(singleRandomDraw([100,100], 500, 10, Random(1)), isNull);
|
||||
});
|
||||
test('selectCoins prefers changeless BnB, else SRD', () {
|
||||
expect(selectCoins(values:[200,100,90],target:300,inputCost:0,costOfChange:20,minChange:10).hasChange, isFalse);
|
||||
expect(selectCoins(values:[500,500,500],target:700,inputCost:0,costOfChange:5,minChange:10,rng:Random(1)).hasChange, isTrue);
|
||||
});
|
||||
test('selectCoins drops non-positive effective values', () {
|
||||
// coin of value 50 with inputCost 136 -> effective -86 -> dropped; only the 1000 usable
|
||||
final r = selectCoins(values:[50,1000],target:500,inputCost:136,costOfChange:5,minChange:10,rng:Random(1));
|
||||
expect(r.indices, equals([1]));
|
||||
});
|
||||
test('changelessMatch finds a set whose effective sum lands in the window', () {
|
||||
// inputCost 100: values [50, 400, 300, 200] -> eff [dropped, 300, 200, 100]
|
||||
// target 500, window 46: eff {300, 200} = 500, exact
|
||||
final r = changelessMatch(
|
||||
values: [50, 400, 300, 200], target: 500, inputCosts: [100, 100, 100, 100], window: 46);
|
||||
expect(r, isNotNull);
|
||||
expect(r!.hasChange, isFalse);
|
||||
final effSum = r.indices.map((i) => [50, 400, 300, 200][i] - 100).reduce((a, b) => a + b);
|
||||
expect(effSum >= 500 && effSum <= 546, isTrue);
|
||||
expect(r.indices.contains(0), isFalse); // negative-eff coin never selected
|
||||
});
|
||||
test('changelessMatch returns null when no subset lands in the window', () {
|
||||
expect(
|
||||
changelessMatch(values: [10000, 9000], target: 500, inputCosts: [100, 100], window: 46),
|
||||
isNull);
|
||||
});
|
||||
test('selectCoins throws when insufficient', () {
|
||||
expect(() => selectCoins(values:[100,100],target:500,inputCost:0,costOfChange:5,minChange:10),
|
||||
throwsA(isA<InsufficientFundsException>()));
|
||||
});
|
||||
test('changeless pipeline: leftover absorbed into fee is non-negative and below dust', () {
|
||||
// Mirrors the _createUTXOS / estimateTxForAmount arithmetic with the wallet's
|
||||
// 68*inputs + 34*outputs + 10 vBytes model, at 10 sat/vB with 1 recipient output.
|
||||
const feeRate = 10;
|
||||
const amount = 50000;
|
||||
final values = [60700, 30000, 21800, 9000, 5000];
|
||||
const target = amount + (34 * 1 + 10) * feeRate;
|
||||
final r = changelessMatch(
|
||||
values: values,
|
||||
target: target,
|
||||
inputCosts: List.filled(values.length, 68 * feeRate),
|
||||
window: 546);
|
||||
expect(r, isNotNull);
|
||||
final inAmount = r!.indices.map((i) => values[i]).reduce((a, b) => a + b);
|
||||
final feeNoChange = (68 * r.indices.length + 34 * 1 + 10) * feeRate;
|
||||
final leftover = inAmount - amount - feeNoChange;
|
||||
expect(leftover >= 0, isTrue); // the caller never recurses for more inputs
|
||||
expect(leftover <= 546, isTrue); // fee overpay is bounded by the dust limit
|
||||
});
|
||||
test('BnB terminates and returns null on large pools with no possible match', () {
|
||||
// 300 even effective values, odd target, zero window: no subset can ever match,
|
||||
// so the search must stop at maxTries instead of exploring 2^300 branches.
|
||||
final values = List<int>.generate(300, (i) => 1000000 + i * 2);
|
||||
final r = changelessMatch(
|
||||
values: values, target: 1500001, inputCosts: List.filled(300, 10), window: 0);
|
||||
expect(r, isNull);
|
||||
});
|
||||
test('changelessMatch charges each input its own script-type cost', () {
|
||||
// Legacy input (148 vB) vs segwit input (68 vB) at 10 sat/vB: same value, but
|
||||
// the legacy coin's effective value is 800 lower. Target only reachable when
|
||||
// the cheaper segwit coin is chosen: eff segwit = 10000-680 = 9320.
|
||||
const feeRate = 10;
|
||||
final r = changelessMatch(
|
||||
values: [10000, 10000],
|
||||
target: 9320,
|
||||
inputCosts: [148 * feeRate, 68 * feeRate],
|
||||
window: 0,
|
||||
);
|
||||
expect(r, isNotNull);
|
||||
expect(r!.indices, equals([1]));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:cw_bitcoin/locktime.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('antiFeeSnipingLocktime', () {
|
||||
test('returns 0 when not synced', () {
|
||||
expect(antiFeeSnipingLocktime(chainTip: 800000, synced: false), 0);
|
||||
});
|
||||
|
||||
test('returns 0 when chainTip <= 0', () {
|
||||
expect(antiFeeSnipingLocktime(chainTip: 0, synced: true), 0);
|
||||
});
|
||||
|
||||
test('returns the current tip when synced', () {
|
||||
expect(antiFeeSnipingLocktime(chainTip: 800000, synced: true), 800000);
|
||||
});
|
||||
});
|
||||
|
||||
group('locktimeToBytes', () {
|
||||
test('encodes 0 as four zero bytes', () {
|
||||
expect(locktimeToBytes(0), [0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
test('encodes a block height little-endian', () {
|
||||
expect(locktimeToBytes(800000), [0x00, 0x35, 0x0C, 0x00]);
|
||||
});
|
||||
|
||||
test('encodes max 32-bit value', () {
|
||||
expect(locktimeToBytes(0xFFFFFFFF), [0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('orderOutputs', () {
|
||||
test('none preserves the given order', () {
|
||||
final outputs = [0, 1, 2, 3, 4];
|
||||
expect(orderOutputs(outputs, BitcoinOrdering.none), outputs);
|
||||
});
|
||||
|
||||
test('does not mutate the input list', () {
|
||||
final outputs = [0, 1, 2, 3, 4];
|
||||
orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(1));
|
||||
expect(outputs, [0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test('returns a new list instance', () {
|
||||
final outputs = [0, 1, 2];
|
||||
expect(identical(orderOutputs(outputs, BitcoinOrdering.none), outputs), isFalse);
|
||||
});
|
||||
|
||||
test('shuffle preserves the multiset of outputs', () {
|
||||
final outputs = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(7));
|
||||
expect(shuffled.length, outputs.length);
|
||||
expect(shuffled.toSet(), outputs.toSet());
|
||||
});
|
||||
|
||||
test('shuffle is deterministic for a given seed', () {
|
||||
final outputs = [0, 1, 2, 3, 4, 5];
|
||||
final a = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
|
||||
final b = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
|
||||
expect(a, b);
|
||||
});
|
||||
|
||||
test('shuffle does not keep the change output deterministically last', () {
|
||||
// Last element (99) models the change output, appended last today.
|
||||
final outputs = [0, 1, 2, 3, 99];
|
||||
final changePositions = <int>{};
|
||||
for (var seed = 0; seed < 50; seed++) {
|
||||
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(seed));
|
||||
changePositions.add(shuffled.indexOf(99));
|
||||
}
|
||||
// Across seeds the change lands in more than one position, and not always last.
|
||||
expect(changePositions.length, greaterThan(1));
|
||||
expect(changePositions, isNot(equals({outputs.length - 1})));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user