diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 036517698..337539412 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -141,7 +141,11 @@ class CachedElectrumXClient { set["blockHash"] = newSet["blockHash"]; for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { // TODO verify this is correct (or append?) - set["coins"].insert(0, newSet["coins"][i]); + if ((set["coins"] as List) + .where((e) => e[0] == newSet["coins"][i][0]) + .isEmpty) { + set["coins"].insert(0, newSet["coins"][i]); + } } // save set to db await box.put(groupId, set); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 0e1a8d5e3..5dd8e0ae3 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -535,7 +535,6 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, @@ -570,7 +569,6 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 770e60cdb..cd3f1ee6d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -332,7 +332,6 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, @@ -367,7 +366,6 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index f1b8bfa54..9602a4e11 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -60,7 +60,6 @@ class TxData { ({ String address, Amount amount, - bool subtractFeeFromAmount, String memo, })>? sparkRecipients; @@ -148,7 +147,6 @@ class TxData { ({ String address, Amount amount, - bool subtractFeeFromAmount, String memo, })>? sparkRecipients, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 47eb71716..24ac235e2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -125,8 +125,32 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .isUsedEqualTo(false) .and() .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") .findAll(); + final available = info.cachedBalanceTertiary.spendable; + + final txAmount = (txData.recipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e) + + (txData.sparkRecipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e); + + if (txAmount > available) { + throw Exception("Insufficient Spark balance"); + } + + final bool isSendAll = available == txAmount; + // prepare coin data for ffi final serializedCoins = coins .map((e) => ( @@ -177,34 +201,89 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final privateKey = root.derivePath(derivationPath).privateKey.data; - final txb = btc.TransactionBuilder( - network: btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, + final btcDartNetwork = btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + final txb = btc.TransactionBuilder( + network: btcDartNetwork, ); txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); + final List<({String address, Amount amount})> recipientsWithFeeSubtracted = + []; + final List< + ({ + String address, + Amount amount, + String memo, + })> sparkRecipientsWithFeeSubtracted = []; + final outputCount = (txData.recipients + ?.where( + (e) => e.amount.raw > BigInt.zero, + ) + .length ?? + 0) + + (txData.sparkRecipients?.length ?? 0); + final BigInt estimatedFee; + if (isSendAll) { + final estFee = LibSpark.estimateSparkFee( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + sendAmount: txAmount.raw.toInt(), + subtractFeeFromAmount: true, + serializedCoins: serializedCoins, + privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), + ); + estimatedFee = BigInt.from(estFee); + } else { + estimatedFee = BigInt.zero; + } + + for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { + sparkRecipientsWithFeeSubtracted.add( + ( + address: txData.sparkRecipients![i].address, + amount: Amount( + rawValue: txData.sparkRecipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(outputCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + memo: txData.sparkRecipients![i].memo, + ), + ); + } + for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; } - if (txData.recipients![i].amount < cryptoCurrency.dustLimit) { - throw Exception("Output below dust limit"); - } - // - // transparentOut += txData.recipients![i].amount.raw.toInt(); - txb.addOutput( + recipientsWithFeeSubtracted.add( + ( + address: txData.recipients![i].address, + amount: Amount( + rawValue: txData.recipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(outputCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ); + + final scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, - txData.recipients![i].amount.raw.toInt(), + btcDartNetwork, + ); + txb.addOutput( + scriptPubKey, + recipientsWithFeeSubtracted[i].amount.raw.toInt(), ); } @@ -221,12 +300,19 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: privateKey.toHex, index: kDefaultSparkIndex, - recipients: [], + recipients: txData.recipients + ?.map((e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + )) + .toList() ?? + [], privateRecipients: txData.sparkRecipients ?.map((e) => ( sparkAddress: e.address, amount: e.amount.raw.toInt(), - subtractFeeFromAmount: e.subtractFeeFromAmount, + subtractFeeFromAmount: isSendAll, memo: e.memo, )) .toList() ?? @@ -246,6 +332,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { extractedTx.setPayload(spend.serializedSpendPayload); final rawTxHex = extractedTx.toHex(); + if (isSendAll) { + txData = txData.copyWith( + recipients: recipientsWithFeeSubtracted, + sparkRecipients: sparkRecipientsWithFeeSubtracted, + ); + } + return txData.copyWith( raw: rawTxHex, vSize: extractedTx.virtualSize(), @@ -279,8 +372,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txHash: txHash, txid: txHash, ); - // mark utxos as used - await mainDB.putUTXOs(txData.usedUTXOs!); + // // mark utxos as used + // await mainDB.putUTXOs(txData.usedUTXOs!); return txData; } catch (e, s) {