From b11694220b8aef897f40378687b8cb29248fe390 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 5 Jan 2024 12:59:01 -0600 Subject: [PATCH] dirty hack for showing firo transactions right away until we can add functionality to sparkmobile --- lib/wallets/models/tx_data.dart | 7 + lib/wallets/wallet/impl/firo_wallet.dart | 27 ++- lib/wallets/wallet/wallet.dart | 10 +- .../electrumx_interface.dart | 96 ++++++++-- .../spark_interface.dart | 170 ++++++++++++++++-- 5 files changed, 282 insertions(+), 28 deletions(-) diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 9148d182e..1469196cf 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -1,4 +1,5 @@ import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -64,6 +65,8 @@ class TxData { })>? sparkRecipients; final List? sparkMints; + final TransactionV2? tempTx; + TxData({ this.feeRateType, this.feeRateAmount, @@ -96,6 +99,7 @@ class TxData { this.tezosOperationsList, this.sparkRecipients, this.sparkMints, + this.tempTx, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -153,6 +157,7 @@ class TxData { })>? sparkRecipients, List? sparkMints, + TransactionV2? tempTx, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -187,6 +192,7 @@ class TxData { tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, sparkRecipients: sparkRecipients ?? this.sparkRecipients, sparkMints: sparkMints ?? this.sparkMints, + tempTx: tempTx ?? this.tempTx, ); } @@ -223,5 +229,6 @@ class TxData { 'tezosOperationsList: $tezosOperationsList, ' 'sparkRecipients: $sparkRecipients, ' 'sparkMints: $sparkMints, ' + 'tempTx: $tempTx, ' '}'; } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 8a2447db7..fc27072be 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; @@ -41,8 +42,23 @@ class FiroWallet extends Bip39HDWallet FilterOperation? get receivingAddressFilterOperation => FilterGroup.and(standardReceivingAddressFilters); + final Set _unconfirmedTxids = {}; + // =========================================================================== + @override + Future updateSentCachedTxData({required TxData txData}) async { + if (txData.tempTx != null) { + await mainDB.updateOrPutTransactionV2s([txData.tempTx!]); + _unconfirmedTxids.add(txData.tempTx!.txid); + Logging.instance.log( + "Added firo unconfirmed: ${txData.tempTx!.txid}", + level: LogLevel.Info, + ); + } + return txData; + } + @override Future updateTransactions() async { List
allAddressesOld = await fetchAddressesForElectrumXScan(); @@ -487,7 +503,16 @@ class FiroWallet extends Bip39HDWallet otherData: otherData, ); - txns.add(tx); + if (_unconfirmedTxids.contains(tx.txid)) { + if (tx.isConfirmed(await chainHeight, cryptoCurrency.minConfirms)) { + txns.add(tx); + _unconfirmedTxids.removeWhere((e) => e == tx.txid); + } else { + // don't update in db until confirmed + } + } else { + txns.add(tx); + } } await mainDB.updateOrPutTransactionV2s(txns); diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 32e84838f..d126712fd 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -368,7 +368,7 @@ abstract class Wallet { Future updateTransactions(); Future updateBalance(); - // returns true if new utxos were added to local db + /// returns true if new utxos were added to local db Future updateUTXOs(); /// updates the wallet info's cachedChainHeight @@ -381,6 +381,14 @@ abstract class Wallet { Future pingCheck(); //=========================================== + /// add transaction to local db temporarily. Used for quickly updating ui + /// before refresh can fetch data from server + Future updateSentCachedTxData({required TxData txData}) async { + if (txData.tempTx != null) { + await mainDB.updateOrPutTransactionV2s([txData.tempTx!]); + } + return txData; + } NodeModel getCurrentNode() { final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin) ?? diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 9dcb2b8e5..de95b7e38 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -8,6 +8,9 @@ import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; @@ -172,7 +175,7 @@ mixin ElectrumXInterface on Bip39HDWallet { ); } - final int vSizeForOneOutput = buildTransaction( + final int vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -180,7 +183,8 @@ mixin ElectrumXInterface on Bip39HDWallet { [satoshisBeingUsed - 1], ), ), - ).vSize!; + )) + .vSize!; int feeForOneOutput = satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( @@ -200,7 +204,7 @@ mixin ElectrumXInterface on Bip39HDWallet { } final int amount = satoshiAmountToSend - feeForOneOutput; - final data = buildTransaction( + final data = await buildTransaction( txData: txData.copyWith( recipients: _helperRecipientsConvert( [recipientAddress], @@ -221,7 +225,7 @@ mixin ElectrumXInterface on Bip39HDWallet { final int vSizeForOneOutput; try { - vSizeForOneOutput = buildTransaction( + vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -229,7 +233,8 @@ mixin ElectrumXInterface on Bip39HDWallet { [satoshisBeingUsed - 1], ), ), - ).vSize!; + )) + .vSize!; } catch (e) { Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error); rethrow; @@ -237,7 +242,7 @@ mixin ElectrumXInterface on Bip39HDWallet { final int vSizeForTwoOutPuts; try { - vSizeForTwoOutPuts = buildTransaction( + vSizeForTwoOutPuts = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -248,7 +253,8 @@ mixin ElectrumXInterface on Bip39HDWallet { ], ), ), - ).vSize!; + )) + .vSize!; } catch (e) { Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error); rethrow; @@ -312,7 +318,7 @@ mixin ElectrumXInterface on Bip39HDWallet { Logging.instance .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); - var txn = buildTransaction( + var txn = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -343,7 +349,7 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info); Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); - txn = buildTransaction( + txn = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -374,7 +380,7 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info); Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - final txn = buildTransaction( + final txn = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -406,7 +412,7 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info); Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - final txn = buildTransaction( + final txn = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -438,7 +444,7 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info); Logging.instance .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - final txn = buildTransaction( + final txn = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( recipients: _helperRecipientsConvert( @@ -615,10 +621,10 @@ mixin ElectrumXInterface on Bip39HDWallet { } /// Builds and signs a transaction - TxData buildTransaction({ + Future buildTransaction({ required TxData txData, required List utxoSigningData, - }) { + }) async { Logging.instance .log("Starting buildTransaction ----------", level: LogLevel.Info); @@ -637,7 +643,12 @@ mixin ElectrumXInterface on Bip39HDWallet { wif: cryptoCurrency.networkParams.wifPrefix, ), ); - txb.setVersion(1); // TODO possibly override this for certain coins? + const version = 1; // TODO possibly override this for certain coins? + txb.setVersion(version); + + // temp tx data to show in gui while waiting for real data from server + final List tempInputs = []; + final List tempOutputs = []; // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { @@ -649,6 +660,25 @@ mixin ElectrumXInterface on Bip39HDWallet { utxoSigningData[i].output!, cryptoCurrency.networkParams.bech32Hrp, ); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: txb.inputs.first.script?.toHex, + sequence: 0xffffffff - 1, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: utxoSigningData[i].utxo.txid, + vout: utxoSigningData[i].utxo.vout, + ), + addresses: utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], + valueStringSats: utxoSigningData[i].utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); } // Add transaction output @@ -658,6 +688,24 @@ mixin ElectrumXInterface on Bip39HDWallet { txData.recipients![i].amount.raw.toInt(), cryptoCurrency.networkParams.bech32Hrp, ); + + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "000000", + valueStringSats: txData.recipients![i].amount.raw.toString(), + addresses: [ + txData.recipients![i].address.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(txData.recipients![i].address) + .valueProperty() + .findFirst()) != + null, + ), + ); } try { @@ -683,6 +731,22 @@ mixin ElectrumXInterface on Bip39HDWallet { return txData.copyWith( raw: builtTx.toHex(), vSize: vSize, + tempTx: TransactionV2( + walletId: walletId, + blockHash: null, + hash: builtTx.getId(), + txid: builtTx.getId(), + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: version, + type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), ); } @@ -1749,7 +1813,7 @@ mixin ElectrumXInterface on Bip39HDWallet { // mark utxos as used await mainDB.putUTXOs(txData.usedUTXOs!); - return txData; + return await updateSentCachedTxData(txData: txData); } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", level: LogLevel.Error); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 56b188e41..c2d8e8ba3 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -7,6 +7,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -331,6 +334,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } + // temp tx data to show in gui while waiting for real data from server + final List tempInputs = []; + final List tempOutputs = []; + for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; @@ -354,8 +361,62 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { scriptPubKey, recipientsWithFeeSubtracted[i].amount.raw.toInt(), ); + + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: scriptPubKey.toHex, + valueStringSats: recipientsWithFeeSubtracted[i].amount.raw.toString(), + addresses: [ + recipientsWithFeeSubtracted[i].address.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(recipientsWithFeeSubtracted[i].address) + .valueProperty() + .findFirst()) != + null, + ), + ); } + if (sparkRecipientsWithFeeSubtracted != null) { + for (final recip in sparkRecipientsWithFeeSubtracted) { + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: Uint8List.fromList([OP_SPARKSMINT]).toHex, + valueStringSats: recip.amount.raw.toString(), + addresses: [ + recip.address.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(recip.address) + .valueProperty() + .findFirst()) != + null, + ), + ); + } + } + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: "d3", + sequence: 0xffffffff, + outpoint: null, + addresses: [], + valueStringSats: "0", + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + final extractedTx = txb.buildIncomplete(); extractedTx.addInput( '0000000000000000000000000000000000000000000000000000000000000000' @@ -412,12 +473,33 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } + final fee = Amount( + rawValue: BigInt.from(spend.fee), + fractionDigits: cryptoCurrency.fractionDigits, + ); return txData.copyWith( raw: rawTxHex, vSize: extractedTx.virtualSize(), - fee: Amount( - rawValue: BigInt.from(spend.fee), - fractionDigits: cryptoCurrency.fractionDigits, + fee: fee, + tempTx: TransactionV2( + walletId: walletId, + blockHash: null, + hash: extractedTx.getId(), + txid: extractedTx.getId(), + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.sparkSpend, + otherData: jsonEncode( + { + "anonFees": fee.toJsonString(), + }, + ), + height: null, + version: 3, ), // TODO used coins ); @@ -448,7 +530,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // // mark utxos as used // await mainDB.putUTXOs(txData.usedUTXOs!); - return txData; + return await updateSentCachedTxData(txData: txData); } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", level: LogLevel.Error); @@ -702,7 +784,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { : currentHeight; const txVersion = 1; final List vin = []; - final List<(dynamic, int)> vout = []; + final List<(dynamic, int, String?)> vout = []; BigInt nFeeRet = BigInt.zero; @@ -821,13 +903,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final dummyTxb = btc.TransactionBuilder(network: _bitcoinDartNetwork); dummyTxb.setVersion(txVersion); dummyTxb.setLockTime(lockTime); - for (final recipient in dummyRecipients) { + for (int i = 0; i < dummyRecipients.length; i++) { + final recipient = dummyRecipients[i]; if (recipient.amount < cryptoCurrency.dustLimit.raw.toInt()) { throw Exception("Output amount too small"); } vout.add(( recipient.scriptPubKey, recipient.amount, + singleTxOutputs[i].address, )); } @@ -860,7 +944,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final changeAddress = await getCurrentChangeAddress(); vout.insert( nChangePosInOut, - (changeAddress!.value, nChange.toInt()), + (changeAddress!.value, nChange.toInt(), null), ); } } @@ -942,8 +1026,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); int i = 0; - for (final recipient in recipients) { - final out = (recipient.scriptPubKey, recipient.amount); + for (int i = 0; i < recipients.length; i++) { + final recipient = recipients[i]; + final out = ( + recipient.scriptPubKey, + recipient.amount, + singleTxOutputs[i].address, + ); while (i < vout.length) { if (vout[i].$1 is Uint8List && (vout[i].$1 as Uint8List).isNotEmpty && @@ -973,6 +1062,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { continue; } + // temp tx data to show in gui while waiting for real data from server + final List tempInputs = []; + final List tempOutputs = []; + // sign final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); txb.setVersion(txVersion); @@ -985,10 +1078,50 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { 1, // minus 1 is important. 0xffffffff on its own will burn funds input.output, ); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: txb.inputs.first.script?.toHex, + sequence: 0xffffffff - 1, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: input.utxo.txid, + vout: input.utxo.vout, + ), + addresses: input.utxo.address == null ? [] : [input.utxo.address!], + valueStringSats: input.utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); } for (final output in vout) { - txb.addOutput(output.$1, output.$2); + final addressOrScript = output.$1; + final value = output.$2; + txb.addOutput(addressOrScript, value); + + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: + addressOrScript is Uint8List ? addressOrScript.toHex : "000000", + valueStringSats: value.toString(), + addresses: [ + if (addressOrScript is String) addressOrScript.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(addressOrScript is Uint8List + ? output.$3! + : addressOrScript as String) + .valueProperty() + .findFirst()) != + null, + ), + ); } try { @@ -1035,6 +1168,23 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { fractionDigits: cryptoCurrency.fractionDigits, ), usedUTXOs: vin.map((e) => e.utxo).toList(), + tempTx: TransactionV2( + walletId: walletId, + blockHash: null, + hash: builtTx.getId(), + txid: builtTx.getId(), + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + type: + tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.sparkMint, + otherData: null, + height: null, + version: 3, + ), ); if (nFeeRet.toInt() < data.vSize!) {