enable coin control for selected other coins

This commit is contained in:
julian 2023-03-08 16:11:46 -06:00
parent a2f75a2c7b
commit b30f1db45b
5 changed files with 587 additions and 367 deletions

View file

@ -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/wallet_sync_status_changed_event.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_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -98,7 +99,8 @@ String constructDerivePath({
return "m/$purpose'/$coinType'/$account'/$chain/$index";
}
class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
class BitcoinCashWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
BitcoinCashWallet({
required String walletId,
required String walletName,
@ -118,6 +120,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
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 =
@ -1041,6 +1054,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1067,9 +1081,17 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
isSendAll = true;
}
final result =
await coinSelection(satoshiAmount, rate, address, isSendAll);
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
final result = await coinSelection(
satoshiAmountToSend: satoshiAmount,
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) {
switch (result) {
case 1:
@ -1115,6 +1137,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
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;
} catch (e, 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 = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
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(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] 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);
}
}
@ -1769,27 +1795,20 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
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
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2338,18 +2358,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
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}",
level: LogLevel.Info);
@ -2378,19 +2406,26 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2403,7 +2438,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2416,7 +2451,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2440,6 +2475,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2447,14 +2483,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2572,6 +2608,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2599,6 +2636,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2628,6 +2666,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2657,6 +2696,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2668,9 +2708,15 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -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/wallet_sync_status_changed_event.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/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -85,7 +86,7 @@ String constructDerivePath({
}
class DogecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
DogecoinWallet({
required String walletId,
required String walletName,
@ -105,6 +106,17 @@ class DogecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
coin: coin,
db: db,
getChainHeight: () => chainHeight,
refreshedBalanceCallback: (balance) async {
_balance = balance;
await updateCachedBalance(_balance!);
},
);
// paynym stuff
// initPaynymWalletInterface(
@ -907,6 +919,7 @@ class DogecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -933,9 +946,17 @@ class DogecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final result =
await coinSelection(satoshiAmount, rate, address, isSendAll);
Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info);
final result = await coinSelection(
satoshiAmountToSend: satoshiAmount,
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) {
switch (result) {
case 1:
@ -981,6 +1002,12 @@ class DogecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
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;
} catch (e, 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 = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: coin,
);
@ -1556,7 +1579,7 @@ class DogecoinWallet extends CoinServiceAPI
// fetch stored tx to see if paynym notification tx and block utxo
final storedTx = await db.getTransaction(
walletId,
fetchedUtxoList[i][j]["tx_hash"] as String,
jsonUTXO["tx_hash"] as String,
);
bool shouldBlock = false;
@ -1573,32 +1596,35 @@ class DogecoinWallet extends CoinServiceAPI
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(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: shouldBlock,
blockedReason: blockReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] 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);
}
}
@ -1606,27 +1632,20 @@ class DogecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2022,11 +2041,12 @@ class DogecoinWallet extends CoinServiceAPI
/// 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
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2038,18 +2058,26 @@ class DogecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
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}",
level: LogLevel.Info);
@ -2078,19 +2106,26 @@ class DogecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2103,7 +2138,7 @@ class DogecoinWallet extends CoinServiceAPI
.log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2116,7 +2151,7 @@ class DogecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2140,6 +2175,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2147,14 +2183,14 @@ class DogecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2272,6 +2308,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2299,6 +2336,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2328,6 +2366,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2357,6 +2396,7 @@ class DogecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2368,9 +2408,15 @@ class DogecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -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/wallet_sync_status_changed_event.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/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -90,7 +91,7 @@ String constructDerivePath({
}
class LitecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
LitecoinWallet({
required String walletId,
required String walletName,
@ -110,6 +111,17 @@ class LitecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
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 =
@ -1018,6 +1030,7 @@ class LitecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1045,8 +1058,14 @@ class LitecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
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);
try {
@ -1109,6 +1128,11 @@ class LitecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
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;
} catch (e, 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 = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
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(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] 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);
}
}
@ -1774,27 +1796,20 @@ class LitecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2214,11 +2229,12 @@ class LitecoinWallet extends CoinServiceAPI
/// 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
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2230,18 +2246,26 @@ class LitecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
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}",
level: LogLevel.Info);
@ -2270,19 +2294,26 @@ class LitecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2293,7 +2324,7 @@ class LitecoinWallet extends CoinServiceAPI
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2306,7 +2337,7 @@ class LitecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2333,6 +2364,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2340,14 +2372,14 @@ class LitecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2451,6 +2483,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2478,6 +2511,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2507,6 +2541,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2536,6 +2571,7 @@ class LitecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2547,9 +2583,15 @@ class LitecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -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/wallet_sync_status_changed_event.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/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
@ -87,7 +88,7 @@ String constructDerivePath({
}
class NamecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface {
NamecoinWallet({
required String walletId,
required String walletName,
@ -107,6 +108,17 @@ class NamecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
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 =
@ -1009,6 +1021,7 @@ class NamecoinWallet extends CoinServiceAPI
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -1036,8 +1049,14 @@ class NamecoinWallet extends CoinServiceAPI
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
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);
try {
@ -1100,6 +1119,11 @@ class NamecoinWallet extends CoinServiceAPI
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
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;
} catch (e, 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 = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
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(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] 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);
}
}
@ -1759,27 +1781,20 @@ class NamecoinWallet extends CoinServiceAPI
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@ -2207,11 +2222,12 @@ class NamecoinWallet extends CoinServiceAPI
/// 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
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2223,18 +2239,26 @@ class NamecoinWallet extends CoinServiceAPI
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
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}",
level: LogLevel.Info);
@ -2263,19 +2287,26 @@ class NamecoinWallet extends CoinServiceAPI
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2286,7 +2317,7 @@ class NamecoinWallet extends CoinServiceAPI
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2299,7 +2330,7 @@ class NamecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2326,6 +2357,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2333,14 +2365,14 @@ class NamecoinWallet extends CoinServiceAPI
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2444,6 +2476,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2471,6 +2504,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2500,6 +2534,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2529,6 +2564,7 @@ class NamecoinWallet extends CoinServiceAPI
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2540,9 +2576,15 @@ class NamecoinWallet extends CoinServiceAPI
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}

View file

@ -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/wallet_sync_status_changed_event.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_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -81,7 +82,8 @@ String constructDerivePath({
return "m/$purpose'/$coinType'/$account'/$chain/$index";
}
class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
class ParticlWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
ParticlWallet({
required String walletId,
required String walletName,
@ -101,6 +103,17 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
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 =
@ -935,6 +948,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
final utxos = args?["UTXOs"] as Set<isar_models.UTXO>?;
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
@ -962,8 +976,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
final txData = await coinSelection(
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);
try {
@ -1026,6 +1046,11 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex);
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;
} catch (e, 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 = [];
int satoshiBalanceTotal = 0;
int satoshiBalancePending = 0;
int satoshiBalanceSpendable = 0;
int satoshiBalanceBlocked = 0;
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final jsonUTXO = fetchedUtxoList[i][j];
final txn = await cachedElectrumXClient.getTransaction(
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
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(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
blockHeight: jsonUTXO["height"] 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);
}
}
@ -1645,27 +1668,20 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
await db.updateUTXOs(walletId, outputArray);
// finally update balance
_balance = Balance(
coin: coin,
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await updateCachedBalance(_balance!);
await _updateBalance();
} catch (e, s) {
Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
}
}
Future<void> _updateBalance() async {
await refreshBalance();
}
@override
Balance get balance => _balance ??= getCachedBalance();
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
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
dynamic coinSelection({
required int satoshiAmountToSend,
required int selectedTxFeeRate,
required String recipientAddress,
required bool coinControl,
required bool isSendAll,
int additionalOutputs = 0,
List<isar_models.UTXO>? utxos,
}) async {
@ -2381,19 +2398,28 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final currentChainHeight = await chainHeight;
final List<isar_models.UTXO> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
for (final utxo in availableOutputs) {
if (utxo.isBlocked == false &&
utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
utxo.used != true) {
spendableOutputs.add(utxo);
spendableSatoshiValue += utxo.value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
if (coinControl) {
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}",
level: LogLevel.Info);
@ -2422,19 +2448,26 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
int inputsBeingConsumed = 0;
List<isar_models.UTXO> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
if (!coinControl) {
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend &&
i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs &&
inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
} else {
satoshisBeingUsed = spendableSatoshiValue;
utxoObjectsToUse = spendableOutputs;
}
Logging.instance
@ -2445,7 +2478,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<String> recipientsArray = [recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
@ -2458,7 +2491,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
@ -2485,6 +2518,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2492,14 +2526,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
recipients: [recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
@ -2603,6 +2637,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2630,6 +2665,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2659,6 +2695,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
}
@ -2688,6 +2725,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
"usedUTXOs": utxoObjectsToUse,
};
return transactionObject;
} else {
@ -2699,9 +2737,15 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
return coinSelection(
satoshiAmountToSend: satoshiAmountToSend,
selectedTxFeeRate: selectedTxFeeRate,
recipientAddress: recipientAddress,
isSendAll: isSendAll,
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
);
}
return 2;
}