mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 19:49: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 'dart:convert';
|
||||||
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
|
||||||
|
|
||||||
abstract class BaseBitcoinAddressRecord {
|
abstract class BaseBitcoinAddressRecord {
|
||||||
|
@ -84,11 +85,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
|
||||||
throw ArgumentError('either scriptHash or network must be provided');
|
throw ArgumentError('either scriptHash or network must be provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
|
||||||
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
|
|
||||||
} catch (_) {
|
|
||||||
this.scriptHash = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
||||||
|
@ -211,7 +208,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
||||||
final ECPrivate spendKey;
|
final String tweak;
|
||||||
|
|
||||||
BitcoinReceivedSPAddressRecord(
|
BitcoinReceivedSPAddressRecord(
|
||||||
super.address, {
|
super.address, {
|
||||||
|
@ -220,11 +217,32 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
||||||
super.balance = 0,
|
super.balance = 0,
|
||||||
super.name = '',
|
super.name = '',
|
||||||
super.isUsed = false,
|
super.isUsed = false,
|
||||||
required this.spendKey,
|
required this.tweak,
|
||||||
super.addressType = SegwitAddresType.p2tr,
|
super.addressType = SegwitAddresType.p2tr,
|
||||||
super.labelHex,
|
super.labelHex,
|
||||||
}) : super(isHidden: true);
|
}) : 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}) {
|
factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
|
||||||
final decoded = json.decode(jsonSource) as Map;
|
final decoded = json.decode(jsonSource) as Map;
|
||||||
|
|
||||||
|
@ -236,9 +254,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
||||||
name: decoded['name'] as String? ?? '',
|
name: decoded['name'] as String? ?? '',
|
||||||
balance: decoded['balance'] as int? ?? 0,
|
balance: decoded['balance'] as int? ?? 0,
|
||||||
labelHex: decoded['label'] as String?,
|
labelHex: decoded['label'] as String?,
|
||||||
spendKey: (decoded['spendKey'] as String?) == null
|
tweak: decoded['tweak'] as String? ?? '',
|
||||||
? ECPrivate.random()
|
|
||||||
: ECPrivate.fromHex(decoded['spendKey'] as String),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +268,6 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
|
||||||
'balance': balance,
|
'balance': balance,
|
||||||
'type': addressType.toString(),
|
'type': addressType.toString(),
|
||||||
'labelHex': labelHex,
|
'labelHex': labelHex,
|
||||||
'spend_key': spendKey.toString(),
|
'tweak': tweak,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,11 @@ import 'dart:convert';
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.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/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/psbt_transaction_builder.dart';
|
||||||
// import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
|
||||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||||
import 'package:cw_bitcoin/electrum_wallet_addresses.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.dart';
|
||||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||||
import 'package:cw_core/crypto_currency.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/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_direction.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/unspent_coins_info.dart';
|
||||||
import 'package:cw_core/utils/print_verbose.dart';
|
import 'package:cw_core/utils/print_verbose.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_core/wallet_keys_file.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:hive/hive.dart';
|
||||||
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
|
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
|
||||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.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;
|
final passphrase = keysData.passphrase;
|
||||||
|
|
||||||
if (mnemonic != null) {
|
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) {
|
if (derivation.description?.contains("SP") ?? false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -289,16 +292,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
hdWallets[CWBitcoinDerivationType.electrum]!;
|
hdWallets[CWBitcoinDerivationType.electrum]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (walletInfo.derivationInfo?.derivationType) {
|
if (derivations.isEmpty) {
|
||||||
case DerivationType.bip39:
|
switch (walletInfo.derivationInfo?.derivationType) {
|
||||||
seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
case DerivationType.bip39:
|
||||||
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||||
break;
|
hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||||
case DerivationType.electrum:
|
break;
|
||||||
default:
|
case DerivationType.electrum:
|
||||||
seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
|
default:
|
||||||
hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase);
|
||||||
break;
|
hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -914,4 +919,566 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
super.syncStatusReaction(syncStatus);
|
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,
|
required bool isPending,
|
||||||
bool isReplaced = false,
|
bool isReplaced = false,
|
||||||
required DateTime date,
|
required DateTime date,
|
||||||
required int? time,
|
int? time,
|
||||||
bool? isDateValidated,
|
bool? isDateValidated,
|
||||||
required int confirmations,
|
required int confirmations,
|
||||||
String? to,
|
String? to,
|
||||||
|
|
|
@ -417,7 +417,6 @@ abstract class ElectrumWalletBase
|
||||||
_workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort);
|
_workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort);
|
||||||
|
|
||||||
_workerSubscription = receivePort!.listen((message) {
|
_workerSubscription = receivePort!.listen((message) {
|
||||||
printV('Main: received message: $message');
|
|
||||||
if (message is SendPort) {
|
if (message is SendPort) {
|
||||||
workerSendPort = message;
|
workerSendPort = message;
|
||||||
workerSendPort!.send(
|
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 sendAll,
|
||||||
required bool paysToSilentPayment,
|
|
||||||
int credentialsAmount = 0,
|
int credentialsAmount = 0,
|
||||||
int? inputsCount,
|
int? inputsCount,
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
|
@ -455,7 +453,6 @@ abstract class ElectrumWalletBase
|
||||||
List<ECPrivateInfo> inputPrivKeyInfos = [];
|
List<ECPrivateInfo> inputPrivKeyInfos = [];
|
||||||
final publicKeys = <String, PublicKeyWithDerivationPath>{};
|
final publicKeys = <String, PublicKeyWithDerivationPath>{};
|
||||||
int allInputsAmount = 0;
|
int allInputsAmount = 0;
|
||||||
bool spendsSilentPayment = false;
|
|
||||||
bool spendsUnconfirmedTX = false;
|
bool spendsUnconfirmedTX = false;
|
||||||
|
|
||||||
int leftAmount = credentialsAmount;
|
int leftAmount = credentialsAmount;
|
||||||
|
@ -483,25 +480,13 @@ abstract class ElectrumWalletBase
|
||||||
final utx = availableInputs[i];
|
final utx = availableInputs[i];
|
||||||
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
|
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;
|
allInputsAmount += utx.value;
|
||||||
leftAmount = leftAmount - utx.value;
|
leftAmount = leftAmount - utx.value;
|
||||||
|
|
||||||
final address = RegexUtils.addressTypeFromStr(utx.address, network);
|
final address = RegexUtils.addressTypeFromStr(utx.address, network);
|
||||||
ECPrivate? privkey;
|
ECPrivate? privkey;
|
||||||
bool? isSilentPayment = false;
|
|
||||||
|
|
||||||
if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) {
|
if (!isHardwareWallet) {
|
||||||
privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey;
|
|
||||||
spendsSilentPayment = true;
|
|
||||||
isSilentPayment = true;
|
|
||||||
} else if (!isHardwareWallet) {
|
|
||||||
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
|
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
|
||||||
final path = addressRecord.derivationInfo.derivationPath
|
final path = addressRecord.derivationInfo.derivationPath
|
||||||
.addElem(Bip32KeyIndex(
|
.addElem(Bip32KeyIndex(
|
||||||
|
@ -516,11 +501,7 @@ abstract class ElectrumWalletBase
|
||||||
String pubKeyHex;
|
String pubKeyHex;
|
||||||
|
|
||||||
if (privkey != null) {
|
if (privkey != null) {
|
||||||
inputPrivKeyInfos.add(ECPrivateInfo(
|
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
|
||||||
privkey,
|
|
||||||
address.type == SegwitAddresType.p2tr,
|
|
||||||
tweak: !isSilentPayment,
|
|
||||||
));
|
|
||||||
|
|
||||||
pubKeyHex = privkey.getPublic().toHex();
|
pubKeyHex = privkey.getPublic().toHex();
|
||||||
} else {
|
} else {
|
||||||
|
@ -545,7 +526,6 @@ abstract class ElectrumWalletBase
|
||||||
value: BigInt.from(utx.value),
|
value: BigInt.from(utx.value),
|
||||||
vout: utx.vout,
|
vout: utx.vout,
|
||||||
scriptType: BitcoinAddressUtils.getScriptType(address),
|
scriptType: BitcoinAddressUtils.getScriptType(address),
|
||||||
isSilentPayment: isSilentPayment,
|
|
||||||
),
|
),
|
||||||
ownerDetails: UtxoAddressDetails(
|
ownerDetails: UtxoAddressDetails(
|
||||||
publicKey: pubKeyHex,
|
publicKey: pubKeyHex,
|
||||||
|
@ -575,7 +555,6 @@ abstract class ElectrumWalletBase
|
||||||
inputPrivKeyInfos: inputPrivKeyInfos,
|
inputPrivKeyInfos: inputPrivKeyInfos,
|
||||||
publicKeys: publicKeys,
|
publicKeys: publicKeys,
|
||||||
allInputsAmount: allInputsAmount,
|
allInputsAmount: allInputsAmount,
|
||||||
spendsSilentPayment: spendsSilentPayment,
|
|
||||||
spendsUnconfirmedTX: spendsUnconfirmedTX,
|
spendsUnconfirmedTX: spendsUnconfirmedTX,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -584,14 +563,9 @@ abstract class ElectrumWalletBase
|
||||||
List<BitcoinOutput> outputs,
|
List<BitcoinOutput> outputs,
|
||||||
int feeRate, {
|
int feeRate, {
|
||||||
String? memo,
|
String? memo,
|
||||||
bool hasSilentPayment = false,
|
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
}) async {
|
}) async {
|
||||||
final utxoDetails = _createUTXOS(
|
final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom);
|
||||||
sendAll: true,
|
|
||||||
paysToSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
|
||||||
);
|
|
||||||
|
|
||||||
int fee = await calcFee(
|
int fee = await calcFee(
|
||||||
utxos: utxoDetails.utxos,
|
utxos: utxoDetails.utxos,
|
||||||
|
@ -612,7 +586,7 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempting to send less than the dust limit
|
// Attempting to send less than the dust limit
|
||||||
if (_isBelowDust(amount)) {
|
if (isBelowDust(amount)) {
|
||||||
throw BitcoinTransactionNoDustException();
|
throw BitcoinTransactionNoDustException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,31 +604,27 @@ abstract class ElectrumWalletBase
|
||||||
hasChange: false,
|
hasChange: false,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<EstimatedTxResult> estimateTxForAmount(
|
Future<EstimatedTxResult> estimateTxForAmount(
|
||||||
int credentialsAmount,
|
int credentialsAmount,
|
||||||
List<BitcoinOutput> outputs,
|
List<BitcoinOutput> outputs,
|
||||||
List<BitcoinOutput> updatedOutputs,
|
|
||||||
int feeRate, {
|
int feeRate, {
|
||||||
int? inputsCount,
|
int? inputsCount,
|
||||||
String? memo,
|
String? memo,
|
||||||
bool? useUnconfirmed,
|
bool? useUnconfirmed,
|
||||||
bool hasSilentPayment = false,
|
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
}) async {
|
}) async {
|
||||||
// Attempting to send less than the dust limit
|
// Attempting to send less than the dust limit
|
||||||
if (_isBelowDust(credentialsAmount)) {
|
if (isBelowDust(credentialsAmount)) {
|
||||||
throw BitcoinTransactionNoDustException();
|
throw BitcoinTransactionNoDustException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final utxoDetails = _createUTXOS(
|
final utxoDetails = createUTXOS(
|
||||||
sendAll: false,
|
sendAll: false,
|
||||||
credentialsAmount: credentialsAmount,
|
credentialsAmount: credentialsAmount,
|
||||||
inputsCount: inputsCount,
|
inputsCount: inputsCount,
|
||||||
paysToSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -671,11 +641,9 @@ abstract class ElectrumWalletBase
|
||||||
return estimateTxForAmount(
|
return estimateTxForAmount(
|
||||||
credentialsAmount,
|
credentialsAmount,
|
||||||
outputs,
|
outputs,
|
||||||
updatedOutputs,
|
|
||||||
feeRate,
|
feeRate,
|
||||||
inputsCount: utxoDetails.utxos.length + 1,
|
inputsCount: utxoDetails.utxos.length + 1,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -685,14 +653,9 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
final changeAddress = await walletAddresses.getChangeAddress(
|
final changeAddress = await walletAddresses.getChangeAddress(
|
||||||
inputs: utxoDetails.availableInputs,
|
inputs: utxoDetails.availableInputs,
|
||||||
outputs: updatedOutputs,
|
outputs: outputs,
|
||||||
);
|
);
|
||||||
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
|
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
|
||||||
updatedOutputs.add(BitcoinOutput(
|
|
||||||
address: address,
|
|
||||||
value: BigInt.from(amountLeftForChangeAndFee),
|
|
||||||
isChange: true,
|
|
||||||
));
|
|
||||||
outputs.add(BitcoinOutput(
|
outputs.add(BitcoinOutput(
|
||||||
address: address,
|
address: address,
|
||||||
value: BigInt.from(amountLeftForChangeAndFee),
|
value: BigInt.from(amountLeftForChangeAndFee),
|
||||||
|
@ -703,34 +666,25 @@ abstract class ElectrumWalletBase
|
||||||
utxoDetails.publicKeys[address.pubKeyHash()] =
|
utxoDetails.publicKeys[address.pubKeyHash()] =
|
||||||
PublicKeyWithDerivationPath('', changeDerivationPath);
|
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(
|
int fee = await calcFee(
|
||||||
utxos: utxoDetails.utxos,
|
utxos: utxoDetails.utxos,
|
||||||
// Always take only not updated bitcoin outputs here so for every estimation
|
outputs: outputs,
|
||||||
// the SP outputs are re-generated to the proper taproot addresses
|
|
||||||
outputs: temp,
|
|
||||||
memo: memo,
|
memo: memo,
|
||||||
feeRate: feeRate,
|
feeRate: feeRate,
|
||||||
);
|
);
|
||||||
|
|
||||||
updatedOutputs.clear();
|
|
||||||
updatedOutputs.addAll(temp);
|
|
||||||
|
|
||||||
if (fee == 0) {
|
if (fee == 0) {
|
||||||
throw BitcoinTransactionNoFeeException();
|
throw BitcoinTransactionNoFeeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
int amount = credentialsAmount;
|
int amount = credentialsAmount;
|
||||||
final lastOutput = updatedOutputs.last;
|
final lastOutput = outputs.last;
|
||||||
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
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
|
// If has change that is lower than dust, will end up with tx rejected by network rules
|
||||||
// so remove the change amount
|
// so remove the change amount
|
||||||
updatedOutputs.removeLast();
|
outputs.removeLast();
|
||||||
outputs.removeLast();
|
outputs.removeLast();
|
||||||
|
|
||||||
if (amountLeftForChange < 0) {
|
if (amountLeftForChange < 0) {
|
||||||
|
@ -738,12 +692,10 @@ abstract class ElectrumWalletBase
|
||||||
return estimateTxForAmount(
|
return estimateTxForAmount(
|
||||||
credentialsAmount,
|
credentialsAmount,
|
||||||
outputs,
|
outputs,
|
||||||
updatedOutputs,
|
|
||||||
feeRate,
|
feeRate,
|
||||||
inputsCount: utxoDetails.utxos.length + 1,
|
inputsCount: utxoDetails.utxos.length + 1,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -761,20 +713,12 @@ abstract class ElectrumWalletBase
|
||||||
isSendAll: spendingAllCoins,
|
isSendAll: spendingAllCoins,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
// 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(
|
outputs[outputs.length - 1] = BitcoinOutput(
|
||||||
address: lastOutput.address,
|
address: lastOutput.address,
|
||||||
value: BigInt.from(amountLeftForChange),
|
value: BigInt.from(amountLeftForChange),
|
||||||
isSilentPayment: lastOutput.isSilentPayment,
|
|
||||||
isChange: true,
|
isChange: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -788,7 +732,6 @@ abstract class ElectrumWalletBase
|
||||||
isSendAll: spendingAllCoins,
|
isSendAll: spendingAllCoins,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -818,12 +761,11 @@ abstract class ElectrumWalletBase
|
||||||
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
|
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
|
||||||
|
|
||||||
int credentialsAmount = 0;
|
int credentialsAmount = 0;
|
||||||
bool hasSilentPayment = false;
|
|
||||||
|
|
||||||
for (final out in transactionCredentials.outputs) {
|
for (final out in transactionCredentials.outputs) {
|
||||||
final outputAmount = out.formattedCryptoAmount!;
|
final outputAmount = out.formattedCryptoAmount!;
|
||||||
|
|
||||||
if (!sendAll && _isBelowDust(outputAmount)) {
|
if (!sendAll && isBelowDust(outputAmount)) {
|
||||||
throw BitcoinTransactionNoDustException();
|
throw BitcoinTransactionNoDustException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -836,25 +778,20 @@ abstract class ElectrumWalletBase
|
||||||
credentialsAmount += outputAmount;
|
credentialsAmount += outputAmount;
|
||||||
|
|
||||||
final address = RegexUtils.addressTypeFromStr(
|
final address = RegexUtils.addressTypeFromStr(
|
||||||
out.isParsedAddress ? out.extractedAddress! : out.address, network);
|
out.isParsedAddress ? out.extractedAddress! : out.address,
|
||||||
final isSilentPayment = address is SilentPaymentAddress;
|
network,
|
||||||
|
);
|
||||||
if (isSilentPayment) {
|
|
||||||
hasSilentPayment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendAll) {
|
if (sendAll) {
|
||||||
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
|
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
|
||||||
outputs.add(BitcoinOutput(
|
outputs.add(BitcoinOutput(
|
||||||
address: address,
|
address: address,
|
||||||
value: BigInt.from(0),
|
value: BigInt.from(0),
|
||||||
isSilentPayment: isSilentPayment,
|
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
outputs.add(BitcoinOutput(
|
outputs.add(BitcoinOutput(
|
||||||
address: address,
|
address: address,
|
||||||
value: BigInt.from(outputAmount),
|
value: BigInt.from(outputAmount),
|
||||||
isSilentPayment: isSilentPayment,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -864,31 +801,19 @@ abstract class ElectrumWalletBase
|
||||||
: feeRate(transactionCredentials.priority!);
|
: feeRate(transactionCredentials.priority!);
|
||||||
|
|
||||||
EstimatedTxResult estimatedTx;
|
EstimatedTxResult estimatedTx;
|
||||||
final updatedOutputs = outputs
|
|
||||||
.map((e) => BitcoinOutput(
|
|
||||||
address: e.address,
|
|
||||||
value: e.value,
|
|
||||||
isSilentPayment: e.isSilentPayment,
|
|
||||||
isChange: e.isChange,
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (sendAll) {
|
if (sendAll) {
|
||||||
estimatedTx = await estimateSendAllTx(
|
estimatedTx = await estimateSendAllTx(
|
||||||
updatedOutputs,
|
outputs,
|
||||||
feeRateInt,
|
feeRateInt,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
estimatedTx = await estimateTxForAmount(
|
estimatedTx = await estimateTxForAmount(
|
||||||
credentialsAmount,
|
credentialsAmount,
|
||||||
outputs,
|
outputs,
|
||||||
updatedOutputs,
|
|
||||||
feeRateInt,
|
feeRateInt,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -896,7 +821,7 @@ abstract class ElectrumWalletBase
|
||||||
if (walletInfo.isHardwareWallet) {
|
if (walletInfo.isHardwareWallet) {
|
||||||
final transaction = await buildHardwareWalletTransaction(
|
final transaction = await buildHardwareWalletTransaction(
|
||||||
utxos: estimatedTx.utxos,
|
utxos: estimatedTx.utxos,
|
||||||
outputs: updatedOutputs,
|
outputs: outputs,
|
||||||
publicKeys: estimatedTx.publicKeys,
|
publicKeys: estimatedTx.publicKeys,
|
||||||
fee: BigInt.from(estimatedTx.fee),
|
fee: BigInt.from(estimatedTx.fee),
|
||||||
network: network,
|
network: network,
|
||||||
|
@ -925,7 +850,7 @@ abstract class ElectrumWalletBase
|
||||||
if (network is BitcoinCashNetwork) {
|
if (network is BitcoinCashNetwork) {
|
||||||
txb = ForkedTransactionBuilder(
|
txb = ForkedTransactionBuilder(
|
||||||
utxos: estimatedTx.utxos,
|
utxos: estimatedTx.utxos,
|
||||||
outputs: updatedOutputs,
|
outputs: outputs,
|
||||||
fee: BigInt.from(estimatedTx.fee),
|
fee: BigInt.from(estimatedTx.fee),
|
||||||
network: network,
|
network: network,
|
||||||
memo: estimatedTx.memo,
|
memo: estimatedTx.memo,
|
||||||
|
@ -935,7 +860,7 @@ abstract class ElectrumWalletBase
|
||||||
} else {
|
} else {
|
||||||
txb = BitcoinTransactionBuilder(
|
txb = BitcoinTransactionBuilder(
|
||||||
utxos: estimatedTx.utxos,
|
utxos: estimatedTx.utxos,
|
||||||
outputs: updatedOutputs,
|
outputs: outputs,
|
||||||
fee: BigInt.from(estimatedTx.fee),
|
fee: BigInt.from(estimatedTx.fee),
|
||||||
network: network,
|
network: network,
|
||||||
memo: estimatedTx.memo,
|
memo: estimatedTx.memo,
|
||||||
|
@ -974,11 +899,7 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
if (utxo.utxo.isP2tr()) {
|
if (utxo.utxo.isP2tr()) {
|
||||||
hasTaprootInputs = true;
|
hasTaprootInputs = true;
|
||||||
return key.privkey.signTapRoot(
|
return key.privkey.signTapRoot(txDigest, sighash: sighash);
|
||||||
txDigest,
|
|
||||||
sighash: sighash,
|
|
||||||
tweak: utxo.utxo.isSilentPayment != true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return key.privkey.signInput(txDigest, sigHash: sighash);
|
return key.privkey.signInput(txDigest, sigHash: sighash);
|
||||||
}
|
}
|
||||||
|
@ -997,20 +918,14 @@ abstract class ElectrumWalletBase
|
||||||
utxos: estimatedTx.utxos,
|
utxos: estimatedTx.utxos,
|
||||||
)..addListener((transaction) async {
|
)..addListener((transaction) async {
|
||||||
transactionHistory.addOne(transaction);
|
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
|
unspentCoins
|
||||||
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
|
||||||
|
|
||||||
await updateBalance();
|
await updateBalance();
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
print([e, s]);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1974,7 +1889,7 @@ class EstimatedTxResult {
|
||||||
required this.hasChange,
|
required this.hasChange,
|
||||||
required this.isSendAll,
|
required this.isSendAll,
|
||||||
this.memo,
|
this.memo,
|
||||||
required this.spendsSilentPayment,
|
this.spendsSilentPayment = false,
|
||||||
required this.spendsUnconfirmedTX,
|
required this.spendsUnconfirmedTX,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1985,7 +1900,6 @@ class EstimatedTxResult {
|
||||||
final int amount;
|
final int amount;
|
||||||
final bool spendsSilentPayment;
|
final bool spendsSilentPayment;
|
||||||
|
|
||||||
// final bool sendsToSilentPayment;
|
|
||||||
final bool hasChange;
|
final bool hasChange;
|
||||||
final bool isSendAll;
|
final bool isSendAll;
|
||||||
final String? memo;
|
final String? memo;
|
||||||
|
@ -2018,7 +1932,7 @@ class TxCreateUtxoDetails {
|
||||||
required this.inputPrivKeyInfos,
|
required this.inputPrivKeyInfos,
|
||||||
required this.publicKeys,
|
required this.publicKeys,
|
||||||
required this.allInputsAmount,
|
required this.allInputsAmount,
|
||||||
required this.spendsSilentPayment,
|
this.spendsSilentPayment = false,
|
||||||
required this.spendsUnconfirmedTX,
|
required this.spendsUnconfirmedTX,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,11 +382,20 @@ class ElectrumWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
|
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
|
||||||
|
final rpcId = _electrumClient!.id + 1;
|
||||||
final txHash = await _electrumClient!.request(
|
final txHash = await _electrumClient!.request(
|
||||||
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
|
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 {
|
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
|
||||||
|
@ -586,6 +595,7 @@ class ElectrumWorker {
|
||||||
if (scanData.shouldSwitchNodes) {
|
if (scanData.shouldSwitchNodes) {
|
||||||
scanningClient = await ElectrumApiProvider.connect(
|
scanningClient = await ElectrumApiProvider.connect(
|
||||||
ElectrumTCPService.connect(
|
ElectrumTCPService.connect(
|
||||||
|
// TODO: ssl
|
||||||
Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
Uri.parse("tcp://electrs.cakewallet.com:50001"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -714,7 +724,6 @@ class ElectrumWorker {
|
||||||
date: scanData.network == BitcoinNetwork.mainnet
|
date: scanData.network == BitcoinNetwork.mainnet
|
||||||
? getDateByBitcoinHeight(tweakHeight)
|
? getDateByBitcoinHeight(tweakHeight)
|
||||||
: DateTime.now(),
|
: DateTime.now(),
|
||||||
time: null,
|
|
||||||
confirmations: scanData.chainTip - tweakHeight + 1,
|
confirmations: scanData.chainTip - tweakHeight + 1,
|
||||||
unspents: [],
|
unspents: [],
|
||||||
isReceivedSilentPayment: true,
|
isReceivedSilentPayment: true,
|
||||||
|
@ -736,10 +745,7 @@ class ElectrumWorker {
|
||||||
receivingOutputAddress,
|
receivingOutputAddress,
|
||||||
labelIndex: 1, // TODO: get actual index/label
|
labelIndex: 1, // TODO: get actual index/label
|
||||||
isUsed: true,
|
isUsed: true,
|
||||||
// TODO: use right wallet
|
tweak: t_k,
|
||||||
spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd(
|
|
||||||
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
|
|
||||||
),
|
|
||||||
txCount: 1,
|
txCount: 1,
|
||||||
balance: amount,
|
balance: amount,
|
||||||
);
|
);
|
||||||
|
|
|
@ -559,7 +559,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
direction: TransactionDirection.incoming,
|
direction: TransactionDirection.incoming,
|
||||||
isPending: utxo.height == 0,
|
isPending: utxo.height == 0,
|
||||||
date: date,
|
date: date,
|
||||||
time: null,
|
|
||||||
confirmations: confirmations,
|
confirmations: confirmations,
|
||||||
inputAddresses: [],
|
inputAddresses: [],
|
||||||
outputAddresses: [utxo.outputId],
|
outputAddresses: [utxo.outputId],
|
||||||
|
@ -763,7 +762,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||||
direction: TransactionDirection.outgoing,
|
direction: TransactionDirection.outgoing,
|
||||||
isPending: false,
|
isPending: false,
|
||||||
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
|
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
|
||||||
time: null,
|
|
||||||
confirmations: 1,
|
confirmations: 1,
|
||||||
inputAddresses: inputAddresses.toList(),
|
inputAddresses: inputAddresses.toList(),
|
||||||
outputAddresses: [],
|
outputAddresses: [],
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
|
||||||
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
|
||||||
import 'package:grpc/grpc.dart';
|
import 'package:grpc/grpc.dart';
|
||||||
|
@ -24,6 +26,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||||
this.hasTaprootInputs = false,
|
this.hasTaprootInputs = false,
|
||||||
this.isMweb = false,
|
this.isMweb = false,
|
||||||
this.utxos = const [],
|
this.utxos = const [],
|
||||||
|
this.hasSilentPayment = false,
|
||||||
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||||
|
|
||||||
final WalletType type;
|
final WalletType type;
|
||||||
|
@ -59,7 +62,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||||
|
|
||||||
List<TxOutput> get outputs => _tx.outputs;
|
List<TxOutput> get outputs => _tx.outputs;
|
||||||
|
|
||||||
bool get hasSilentPayment => _tx.hasSilentPayment;
|
bool hasSilentPayment;
|
||||||
|
|
||||||
PendingChange? get change {
|
PendingChange? get change {
|
||||||
try {
|
try {
|
||||||
|
@ -76,41 +79,41 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||||
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
||||||
|
|
||||||
Future<void> _commit() async {
|
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 (error != null) {
|
||||||
// if (callId != null) {
|
if (error.contains("dust")) {
|
||||||
// final error = sendWorker(getErrorMessage(callId!));
|
if (hasChange) {
|
||||||
|
throw BitcoinTransactionCommitFailedDustChange();
|
||||||
|
} else if (!isSendAll) {
|
||||||
|
throw BitcoinTransactionCommitFailedDustOutput();
|
||||||
|
} else {
|
||||||
|
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if (error.contains("dust")) {
|
if (error.contains("bad-txns-vout-negative")) {
|
||||||
// if (hasChange) {
|
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||||
// throw BitcoinTransactionCommitFailedDustChange();
|
}
|
||||||
// } else if (!isSendAll) {
|
|
||||||
// throw BitcoinTransactionCommitFailedDustOutput();
|
|
||||||
// } else {
|
|
||||||
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (error.contains("bad-txns-vout-negative")) {
|
if (error.contains("non-BIP68-final")) {
|
||||||
// throw BitcoinTransactionCommitFailedVoutNegative();
|
throw BitcoinTransactionCommitFailedBIP68Final();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (error.contains("non-BIP68-final")) {
|
if (error.contains("min fee not met")) {
|
||||||
// throw BitcoinTransactionCommitFailedBIP68Final();
|
throw BitcoinTransactionCommitFailedLessThanMin();
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (error.contains("min fee not met")) {
|
throw BitcoinTransactionCommitFailed(errorMessage: error);
|
||||||
// throw BitcoinTransactionCommitFailedLessThanMin();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// throw BitcoinTransactionCommitFailed(errorMessage: error);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// throw BitcoinTransactionCommitFailed();
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _ltcCommit() async {
|
Future<void> _ltcCommit() async {
|
||||||
|
@ -151,7 +154,6 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||||
outputAddresses: outputAddresses,
|
outputAddresses: outputAddresses,
|
||||||
fee: fee,
|
fee: fee,
|
||||||
time: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@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:cw_bitcoin/exceptions.dart';
|
||||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
import 'package:cw_bitcoin/electrum.dart';
|
|
||||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||||
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
||||||
import 'package:cw_core/transaction_direction.dart';
|
import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
|
||||||
class PendingBitcoinCashTransaction with PendingTransaction {
|
class PendingBitcoinCashTransaction with PendingTransaction {
|
||||||
PendingBitcoinCashTransaction(this._tx, this.type,
|
PendingBitcoinCashTransaction(
|
||||||
{required this.electrumClient,
|
this._tx,
|
||||||
required this.amount,
|
this.type, {
|
||||||
required this.fee,
|
required this.sendWorker,
|
||||||
required this.hasChange,
|
required this.amount,
|
||||||
required this.isSendAll})
|
required this.fee,
|
||||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
required this.hasChange,
|
||||||
|
required this.isSendAll,
|
||||||
|
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||||
|
|
||||||
final WalletType type;
|
final WalletType type;
|
||||||
final bitbox.Transaction _tx;
|
final bitbox.Transaction _tx;
|
||||||
final ElectrumClient electrumClient;
|
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
|
||||||
final int amount;
|
final int amount;
|
||||||
final int fee;
|
final int fee;
|
||||||
final bool hasChange;
|
final bool hasChange;
|
||||||
|
@ -40,32 +45,40 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> commit() async {
|
Future<void> commit() async {
|
||||||
int? callId;
|
final result = await sendWorker(
|
||||||
|
ElectrumWorkerBroadcastRequest(transactionRaw: hex),
|
||||||
|
) as String;
|
||||||
|
|
||||||
final result = await electrumClient.broadcastTransaction(
|
String? error;
|
||||||
transactionRaw: hex, idCallback: (id) => callId = id);
|
try {
|
||||||
|
final resultJson = jsonDecode(result) as Map<String, dynamic>;
|
||||||
|
error = resultJson["error"] as String;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
if (result.isEmpty) {
|
if (error != null) {
|
||||||
if (callId != null) {
|
if (error.contains("dust")) {
|
||||||
final error = electrumClient.getErrorMessage(callId!);
|
if (hasChange) {
|
||||||
|
throw BitcoinTransactionCommitFailedDustChange();
|
||||||
if (error.contains("dust")) {
|
} else if (!isSendAll) {
|
||||||
if (hasChange) {
|
throw BitcoinTransactionCommitFailedDustOutput();
|
||||||
throw BitcoinTransactionCommitFailedDustChange();
|
} else {
|
||||||
} else if (!isSendAll) {
|
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||||
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()));
|
_listeners.forEach((listener) => listener(transactionInfo()));
|
||||||
|
@ -86,6 +99,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
||||||
fee: fee,
|
fee: fee,
|
||||||
isReplaced: false,
|
isReplaced: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> commitUR() {
|
Future<String?> commitUR() {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
|
|
|
@ -695,7 +695,7 @@
|
||||||
"sent": "Enviada",
|
"sent": "Enviada",
|
||||||
"service_health_disabled": "O Boletim de Saúde de Serviço está desativado",
|
"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",
|
"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": "Configurações",
|
||||||
"settings_all": "Tudo",
|
"settings_all": "Tudo",
|
||||||
"settings_allow_biometrical_authentication": "Permitir autenticação biométrica",
|
"settings_allow_biometrical_authentication": "Permitir autenticação biométrica",
|
||||||
|
@ -990,4 +990,4 @@
|
||||||
"you_will_get": "Converter para",
|
"you_will_get": "Converter para",
|
||||||
"you_will_send": "Converter de",
|
"you_will_send": "Converter de",
|
||||||
"yy": "aa"
|
"yy": "aa"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue