mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
feat: tx broadcast, error handling, remove electrum.dart
This commit is contained in:
parent
6cbb4c60d6
commit
51590a668d
10 changed files with 728 additions and 832 deletions
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
|
||||
abstract class BaseBitcoinAddressRecord {
|
||||
|
@ -84,11 +85,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
|||
throw ArgumentError('either scriptHash or network must be provided');
|
||||
}
|
||||
|
||||
try {
|
||||
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
|
||||
} catch (_) {
|
||||
this.scriptHash = '';
|
||||
}
|
||||
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
|
||||
}
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
||||
|
@ -211,7 +208,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
|||
}
|
||||
|
||||
class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
||||
final ECPrivate spendKey;
|
||||
final String tweak;
|
||||
|
||||
BitcoinReceivedSPAddressRecord(
|
||||
super.address, {
|
||||
|
@ -220,11 +217,32 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
|||
super.balance = 0,
|
||||
super.name = '',
|
||||
super.isUsed = false,
|
||||
required this.spendKey,
|
||||
required this.tweak,
|
||||
super.addressType = SegwitAddresType.p2tr,
|
||||
super.labelHex,
|
||||
}) : super(isHidden: true);
|
||||
|
||||
SilentPaymentOwner getSPWallet(
|
||||
List<SilentPaymentOwner> silentPaymentsWallets, [
|
||||
BasedUtxoNetwork network = BitcoinNetwork.mainnet,
|
||||
]) {
|
||||
final spAddress = silentPaymentsWallets.firstWhere(
|
||||
(wallet) => wallet.toAddress(network) == this.address,
|
||||
orElse: () => throw ArgumentError('SP wallet not found'),
|
||||
);
|
||||
|
||||
return spAddress;
|
||||
}
|
||||
|
||||
ECPrivate getSpendKey(
|
||||
List<SilentPaymentOwner> silentPaymentsWallets, [
|
||||
BasedUtxoNetwork network = BitcoinNetwork.mainnet,
|
||||
]) {
|
||||
return getSPWallet(silentPaymentsWallets, network)
|
||||
.b_spend
|
||||
.tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(tweak)));
|
||||
}
|
||||
|
||||
factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
|
@ -236,9 +254,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
|||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0,
|
||||
labelHex: decoded['label'] as String?,
|
||||
spendKey: (decoded['spendKey'] as String?) == null
|
||||
? ECPrivate.random()
|
||||
: ECPrivate.fromHex(decoded['spendKey'] as String),
|
||||
tweak: decoded['tweak'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -252,6 +268,6 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
|||
'balance': balance,
|
||||
'type': addressType.toString(),
|
||||
'labelHex': labelHex,
|
||||
'spend_key': spendKey.toString(),
|
||||
'tweak': tweak,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import 'dart:convert';
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
|
||||
// import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||
|
@ -17,15 +19,14 @@ import 'package:cw_bitcoin/electrum_balance.dart';
|
|||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
// import 'package:cw_core/get_height_by_date.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/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:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
|
@ -251,7 +252,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
final passphrase = keysData.passphrase;
|
||||
|
||||
if (mnemonic != null) {
|
||||
for (final derivation in walletInfo.derivations ?? <DerivationInfo>[]) {
|
||||
final derivations = walletInfo.derivations ?? <DerivationInfo>[];
|
||||
|
||||
for (final derivation in derivations) {
|
||||
if (derivation.description?.contains("SP") ?? false) {
|
||||
continue;
|
||||
}
|
||||
|
@ -289,16 +292,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
hdWallets[CWBitcoinDerivationType.electrum]!;
|
||||
}
|
||||
|
||||
switch (walletInfo.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
break;
|
||||
if (derivations.isEmpty) {
|
||||
switch (walletInfo.derivationInfo?.derivationType) {
|
||||
case DerivationType.bip39:
|
||||
seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
break;
|
||||
case DerivationType.electrum:
|
||||
default:
|
||||
seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||
hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -914,4 +919,566 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
super.syncStatusReaction(syncStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> calcFee({
|
||||
required List<UtxoWithAddress> utxos,
|
||||
required List<BitcoinBaseOutput> outputs,
|
||||
String? memo,
|
||||
required int feeRate,
|
||||
List<ECPrivateInfo>? inputPrivKeyInfos,
|
||||
List<Outpoint>? vinOutpoints,
|
||||
}) async =>
|
||||
feeRate *
|
||||
BitcoinTransactionBuilder.estimateTransactionSize(
|
||||
utxos: utxos,
|
||||
outputs: outputs,
|
||||
network: network,
|
||||
memo: memo,
|
||||
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||
vinOutpoints: vinOutpoints,
|
||||
);
|
||||
|
||||
@override
|
||||
TxCreateUtxoDetails createUTXOS({
|
||||
required bool sendAll,
|
||||
bool paysToSilentPayment = false,
|
||||
int credentialsAmount = 0,
|
||||
int? inputsCount,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) {
|
||||
List<UtxoWithAddress> utxos = [];
|
||||
List<Outpoint> vinOutpoints = [];
|
||||
List<ECPrivateInfo> inputPrivKeyInfos = [];
|
||||
final publicKeys = <String, PublicKeyWithDerivationPath>{};
|
||||
int allInputsAmount = 0;
|
||||
bool spendsSilentPayment = false;
|
||||
bool spendsUnconfirmedTX = false;
|
||||
|
||||
int leftAmount = credentialsAmount;
|
||||
var availableInputs = unspentCoins.where((utx) {
|
||||
if (!utx.isSending || utx.isFrozen) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
|
||||
|
||||
for (int i = 0; i < availableInputs.length; i++) {
|
||||
final utx = availableInputs[i];
|
||||
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
|
||||
|
||||
if (paysToSilentPayment) {
|
||||
// Check inputs for shared secret derivation
|
||||
if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) {
|
||||
throw BitcoinTransactionSilentPaymentsNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
allInputsAmount += utx.value;
|
||||
leftAmount = leftAmount - utx.value;
|
||||
|
||||
final address = RegexUtils.addressTypeFromStr(utx.address, network);
|
||||
ECPrivate? privkey;
|
||||
bool? isSilentPayment = false;
|
||||
|
||||
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
||||
privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).getSpendKey(
|
||||
(walletAddresses as BitcoinWalletAddresses).silentPaymentWallets,
|
||||
network,
|
||||
);
|
||||
spendsSilentPayment = true;
|
||||
isSilentPayment = true;
|
||||
} else if (!isHardwareWallet) {
|
||||
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
|
||||
final path = addressRecord.derivationInfo.derivationPath
|
||||
.addElem(Bip32KeyIndex(
|
||||
BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange),
|
||||
))
|
||||
.addElem(Bip32KeyIndex(addressRecord.index));
|
||||
|
||||
privkey = ECPrivate.fromBip32(bip32: bip32.derive(path));
|
||||
}
|
||||
|
||||
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
|
||||
String pubKeyHex;
|
||||
|
||||
if (privkey != null) {
|
||||
inputPrivKeyInfos.add(ECPrivateInfo(
|
||||
privkey,
|
||||
address.type == SegwitAddresType.p2tr,
|
||||
tweak: !isSilentPayment,
|
||||
));
|
||||
|
||||
pubKeyHex = privkey.getPublic().toHex();
|
||||
} else {
|
||||
pubKeyHex = walletAddresses.hdWallet
|
||||
.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index))
|
||||
.publicKey
|
||||
.toHex();
|
||||
}
|
||||
|
||||
if (utx.bitcoinAddressRecord is BitcoinAddressRecord) {
|
||||
final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord)
|
||||
.derivationInfo
|
||||
.derivationPath
|
||||
.toString();
|
||||
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
|
||||
}
|
||||
|
||||
utxos.add(
|
||||
UtxoWithAddress(
|
||||
utxo: BitcoinUtxo(
|
||||
txHash: utx.hash,
|
||||
value: BigInt.from(utx.value),
|
||||
vout: utx.vout,
|
||||
scriptType: BitcoinAddressUtils.getScriptType(address),
|
||||
isSilentPayment: isSilentPayment,
|
||||
),
|
||||
ownerDetails: UtxoAddressDetails(
|
||||
publicKey: pubKeyHex,
|
||||
address: address,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// sendAll continues for all inputs
|
||||
if (!sendAll) {
|
||||
bool amountIsAcquired = leftAmount <= 0;
|
||||
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (utxos.isEmpty) {
|
||||
throw BitcoinTransactionNoInputsException();
|
||||
}
|
||||
|
||||
return TxCreateUtxoDetails(
|
||||
availableInputs: availableInputs,
|
||||
unconfirmedCoins: unconfirmedCoins,
|
||||
utxos: utxos,
|
||||
vinOutpoints: vinOutpoints,
|
||||
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||
publicKeys: publicKeys,
|
||||
allInputsAmount: allInputsAmount,
|
||||
spendsSilentPayment: spendsSilentPayment,
|
||||
spendsUnconfirmedTX: spendsUnconfirmedTX,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EstimatedTxResult> estimateSendAllTx(
|
||||
List<BitcoinOutput> outputs,
|
||||
int feeRate, {
|
||||
String? memo,
|
||||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment);
|
||||
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
outputs: outputs,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionNoFeeException();
|
||||
}
|
||||
|
||||
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
|
||||
int amount = utxoDetails.allInputsAmount - fee;
|
||||
|
||||
if (amount <= 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
|
||||
}
|
||||
|
||||
// Attempting to send less than the dust limit
|
||||
if (isBelowDust(amount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
if (outputs.length == 1) {
|
||||
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
|
||||
}
|
||||
|
||||
return EstimatedTxResult(
|
||||
utxos: utxoDetails.utxos,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
publicKeys: utxoDetails.publicKeys,
|
||||
fee: fee,
|
||||
amount: amount,
|
||||
isSendAll: true,
|
||||
hasChange: false,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EstimatedTxResult> estimateTxForAmount(
|
||||
int credentialsAmount,
|
||||
List<BitcoinOutput> outputs,
|
||||
int feeRate, {
|
||||
List<BitcoinOutput> updatedOutputs = const [],
|
||||
int? inputsCount,
|
||||
String? memo,
|
||||
bool? useUnconfirmed,
|
||||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
// Attempting to send less than the dust limit
|
||||
if (isBelowDust(credentialsAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
final utxoDetails = createUTXOS(
|
||||
sendAll: false,
|
||||
credentialsAmount: credentialsAmount,
|
||||
inputsCount: inputsCount,
|
||||
paysToSilentPayment: hasSilentPayment,
|
||||
);
|
||||
|
||||
final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length;
|
||||
final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX &&
|
||||
utxoDetails.utxos.length ==
|
||||
utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length;
|
||||
|
||||
// How much is being spent - how much is being sent
|
||||
int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount;
|
||||
|
||||
if (amountLeftForChangeAndFee <= 0) {
|
||||
if (!spendingAllCoins) {
|
||||
return estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
feeRate,
|
||||
updatedOutputs: updatedOutputs,
|
||||
inputsCount: utxoDetails.utxos.length + 1,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
);
|
||||
}
|
||||
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
final changeAddress = await walletAddresses.getChangeAddress(
|
||||
inputs: utxoDetails.availableInputs,
|
||||
outputs: updatedOutputs,
|
||||
);
|
||||
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
|
||||
updatedOutputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(amountLeftForChangeAndFee),
|
||||
isChange: true,
|
||||
));
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(amountLeftForChangeAndFee),
|
||||
isChange: true,
|
||||
));
|
||||
|
||||
// Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
|
||||
final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString();
|
||||
utxoDetails.publicKeys[address.pubKeyHash()] =
|
||||
PublicKeyWithDerivationPath('', changeDerivationPath);
|
||||
|
||||
// calcFee updates the silent payment outputs to calculate the tx size accounting
|
||||
// for taproot addresses, but if more inputs are needed to make up for fees,
|
||||
// the silent payment outputs need to be recalculated for the new inputs
|
||||
var temp = outputs.map((output) => output).toList();
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
// Always take only not updated bitcoin outputs here so for every estimation
|
||||
// the SP outputs are re-generated to the proper taproot addresses
|
||||
outputs: temp,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
vinOutpoints: utxoDetails.vinOutpoints,
|
||||
);
|
||||
|
||||
updatedOutputs.clear();
|
||||
updatedOutputs.addAll(temp);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionNoFeeException();
|
||||
}
|
||||
|
||||
int amount = credentialsAmount;
|
||||
final lastOutput = updatedOutputs.last;
|
||||
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
||||
|
||||
if (isBelowDust(amountLeftForChange)) {
|
||||
// If has change that is lower than dust, will end up with tx rejected by network rules
|
||||
// so remove the change amount
|
||||
updatedOutputs.removeLast();
|
||||
outputs.removeLast();
|
||||
|
||||
if (amountLeftForChange < 0) {
|
||||
if (!spendingAllCoins) {
|
||||
return estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
feeRate,
|
||||
updatedOutputs: updatedOutputs,
|
||||
inputsCount: utxoDetails.utxos.length + 1,
|
||||
memo: memo,
|
||||
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
);
|
||||
} else {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
}
|
||||
|
||||
return EstimatedTxResult(
|
||||
utxos: utxoDetails.utxos,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
publicKeys: utxoDetails.publicKeys,
|
||||
fee: fee,
|
||||
amount: amount,
|
||||
hasChange: false,
|
||||
isSendAll: spendingAllCoins,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
} else {
|
||||
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
||||
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
value: BigInt.from(amountLeftForChange),
|
||||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
isChange: true,
|
||||
);
|
||||
outputs[outputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
value: BigInt.from(amountLeftForChange),
|
||||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
isChange: true,
|
||||
);
|
||||
|
||||
return EstimatedTxResult(
|
||||
utxos: utxoDetails.utxos,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
publicKeys: utxoDetails.publicKeys,
|
||||
fee: fee,
|
||||
amount: amount,
|
||||
hasChange: true,
|
||||
isSendAll: spendingAllCoins,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
try {
|
||||
final outputs = <BitcoinOutput>[];
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final hasMultiDestination = transactionCredentials.outputs.length > 1;
|
||||
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
|
||||
final memo = transactionCredentials.outputs.first.memo;
|
||||
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
|
||||
|
||||
int credentialsAmount = 0;
|
||||
bool hasSilentPayment = false;
|
||||
|
||||
for (final out in transactionCredentials.outputs) {
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
|
||||
if (!sendAll && isBelowDust(outputAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (out.sendAll) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
}
|
||||
|
||||
credentialsAmount += outputAmount;
|
||||
|
||||
final address = RegexUtils.addressTypeFromStr(
|
||||
out.isParsedAddress ? out.extractedAddress! : out.address, network);
|
||||
final isSilentPayment = address is SilentPaymentAddress;
|
||||
|
||||
if (isSilentPayment) {
|
||||
hasSilentPayment = true;
|
||||
}
|
||||
|
||||
if (sendAll) {
|
||||
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(0),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
} else {
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(outputAmount),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final feeRateInt = transactionCredentials.feeRate != null
|
||||
? transactionCredentials.feeRate!
|
||||
: feeRate(transactionCredentials.priority!);
|
||||
|
||||
EstimatedTxResult estimatedTx;
|
||||
final updatedOutputs = outputs
|
||||
.map((e) => BitcoinOutput(
|
||||
address: e.address,
|
||||
value: e.value,
|
||||
isSilentPayment: e.isSilentPayment,
|
||||
isChange: e.isChange,
|
||||
))
|
||||
.toList();
|
||||
|
||||
if (sendAll) {
|
||||
estimatedTx = await estimateSendAllTx(
|
||||
updatedOutputs,
|
||||
feeRateInt,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
} else {
|
||||
estimatedTx = await estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
feeRateInt,
|
||||
updatedOutputs: updatedOutputs,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
}
|
||||
|
||||
if (walletInfo.isHardwareWallet) {
|
||||
final transaction = await buildHardwareWalletTransaction(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: updatedOutputs,
|
||||
publicKeys: estimatedTx.publicKeys,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
enableRBF: true,
|
||||
);
|
||||
|
||||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
sendWorker: sendWorker,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
feeRate: feeRateInt.toString(),
|
||||
hasChange: estimatedTx.hasChange,
|
||||
isSendAll: estimatedTx.isSendAll,
|
||||
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
});
|
||||
}
|
||||
|
||||
final txb = BitcoinTransactionBuilder(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: updatedOutputs,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
enableRBF: !estimatedTx.spendsUnconfirmedTX,
|
||||
);
|
||||
|
||||
bool hasTaprootInputs = false;
|
||||
|
||||
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
|
||||
String error = "Cannot find private key.";
|
||||
|
||||
ECPrivateInfo? key;
|
||||
|
||||
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
|
||||
error += "\nNo private keys generated.";
|
||||
} else {
|
||||
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
|
||||
|
||||
try {
|
||||
key = estimatedTx.inputPrivKeyInfos.firstWhere((element) {
|
||||
final elemPubkey = element.privkey.getPublic().toHex();
|
||||
if (elemPubkey == publicKey) {
|
||||
return true;
|
||||
} else {
|
||||
error += "\nExpected: $publicKey";
|
||||
error += "\nPubkey: $elemPubkey";
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
throw Exception(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (key == null) {
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
if (utxo.utxo.isP2tr()) {
|
||||
hasTaprootInputs = true;
|
||||
return key.privkey.signTapRoot(
|
||||
txDigest,
|
||||
sighash: sighash,
|
||||
tweak: utxo.utxo.isSilentPayment != true,
|
||||
);
|
||||
} else {
|
||||
return key.privkey.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
});
|
||||
|
||||
return PendingBitcoinTransaction(
|
||||
transaction,
|
||||
type,
|
||||
sendWorker: sendWorker,
|
||||
amount: estimatedTx.amount,
|
||||
fee: estimatedTx.fee,
|
||||
feeRate: feeRateInt.toString(),
|
||||
hasChange: estimatedTx.hasChange,
|
||||
isSendAll: estimatedTx.isSendAll,
|
||||
hasTaprootInputs: hasTaprootInputs,
|
||||
utxos: estimatedTx.utxos,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
if (estimatedTx.spendsSilentPayment) {
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
tx.unspents?.removeWhere(
|
||||
(unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash));
|
||||
transactionHistory.addOne(tx);
|
||||
});
|
||||
}
|
||||
|
||||
unspentCoins
|
||||
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
||||
|
||||
await updateBalance();
|
||||
});
|
||||
} catch (e, s) {
|
||||
print([e, s]);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,621 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
String jsonrpc(
|
||||
{required String method,
|
||||
required List<Object> params,
|
||||
required int id,
|
||||
double version = 2.0}) =>
|
||||
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
|
||||
|
||||
class SocketTask {
|
||||
SocketTask({required this.isSubscription, this.completer, this.subject});
|
||||
|
||||
final Completer<dynamic>? completer;
|
||||
final BehaviorSubject<dynamic>? subject;
|
||||
final bool isSubscription;
|
||||
}
|
||||
|
||||
class ElectrumClient {
|
||||
ElectrumClient()
|
||||
: _id = 0,
|
||||
_isConnected = false,
|
||||
_tasks = {},
|
||||
_errors = {},
|
||||
unterminatedString = '';
|
||||
|
||||
static const connectionTimeout = Duration(seconds: 5);
|
||||
static const aliveTimerDuration = Duration(seconds: 4);
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
Socket? socket;
|
||||
void Function(ConnectionStatus)? onConnectionStatusChange;
|
||||
int _id;
|
||||
final Map<String, SocketTask> _tasks;
|
||||
Map<String, SocketTask> get tasks => _tasks;
|
||||
final Map<String, String> _errors;
|
||||
ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
|
||||
bool _isConnected;
|
||||
Timer? _aliveTimer;
|
||||
String unterminatedString;
|
||||
|
||||
Uri? uri;
|
||||
bool? useSSL;
|
||||
|
||||
Future<void> connectToUri(Uri uri, {bool? useSSL}) async {
|
||||
this.uri = uri;
|
||||
if (useSSL != null) {
|
||||
this.useSSL = useSSL;
|
||||
}
|
||||
await connect(host: uri.host, port: uri.port);
|
||||
}
|
||||
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
_setConnectionStatus(ConnectionStatus.connecting);
|
||||
|
||||
try {
|
||||
await socket?.close();
|
||||
} catch (_) {}
|
||||
socket = null;
|
||||
|
||||
try {
|
||||
if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
|
||||
socket = await Socket.connect(host, port, timeout: connectionTimeout);
|
||||
} else {
|
||||
socket = await SecureSocket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is HandshakeException) {
|
||||
useSSL = !(useSSL ?? false);
|
||||
}
|
||||
|
||||
if (_connectionStatus != ConnectionStatus.connecting) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket == null) {
|
||||
if (_connectionStatus != ConnectionStatus.connecting) {
|
||||
_setConnectionStatus(ConnectionStatus.failed);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// use ping to determine actual connection status since we could've just not timed out yet:
|
||||
// _setConnectionStatus(ConnectionStatus.connected);
|
||||
|
||||
socket!.listen(
|
||||
(Uint8List event) {
|
||||
try {
|
||||
final msg = utf8.decode(event.toList());
|
||||
final messagesList = msg.split("\n");
|
||||
for (var message in messagesList) {
|
||||
if (message.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
_parseResponse(message);
|
||||
}
|
||||
} catch (e) {
|
||||
printV("socket.listen: $e");
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
final errorMsg = error.toString();
|
||||
printV(errorMsg);
|
||||
unterminatedString = '';
|
||||
socket = null;
|
||||
},
|
||||
onDone: () {
|
||||
printV("SOCKET CLOSED!!!!!");
|
||||
unterminatedString = '';
|
||||
try {
|
||||
if (host == socket?.address.host || socket == null) {
|
||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||
socket?.destroy();
|
||||
socket = null;
|
||||
}
|
||||
} catch (e) {
|
||||
printV("onDone: $e");
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
keepAlive();
|
||||
}
|
||||
|
||||
void _parseResponse(String message) {
|
||||
try {
|
||||
final response = json.decode(message) as Map<String, dynamic>;
|
||||
_handleResponse(response);
|
||||
} on FormatException catch (e) {
|
||||
final msg = e.message.toLowerCase();
|
||||
|
||||
if (e.source is String) {
|
||||
unterminatedString += e.source as String;
|
||||
}
|
||||
|
||||
if (msg.contains("not a subtype of type")) {
|
||||
unterminatedString += e.source as String;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response = json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
_handleResponse(response);
|
||||
unterminatedString = '';
|
||||
}
|
||||
} on TypeError catch (e) {
|
||||
if (!e.toString().contains('Map<String, Object>') &&
|
||||
!e.toString().contains('Map<String, dynamic>')) {
|
||||
return;
|
||||
}
|
||||
|
||||
unterminatedString += message;
|
||||
|
||||
if (isJSONStringCorrect(unterminatedString)) {
|
||||
final response = json.decode(unterminatedString) as Map<String, dynamic>;
|
||||
_handleResponse(response);
|
||||
// unterminatedString = null;
|
||||
unterminatedString = '';
|
||||
}
|
||||
} catch (e) {
|
||||
printV("parse $e");
|
||||
}
|
||||
}
|
||||
|
||||
void keepAlive() {
|
||||
_aliveTimer?.cancel();
|
||||
_aliveTimer = Timer.periodic(aliveTimerDuration, (_) async => ping());
|
||||
}
|
||||
|
||||
Future<void> ping() async {
|
||||
try {
|
||||
await callWithTimeout(method: 'server.ping');
|
||||
_setConnectionStatus(ConnectionStatus.connected);
|
||||
} catch (_) {
|
||||
_setConnectionStatus(ConnectionStatus.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> version() =>
|
||||
call(method: 'server.version', params: ["", "1.4"]).then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) => val.toString()).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> getBalance(String scriptHash) =>
|
||||
call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
|
||||
.then((dynamic result) {
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
|
||||
call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
|
||||
.then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) {
|
||||
if (val is Map<String, dynamic>) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) async {
|
||||
final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]);
|
||||
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) {
|
||||
if (val is Map<String, dynamic>) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getMempool(String scriptHash) =>
|
||||
call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash])
|
||||
.then((dynamic result) {
|
||||
if (result is List) {
|
||||
return result.map((dynamic val) {
|
||||
if (val is Map<String, dynamic>) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
Future<dynamic> getTransaction({required String hash, required bool verbose}) async {
|
||||
try {
|
||||
final result = await callWithTimeout(
|
||||
method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000);
|
||||
return result;
|
||||
} on RequestFailedTimeoutException catch (_) {
|
||||
return <String, dynamic>{};
|
||||
} catch (e) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTransactionVerbose({required String hash}) =>
|
||||
getTransaction(hash: hash, verbose: true).then((dynamic result) {
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return <String, dynamic>{};
|
||||
});
|
||||
|
||||
Future<String> getTransactionHex({required String hash}) =>
|
||||
getTransaction(hash: hash, verbose: false).then((dynamic result) {
|
||||
if (result is String) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
Future<String> broadcastTransaction(
|
||||
{required String transactionRaw,
|
||||
BasedUtxoNetwork? network,
|
||||
Function(int)? idCallback}) async =>
|
||||
call(
|
||||
method: 'blockchain.transaction.broadcast',
|
||||
params: [transactionRaw],
|
||||
idCallback: idCallback)
|
||||
.then((dynamic result) {
|
||||
if (result is String) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
|
||||
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
|
||||
as Map<String, dynamic>;
|
||||
|
||||
Future<Map<String, dynamic>> getHeader({required int height}) async =>
|
||||
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
|
||||
|
||||
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) =>
|
||||
subscribe<Object>(
|
||||
id: 'blockchain.tweaks.subscribe',
|
||||
method: 'blockchain.tweaks.subscribe',
|
||||
params: [height, count, true],
|
||||
);
|
||||
|
||||
Future<dynamic> tweaksRegister({
|
||||
required String secViewKey,
|
||||
required String pubSpendKey,
|
||||
List<int> labels = const [],
|
||||
}) =>
|
||||
call(
|
||||
method: 'blockchain.tweaks.register',
|
||||
params: [secViewKey, pubSpendKey, labels],
|
||||
);
|
||||
|
||||
Future<dynamic> tweaksErase({required String pubSpendKey}) => call(
|
||||
method: 'blockchain.tweaks.erase',
|
||||
params: [pubSpendKey],
|
||||
);
|
||||
|
||||
BehaviorSubject<Object>? tweaksScan({required String pubSpendKey}) => subscribe<Object>(
|
||||
id: 'blockchain.tweaks.scan',
|
||||
method: 'blockchain.tweaks.scan',
|
||||
params: [pubSpendKey],
|
||||
);
|
||||
|
||||
Future<dynamic> tweaksGet({required String pubSpendKey}) => call(
|
||||
method: 'blockchain.tweaks.get',
|
||||
params: [pubSpendKey],
|
||||
);
|
||||
|
||||
Future<dynamic> getTweaks({required int height}) async =>
|
||||
await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]);
|
||||
|
||||
Future<double> estimatefee({required int p}) =>
|
||||
call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
|
||||
if (result is double) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result is String) {
|
||||
return double.parse(result);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
Future<List<List<int>>> feeHistogram() =>
|
||||
call(method: 'mempool.get_fee_histogram').then((dynamic result) {
|
||||
if (result is List) {
|
||||
// return result.map((dynamic e) {
|
||||
// if (e is List) {
|
||||
// return e.map((dynamic ee) => ee is int ? ee : null).toList();
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }).toList();
|
||||
final histogram = <List<int>>[];
|
||||
for (final e in result) {
|
||||
if (e is List) {
|
||||
final eee = <int>[];
|
||||
for (final ee in e) {
|
||||
if (ee is int) {
|
||||
eee.add(ee);
|
||||
}
|
||||
}
|
||||
histogram.add(eee);
|
||||
}
|
||||
}
|
||||
return histogram;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
// return [bottom, middle, top];
|
||||
// } catch (_) {
|
||||
// return [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
|
||||
// example response:
|
||||
// {
|
||||
// "height": 520481,
|
||||
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
|
||||
// }
|
||||
|
||||
Future<int?> getCurrentBlockChainTip() async {
|
||||
try {
|
||||
final result = await callWithTimeout(method: 'blockchain.headers.subscribe');
|
||||
if (result is Map<String, dynamic>) {
|
||||
return result["height"] as int;
|
||||
}
|
||||
return null;
|
||||
} on RequestFailedTimeoutException catch (_) {
|
||||
return null;
|
||||
} catch (e) {
|
||||
printV("getCurrentBlockChainTip: ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
BehaviorSubject<Object>? chainTipSubscribe() {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
|
||||
}
|
||||
|
||||
BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
|
||||
_id += 1;
|
||||
return subscribe<Object>(
|
||||
id: 'blockchain.scripthash.subscribe:$scripthash',
|
||||
method: 'blockchain.scripthash.subscribe',
|
||||
params: [scripthash]);
|
||||
}
|
||||
|
||||
BehaviorSubject<T>? subscribe<T>(
|
||||
{required String id, required String method, List<Object> params = const []}) {
|
||||
try {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final subscription = BehaviorSubject<T>();
|
||||
_regisrySubscription(id, subscription);
|
||||
socket!.write(jsonrpc(method: method, id: _id, params: params));
|
||||
|
||||
return subscription;
|
||||
} catch (e) {
|
||||
printV("subscribe $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> call(
|
||||
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
idCallback?.call(id);
|
||||
_registryTask(id, completer);
|
||||
socket!.write(jsonrpc(method: method, id: id, params: params));
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<dynamic> callWithTimeout(
|
||||
{required String method, List<Object> params = const [], int timeout = 5000}) async {
|
||||
try {
|
||||
if (socket == null) {
|
||||
return null;
|
||||
}
|
||||
final completer = Completer<dynamic>();
|
||||
_id += 1;
|
||||
final id = _id;
|
||||
_registryTask(id, completer);
|
||||
socket!.write(jsonrpc(method: method, id: id, params: params));
|
||||
Timer(Duration(milliseconds: timeout), () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(RequestFailedTimeoutException(method, id));
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
} catch (e) {
|
||||
printV("callWithTimeout $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
_aliveTimer?.cancel();
|
||||
try {
|
||||
await socket?.close();
|
||||
socket = null;
|
||||
} catch (_) {}
|
||||
onConnectionStatusChange = null;
|
||||
}
|
||||
|
||||
void _registryTask(int id, Completer<dynamic> completer) =>
|
||||
_tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false);
|
||||
|
||||
void _regisrySubscription(String id, BehaviorSubject<dynamic> subject) =>
|
||||
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
|
||||
|
||||
void _finish(String id, Object? data) {
|
||||
if (_tasks[id] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
|
||||
_tasks[id]?.completer!.complete(data);
|
||||
}
|
||||
|
||||
if (!(_tasks[id]?.isSubscription ?? false)) {
|
||||
_tasks.remove(id);
|
||||
} else {
|
||||
_tasks[id]?.subject?.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
void _methodHandler({required String method, required Map<String, dynamic> request}) {
|
||||
switch (method) {
|
||||
case 'blockchain.headers.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
final id = 'blockchain.headers.subscribe';
|
||||
|
||||
_tasks[id]?.subject?.add(params.last);
|
||||
break;
|
||||
case 'blockchain.scripthash.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
final scripthash = params.first as String?;
|
||||
final id = 'blockchain.scripthash.subscribe:$scripthash';
|
||||
|
||||
_tasks[id]?.subject?.add(params.last);
|
||||
break;
|
||||
case 'blockchain.headers.subscribe':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
_tasks[method]?.subject?.add(params.last);
|
||||
break;
|
||||
case 'blockchain.tweaks.subscribe':
|
||||
case 'blockchain.tweaks.scan':
|
||||
final params = request['params'] as List<dynamic>;
|
||||
_tasks[_tasks.keys.first]?.subject?.add(params.last);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _setConnectionStatus(ConnectionStatus status) {
|
||||
onConnectionStatusChange?.call(status);
|
||||
_connectionStatus = status;
|
||||
_isConnected = status == ConnectionStatus.connected;
|
||||
if (!_isConnected) {
|
||||
try {
|
||||
socket?.destroy();
|
||||
} catch (_) {}
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleResponse(Map<String, dynamic> response) {
|
||||
final method = response['method'];
|
||||
final id = response['id'] as String?;
|
||||
final result = response['result'];
|
||||
|
||||
try {
|
||||
final error = response['error'] as Map<String, dynamic>?;
|
||||
if (error != null) {
|
||||
final errorMessage = error['message'] as String?;
|
||||
if (errorMessage != null) {
|
||||
_errors[id!] = errorMessage;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
final error = response['error'] as String?;
|
||||
if (error != null) {
|
||||
_errors[id!] = error;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (method is String) {
|
||||
_methodHandler(method: method, request: response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != null) {
|
||||
_finish(id, result);
|
||||
}
|
||||
}
|
||||
|
||||
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
|
||||
}
|
||||
|
||||
// FIXME: move me
|
||||
bool isJSONStringCorrect(String source) {
|
||||
try {
|
||||
json.decode(source);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestFailedTimeoutException implements Exception {
|
||||
RequestFailedTimeoutException(this.method, this.id);
|
||||
|
||||
final String method;
|
||||
final int id;
|
||||
}
|
|
@ -62,7 +62,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
required bool isPending,
|
||||
bool isReplaced = false,
|
||||
required DateTime date,
|
||||
required int? time,
|
||||
int? time,
|
||||
bool? isDateValidated,
|
||||
required int confirmations,
|
||||
String? to,
|
||||
|
|
|
@ -417,7 +417,6 @@ abstract class ElectrumWalletBase
|
|||
_workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort);
|
||||
|
||||
_workerSubscription = receivePort!.listen((message) {
|
||||
printV('Main: received message: $message');
|
||||
if (message is SendPort) {
|
||||
workerSendPort = message;
|
||||
workerSendPort!.send(
|
||||
|
@ -439,13 +438,12 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
int get _dustAmount => 546;
|
||||
int get _dustAmount => 0;
|
||||
|
||||
bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
|
||||
bool isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
|
||||
|
||||
TxCreateUtxoDetails _createUTXOS({
|
||||
TxCreateUtxoDetails createUTXOS({
|
||||
required bool sendAll,
|
||||
required bool paysToSilentPayment,
|
||||
int credentialsAmount = 0,
|
||||
int? inputsCount,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
|
@ -455,7 +453,6 @@ abstract class ElectrumWalletBase
|
|||
List<ECPrivateInfo> inputPrivKeyInfos = [];
|
||||
final publicKeys = <String, PublicKeyWithDerivationPath>{};
|
||||
int allInputsAmount = 0;
|
||||
bool spendsSilentPayment = false;
|
||||
bool spendsUnconfirmedTX = false;
|
||||
|
||||
int leftAmount = credentialsAmount;
|
||||
|
@ -483,25 +480,13 @@ abstract class ElectrumWalletBase
|
|||
final utx = availableInputs[i];
|
||||
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
|
||||
|
||||
if (paysToSilentPayment) {
|
||||
// Check inputs for shared secret derivation
|
||||
if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) {
|
||||
throw BitcoinTransactionSilentPaymentsNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
allInputsAmount += utx.value;
|
||||
leftAmount = leftAmount - utx.value;
|
||||
|
||||
final address = RegexUtils.addressTypeFromStr(utx.address, network);
|
||||
ECPrivate? privkey;
|
||||
bool? isSilentPayment = false;
|
||||
|
||||
if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) {
|
||||
privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey;
|
||||
spendsSilentPayment = true;
|
||||
isSilentPayment = true;
|
||||
} else if (!isHardwareWallet) {
|
||||
if (!isHardwareWallet) {
|
||||
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
|
||||
final path = addressRecord.derivationInfo.derivationPath
|
||||
.addElem(Bip32KeyIndex(
|
||||
|
@ -516,11 +501,7 @@ abstract class ElectrumWalletBase
|
|||
String pubKeyHex;
|
||||
|
||||
if (privkey != null) {
|
||||
inputPrivKeyInfos.add(ECPrivateInfo(
|
||||
privkey,
|
||||
address.type == SegwitAddresType.p2tr,
|
||||
tweak: !isSilentPayment,
|
||||
));
|
||||
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
|
||||
|
||||
pubKeyHex = privkey.getPublic().toHex();
|
||||
} else {
|
||||
|
@ -545,7 +526,6 @@ abstract class ElectrumWalletBase
|
|||
value: BigInt.from(utx.value),
|
||||
vout: utx.vout,
|
||||
scriptType: BitcoinAddressUtils.getScriptType(address),
|
||||
isSilentPayment: isSilentPayment,
|
||||
),
|
||||
ownerDetails: UtxoAddressDetails(
|
||||
publicKey: pubKeyHex,
|
||||
|
@ -575,7 +555,6 @@ abstract class ElectrumWalletBase
|
|||
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||
publicKeys: publicKeys,
|
||||
allInputsAmount: allInputsAmount,
|
||||
spendsSilentPayment: spendsSilentPayment,
|
||||
spendsUnconfirmedTX: spendsUnconfirmedTX,
|
||||
);
|
||||
}
|
||||
|
@ -584,14 +563,9 @@ abstract class ElectrumWalletBase
|
|||
List<BitcoinOutput> outputs,
|
||||
int feeRate, {
|
||||
String? memo,
|
||||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
final utxoDetails = _createUTXOS(
|
||||
sendAll: true,
|
||||
paysToSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom);
|
||||
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
|
@ -612,7 +586,7 @@ abstract class ElectrumWalletBase
|
|||
}
|
||||
|
||||
// Attempting to send less than the dust limit
|
||||
if (_isBelowDust(amount)) {
|
||||
if (isBelowDust(amount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
|
@ -630,31 +604,27 @@ abstract class ElectrumWalletBase
|
|||
hasChange: false,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
|
||||
Future<EstimatedTxResult> estimateTxForAmount(
|
||||
int credentialsAmount,
|
||||
List<BitcoinOutput> outputs,
|
||||
List<BitcoinOutput> updatedOutputs,
|
||||
int feeRate, {
|
||||
int? inputsCount,
|
||||
String? memo,
|
||||
bool? useUnconfirmed,
|
||||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
// Attempting to send less than the dust limit
|
||||
if (_isBelowDust(credentialsAmount)) {
|
||||
if (isBelowDust(credentialsAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
final utxoDetails = _createUTXOS(
|
||||
final utxoDetails = createUTXOS(
|
||||
sendAll: false,
|
||||
credentialsAmount: credentialsAmount,
|
||||
inputsCount: inputsCount,
|
||||
paysToSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
|
||||
|
@ -671,11 +641,9 @@ abstract class ElectrumWalletBase
|
|||
return estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
updatedOutputs,
|
||||
feeRate,
|
||||
inputsCount: utxoDetails.utxos.length + 1,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
}
|
||||
|
@ -685,14 +653,9 @@ abstract class ElectrumWalletBase
|
|||
|
||||
final changeAddress = await walletAddresses.getChangeAddress(
|
||||
inputs: utxoDetails.availableInputs,
|
||||
outputs: updatedOutputs,
|
||||
outputs: outputs,
|
||||
);
|
||||
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
|
||||
updatedOutputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(amountLeftForChangeAndFee),
|
||||
isChange: true,
|
||||
));
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(amountLeftForChangeAndFee),
|
||||
|
@ -703,34 +666,25 @@ abstract class ElectrumWalletBase
|
|||
utxoDetails.publicKeys[address.pubKeyHash()] =
|
||||
PublicKeyWithDerivationPath('', changeDerivationPath);
|
||||
|
||||
// calcFee updates the silent payment outputs to calculate the tx size accounting
|
||||
// for taproot addresses, but if more inputs are needed to make up for fees,
|
||||
// the silent payment outputs need to be recalculated for the new inputs
|
||||
var temp = outputs.map((output) => output).toList();
|
||||
int fee = await calcFee(
|
||||
utxos: utxoDetails.utxos,
|
||||
// Always take only not updated bitcoin outputs here so for every estimation
|
||||
// the SP outputs are re-generated to the proper taproot addresses
|
||||
outputs: temp,
|
||||
outputs: outputs,
|
||||
memo: memo,
|
||||
feeRate: feeRate,
|
||||
);
|
||||
|
||||
updatedOutputs.clear();
|
||||
updatedOutputs.addAll(temp);
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionNoFeeException();
|
||||
}
|
||||
|
||||
int amount = credentialsAmount;
|
||||
final lastOutput = updatedOutputs.last;
|
||||
final lastOutput = outputs.last;
|
||||
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
||||
|
||||
if (_isBelowDust(amountLeftForChange)) {
|
||||
if (isBelowDust(amountLeftForChange)) {
|
||||
// If has change that is lower than dust, will end up with tx rejected by network rules
|
||||
// so remove the change amount
|
||||
updatedOutputs.removeLast();
|
||||
outputs.removeLast();
|
||||
outputs.removeLast();
|
||||
|
||||
if (amountLeftForChange < 0) {
|
||||
|
@ -738,12 +692,10 @@ abstract class ElectrumWalletBase
|
|||
return estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
updatedOutputs,
|
||||
feeRate,
|
||||
inputsCount: utxoDetails.utxos.length + 1,
|
||||
memo: memo,
|
||||
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
} else {
|
||||
|
@ -761,20 +713,12 @@ abstract class ElectrumWalletBase
|
|||
isSendAll: spendingAllCoins,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
} else {
|
||||
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
||||
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
value: BigInt.from(amountLeftForChange),
|
||||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
isChange: true,
|
||||
);
|
||||
outputs[outputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
value: BigInt.from(amountLeftForChange),
|
||||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
isChange: true,
|
||||
);
|
||||
|
||||
|
@ -788,7 +732,6 @@ abstract class ElectrumWalletBase
|
|||
isSendAll: spendingAllCoins,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -818,12 +761,11 @@ abstract class ElectrumWalletBase
|
|||
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
|
||||
|
||||
int credentialsAmount = 0;
|
||||
bool hasSilentPayment = false;
|
||||
|
||||
for (final out in transactionCredentials.outputs) {
|
||||
final outputAmount = out.formattedCryptoAmount!;
|
||||
|
||||
if (!sendAll && _isBelowDust(outputAmount)) {
|
||||
if (!sendAll && isBelowDust(outputAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
|
@ -836,25 +778,20 @@ abstract class ElectrumWalletBase
|
|||
credentialsAmount += outputAmount;
|
||||
|
||||
final address = RegexUtils.addressTypeFromStr(
|
||||
out.isParsedAddress ? out.extractedAddress! : out.address, network);
|
||||
final isSilentPayment = address is SilentPaymentAddress;
|
||||
|
||||
if (isSilentPayment) {
|
||||
hasSilentPayment = true;
|
||||
}
|
||||
out.isParsedAddress ? out.extractedAddress! : out.address,
|
||||
network,
|
||||
);
|
||||
|
||||
if (sendAll) {
|
||||
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(0),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
} else {
|
||||
outputs.add(BitcoinOutput(
|
||||
address: address,
|
||||
value: BigInt.from(outputAmount),
|
||||
isSilentPayment: isSilentPayment,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -864,31 +801,19 @@ abstract class ElectrumWalletBase
|
|||
: feeRate(transactionCredentials.priority!);
|
||||
|
||||
EstimatedTxResult estimatedTx;
|
||||
final updatedOutputs = outputs
|
||||
.map((e) => BitcoinOutput(
|
||||
address: e.address,
|
||||
value: e.value,
|
||||
isSilentPayment: e.isSilentPayment,
|
||||
isChange: e.isChange,
|
||||
))
|
||||
.toList();
|
||||
|
||||
if (sendAll) {
|
||||
estimatedTx = await estimateSendAllTx(
|
||||
updatedOutputs,
|
||||
outputs,
|
||||
feeRateInt,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
} else {
|
||||
estimatedTx = await estimateTxForAmount(
|
||||
credentialsAmount,
|
||||
outputs,
|
||||
updatedOutputs,
|
||||
feeRateInt,
|
||||
memo: memo,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
}
|
||||
|
@ -896,7 +821,7 @@ abstract class ElectrumWalletBase
|
|||
if (walletInfo.isHardwareWallet) {
|
||||
final transaction = await buildHardwareWalletTransaction(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: updatedOutputs,
|
||||
outputs: outputs,
|
||||
publicKeys: estimatedTx.publicKeys,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network,
|
||||
|
@ -925,7 +850,7 @@ abstract class ElectrumWalletBase
|
|||
if (network is BitcoinCashNetwork) {
|
||||
txb = ForkedTransactionBuilder(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: updatedOutputs,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
|
@ -935,7 +860,7 @@ abstract class ElectrumWalletBase
|
|||
} else {
|
||||
txb = BitcoinTransactionBuilder(
|
||||
utxos: estimatedTx.utxos,
|
||||
outputs: updatedOutputs,
|
||||
outputs: outputs,
|
||||
fee: BigInt.from(estimatedTx.fee),
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
|
@ -974,11 +899,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
if (utxo.utxo.isP2tr()) {
|
||||
hasTaprootInputs = true;
|
||||
return key.privkey.signTapRoot(
|
||||
txDigest,
|
||||
sighash: sighash,
|
||||
tweak: utxo.utxo.isSilentPayment != true,
|
||||
);
|
||||
return key.privkey.signTapRoot(txDigest, sighash: sighash);
|
||||
} else {
|
||||
return key.privkey.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
|
@ -997,20 +918,14 @@ abstract class ElectrumWalletBase
|
|||
utxos: estimatedTx.utxos,
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.addOne(transaction);
|
||||
if (estimatedTx.spendsSilentPayment) {
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
tx.unspents?.removeWhere(
|
||||
(unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash));
|
||||
transactionHistory.addOne(tx);
|
||||
});
|
||||
}
|
||||
|
||||
unspentCoins
|
||||
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
||||
|
||||
await updateBalance();
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
print([e, s]);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -1974,7 +1889,7 @@ class EstimatedTxResult {
|
|||
required this.hasChange,
|
||||
required this.isSendAll,
|
||||
this.memo,
|
||||
required this.spendsSilentPayment,
|
||||
this.spendsSilentPayment = false,
|
||||
required this.spendsUnconfirmedTX,
|
||||
});
|
||||
|
||||
|
@ -1985,7 +1900,6 @@ class EstimatedTxResult {
|
|||
final int amount;
|
||||
final bool spendsSilentPayment;
|
||||
|
||||
// final bool sendsToSilentPayment;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
final String? memo;
|
||||
|
@ -2018,7 +1932,7 @@ class TxCreateUtxoDetails {
|
|||
required this.inputPrivKeyInfos,
|
||||
required this.publicKeys,
|
||||
required this.allInputsAmount,
|
||||
required this.spendsSilentPayment,
|
||||
this.spendsSilentPayment = false,
|
||||
required this.spendsUnconfirmedTX,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -382,11 +382,20 @@ class ElectrumWorker {
|
|||
}
|
||||
|
||||
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
|
||||
final rpcId = _electrumClient!.id + 1;
|
||||
final txHash = await _electrumClient!.request(
|
||||
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
|
||||
);
|
||||
|
||||
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
|
||||
if (txHash == null) {
|
||||
final error = (_electrumClient!.rpc as ElectrumSSLService).getError(rpcId);
|
||||
|
||||
if (error?.message != null) {
|
||||
return _sendError(ElectrumWorkerBroadcastError(error: error!.message, id: request.id));
|
||||
}
|
||||
} else {
|
||||
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
|
||||
|
@ -586,6 +595,7 @@ class ElectrumWorker {
|
|||
if (scanData.shouldSwitchNodes) {
|
||||
scanningClient = await ElectrumApiProvider.connect(
|
||||
ElectrumTCPService.connect(
|
||||
// TODO: ssl
|
||||
Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
||||
),
|
||||
);
|
||||
|
@ -714,7 +724,6 @@ class ElectrumWorker {
|
|||
date: scanData.network == BitcoinNetwork.mainnet
|
||||
? getDateByBitcoinHeight(tweakHeight)
|
||||
: DateTime.now(),
|
||||
time: null,
|
||||
confirmations: scanData.chainTip - tweakHeight + 1,
|
||||
unspents: [],
|
||||
isReceivedSilentPayment: true,
|
||||
|
@ -736,10 +745,7 @@ class ElectrumWorker {
|
|||
receivingOutputAddress,
|
||||
labelIndex: 1, // TODO: get actual index/label
|
||||
isUsed: true,
|
||||
// TODO: use right wallet
|
||||
spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd(
|
||||
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
|
||||
),
|
||||
tweak: t_k,
|
||||
txCount: 1,
|
||||
balance: amount,
|
||||
);
|
||||
|
|
|
@ -559,7 +559,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
direction: TransactionDirection.incoming,
|
||||
isPending: utxo.height == 0,
|
||||
date: date,
|
||||
time: null,
|
||||
confirmations: confirmations,
|
||||
inputAddresses: [],
|
||||
outputAddresses: [utxo.outputId],
|
||||
|
@ -763,7 +762,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
direction: TransactionDirection.outgoing,
|
||||
isPending: false,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
|
||||
time: null,
|
||||
confirmations: 1,
|
||||
inputAddresses: inputAddresses.toList(),
|
||||
outputAddresses: [],
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
|
@ -24,6 +26,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
this.hasTaprootInputs = false,
|
||||
this.isMweb = false,
|
||||
this.utxos = const [],
|
||||
this.hasSilentPayment = false,
|
||||
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
|
@ -59,7 +62,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
|
||||
List<TxOutput> get outputs => _tx.outputs;
|
||||
|
||||
bool get hasSilentPayment => _tx.hasSilentPayment;
|
||||
bool hasSilentPayment;
|
||||
|
||||
PendingChange? get change {
|
||||
try {
|
||||
|
@ -76,41 +79,41 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
||||
|
||||
Future<void> _commit() async {
|
||||
int? callId;
|
||||
final result = await sendWorker(
|
||||
ElectrumWorkerBroadcastRequest(transactionRaw: hex),
|
||||
) as String;
|
||||
|
||||
final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
|
||||
String? error;
|
||||
try {
|
||||
final resultJson = jsonDecode(result) as Map<String, dynamic>;
|
||||
error = resultJson["error"] as String;
|
||||
} catch (_) {}
|
||||
|
||||
// if (result.isEmpty) {
|
||||
// if (callId != null) {
|
||||
// final error = sendWorker(getErrorMessage(callId!));
|
||||
if (error != null) {
|
||||
if (error.contains("dust")) {
|
||||
if (hasChange) {
|
||||
throw BitcoinTransactionCommitFailedDustChange();
|
||||
} else if (!isSendAll) {
|
||||
throw BitcoinTransactionCommitFailedDustOutput();
|
||||
} else {
|
||||
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
}
|
||||
}
|
||||
|
||||
// if (error.contains("dust")) {
|
||||
// if (hasChange) {
|
||||
// throw BitcoinTransactionCommitFailedDustChange();
|
||||
// } else if (!isSendAll) {
|
||||
// throw BitcoinTransactionCommitFailedDustOutput();
|
||||
// } else {
|
||||
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
// }
|
||||
// }
|
||||
if (error.contains("bad-txns-vout-negative")) {
|
||||
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
}
|
||||
|
||||
// if (error.contains("bad-txns-vout-negative")) {
|
||||
// throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
// }
|
||||
if (error.contains("non-BIP68-final")) {
|
||||
throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
}
|
||||
|
||||
// if (error.contains("non-BIP68-final")) {
|
||||
// throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
// }
|
||||
if (error.contains("min fee not met")) {
|
||||
throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
}
|
||||
|
||||
// if (error.contains("min fee not met")) {
|
||||
// throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
// }
|
||||
|
||||
// throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
// }
|
||||
|
||||
// throw BitcoinTransactionCommitFailed();
|
||||
// }
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ltcCommit() async {
|
||||
|
@ -151,7 +154,6 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee,
|
||||
time: null,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
class PendingBitcoinCashTransaction with PendingTransaction {
|
||||
PendingBitcoinCashTransaction(this._tx, this.type,
|
||||
{required this.electrumClient,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.hasChange,
|
||||
required this.isSendAll})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
PendingBitcoinCashTransaction(
|
||||
this._tx,
|
||||
this.type, {
|
||||
required this.sendWorker,
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.hasChange,
|
||||
required this.isSendAll,
|
||||
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
final bitbox.Transaction _tx;
|
||||
final ElectrumClient electrumClient;
|
||||
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final bool hasChange;
|
||||
|
@ -40,32 +45,40 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
int? callId;
|
||||
final result = await sendWorker(
|
||||
ElectrumWorkerBroadcastRequest(transactionRaw: hex),
|
||||
) as String;
|
||||
|
||||
final result = await electrumClient.broadcastTransaction(
|
||||
transactionRaw: hex, idCallback: (id) => callId = id);
|
||||
String? error;
|
||||
try {
|
||||
final resultJson = jsonDecode(result) as Map<String, dynamic>;
|
||||
error = resultJson["error"] as String;
|
||||
} catch (_) {}
|
||||
|
||||
if (result.isEmpty) {
|
||||
if (callId != null) {
|
||||
final error = electrumClient.getErrorMessage(callId!);
|
||||
|
||||
if (error.contains("dust")) {
|
||||
if (hasChange) {
|
||||
throw BitcoinTransactionCommitFailedDustChange();
|
||||
} else if (!isSendAll) {
|
||||
throw BitcoinTransactionCommitFailedDustOutput();
|
||||
} else {
|
||||
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
}
|
||||
if (error != null) {
|
||||
if (error.contains("dust")) {
|
||||
if (hasChange) {
|
||||
throw BitcoinTransactionCommitFailedDustChange();
|
||||
} else if (!isSendAll) {
|
||||
throw BitcoinTransactionCommitFailedDustOutput();
|
||||
} else {
|
||||
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
}
|
||||
|
||||
if (error.contains("bad-txns-vout-negative")) {
|
||||
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
}
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
}
|
||||
|
||||
throw BitcoinTransactionCommitFailed();
|
||||
if (error.contains("bad-txns-vout-negative")) {
|
||||
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
}
|
||||
|
||||
if (error.contains("non-BIP68-final")) {
|
||||
throw BitcoinTransactionCommitFailedBIP68Final();
|
||||
}
|
||||
|
||||
if (error.contains("min fee not met")) {
|
||||
throw BitcoinTransactionCommitFailedLessThanMin();
|
||||
}
|
||||
|
||||
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||
}
|
||||
|
||||
_listeners.forEach((listener) => listener(transactionInfo()));
|
||||
|
@ -86,6 +99,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
fee: fee,
|
||||
isReplaced: false,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
|
|
|
@ -695,7 +695,7 @@
|
|||
"sent": "Enviada",
|
||||
"service_health_disabled": "O Boletim de Saúde de Serviço está desativado",
|
||||
"service_health_disabled_message": "Esta é a página do Boletim de Saúde de Serviço, você pode ativar esta página em Configurações -> Privacidade",
|
||||
"set_a_pin": "Defina um pino",
|
||||
"set_a_pin": "Defina um pin",
|
||||
"settings": "Configurações",
|
||||
"settings_all": "Tudo",
|
||||
"settings_allow_biometrical_authentication": "Permitir autenticação biométrica",
|
||||
|
@ -990,4 +990,4 @@
|
|||
"you_will_get": "Converter para",
|
||||
"you_will_send": "Converter de",
|
||||
"yy": "aa"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue