various address and SWB fixes, as well as some electrumx_interface unused function cleanup

This commit is contained in:
julian 2024-01-16 11:58:16 -06:00
parent f1f8a0c49a
commit f319aaf594
18 changed files with 153 additions and 418 deletions

View file

@ -27,6 +27,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
@ -98,15 +99,21 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
final walletId = widget.routeOnSuccessArguments as String;
final wallet = ref.read(pWallets).getWallet(walletId);
if (wallet.info.coin == Coin.monero) {
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();
}
await showLoading(
opaqueBG: true,
whileFuture: wallet.init(),
whileFuture: loadFuture,
context: context,
message: "Loading ${wallet.info.coin.prettyName} wallet...",
);
}
}
if (mounted) {
unawaited(

View file

@ -368,7 +368,7 @@ abstract class SWB {
return backupJson;
}
static Future<bool> asyncRestore(
static Future<bool> _asyncRestore(
Tuple2<dynamic, WalletInfo> tuple,
Prefs prefs,
NodeService nodeService,
@ -757,7 +757,7 @@ abstract class SWB {
)) {
return false;
}
final bools = await asyncRestore(
final bools = await _asyncRestore(
tuple,
_prefs,
nodeService,
@ -796,10 +796,10 @@ abstract class SWB {
}
Logging.instance.log("done with SWB restore", level: LogLevel.Warning);
if (Util.isDesktop) {
await Wallets.sharedInstance
.loadAfterStackRestore(_prefs, uiState?.wallets ?? []);
}
.loadAfterStackRestore(_prefs, uiState?.wallets ?? [], Util.isDesktop);
return true;
}

View file

@ -218,7 +218,7 @@ class _StackRestoreProgressViewState
ref.read(pWallets).loadAfterStackRestore(
ref.read(prefsChangeNotifierProvider),
ref.read(stackRestoringUIStateProvider).wallets,
);
Util.isDesktop);
}
@override

View file

@ -115,17 +115,21 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
child: GestureDetector(
onTap: () async {
final wallet = ref.read(pWallets).getWallet(walletId);
await wallet.init();
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {
if (mounted) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();
}
await showLoading(
whileFuture: wallet.open(),
whileFuture: loadFuture,
context: context,
message: 'Opening ${wallet.info.name}',
isDesktop: Util.isDesktop,
);
}
}
if (mounted) {
if (Util.isDesktop) {
await Navigator.of(context).pushNamed(

View file

@ -63,17 +63,19 @@ class WalletListItem extends ConsumerWidget {
.read(pWallets)
.wallets
.firstWhere((e) => e.info.coin == coin);
await wallet.init();
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {
if (context.mounted) {
loadFuture =
wallet.init().then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();
}
await showLoading(
whileFuture: wallet.open(),
whileFuture: loadFuture,
context: context,
message: 'Opening ${wallet.info.name}',
isDesktop: Util.isDesktop,
);
}
}
if (context.mounted) {
unawaited(
Navigator.of(context).pushNamed(

View file

@ -80,17 +80,20 @@ class CoinWalletsTable extends ConsumerWidget {
final wallet =
ref.read(pWallets).getWallet(walletIds[i]);
await wallet.init();
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {
if (context.mounted) {
loadFuture = wallet
.init()
.then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();
}
await showLoading(
whileFuture: wallet.open(),
whileFuture: loadFuture,
context: context,
message: 'Opening ${wallet.info.name}',
isDesktop: Util.isDesktop,
);
}
}
if (context.mounted) {
await Navigator.of(context).pushNamed(

View file

@ -25,6 +25,7 @@ import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
class Wallets {
Wallets._private();
@ -192,8 +193,7 @@ class Wallets {
final shouldSetAutoSync = shouldAutoSyncAll ||
walletIdsToEnableAutoSync.contains(walletInfo.walletId);
if (walletInfo.coin == Coin.monero ||
walletInfo.coin == Coin.wownero) {
if (wallet is CwBasedInterface) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
} else {
walletInitFutures.add(wallet.init().then((_) {
@ -230,6 +230,7 @@ class Wallets {
Future<void> loadAfterStackRestore(
Prefs prefs,
List<Wallet> wallets,
bool isDesktop,
) async {
List<Future<void>> walletInitFutures = [];
List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = [];
@ -259,8 +260,8 @@ class Wallets {
final shouldSetAutoSync = shouldAutoSyncAll ||
walletIdsToEnableAutoSync.contains(wallet.walletId);
if (wallet.info.coin == Coin.monero ||
wallet.info.coin == Coin.wownero) {
if (isDesktop) {
if (wallet is CwBasedInterface) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
} else {
walletInitFutures.add(wallet.init().then((value) {
@ -269,6 +270,7 @@ class Wallets {
}
}));
}
}
_wallets[wallet.walletId] = wallet;
} else {
@ -278,6 +280,7 @@ class Wallets {
}
}
if (isDesktop) {
if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) {
await Future.wait([
_initLinearly(walletsToInitLinearly),
@ -289,6 +292,7 @@ class Wallets {
await _initLinearly(walletsToInitLinearly);
}
}
}
Future<void> _initLinearly(
List<({Wallet wallet, bool shouldAutoSync})> dataList,

View file

@ -484,6 +484,11 @@ class EpiccashWallet extends Bip39Wallet {
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> checkSaveInitialReceivingAddress() async {
// epiccash seems ok with nothing here?
}
@override
Future<void> init({bool? isRestore}) async {
if (isRestore != true) {

View file

@ -86,25 +86,6 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
_credentials = web3.EthPrivateKey.fromHex(privateKey);
}
Future<void> _generateAndSaveAddress(
String mnemonic,
String mnemonicPassphrase,
) async {
await _initCredentials(mnemonic, mnemonicPassphrase);
final address = Address(
walletId: walletId,
value: _credentials.address.hexEip55,
publicKey: [], // maybe store address bytes here? seems a waste of space though
derivationIndex: 0,
derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
type: AddressType.ethereum,
subType: AddressSubType.receiving,
);
await mainDB.updateOrPutAddresses([address]);
}
// ==================== Overrides ============================================
@override
@ -127,15 +108,27 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> init({bool? isRestore}) async {
Future<void> checkSaveInitialReceivingAddress() async {
final address = await getCurrentReceivingAddress();
if (address == null) {
await _generateAndSaveAddress(
await _initCredentials(
await getMnemonic(),
await getMnemonicPassphrase(),
);
final address = Address(
walletId: walletId,
value: _credentials.address.hexEip55,
publicKey: [],
// maybe store address bytes here? seems a waste of space though
derivationIndex: 0,
derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
type: AddressType.ethereum,
subType: AddressSubType.receiving,
);
await mainDB.updateOrPutAddresses([address]);
}
return super.init();
}
@override
@ -475,17 +468,11 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
await refreshMutex.protect(() async {
if (isRescan) {
await mainDB.deleteWalletBlockchainData(walletId);
await _generateAndSaveAddress(
await getMnemonic(),
await getMnemonicPassphrase(),
);
await checkSaveInitialReceivingAddress();
await updateBalance();
await updateTransactions(isRescan: true);
} else {
await _generateAndSaveAddress(
await getMnemonic(),
await getMnemonicPassphrase(),
);
await checkSaveInitialReceivingAddress();
unawaited(updateBalance());
unawaited(updateTransactions());
}

View file

@ -111,7 +111,7 @@ class StellarWallet extends Bip39Wallet<Stellar> {
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> init() async {
Future<void> checkSaveInitialReceivingAddress() async {
try {
final address = await getCurrentReceivingAddress();
if (address == null) {
@ -121,11 +121,10 @@ class StellarWallet extends Bip39Wallet<Stellar> {
} catch (e, s) {
// do nothing, still allow user into wallet
Logging.instance.log(
"$runtimeType init() failed: $e\n$s",
"$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s",
level: LogLevel.Error,
);
}
return super.init();
}
@override

View file

@ -527,4 +527,9 @@ class EthTokenWallet extends Wallet {
value: TransactionSubType.ethToken,
),
]);
@override
Future<void> checkSaveInitialReceivingAddress() async {
await ethWallet.checkSaveInitialReceivingAddress();
}
}

View file

@ -145,7 +145,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
// ===========================================================================
@override
Future<void> init() async {
Future<void> checkSaveInitialReceivingAddress() async {
try {
final _address = await getCurrentReceivingAddress();
if (_address == null) {
@ -155,12 +155,10 @@ class TezosWallet extends Bip39Wallet<Tezos> {
} catch (e, s) {
// do nothing, still allow user into wallet
Logging.instance.log(
"$runtimeType init() failed: $e\n$s",
"$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s",
level: LogLevel.Error,
);
}
await super.init();
}
@override

View file

@ -61,6 +61,20 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
await mainDB.updateOrPutAddresses([address]);
}
@override
Future<void> checkSaveInitialReceivingAddress() async {
final current = await getCurrentChangeAddress();
if (current == null) {
final address = await _generateAddress(
chain: 0, // receiving
index: 0, // initial index
derivePathType: DerivePathTypeExt.primaryFor(info.coin),
);
await mainDB.updateOrPutAddresses([address]);
}
}
// ========== Subclasses may override ========================================
/// To be overridden by crypto currencies that do extra address conversions

View file

@ -435,6 +435,8 @@ abstract class Wallet<T extends CryptoCurrency> {
Future<bool> pingCheck();
Future<void> checkSaveInitialReceivingAddress();
//===========================================
/// add transaction to local db temporarily. Used for quickly updating ui
/// before refresh can fetch data from server
@ -600,6 +602,7 @@ abstract class Wallet<T extends CryptoCurrency> {
@mustCallSuper
Future<void> init() async {
await checkSaveInitialReceivingAddress();
final address = await getCurrentReceivingAddress();
if (address != null) {
await info.updateReceivingAddress(
@ -607,9 +610,6 @@ abstract class Wallet<T extends CryptoCurrency> {
isar: mainDB.isar,
);
}
// TODO: make sure subclasses override this if they require some set up
// especially xmr/wow/epiccash
}
// ===========================================================================

View file

@ -179,6 +179,11 @@ mixin CwBasedInterface<T extends CryptonoteCurrency> on CryptonoteWallet<T>
}
}
@override
Future<void> checkSaveInitialReceivingAddress() async {
// this doesn't work without opening the wallet first which takes a while
}
// ============ Interface ====================================================
Future<Amount> get availableBalance;

View file

@ -1,11 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:bip47/src/util.dart';
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
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';
@ -831,55 +829,6 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
}
}
Future<List<({Transaction transaction, Address address})>>
fetchTransactionsV1({
required List<Address> addresses,
required int currentChainHeight,
}) async {
final List<({String txHash, int height, String address})> allTxHashes =
(await fetchHistory(addresses.map((e) => e.value).toList()))
.map(
(e) => (
txHash: e["tx_hash"] as String,
height: e["height"] as int,
address: e["address"] as String,
),
)
.toList();
List<Map<String, dynamic>> allTransactions = [];
for (final data in allTxHashes) {
final tx = await electrumXCachedClient.getTransaction(
txHash: data.txHash,
verbose: true,
coin: cryptoCurrency.coin,
);
// check for duplicates before adding to list
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
tx["address"] = addresses.firstWhere((e) => e.value == data.address);
tx["height"] = data.height;
allTransactions.add(tx);
}
}
final List<({Transaction transaction, Address address})> txnsData = [];
for (final txObject in allTransactions) {
final data = await _parseTransactionV1(
txObject,
addresses,
);
txnsData.add(data);
}
return txnsData;
}
Future<ElectrumXNode> getCurrentElectrumXNode() async {
final node = getCurrentNode();
@ -1185,257 +1134,6 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
return utxo;
}
Future<({Transaction transaction, Address address})> _parseTransactionV1(
Map<String, dynamic> txData,
List<Address> myAddresses,
) async {
Set<String> receivingAddresses = myAddresses
.where((e) =>
e.subType == AddressSubType.receiving ||
e.subType == AddressSubType.paynymReceive ||
e.subType == AddressSubType.paynymNotification)
.map((e) => e.value)
.toSet();
Set<String> changeAddresses = myAddresses
.where((e) => e.subType == AddressSubType.change)
.map((e) => e.value)
.toSet();
Set<String> inputAddresses = {};
Set<String> outputAddresses = {};
Amount totalInputValue = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.coin.decimals,
);
Amount totalOutputValue = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.coin.decimals,
);
Amount amountSentFromWallet = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.coin.decimals,
);
Amount amountReceivedInWallet = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.coin.decimals,
);
Amount changeAmount = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.coin.decimals,
);
// parse inputs
for (final input in txData["vin"] as List) {
final prevTxid = input["txid"] as String;
final prevOut = input["vout"] as int;
// fetch input tx to get address
final inputTx = await electrumXCachedClient.getTransaction(
txHash: prevTxid,
coin: cryptoCurrency.coin,
);
for (final output in inputTx["vout"] as List) {
// check matching output
if (prevOut == output["n"]) {
// get value
final value = Amount.fromDecimal(
Decimal.parse(output["value"].toString()),
fractionDigits: cryptoCurrency.coin.decimals,
);
// add value to total
totalInputValue += value;
// get input(prevOut) address
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
inputAddresses.add(address);
// if input was from my wallet, add value to amount sent
if (receivingAddresses.contains(address) ||
changeAddresses.contains(address)) {
amountSentFromWallet += value;
}
}
}
}
}
// parse outputs
for (final output in txData["vout"] as List) {
// get value
final value = Amount.fromDecimal(
Decimal.parse(output["value"].toString()),
fractionDigits: cryptoCurrency.coin.decimals,
);
// add value to total
totalOutputValue += value;
// get output address
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
outputAddresses.add(address);
// if output was to my wallet, add value to amount received
if (receivingAddresses.contains(address)) {
amountReceivedInWallet += value;
} else if (changeAddresses.contains(address)) {
changeAmount += value;
}
}
}
final mySentFromAddresses = [
...receivingAddresses.intersection(inputAddresses),
...changeAddresses.intersection(inputAddresses)
];
final myReceivedOnAddresses =
receivingAddresses.intersection(outputAddresses);
final myChangeReceivedOnAddresses =
changeAddresses.intersection(outputAddresses);
final fee = totalInputValue - totalOutputValue;
// this is the address initially used to fetch the txid
Address transactionAddress = txData["address"] as Address;
TransactionType type;
Amount amount;
if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) {
// tx is sent to self
type = TransactionType.sentToSelf;
// should be 0
amount =
amountSentFromWallet - amountReceivedInWallet - fee - changeAmount;
} else if (mySentFromAddresses.isNotEmpty) {
// outgoing tx
type = TransactionType.outgoing;
amount = amountSentFromWallet - changeAmount - fee;
// non wallet addresses found in tx outputs
final nonWalletOutAddresses = outputAddresses.difference(
myChangeReceivedOnAddresses,
);
if (nonWalletOutAddresses.isNotEmpty) {
final possible = nonWalletOutAddresses.first;
if (transactionAddress.value != possible) {
transactionAddress = Address(
walletId: myAddresses.first.walletId,
value: possible,
derivationIndex: -1,
derivationPath: null,
subType: AddressSubType.nonWallet,
type: AddressType.nonWallet,
publicKey: [],
);
}
} else {
// some other type of tx where the receiving address is
// one of my change addresses
type = TransactionType.sentToSelf;
amount = changeAmount;
}
} else {
// incoming tx
type = TransactionType.incoming;
amount = amountReceivedInWallet;
}
List<Output> outs = [];
List<Input> ins = [];
for (final json in txData["vin"] as List) {
bool isCoinBase = json['coinbase'] != null;
String? witness;
if (json['witness'] != null && json['witness'] is String) {
witness = json['witness'] as String;
} else if (json['txinwitness'] != null) {
if (json['txinwitness'] is List) {
witness = jsonEncode(json['txinwitness']);
}
}
final input = Input(
txid: json['txid'] as String,
vout: json['vout'] as int? ?? -1,
scriptSig: json['scriptSig']?['hex'] as String?,
scriptSigAsm: json['scriptSig']?['asm'] as String?,
isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?,
sequence: json['sequence'] as int?,
innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?,
witness: witness,
);
ins.add(input);
}
for (final json in txData["vout"] as List) {
final output = Output(
scriptPubKey: json['scriptPubKey']?['hex'] as String?,
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?,
scriptPubKeyType: json['scriptPubKey']?['type'] as String?,
scriptPubKeyAddress:
json["scriptPubKey"]?["addresses"]?[0] as String? ??
json['scriptPubKey']?['type'] as String? ??
"",
value: Amount.fromDecimal(
Decimal.parse(json["value"].toString()),
fractionDigits: cryptoCurrency.coin.decimals,
).raw.toInt(),
);
outs.add(output);
}
TransactionSubType txSubType = TransactionSubType.none;
if (this is PaynymInterface && outs.length > 1 && ins.isNotEmpty) {
for (int i = 0; i < outs.length; i++) {
List<String>? scriptChunks = outs[i].scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
txSubType = TransactionSubType.bip47Notification;
}
}
}
}
final tx = Transaction(
walletId: myAddresses.first.walletId,
txid: txData["txid"] as String,
timestamp: txData["blocktime"] as int? ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
type: type,
subType: txSubType,
// amount may overflow. Deprecated. Use amountString
amount: amount.raw.toInt(),
amountString: amount.toJsonString(),
fee: fee.raw.toInt(),
height: txData["height"] as int?,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: null,
nonce: null,
inputs: ins,
outputs: outs,
numberOfMessages: null,
);
return (transaction: tx, address: transactionAddress);
}
//============================================================================
@override
@ -1993,12 +1691,15 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
}
}
@override
Future<void> checkSaveInitialReceivingAddress() async {}
@override
Future<void> init() async {
try {
final features = await electrumXClient
.getServerFeatures()
.timeout(const Duration(seconds: 2));
.timeout(const Duration(seconds: 4));
Logging.instance.log("features: $features", level: LogLevel.Info);

View file

@ -315,22 +315,20 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
}
@override
Future<void> init() async {
Future<void> checkSaveInitialReceivingAddress() async {
try {
_cachedAddress = await getCurrentReceivingAddress();
if (_cachedAddress == null) {
_cachedAddress = await _getAddressFromMnemonic();
await mainDB.putAddress(_cachedAddress!);
await mainDB.updateOrPutAddresses([_cachedAddress!]);
}
} catch (e, s) {
// do nothing, still allow user into wallet
Logging.instance.log(
"$runtimeType init() failed: $e\n$s",
"$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s",
level: LogLevel.Error,
);
}
return super.init();
}
@override

View file

@ -93,17 +93,20 @@ class SimpleWalletCard extends ConsumerWidget {
final nav = Navigator.of(context);
final wallet = ref.read(pWallets).getWallet(walletId);
await wallet.init();
if (context.mounted) {
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {
loadFuture = wallet.init().then((value) async => await (wallet).open());
} else {
loadFuture = wallet.init();
}
await showLoading(
whileFuture: wallet.open(),
whileFuture: loadFuture,
context: context,
message: 'Opening ${wallet.info.name}',
isDesktop: Util.isDesktop,
);
}
if (popPrevious) nav.pop();
if (desktopNavigatorState != null) {