diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 055523db8..5aaae5b79 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -1,12 +1,16 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.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/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; -import 'package:tuple/tuple.dart'; class NamecoinWallet extends Bip39HDWallet with ElectrumXInterface, CoinControlInterface { @@ -77,22 +81,210 @@ class NamecoinWallet extends Bip39HDWallet @override Future updateTransactions() async { - final currentChainHeight = await fetchChainHeight(); + // Get all addresses. + List
allAddressesOld = await fetchAddressesForElectrumXScan(); - // TODO: [prio=high] switch to V2 transactions. - final data = await fetchTransactionsV1( - addresses: await fetchAddressesForElectrumXScan(), - currentChainHeight: currentChainHeight, - ); + // Separate receiving and change addresses. + Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); - await mainDB.addNewTransactionData( - data - .map((e) => Tuple2( - e.transaction, - e.address, - )) - .toList(), - walletId, - ); + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = + await fetchHistory(allAddressesSet); + + // Only parse new txs (not in db yet). + List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + // Only tx to list once. + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Namecoin doesn't have special outputs like tokens, ordinals, etc. + // But this is where you'd check for special outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); } }