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/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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue