mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-22 02:24:30 +00:00
enable coin control for selected other coins
This commit is contained in:
parent
a2f75a2c7b
commit
b30f1db45b
5 changed files with 587 additions and 367 deletions
|
@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||||
import 'package:stackwallet/services/node_service.dart';
|
import 'package:stackwallet/services/node_service.dart';
|
||||||
|
@ -98,7 +99,8 @@ String constructDerivePath({
|
||||||
return "m/$purpose'/$coinType'/$account'/$chain/$index";
|
return "m/$purpose'/$coinType'/$account'/$chain/$index";
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
class BitcoinCashWallet extends CoinServiceAPI
|
||||||
|
with WalletCache, WalletDB, CoinControlInterface {
|
||||||
BitcoinCashWallet({
|
BitcoinCashWallet({
|
||||||
required String walletId,
|
required String walletId,
|
||||||
required String walletName,
|
required String walletName,
|
||||||
|
@ -118,6 +120,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
_secureStore = secureStore;
|
_secureStore = secureStore;
|
||||||
initCache(walletId, coin);
|
initCache(walletId, coin);
|
||||||
initWalletDB(mockableOverride: mockableOverride);
|
initWalletDB(mockableOverride: mockableOverride);
|
||||||
|
initCoinControlInterface(
|
||||||
|
walletId: walletId,
|
||||||
|
walletName: walletName,
|
||||||
|
coin: coin,
|
||||||
|
db: db,
|
||||||
|
getChainHeight: () => chainHeight,
|
||||||
|
refreshedBalanceCallback: (balance) async {
|
||||||
|
_balance = balance;
|
||||||
|
await updateCachedBalance(_balance!);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const integrationTestFlag =
|
static const integrationTestFlag =
|
||||||
|
@ -1041,6 +1054,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
try {
|
try {
|
||||||
final feeRateType = args?["feeRate"];
|
final feeRateType = args?["feeRate"];
|
||||||
final feeRateAmount = args?["feeRateAmount"];
|
final feeRateAmount = args?["feeRateAmount"];
|
||||||
|
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
|
||||||
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
||||||
late final int rate;
|
late final int rate;
|
||||||
if (feeRateType is FeeRateType) {
|
if (feeRateType is FeeRateType) {
|
||||||
|
@ -1067,9 +1081,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
isSendAll = true;
|
isSendAll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result =
|
final result = await coinSelection(
|
||||||
await coinSelection(satoshiAmount, rate, address, isSendAll);
|
satoshiAmountToSend: satoshiAmount,
|
||||||
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
|
selectedTxFeeRate: rate,
|
||||||
|
recipientAddress: address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
utxos: utxos?.toList(),
|
||||||
|
coinControl: utxos is List<isar_models.UTXO>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Logging.instance
|
||||||
|
.log("PREPARE SEND RESULT: $result", level: LogLevel.Info);
|
||||||
if (result is int) {
|
if (result is int) {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -1115,6 +1137,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(
|
final txHash = await _electrumXClient.broadcastTransaction(
|
||||||
rawTx: txData["hex"] as String);
|
rawTx: txData["hex"] as String);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
|
||||||
|
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
|
||||||
|
|
||||||
|
// mark utxos as used
|
||||||
|
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -1719,49 +1747,47 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentChainHeight = await chainHeight;
|
|
||||||
|
|
||||||
final List<isar_models.UTXO> outputArray = [];
|
final List<isar_models.UTXO> outputArray = [];
|
||||||
int satoshiBalanceTotal = 0;
|
|
||||||
int satoshiBalancePending = 0;
|
|
||||||
int satoshiBalanceSpendable = 0;
|
|
||||||
int satoshiBalanceBlocked = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
|
final jsonUTXO = fetchedUtxoList[i][j];
|
||||||
|
|
||||||
final txn = await cachedElectrumXClient.getTransaction(
|
final txn = await cachedElectrumXClient.getTransaction(
|
||||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
txHash: jsonUTXO["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coin: coin,
|
coin: coin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// todo check here if we should mark as blocked
|
final vout = jsonUTXO["tx_pos"] as int;
|
||||||
|
|
||||||
|
final outputs = txn["vout"] as List;
|
||||||
|
|
||||||
|
String? utxoOwnerAddress;
|
||||||
|
// get UTXO owner address
|
||||||
|
for (final output in outputs) {
|
||||||
|
if (output["n"] == vout) {
|
||||||
|
utxoOwnerAddress =
|
||||||
|
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||||
|
output["scriptPubKey"]?["address"] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final utxo = isar_models.UTXO(
|
final utxo = isar_models.UTXO(
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
txid: txn["txid"] as String,
|
txid: txn["txid"] as String,
|
||||||
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
|
vout: vout,
|
||||||
value: fetchedUtxoList[i][j]["value"] as int,
|
value: jsonUTXO["value"] as int,
|
||||||
name: "",
|
name: "",
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
||||||
blockHash: txn["blockhash"] as String?,
|
blockHash: txn["blockhash"] as String?,
|
||||||
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
|
blockHeight: jsonUTXO["height"] as int?,
|
||||||
blockTime: txn["blocktime"] as int?,
|
blockTime: txn["blocktime"] as int?,
|
||||||
|
address: utxoOwnerAddress,
|
||||||
);
|
);
|
||||||
|
|
||||||
satoshiBalanceTotal += utxo.value;
|
|
||||||
|
|
||||||
if (utxo.isBlocked) {
|
|
||||||
satoshiBalanceBlocked += utxo.value;
|
|
||||||
} else {
|
|
||||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
|
||||||
satoshiBalanceSpendable += utxo.value;
|
|
||||||
} else {
|
|
||||||
satoshiBalancePending += utxo.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1769,27 +1795,20 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||||
|
|
||||||
// TODO move this out of here and into IDB
|
await db.updateUTXOs(walletId, outputArray);
|
||||||
await db.isar.writeTxn(() async {
|
|
||||||
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
|
|
||||||
await db.isar.utxos.putAll(outputArray);
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally update balance
|
// finally update balance
|
||||||
_balance = Balance(
|
await _updateBalance();
|
||||||
coin: coin,
|
|
||||||
total: satoshiBalanceTotal,
|
|
||||||
spendable: satoshiBalanceSpendable,
|
|
||||||
blockedTotal: satoshiBalanceBlocked,
|
|
||||||
pendingSpendable: satoshiBalancePending,
|
|
||||||
);
|
|
||||||
await updateCachedBalance(_balance!);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBalance() async {
|
||||||
|
await refreshBalance();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Balance get balance => _balance ??= getCachedBalance();
|
Balance get balance => _balance ??= getCachedBalance();
|
||||||
Balance? _balance;
|
Balance? _balance;
|
||||||
|
@ -2322,11 +2341,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
||||||
/// a map containing the tx hex along with other important information. If not, then it will return
|
/// a map containing the tx hex along with other important information. If not, then it will return
|
||||||
/// an integer (1 or 2)
|
/// an integer (1 or 2)
|
||||||
dynamic coinSelection(
|
dynamic coinSelection({
|
||||||
int satoshiAmountToSend,
|
required int satoshiAmountToSend,
|
||||||
int selectedTxFeeRate,
|
required int selectedTxFeeRate,
|
||||||
String _recipientAddress,
|
required String recipientAddress,
|
||||||
bool isSendAll, {
|
required bool coinControl,
|
||||||
|
required bool isSendAll,
|
||||||
int additionalOutputs = 0,
|
int additionalOutputs = 0,
|
||||||
List<isar_models.UTXO>? utxos,
|
List<isar_models.UTXO>? utxos,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -2338,18 +2358,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
int spendableSatoshiValue = 0;
|
int spendableSatoshiValue = 0;
|
||||||
|
|
||||||
// Build list of spendable outputs and totaling their satoshi amount
|
// Build list of spendable outputs and totaling their satoshi amount
|
||||||
for (var i = 0; i < availableOutputs.length; i++) {
|
for (final utxo in availableOutputs) {
|
||||||
if (availableOutputs[i].isBlocked == false &&
|
if (utxo.isBlocked == false &&
|
||||||
availableOutputs[i]
|
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
utxo.used != true) {
|
||||||
true) {
|
spendableOutputs.add(utxo);
|
||||||
spendableOutputs.add(availableOutputs[i]);
|
spendableSatoshiValue += utxo.value;
|
||||||
spendableSatoshiValue += availableOutputs[i].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort spendable by age (oldest first)
|
if (coinControl) {
|
||||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
if (spendableOutputs.length < availableOutputs.length) {
|
||||||
|
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't care about sorting if using all utxos
|
||||||
|
if (!coinControl) {
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||||
|
}
|
||||||
|
|
||||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
@ -2378,19 +2406,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
int inputsBeingConsumed = 0;
|
int inputsBeingConsumed = 0;
|
||||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||||
|
|
||||||
for (var i = 0;
|
if (!coinControl) {
|
||||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
for (var i = 0;
|
||||||
i++) {
|
satoshisBeingUsed < satoshiAmountToSend &&
|
||||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
i < spendableOutputs.length;
|
||||||
satoshisBeingUsed += spendableOutputs[i].value;
|
i++) {
|
||||||
inputsBeingConsumed += 1;
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
}
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
for (int i = 0;
|
inputsBeingConsumed += 1;
|
||||||
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
|
}
|
||||||
i++) {
|
for (int i = 0;
|
||||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
i < additionalOutputs &&
|
||||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
inputsBeingConsumed < spendableOutputs.length;
|
||||||
inputsBeingConsumed += 1;
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||||
|
inputsBeingConsumed += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
satoshisBeingUsed = spendableSatoshiValue;
|
||||||
|
utxoObjectsToUse = spendableOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging.instance
|
Logging.instance
|
||||||
|
@ -2403,7 +2438,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
|
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
|
||||||
|
|
||||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||||
List<String> recipientsArray = [_recipientAddress];
|
List<String> recipientsArray = [recipientAddress];
|
||||||
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||||
|
|
||||||
// gather required signing data
|
// gather required signing data
|
||||||
|
@ -2416,7 +2451,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
int feeForOneOutput = estimateTxFee(
|
int feeForOneOutput = estimateTxFee(
|
||||||
|
@ -2440,6 +2475,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": amount,
|
"recipientAmt": amount,
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2447,14 +2483,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
final int vSizeForTwoOutPuts = (await buildTransaction(
|
final int vSizeForTwoOutPuts = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
|
@ -2572,6 +2608,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeBeingPaid,
|
"fee": feeBeingPaid,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2599,6 +2636,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2628,6 +2666,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2657,6 +2696,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2668,9 +2708,15 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
level: LogLevel.Warning);
|
level: LogLevel.Warning);
|
||||||
// try adding more outputs
|
// try adding more outputs
|
||||||
if (spendableOutputs.length > inputsBeingConsumed) {
|
if (spendableOutputs.length > inputsBeingConsumed) {
|
||||||
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
|
return coinSelection(
|
||||||
_recipientAddress, isSendAll,
|
satoshiAmountToSend: satoshiAmountToSend,
|
||||||
additionalOutputs: additionalOutputs + 1, utxos: utxos);
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
recipientAddress: recipientAddress,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
utxos: utxos,
|
||||||
|
coinControl: coinControl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||||
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||||
|
@ -85,7 +86,7 @@ String constructDerivePath({
|
||||||
}
|
}
|
||||||
|
|
||||||
class DogecoinWallet extends CoinServiceAPI
|
class DogecoinWallet extends CoinServiceAPI
|
||||||
with WalletCache, WalletDB, ElectrumXParsing {
|
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
|
||||||
DogecoinWallet({
|
DogecoinWallet({
|
||||||
required String walletId,
|
required String walletId,
|
||||||
required String walletName,
|
required String walletName,
|
||||||
|
@ -105,6 +106,17 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
_secureStore = secureStore;
|
_secureStore = secureStore;
|
||||||
initCache(walletId, coin);
|
initCache(walletId, coin);
|
||||||
initWalletDB(mockableOverride: mockableOverride);
|
initWalletDB(mockableOverride: mockableOverride);
|
||||||
|
initCoinControlInterface(
|
||||||
|
walletId: walletId,
|
||||||
|
walletName: walletName,
|
||||||
|
coin: coin,
|
||||||
|
db: db,
|
||||||
|
getChainHeight: () => chainHeight,
|
||||||
|
refreshedBalanceCallback: (balance) async {
|
||||||
|
_balance = balance;
|
||||||
|
await updateCachedBalance(_balance!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// paynym stuff
|
// paynym stuff
|
||||||
// initPaynymWalletInterface(
|
// initPaynymWalletInterface(
|
||||||
|
@ -907,6 +919,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
try {
|
try {
|
||||||
final feeRateType = args?["feeRate"];
|
final feeRateType = args?["feeRate"];
|
||||||
final feeRateAmount = args?["feeRateAmount"];
|
final feeRateAmount = args?["feeRateAmount"];
|
||||||
|
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
|
||||||
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
||||||
late final int rate;
|
late final int rate;
|
||||||
if (feeRateType is FeeRateType) {
|
if (feeRateType is FeeRateType) {
|
||||||
|
@ -933,9 +946,17 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
isSendAll = true;
|
isSendAll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result =
|
final result = await coinSelection(
|
||||||
await coinSelection(satoshiAmount, rate, address, isSendAll);
|
satoshiAmountToSend: satoshiAmount,
|
||||||
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
|
selectedTxFeeRate: rate,
|
||||||
|
recipientAddress: address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
utxos: utxos?.toList(),
|
||||||
|
coinControl: utxos is List<isar_models.UTXO>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Logging.instance
|
||||||
|
.log("PREPARE SEND RESULT: $result", level: LogLevel.Info);
|
||||||
if (result is int) {
|
if (result is int) {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -981,6 +1002,12 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(
|
final txHash = await _electrumXClient.broadcastTransaction(
|
||||||
rawTx: txData["hex"] as String);
|
rawTx: txData["hex"] as String);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
|
||||||
|
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
|
||||||
|
|
||||||
|
// mark utxos as used
|
||||||
|
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -1537,18 +1564,14 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentChainHeight = await chainHeight;
|
|
||||||
|
|
||||||
final List<isar_models.UTXO> outputArray = [];
|
final List<isar_models.UTXO> outputArray = [];
|
||||||
int satoshiBalanceTotal = 0;
|
|
||||||
int satoshiBalancePending = 0;
|
|
||||||
int satoshiBalanceSpendable = 0;
|
|
||||||
int satoshiBalanceBlocked = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
|
final jsonUTXO = fetchedUtxoList[i][j];
|
||||||
|
|
||||||
final txn = await cachedElectrumXClient.getTransaction(
|
final txn = await cachedElectrumXClient.getTransaction(
|
||||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
txHash: jsonUTXO["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coin: coin,
|
coin: coin,
|
||||||
);
|
);
|
||||||
|
@ -1556,7 +1579,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
// fetch stored tx to see if paynym notification tx and block utxo
|
// fetch stored tx to see if paynym notification tx and block utxo
|
||||||
final storedTx = await db.getTransaction(
|
final storedTx = await db.getTransaction(
|
||||||
walletId,
|
walletId,
|
||||||
fetchedUtxoList[i][j]["tx_hash"] as String,
|
jsonUTXO["tx_hash"] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool shouldBlock = false;
|
bool shouldBlock = false;
|
||||||
|
@ -1573,32 +1596,35 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
blockReason = "Incoming paynym notification transaction.";
|
blockReason = "Incoming paynym notification transaction.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final vout = jsonUTXO["tx_pos"] as int;
|
||||||
|
|
||||||
|
final outputs = txn["vout"] as List;
|
||||||
|
|
||||||
|
String? utxoOwnerAddress;
|
||||||
|
// get UTXO owner address
|
||||||
|
for (final output in outputs) {
|
||||||
|
if (output["n"] == vout) {
|
||||||
|
utxoOwnerAddress =
|
||||||
|
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||||
|
output["scriptPubKey"]?["address"] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final utxo = isar_models.UTXO(
|
final utxo = isar_models.UTXO(
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
txid: txn["txid"] as String,
|
txid: txn["txid"] as String,
|
||||||
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
|
vout: vout,
|
||||||
value: fetchedUtxoList[i][j]["value"] as int,
|
value: jsonUTXO["value"] as int,
|
||||||
name: "",
|
name: "",
|
||||||
isBlocked: shouldBlock,
|
isBlocked: shouldBlock,
|
||||||
blockedReason: blockReason,
|
blockedReason: blockReason,
|
||||||
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
||||||
blockHash: txn["blockhash"] as String?,
|
blockHash: txn["blockhash"] as String?,
|
||||||
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
|
blockHeight: jsonUTXO["height"] as int?,
|
||||||
blockTime: txn["blocktime"] as int?,
|
blockTime: txn["blocktime"] as int?,
|
||||||
|
address: utxoOwnerAddress,
|
||||||
);
|
);
|
||||||
|
|
||||||
satoshiBalanceTotal += utxo.value;
|
|
||||||
|
|
||||||
if (utxo.isBlocked) {
|
|
||||||
satoshiBalanceBlocked += utxo.value;
|
|
||||||
} else {
|
|
||||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
|
||||||
satoshiBalanceSpendable += utxo.value;
|
|
||||||
} else {
|
|
||||||
satoshiBalancePending += utxo.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1606,27 +1632,20 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||||
|
|
||||||
// TODO move this out of here and into IDB
|
await db.updateUTXOs(walletId, outputArray);
|
||||||
await db.isar.writeTxn(() async {
|
|
||||||
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
|
|
||||||
await db.isar.utxos.putAll(outputArray);
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally update balance
|
// finally update balance
|
||||||
_balance = Balance(
|
await _updateBalance();
|
||||||
coin: coin,
|
|
||||||
total: satoshiBalanceTotal,
|
|
||||||
spendable: satoshiBalanceSpendable,
|
|
||||||
blockedTotal: satoshiBalanceBlocked,
|
|
||||||
pendingSpendable: satoshiBalancePending,
|
|
||||||
);
|
|
||||||
await updateCachedBalance(_balance!);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBalance() async {
|
||||||
|
await refreshBalance();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Balance get balance => _balance ??= getCachedBalance();
|
Balance get balance => _balance ??= getCachedBalance();
|
||||||
Balance? _balance;
|
Balance? _balance;
|
||||||
|
@ -2022,11 +2041,12 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
||||||
/// a map containing the tx hex along with other important information. If not, then it will return
|
/// a map containing the tx hex along with other important information. If not, then it will return
|
||||||
/// an integer (1 or 2)
|
/// an integer (1 or 2)
|
||||||
dynamic coinSelection(
|
dynamic coinSelection({
|
||||||
int satoshiAmountToSend,
|
required int satoshiAmountToSend,
|
||||||
int selectedTxFeeRate,
|
required int selectedTxFeeRate,
|
||||||
String _recipientAddress,
|
required String recipientAddress,
|
||||||
bool isSendAll, {
|
required bool coinControl,
|
||||||
|
required bool isSendAll,
|
||||||
int additionalOutputs = 0,
|
int additionalOutputs = 0,
|
||||||
List<isar_models.UTXO>? utxos,
|
List<isar_models.UTXO>? utxos,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -2038,18 +2058,26 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
int spendableSatoshiValue = 0;
|
int spendableSatoshiValue = 0;
|
||||||
|
|
||||||
// Build list of spendable outputs and totaling their satoshi amount
|
// Build list of spendable outputs and totaling their satoshi amount
|
||||||
for (var i = 0; i < availableOutputs.length; i++) {
|
for (final utxo in availableOutputs) {
|
||||||
if (availableOutputs[i].isBlocked == false &&
|
if (utxo.isBlocked == false &&
|
||||||
availableOutputs[i]
|
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
utxo.used != true) {
|
||||||
true) {
|
spendableOutputs.add(utxo);
|
||||||
spendableOutputs.add(availableOutputs[i]);
|
spendableSatoshiValue += utxo.value;
|
||||||
spendableSatoshiValue += availableOutputs[i].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort spendable by age (oldest first)
|
if (coinControl) {
|
||||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
if (spendableOutputs.length < availableOutputs.length) {
|
||||||
|
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't care about sorting if using all utxos
|
||||||
|
if (!coinControl) {
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||||
|
}
|
||||||
|
|
||||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
@ -2078,19 +2106,26 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
int inputsBeingConsumed = 0;
|
int inputsBeingConsumed = 0;
|
||||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||||
|
|
||||||
for (var i = 0;
|
if (!coinControl) {
|
||||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
for (var i = 0;
|
||||||
i++) {
|
satoshisBeingUsed < satoshiAmountToSend &&
|
||||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
i < spendableOutputs.length;
|
||||||
satoshisBeingUsed += spendableOutputs[i].value;
|
i++) {
|
||||||
inputsBeingConsumed += 1;
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
}
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
for (int i = 0;
|
inputsBeingConsumed += 1;
|
||||||
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
|
}
|
||||||
i++) {
|
for (int i = 0;
|
||||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
i < additionalOutputs &&
|
||||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
inputsBeingConsumed < spendableOutputs.length;
|
||||||
inputsBeingConsumed += 1;
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||||
|
inputsBeingConsumed += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
satoshisBeingUsed = spendableSatoshiValue;
|
||||||
|
utxoObjectsToUse = spendableOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging.instance
|
Logging.instance
|
||||||
|
@ -2103,7 +2138,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
|
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
|
||||||
|
|
||||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||||
List<String> recipientsArray = [_recipientAddress];
|
List<String> recipientsArray = [recipientAddress];
|
||||||
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||||
|
|
||||||
// gather required signing data
|
// gather required signing data
|
||||||
|
@ -2116,7 +2151,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
int feeForOneOutput = estimateTxFee(
|
int feeForOneOutput = estimateTxFee(
|
||||||
|
@ -2140,6 +2175,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": amount,
|
"recipientAmt": amount,
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2147,14 +2183,14 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
final int vSizeForTwoOutPuts = (await buildTransaction(
|
final int vSizeForTwoOutPuts = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
|
@ -2272,6 +2308,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeBeingPaid,
|
"fee": feeBeingPaid,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2299,6 +2336,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2328,6 +2366,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2357,6 +2396,7 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2368,9 +2408,15 @@ class DogecoinWallet extends CoinServiceAPI
|
||||||
level: LogLevel.Warning);
|
level: LogLevel.Warning);
|
||||||
// try adding more outputs
|
// try adding more outputs
|
||||||
if (spendableOutputs.length > inputsBeingConsumed) {
|
if (spendableOutputs.length > inputsBeingConsumed) {
|
||||||
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
|
return coinSelection(
|
||||||
_recipientAddress, isSendAll,
|
satoshiAmountToSend: satoshiAmountToSend,
|
||||||
additionalOutputs: additionalOutputs + 1, utxos: utxos);
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
recipientAddress: recipientAddress,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
utxos: utxos,
|
||||||
|
coinControl: coinControl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||||
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||||
|
@ -90,7 +91,7 @@ String constructDerivePath({
|
||||||
}
|
}
|
||||||
|
|
||||||
class LitecoinWallet extends CoinServiceAPI
|
class LitecoinWallet extends CoinServiceAPI
|
||||||
with WalletCache, WalletDB, ElectrumXParsing {
|
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
|
||||||
LitecoinWallet({
|
LitecoinWallet({
|
||||||
required String walletId,
|
required String walletId,
|
||||||
required String walletName,
|
required String walletName,
|
||||||
|
@ -110,6 +111,17 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
_secureStore = secureStore;
|
_secureStore = secureStore;
|
||||||
initCache(walletId, coin);
|
initCache(walletId, coin);
|
||||||
initWalletDB(mockableOverride: mockableOverride);
|
initWalletDB(mockableOverride: mockableOverride);
|
||||||
|
initCoinControlInterface(
|
||||||
|
walletId: walletId,
|
||||||
|
walletName: walletName,
|
||||||
|
coin: coin,
|
||||||
|
db: db,
|
||||||
|
getChainHeight: () => chainHeight,
|
||||||
|
refreshedBalanceCallback: (balance) async {
|
||||||
|
_balance = balance;
|
||||||
|
await updateCachedBalance(_balance!);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const integrationTestFlag =
|
static const integrationTestFlag =
|
||||||
|
@ -1018,6 +1030,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
try {
|
try {
|
||||||
final feeRateType = args?["feeRate"];
|
final feeRateType = args?["feeRate"];
|
||||||
final feeRateAmount = args?["feeRateAmount"];
|
final feeRateAmount = args?["feeRateAmount"];
|
||||||
|
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
|
||||||
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
||||||
late final int rate;
|
late final int rate;
|
||||||
if (feeRateType is FeeRateType) {
|
if (feeRateType is FeeRateType) {
|
||||||
|
@ -1045,8 +1058,14 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
isSendAll = true;
|
isSendAll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final txData =
|
final txData = await coinSelection(
|
||||||
await coinSelection(satoshiAmount, rate, address, isSendAll);
|
satoshiAmountToSend: satoshiAmount,
|
||||||
|
selectedTxFeeRate: rate,
|
||||||
|
recipientAddress: address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
utxos: utxos?.toList(),
|
||||||
|
coinControl: utxos is List<isar_models.UTXO>,
|
||||||
|
);
|
||||||
|
|
||||||
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
||||||
try {
|
try {
|
||||||
|
@ -1109,6 +1128,11 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
|
||||||
|
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
|
||||||
|
|
||||||
|
// mark utxos as used
|
||||||
|
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -1724,49 +1748,47 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentChainHeight = await chainHeight;
|
|
||||||
|
|
||||||
final List<isar_models.UTXO> outputArray = [];
|
final List<isar_models.UTXO> outputArray = [];
|
||||||
int satoshiBalanceTotal = 0;
|
|
||||||
int satoshiBalancePending = 0;
|
|
||||||
int satoshiBalanceSpendable = 0;
|
|
||||||
int satoshiBalanceBlocked = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
|
final jsonUTXO = fetchedUtxoList[i][j];
|
||||||
|
|
||||||
final txn = await cachedElectrumXClient.getTransaction(
|
final txn = await cachedElectrumXClient.getTransaction(
|
||||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
txHash: jsonUTXO["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coin: coin,
|
coin: coin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// todo check here if we should mark as blocked
|
final vout = jsonUTXO["tx_pos"] as int;
|
||||||
|
|
||||||
|
final outputs = txn["vout"] as List;
|
||||||
|
|
||||||
|
String? utxoOwnerAddress;
|
||||||
|
// get UTXO owner address
|
||||||
|
for (final output in outputs) {
|
||||||
|
if (output["n"] == vout) {
|
||||||
|
utxoOwnerAddress =
|
||||||
|
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||||
|
output["scriptPubKey"]?["address"] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final utxo = isar_models.UTXO(
|
final utxo = isar_models.UTXO(
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
txid: txn["txid"] as String,
|
txid: txn["txid"] as String,
|
||||||
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
|
vout: vout,
|
||||||
value: fetchedUtxoList[i][j]["value"] as int,
|
value: jsonUTXO["value"] as int,
|
||||||
name: "",
|
name: "",
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
||||||
blockHash: txn["blockhash"] as String?,
|
blockHash: txn["blockhash"] as String?,
|
||||||
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
|
blockHeight: jsonUTXO["height"] as int?,
|
||||||
blockTime: txn["blocktime"] as int?,
|
blockTime: txn["blocktime"] as int?,
|
||||||
|
address: utxoOwnerAddress,
|
||||||
);
|
);
|
||||||
|
|
||||||
satoshiBalanceTotal += utxo.value;
|
|
||||||
|
|
||||||
if (utxo.isBlocked) {
|
|
||||||
satoshiBalanceBlocked += utxo.value;
|
|
||||||
} else {
|
|
||||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
|
||||||
satoshiBalanceSpendable += utxo.value;
|
|
||||||
} else {
|
|
||||||
satoshiBalancePending += utxo.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1774,27 +1796,20 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||||
|
|
||||||
// TODO move this out of here and into IDB
|
await db.updateUTXOs(walletId, outputArray);
|
||||||
await db.isar.writeTxn(() async {
|
|
||||||
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
|
|
||||||
await db.isar.utxos.putAll(outputArray);
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally update balance
|
// finally update balance
|
||||||
_balance = Balance(
|
await _updateBalance();
|
||||||
coin: coin,
|
|
||||||
total: satoshiBalanceTotal,
|
|
||||||
spendable: satoshiBalanceSpendable,
|
|
||||||
blockedTotal: satoshiBalanceBlocked,
|
|
||||||
pendingSpendable: satoshiBalancePending,
|
|
||||||
);
|
|
||||||
await updateCachedBalance(_balance!);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBalance() async {
|
||||||
|
await refreshBalance();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Balance get balance => _balance ??= getCachedBalance();
|
Balance get balance => _balance ??= getCachedBalance();
|
||||||
Balance? _balance;
|
Balance? _balance;
|
||||||
|
@ -2214,11 +2229,12 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
||||||
/// a map containing the tx hex along with other important information. If not, then it will return
|
/// a map containing the tx hex along with other important information. If not, then it will return
|
||||||
/// an integer (1 or 2)
|
/// an integer (1 or 2)
|
||||||
dynamic coinSelection(
|
dynamic coinSelection({
|
||||||
int satoshiAmountToSend,
|
required int satoshiAmountToSend,
|
||||||
int selectedTxFeeRate,
|
required int selectedTxFeeRate,
|
||||||
String _recipientAddress,
|
required String recipientAddress,
|
||||||
bool isSendAll, {
|
required bool coinControl,
|
||||||
|
required bool isSendAll,
|
||||||
int additionalOutputs = 0,
|
int additionalOutputs = 0,
|
||||||
List<isar_models.UTXO>? utxos,
|
List<isar_models.UTXO>? utxos,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -2230,18 +2246,26 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
int spendableSatoshiValue = 0;
|
int spendableSatoshiValue = 0;
|
||||||
|
|
||||||
// Build list of spendable outputs and totaling their satoshi amount
|
// Build list of spendable outputs and totaling their satoshi amount
|
||||||
for (var i = 0; i < availableOutputs.length; i++) {
|
for (final utxo in availableOutputs) {
|
||||||
if (availableOutputs[i].isBlocked == false &&
|
if (utxo.isBlocked == false &&
|
||||||
availableOutputs[i]
|
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
utxo.used != true) {
|
||||||
true) {
|
spendableOutputs.add(utxo);
|
||||||
spendableOutputs.add(availableOutputs[i]);
|
spendableSatoshiValue += utxo.value;
|
||||||
spendableSatoshiValue += availableOutputs[i].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort spendable by age (oldest first)
|
if (coinControl) {
|
||||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
if (spendableOutputs.length < availableOutputs.length) {
|
||||||
|
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't care about sorting if using all utxos
|
||||||
|
if (!coinControl) {
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||||
|
}
|
||||||
|
|
||||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
@ -2270,19 +2294,26 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
int inputsBeingConsumed = 0;
|
int inputsBeingConsumed = 0;
|
||||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||||
|
|
||||||
for (var i = 0;
|
if (!coinControl) {
|
||||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
for (var i = 0;
|
||||||
i++) {
|
satoshisBeingUsed < satoshiAmountToSend &&
|
||||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
i < spendableOutputs.length;
|
||||||
satoshisBeingUsed += spendableOutputs[i].value;
|
i++) {
|
||||||
inputsBeingConsumed += 1;
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
}
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
for (int i = 0;
|
inputsBeingConsumed += 1;
|
||||||
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
|
}
|
||||||
i++) {
|
for (int i = 0;
|
||||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
i < additionalOutputs &&
|
||||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
inputsBeingConsumed < spendableOutputs.length;
|
||||||
inputsBeingConsumed += 1;
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||||
|
inputsBeingConsumed += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
satoshisBeingUsed = spendableSatoshiValue;
|
||||||
|
utxoObjectsToUse = spendableOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging.instance
|
Logging.instance
|
||||||
|
@ -2293,7 +2324,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
||||||
|
|
||||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||||
List<String> recipientsArray = [_recipientAddress];
|
List<String> recipientsArray = [recipientAddress];
|
||||||
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||||
|
|
||||||
// gather required signing data
|
// gather required signing data
|
||||||
|
@ -2306,7 +2337,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
int feeForOneOutput = estimateTxFee(
|
int feeForOneOutput = estimateTxFee(
|
||||||
|
@ -2333,6 +2364,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": amount,
|
"recipientAmt": amount,
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2340,14 +2372,14 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
final int vSizeForTwoOutPuts = (await buildTransaction(
|
final int vSizeForTwoOutPuts = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
|
@ -2451,6 +2483,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeBeingPaid,
|
"fee": feeBeingPaid,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2478,6 +2511,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2507,6 +2541,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2536,6 +2571,7 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2547,9 +2583,15 @@ class LitecoinWallet extends CoinServiceAPI
|
||||||
level: LogLevel.Warning);
|
level: LogLevel.Warning);
|
||||||
// try adding more outputs
|
// try adding more outputs
|
||||||
if (spendableOutputs.length > inputsBeingConsumed) {
|
if (spendableOutputs.length > inputsBeingConsumed) {
|
||||||
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
|
return coinSelection(
|
||||||
_recipientAddress, isSendAll,
|
satoshiAmountToSend: satoshiAmountToSend,
|
||||||
additionalOutputs: additionalOutputs + 1, utxos: utxos);
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
recipientAddress: recipientAddress,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
utxos: utxos,
|
||||||
|
coinControl: coinControl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||||
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||||
|
@ -87,7 +88,7 @@ String constructDerivePath({
|
||||||
}
|
}
|
||||||
|
|
||||||
class NamecoinWallet extends CoinServiceAPI
|
class NamecoinWallet extends CoinServiceAPI
|
||||||
with WalletCache, WalletDB, ElectrumXParsing {
|
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
|
||||||
NamecoinWallet({
|
NamecoinWallet({
|
||||||
required String walletId,
|
required String walletId,
|
||||||
required String walletName,
|
required String walletName,
|
||||||
|
@ -107,6 +108,17 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
_secureStore = secureStore;
|
_secureStore = secureStore;
|
||||||
initCache(walletId, coin);
|
initCache(walletId, coin);
|
||||||
initWalletDB(mockableOverride: mockableOverride);
|
initWalletDB(mockableOverride: mockableOverride);
|
||||||
|
initCoinControlInterface(
|
||||||
|
walletId: walletId,
|
||||||
|
walletName: walletName,
|
||||||
|
coin: coin,
|
||||||
|
db: db,
|
||||||
|
getChainHeight: () => chainHeight,
|
||||||
|
refreshedBalanceCallback: (balance) async {
|
||||||
|
_balance = balance;
|
||||||
|
await updateCachedBalance(_balance!);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const integrationTestFlag =
|
static const integrationTestFlag =
|
||||||
|
@ -1009,6 +1021,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
try {
|
try {
|
||||||
final feeRateType = args?["feeRate"];
|
final feeRateType = args?["feeRate"];
|
||||||
final feeRateAmount = args?["feeRateAmount"];
|
final feeRateAmount = args?["feeRateAmount"];
|
||||||
|
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
|
||||||
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
||||||
late final int rate;
|
late final int rate;
|
||||||
if (feeRateType is FeeRateType) {
|
if (feeRateType is FeeRateType) {
|
||||||
|
@ -1036,8 +1049,14 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
isSendAll = true;
|
isSendAll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final txData =
|
final txData = await coinSelection(
|
||||||
await coinSelection(satoshiAmount, rate, address, isSendAll);
|
satoshiAmountToSend: satoshiAmount,
|
||||||
|
selectedTxFeeRate: rate,
|
||||||
|
recipientAddress: address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
utxos: utxos?.toList(),
|
||||||
|
coinControl: utxos is List<isar_models.UTXO>,
|
||||||
|
);
|
||||||
|
|
||||||
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
||||||
try {
|
try {
|
||||||
|
@ -1100,6 +1119,11 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
|
||||||
|
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
|
||||||
|
|
||||||
|
// mark utxos as used
|
||||||
|
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -1709,49 +1733,47 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentChainHeight = await chainHeight;
|
|
||||||
|
|
||||||
final List<isar_models.UTXO> outputArray = [];
|
final List<isar_models.UTXO> outputArray = [];
|
||||||
int satoshiBalanceTotal = 0;
|
|
||||||
int satoshiBalancePending = 0;
|
|
||||||
int satoshiBalanceSpendable = 0;
|
|
||||||
int satoshiBalanceBlocked = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
|
final jsonUTXO = fetchedUtxoList[i][j];
|
||||||
|
|
||||||
final txn = await cachedElectrumXClient.getTransaction(
|
final txn = await cachedElectrumXClient.getTransaction(
|
||||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
txHash: jsonUTXO["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coin: coin,
|
coin: coin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// todo check here if we should mark as blocked
|
final vout = jsonUTXO["tx_pos"] as int;
|
||||||
|
|
||||||
|
final outputs = txn["vout"] as List;
|
||||||
|
|
||||||
|
String? utxoOwnerAddress;
|
||||||
|
// get UTXO owner address
|
||||||
|
for (final output in outputs) {
|
||||||
|
if (output["n"] == vout) {
|
||||||
|
utxoOwnerAddress =
|
||||||
|
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||||
|
output["scriptPubKey"]?["address"] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final utxo = isar_models.UTXO(
|
final utxo = isar_models.UTXO(
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
txid: txn["txid"] as String,
|
txid: txn["txid"] as String,
|
||||||
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
|
vout: vout,
|
||||||
value: fetchedUtxoList[i][j]["value"] as int,
|
value: jsonUTXO["value"] as int,
|
||||||
name: "",
|
name: "",
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
||||||
blockHash: txn["blockhash"] as String?,
|
blockHash: txn["blockhash"] as String?,
|
||||||
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
|
blockHeight: jsonUTXO["height"] as int?,
|
||||||
blockTime: txn["blocktime"] as int?,
|
blockTime: txn["blocktime"] as int?,
|
||||||
|
address: utxoOwnerAddress,
|
||||||
);
|
);
|
||||||
|
|
||||||
satoshiBalanceTotal += utxo.value;
|
|
||||||
|
|
||||||
if (utxo.isBlocked) {
|
|
||||||
satoshiBalanceBlocked += utxo.value;
|
|
||||||
} else {
|
|
||||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
|
||||||
satoshiBalanceSpendable += utxo.value;
|
|
||||||
} else {
|
|
||||||
satoshiBalancePending += utxo.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1759,27 +1781,20 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||||
|
|
||||||
// TODO move this out of here and into IDB
|
await db.updateUTXOs(walletId, outputArray);
|
||||||
await db.isar.writeTxn(() async {
|
|
||||||
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
|
|
||||||
await db.isar.utxos.putAll(outputArray);
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally update balance
|
// finally update balance
|
||||||
_balance = Balance(
|
await _updateBalance();
|
||||||
coin: coin,
|
|
||||||
total: satoshiBalanceTotal,
|
|
||||||
spendable: satoshiBalanceSpendable,
|
|
||||||
blockedTotal: satoshiBalanceBlocked,
|
|
||||||
pendingSpendable: satoshiBalancePending,
|
|
||||||
);
|
|
||||||
await updateCachedBalance(_balance!);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBalance() async {
|
||||||
|
await refreshBalance();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Balance get balance => _balance ??= getCachedBalance();
|
Balance get balance => _balance ??= getCachedBalance();
|
||||||
Balance? _balance;
|
Balance? _balance;
|
||||||
|
@ -2207,11 +2222,12 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
||||||
/// a map containing the tx hex along with other important information. If not, then it will return
|
/// a map containing the tx hex along with other important information. If not, then it will return
|
||||||
/// an integer (1 or 2)
|
/// an integer (1 or 2)
|
||||||
dynamic coinSelection(
|
dynamic coinSelection({
|
||||||
int satoshiAmountToSend,
|
required int satoshiAmountToSend,
|
||||||
int selectedTxFeeRate,
|
required int selectedTxFeeRate,
|
||||||
String _recipientAddress,
|
required String recipientAddress,
|
||||||
bool isSendAll, {
|
required bool coinControl,
|
||||||
|
required bool isSendAll,
|
||||||
int additionalOutputs = 0,
|
int additionalOutputs = 0,
|
||||||
List<isar_models.UTXO>? utxos,
|
List<isar_models.UTXO>? utxos,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -2223,18 +2239,26 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
int spendableSatoshiValue = 0;
|
int spendableSatoshiValue = 0;
|
||||||
|
|
||||||
// Build list of spendable outputs and totaling their satoshi amount
|
// Build list of spendable outputs and totaling their satoshi amount
|
||||||
for (var i = 0; i < availableOutputs.length; i++) {
|
for (final utxo in availableOutputs) {
|
||||||
if (availableOutputs[i].isBlocked == false &&
|
if (utxo.isBlocked == false &&
|
||||||
availableOutputs[i]
|
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
utxo.used != true) {
|
||||||
true) {
|
spendableOutputs.add(utxo);
|
||||||
spendableOutputs.add(availableOutputs[i]);
|
spendableSatoshiValue += utxo.value;
|
||||||
spendableSatoshiValue += availableOutputs[i].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort spendable by age (oldest first)
|
if (coinControl) {
|
||||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
if (spendableOutputs.length < availableOutputs.length) {
|
||||||
|
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't care about sorting if using all utxos
|
||||||
|
if (!coinControl) {
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||||
|
}
|
||||||
|
|
||||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
@ -2263,19 +2287,26 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
int inputsBeingConsumed = 0;
|
int inputsBeingConsumed = 0;
|
||||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||||
|
|
||||||
for (var i = 0;
|
if (!coinControl) {
|
||||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
for (var i = 0;
|
||||||
i++) {
|
satoshisBeingUsed < satoshiAmountToSend &&
|
||||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
i < spendableOutputs.length;
|
||||||
satoshisBeingUsed += spendableOutputs[i].value;
|
i++) {
|
||||||
inputsBeingConsumed += 1;
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
}
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
for (int i = 0;
|
inputsBeingConsumed += 1;
|
||||||
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
|
}
|
||||||
i++) {
|
for (int i = 0;
|
||||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
i < additionalOutputs &&
|
||||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
inputsBeingConsumed < spendableOutputs.length;
|
||||||
inputsBeingConsumed += 1;
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||||
|
inputsBeingConsumed += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
satoshisBeingUsed = spendableSatoshiValue;
|
||||||
|
utxoObjectsToUse = spendableOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging.instance
|
Logging.instance
|
||||||
|
@ -2286,7 +2317,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
||||||
|
|
||||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||||
List<String> recipientsArray = [_recipientAddress];
|
List<String> recipientsArray = [recipientAddress];
|
||||||
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||||
|
|
||||||
// gather required signing data
|
// gather required signing data
|
||||||
|
@ -2299,7 +2330,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
int feeForOneOutput = estimateTxFee(
|
int feeForOneOutput = estimateTxFee(
|
||||||
|
@ -2326,6 +2357,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": amount,
|
"recipientAmt": amount,
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2333,14 +2365,14 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
final int vSizeForTwoOutPuts = (await buildTransaction(
|
final int vSizeForTwoOutPuts = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
|
@ -2444,6 +2476,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeBeingPaid,
|
"fee": feeBeingPaid,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2471,6 +2504,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2500,6 +2534,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2529,6 +2564,7 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2540,9 +2576,15 @@ class NamecoinWallet extends CoinServiceAPI
|
||||||
level: LogLevel.Warning);
|
level: LogLevel.Warning);
|
||||||
// try adding more outputs
|
// try adding more outputs
|
||||||
if (spendableOutputs.length > inputsBeingConsumed) {
|
if (spendableOutputs.length > inputsBeingConsumed) {
|
||||||
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
|
return coinSelection(
|
||||||
_recipientAddress, isSendAll,
|
satoshiAmountToSend: satoshiAmountToSend,
|
||||||
additionalOutputs: additionalOutputs + 1, utxos: utxos);
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
recipientAddress: recipientAddress,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
utxos: utxos,
|
||||||
|
coinControl: coinControl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||||
import 'package:stackwallet/services/node_service.dart';
|
import 'package:stackwallet/services/node_service.dart';
|
||||||
|
@ -81,7 +82,8 @@ String constructDerivePath({
|
||||||
return "m/$purpose'/$coinType'/$account'/$chain/$index";
|
return "m/$purpose'/$coinType'/$account'/$chain/$index";
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
class ParticlWallet extends CoinServiceAPI
|
||||||
|
with WalletCache, WalletDB, CoinControlInterface {
|
||||||
ParticlWallet({
|
ParticlWallet({
|
||||||
required String walletId,
|
required String walletId,
|
||||||
required String walletName,
|
required String walletName,
|
||||||
|
@ -101,6 +103,17 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
_secureStore = secureStore;
|
_secureStore = secureStore;
|
||||||
initCache(walletId, coin);
|
initCache(walletId, coin);
|
||||||
initWalletDB(mockableOverride: mockableOverride);
|
initWalletDB(mockableOverride: mockableOverride);
|
||||||
|
initCoinControlInterface(
|
||||||
|
walletId: walletId,
|
||||||
|
walletName: walletName,
|
||||||
|
coin: coin,
|
||||||
|
db: db,
|
||||||
|
getChainHeight: () => chainHeight,
|
||||||
|
refreshedBalanceCallback: (balance) async {
|
||||||
|
_balance = balance;
|
||||||
|
await updateCachedBalance(_balance!);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const integrationTestFlag =
|
static const integrationTestFlag =
|
||||||
|
@ -935,6 +948,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
try {
|
try {
|
||||||
final feeRateType = args?["feeRate"];
|
final feeRateType = args?["feeRate"];
|
||||||
final feeRateAmount = args?["feeRateAmount"];
|
final feeRateAmount = args?["feeRateAmount"];
|
||||||
|
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
|
||||||
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
if (feeRateType is FeeRateType || feeRateAmount is int) {
|
||||||
late final int rate;
|
late final int rate;
|
||||||
if (feeRateType is FeeRateType) {
|
if (feeRateType is FeeRateType) {
|
||||||
|
@ -962,8 +976,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
isSendAll = true;
|
isSendAll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final txData =
|
final txData = await coinSelection(
|
||||||
await coinSelection(satoshiAmount, rate, address, isSendAll);
|
satoshiAmountToSend: satoshiAmount,
|
||||||
|
selectedTxFeeRate: rate,
|
||||||
|
recipientAddress: address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
utxos: utxos?.toList(),
|
||||||
|
coinControl: utxos is List<isar_models.UTXO>,
|
||||||
|
);
|
||||||
|
|
||||||
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
||||||
try {
|
try {
|
||||||
|
@ -1026,6 +1046,11 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
|
||||||
|
final utxos = txData["usedUTXOs"] as List<isar_models.UTXO>;
|
||||||
|
|
||||||
|
// mark utxos as used
|
||||||
|
await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList());
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -1595,49 +1620,47 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentChainHeight = await chainHeight;
|
|
||||||
|
|
||||||
final List<isar_models.UTXO> outputArray = [];
|
final List<isar_models.UTXO> outputArray = [];
|
||||||
int satoshiBalanceTotal = 0;
|
|
||||||
int satoshiBalancePending = 0;
|
|
||||||
int satoshiBalanceSpendable = 0;
|
|
||||||
int satoshiBalanceBlocked = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
|
final jsonUTXO = fetchedUtxoList[i][j];
|
||||||
|
|
||||||
final txn = await cachedElectrumXClient.getTransaction(
|
final txn = await cachedElectrumXClient.getTransaction(
|
||||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
txHash: jsonUTXO["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coin: coin,
|
coin: coin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// todo check here if we should mark as blocked
|
final vout = jsonUTXO["tx_pos"] as int;
|
||||||
|
|
||||||
|
final outputs = txn["vout"] as List;
|
||||||
|
|
||||||
|
String? utxoOwnerAddress;
|
||||||
|
// get UTXO owner address
|
||||||
|
for (final output in outputs) {
|
||||||
|
if (output["n"] == vout) {
|
||||||
|
utxoOwnerAddress =
|
||||||
|
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||||
|
output["scriptPubKey"]?["address"] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final utxo = isar_models.UTXO(
|
final utxo = isar_models.UTXO(
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
txid: txn["txid"] as String,
|
txid: txn["txid"] as String,
|
||||||
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
|
vout: vout,
|
||||||
value: fetchedUtxoList[i][j]["value"] as int,
|
value: jsonUTXO["value"] as int,
|
||||||
name: "",
|
name: "",
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
isCoinbase: txn["is_coinbase"] as bool? ?? false,
|
||||||
blockHash: txn["blockhash"] as String?,
|
blockHash: txn["blockhash"] as String?,
|
||||||
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
|
blockHeight: jsonUTXO["height"] as int?,
|
||||||
blockTime: txn["blocktime"] as int?,
|
blockTime: txn["blocktime"] as int?,
|
||||||
|
address: utxoOwnerAddress,
|
||||||
);
|
);
|
||||||
|
|
||||||
satoshiBalanceTotal += utxo.value;
|
|
||||||
|
|
||||||
if (utxo.isBlocked) {
|
|
||||||
satoshiBalanceBlocked += utxo.value;
|
|
||||||
} else {
|
|
||||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
|
||||||
satoshiBalanceSpendable += utxo.value;
|
|
||||||
} else {
|
|
||||||
satoshiBalancePending += utxo.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1645,27 +1668,20 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||||
|
|
||||||
// TODO move this out of here and into IDB
|
await db.updateUTXOs(walletId, outputArray);
|
||||||
await db.isar.writeTxn(() async {
|
|
||||||
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
|
|
||||||
await db.isar.utxos.putAll(outputArray);
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally update balance
|
// finally update balance
|
||||||
_balance = Balance(
|
await _updateBalance();
|
||||||
coin: coin,
|
|
||||||
total: satoshiBalanceTotal,
|
|
||||||
spendable: satoshiBalanceSpendable,
|
|
||||||
blockedTotal: satoshiBalanceBlocked,
|
|
||||||
pendingSpendable: satoshiBalancePending,
|
|
||||||
);
|
|
||||||
await updateCachedBalance(_balance!);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBalance() async {
|
||||||
|
await refreshBalance();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Balance get balance => _balance ??= getCachedBalance();
|
Balance get balance => _balance ??= getCachedBalance();
|
||||||
Balance? _balance;
|
Balance? _balance;
|
||||||
|
@ -2367,11 +2383,12 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
|
||||||
/// a map containing the tx hex along with other important information. If not, then it will return
|
/// a map containing the tx hex along with other important information. If not, then it will return
|
||||||
/// an integer (1 or 2)
|
/// an integer (1 or 2)
|
||||||
dynamic coinSelection(
|
dynamic coinSelection({
|
||||||
int satoshiAmountToSend,
|
required int satoshiAmountToSend,
|
||||||
int selectedTxFeeRate,
|
required int selectedTxFeeRate,
|
||||||
String _recipientAddress,
|
required String recipientAddress,
|
||||||
bool isSendAll, {
|
required bool coinControl,
|
||||||
|
required bool isSendAll,
|
||||||
int additionalOutputs = 0,
|
int additionalOutputs = 0,
|
||||||
List<isar_models.UTXO>? utxos,
|
List<isar_models.UTXO>? utxos,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -2381,19 +2398,28 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final currentChainHeight = await chainHeight;
|
final currentChainHeight = await chainHeight;
|
||||||
final List<isar_models.UTXO> spendableOutputs = [];
|
final List<isar_models.UTXO> spendableOutputs = [];
|
||||||
int spendableSatoshiValue = 0;
|
int spendableSatoshiValue = 0;
|
||||||
|
|
||||||
// Build list of spendable outputs and totaling their satoshi amount
|
// Build list of spendable outputs and totaling their satoshi amount
|
||||||
for (var i = 0; i < availableOutputs.length; i++) {
|
for (final utxo in availableOutputs) {
|
||||||
if (availableOutputs[i].isBlocked == false &&
|
if (utxo.isBlocked == false &&
|
||||||
availableOutputs[i]
|
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
utxo.used != true) {
|
||||||
true) {
|
spendableOutputs.add(utxo);
|
||||||
spendableOutputs.add(availableOutputs[i]);
|
spendableSatoshiValue += utxo.value;
|
||||||
spendableSatoshiValue += availableOutputs[i].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort spendable by age (oldest first)
|
if (coinControl) {
|
||||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
if (spendableOutputs.length < availableOutputs.length) {
|
||||||
|
throw ArgumentError("Attempted to use an unavailable utxo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't care about sorting if using all utxos
|
||||||
|
if (!coinControl) {
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||||
|
}
|
||||||
|
|
||||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
@ -2422,19 +2448,26 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
int inputsBeingConsumed = 0;
|
int inputsBeingConsumed = 0;
|
||||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||||
|
|
||||||
for (var i = 0;
|
if (!coinControl) {
|
||||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
for (var i = 0;
|
||||||
i++) {
|
satoshisBeingUsed < satoshiAmountToSend &&
|
||||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
i < spendableOutputs.length;
|
||||||
satoshisBeingUsed += spendableOutputs[i].value;
|
i++) {
|
||||||
inputsBeingConsumed += 1;
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
}
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
for (int i = 0;
|
inputsBeingConsumed += 1;
|
||||||
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
|
}
|
||||||
i++) {
|
for (int i = 0;
|
||||||
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
i < additionalOutputs &&
|
||||||
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
inputsBeingConsumed < spendableOutputs.length;
|
||||||
inputsBeingConsumed += 1;
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
|
||||||
|
inputsBeingConsumed += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
satoshisBeingUsed = spendableSatoshiValue;
|
||||||
|
utxoObjectsToUse = spendableOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging.instance
|
Logging.instance
|
||||||
|
@ -2445,7 +2478,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
|
||||||
|
|
||||||
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
|
||||||
List<String> recipientsArray = [_recipientAddress];
|
List<String> recipientsArray = [recipientAddress];
|
||||||
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
List<int> recipientsAmtArray = [satoshiAmountToSend];
|
||||||
|
|
||||||
// gather required signing data
|
// gather required signing data
|
||||||
|
@ -2458,7 +2491,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
int feeForOneOutput = estimateTxFee(
|
int feeForOneOutput = estimateTxFee(
|
||||||
|
@ -2485,6 +2518,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": amount,
|
"recipientAmt": amount,
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2492,14 +2526,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
final int vSizeForOneOutput = (await buildTransaction(
|
final int vSizeForOneOutput = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [_recipientAddress],
|
recipients: [recipientAddress],
|
||||||
satoshiAmounts: [satoshisBeingUsed - 1],
|
satoshiAmounts: [satoshisBeingUsed - 1],
|
||||||
))["vSize"] as int;
|
))["vSize"] as int;
|
||||||
final int vSizeForTwoOutPuts = (await buildTransaction(
|
final int vSizeForTwoOutPuts = (await buildTransaction(
|
||||||
utxosToUse: utxoObjectsToUse,
|
utxosToUse: utxoObjectsToUse,
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
|
@ -2603,6 +2637,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeBeingPaid,
|
"fee": feeBeingPaid,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2630,6 +2665,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2659,6 +2695,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
"fee": satoshisBeingUsed - satoshiAmountToSend,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
}
|
}
|
||||||
|
@ -2688,6 +2725,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
"recipientAmt": recipientsAmtArray[0],
|
"recipientAmt": recipientsAmtArray[0],
|
||||||
"fee": feeForOneOutput,
|
"fee": feeForOneOutput,
|
||||||
"vSize": txn["vSize"],
|
"vSize": txn["vSize"],
|
||||||
|
"usedUTXOs": utxoObjectsToUse,
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2699,9 +2737,15 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
||||||
level: LogLevel.Warning);
|
level: LogLevel.Warning);
|
||||||
// try adding more outputs
|
// try adding more outputs
|
||||||
if (spendableOutputs.length > inputsBeingConsumed) {
|
if (spendableOutputs.length > inputsBeingConsumed) {
|
||||||
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
|
return coinSelection(
|
||||||
_recipientAddress, isSendAll,
|
satoshiAmountToSend: satoshiAmountToSend,
|
||||||
additionalOutputs: additionalOutputs + 1, utxos: utxos);
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
recipientAddress: recipientAddress,
|
||||||
|
isSendAll: isSendAll,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
utxos: utxos,
|
||||||
|
coinControl: coinControl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue