WIP very rough refactoring wip

This commit is contained in:
julian 2023-09-18 15:28:31 -06:00
parent a0a653b088
commit f30785616b
22 changed files with 1682 additions and 315 deletions

View file

@ -26,12 +26,13 @@ class DB {
@Deprecated("Left over for migration from old versions of Stack Wallet")
static const String boxNameAddressBook = "addressBook";
static const String boxNameTrades = "exchangeTransactionsBox";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameFavoriteWallets = "favoriteWallets";
// in use
// TODO: migrate
static const String boxNameNodeModels = "nodeModels";
static const String boxNamePrimaryNodes = "primaryNodes";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameNotifications = "notificationModels";
static const String boxNameWatchedTransactions =
"watchedTxNotificationModels";
@ -39,7 +40,6 @@ class DB {
static const String boxNameTradesV2 = "exchangeTradesBox";
static const String boxNameTradeNotes = "tradeNotesBox";
static const String boxNameTradeLookup = "tradeToTxidLookUpBox";
static const String boxNameFavoriteWallets = "favoriteWallets";
static const String boxNameWalletsToDeleteOnStart = "walletsToDeleteOnStart";
static const String boxNamePriceCache = "priceAPIPrice24hCache";

View file

@ -1,22 +0,0 @@
import 'package:coinlib/coinlib.dart' as coinlib;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/wallets/coin/crypto_currency.dart';
abstract class Bip39HDCurrency extends CryptoCurrency {
Bip39HDCurrency(super.network);
coinlib.NetworkParams get networkParams;
String constructDerivePath({
required DerivePathType derivePathType,
int account = 0,
required int chain,
required int index,
});
({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({
required coinlib.ECPublicKey publicKey,
required DerivePathType derivePathType,
});
}

View file

@ -1,29 +0,0 @@
import 'package:coinlib/coinlib.dart';
abstract class CoinParams {
static const bitcoin = BitcoinParams();
}
class BitcoinParams {
const BitcoinParams();
final NetworkParams mainNet = const NetworkParams(
wifPrefix: 0x80,
p2pkhPrefix: 0x00,
p2shPrefix: 0x05,
privHDPrefix: 0x0488ade4,
pubHDPrefix: 0x0488b21e,
bech32Hrp: "bc",
messagePrefix: '\x18Bitcoin Signed Message:\n',
);
final NetworkParams testNet = const NetworkParams(
wifPrefix: 0xef,
p2pkhPrefix: 0x6f,
p2shPrefix: 0xc4,
privHDPrefix: 0x04358394,
pubHDPrefix: 0x043587cf,
bech32Hrp: "tb",
messagePrefix: "\x18Bitcoin Signed Message:\n",
);
}

View file

@ -1,16 +0,0 @@
import 'package:stackwallet/utilities/enums/coin_enum.dart';
enum CryptoCurrencyNetwork {
main,
test,
stage;
}
abstract class CryptoCurrency {
@Deprecated("Should eventually move away from Coin enum")
late final Coin coin;
final CryptoCurrencyNetwork network;
CryptoCurrency(this.network);
}

View file

@ -0,0 +1,5 @@
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
abstract class Bip39Currency extends CryptoCurrency {
Bip39Currency(super.network);
}

View file

@ -0,0 +1,51 @@
import 'package:coinlib/coinlib.dart' as coinlib;
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/bip39_currency.dart';
abstract class Bip39HDCurrency extends Bip39Currency {
Bip39HDCurrency(super.network);
coinlib.NetworkParams get networkParams;
Amount get dustLimit;
String constructDerivePath({
required DerivePathType derivePathType,
int account = 0,
required int chain,
required int index,
});
({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({
required coinlib.ECPublicKey publicKey,
required DerivePathType derivePathType,
});
String addressToScriptHash({required String address}) {
try {
final addr = coinlib.Address.fromString(address, networkParams);
return convertBytesToScriptHash(addr.program.script.compiled);
} catch (e) {
rethrow;
}
}
static String convertBytesToScriptHash(Uint8List bytes) {
final hash = sha256.convert(bytes.toList(growable: false)).toString();
final chars = hash.split("");
final List<String> reversedPairs = [];
// TODO find a better/faster way to do this?
int i = chars.length - 1;
while (i > 0) {
reversedPairs.add(chars[i - 1]);
reversedPairs.add(chars[i]);
i -= 2;
}
return reversedPairs.join("");
}
}

View file

@ -1,10 +1,10 @@
import 'package:coinlib/coinlib.dart' as coinlib;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/wallets/coin/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/coin/coin_params.dart';
import 'package:stackwallet/wallets/coin/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
class Bitcoin extends Bip39HDCurrency {
Bitcoin(super.network) {
@ -18,13 +18,40 @@ class Bitcoin extends Bip39HDCurrency {
}
}
@override
Amount get dustLimit => Amount(
rawValue: BigInt.from(294),
fractionDigits: fractionDigits,
);
Amount get dustLimitP2PKH => Amount(
rawValue: BigInt.from(546),
fractionDigits: fractionDigits,
);
@override
coinlib.NetworkParams get networkParams {
switch (network) {
case CryptoCurrencyNetwork.main:
return CoinParams.bitcoin.mainNet;
return const coinlib.NetworkParams(
wifPrefix: 0x80,
p2pkhPrefix: 0x00,
p2shPrefix: 0x05,
privHDPrefix: 0x0488ade4,
pubHDPrefix: 0x0488b21e,
bech32Hrp: "bc",
messagePrefix: '\x18Bitcoin Signed Message:\n',
);
case CryptoCurrencyNetwork.test:
return CoinParams.bitcoin.testNet;
return const coinlib.NetworkParams(
wifPrefix: 0xef,
p2pkhPrefix: 0x6f,
p2shPrefix: 0xc4,
privHDPrefix: 0x04358394,
pubHDPrefix: 0x043587cf,
bech32Hrp: "tb",
messagePrefix: "\x18Bitcoin Signed Message:\n",
);
default:
throw Exception("Unsupported network: $network");
}
@ -39,13 +66,15 @@ class Bitcoin extends Bip39HDCurrency {
}) {
String coinType;
if (networkParams.wifPrefix == CoinParams.bitcoin.mainNet.wifPrefix) {
coinType = "0"; // btc mainnet
} else if (networkParams.wifPrefix ==
CoinParams.bitcoin.testNet.wifPrefix) {
coinType = "1"; // btc testnet
} else {
throw Exception("Invalid Bitcoin network wif used!");
switch (networkParams.wifPrefix) {
case 0x80:
coinType = "0"; // btc mainnet
break;
case 0xef:
coinType = "1"; // btc testnet
break;
default:
throw Exception("Invalid Bitcoin network wif used!");
}
int purpose;
@ -66,6 +95,7 @@ class Bitcoin extends Bip39HDCurrency {
return "m/$purpose'/$coinType'/$account'/$chain/$index";
}
@override
({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({
required coinlib.ECPublicKey publicKey,
required DerivePathType derivePathType,
@ -112,4 +142,14 @@ class Bitcoin extends Bip39HDCurrency {
throw Exception("DerivePathType $derivePathType not supported");
}
}
@override
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 1;
@override
bool validateAddress(String address) {
// TODO: implement validateAddress
throw UnimplementedError();
}
}

View file

@ -0,0 +1,44 @@
import 'package:flutter_libepiccash/epic_cash.dart' as lib_epiccash;
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/bip39_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
class Epiccash extends Bip39Currency {
Epiccash(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.epicCash;
default:
throw Exception("Unsupported network: $network");
}
}
@override
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 3;
@override
bool validateAddress(String address) {
// Invalid address that contains HTTP and epicbox domain
if ((address.startsWith("http://") || address.startsWith("https://")) &&
address.contains("@")) {
return false;
}
if (address.startsWith("http://") || address.startsWith("https://")) {
if (Uri.tryParse(address) != null) {
return true;
}
}
final String validate = lib_epiccash.validateSendAddress(address);
if (int.parse(validate) == 1) {
// Check if address contains a domain
if (address.contains("@")) {
return true;
}
return false;
} else {
return false;
}
}
}

View file

@ -0,0 +1,25 @@
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
enum CryptoCurrencyNetwork {
main,
test,
stage;
}
abstract class CryptoCurrency {
@Deprecated("[prio=low] Should eventually move away from Coin enum")
late final Coin coin;
final CryptoCurrencyNetwork network;
CryptoCurrency(this.network);
// TODO: [prio=low] require these be overridden in concrete implementations to remove reliance on [coin]
int get fractionDigits => coin.decimals;
BigInt get satsPerCoin => Constants.satsPerCoin(coin);
int get minConfirms;
bool validateAddress(String address);
}

View file

@ -7,7 +7,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
part 'wallet_info.g.dart';
@Collection(accessor: "walletInfo")
@Collection(accessor: "walletInfo", inheritance: false)
class WalletInfo {
Id id = Isar.autoIncrement;
@ -30,10 +30,11 @@ class WalletInfo {
// Only exposed for isar to avoid dealing with storing enums as Coin can change
final String coinName;
final bool isFavourite;
/// User set favourites ordering. No restrictions are placed on uniqueness.
/// Reordering logic in the ui code should ensure this is unique.
///
/// Also represents if the wallet is a favourite. Any number greater then -1
/// denotes a favourite. Any number less than 0 means it is not a favourite.
final int favouriteOrderIndex;
/// Wallets without this flag set to true should be deleted on next app run
@ -43,13 +44,37 @@ class WalletInfo {
/// The highest block height the wallet has scanned.
final int cachedChainHeight;
/// Wallet creation chain height. Applies to select coin only.
final int creationHeight;
// TODO: store these in other data s
// Should contain specific things based on certain coins only
/// Wallet restore chain height. Applies to select coin only.
final int restoreHeight;
// /// Wallet creation chain height. Applies to select coin only.
// final int creationHeight;
//
// /// Wallet restore chain height. Applies to select coin only.
// final int restoreHeight;
final String? otherDataJsonString;
//============================================================================
//=============== Getters ====================================================
bool get isFavourite => favouriteOrderIndex > -1;
List<String> get tokenContractAddresses =>
otherData[WalletInfoKeys.tokenContractAddresses] as List<String>? ?? [];
/// Special case for coins such as firo
@ignore
Balance get cachedSecondaryBalance {
try {
return Balance.fromJson(
otherData[WalletInfoKeys.cachedSecondaryBalance] as String? ?? "",
coin.decimals,
);
} catch (_) {
return Balance.zeroForCoin(coin: coin);
}
}
@ignore
Coin get coin => Coin.values.byName(coinName);
@ -63,19 +88,24 @@ class WalletInfo {
}
}
@ignore
Map<String, dynamic> get otherData => otherDataJsonString == null
? {}
: Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map);
//============================================================================
WalletInfo({
required this.coinName,
required this.walletId,
required this.name,
required this.walletType,
required this.mainAddressType,
this.isFavourite = false,
this.favouriteOrderIndex = 0,
this.cachedChainHeight = 0,
this.creationHeight = 0,
this.restoreHeight = 0,
this.isMnemonicVerified = false,
this.cachedBalanceString,
this.otherDataJsonString,
}) : assert(
Coin.values.map((e) => e.name).contains(coinName),
);
@ -83,13 +113,11 @@ class WalletInfo {
WalletInfo copyWith({
String? coinName,
String? name,
bool? isFavourite,
int? favouriteOrderIndex,
int? cachedChainHeight,
int? creationHeight,
int? restoreHeight,
bool? isMnemonicVerified,
String? cachedBalanceString,
Map<String, dynamic>? otherData,
}) {
return WalletInfo(
coinName: coinName ?? this.coinName,
@ -97,13 +125,12 @@ class WalletInfo {
name: name ?? this.name,
walletType: walletType,
mainAddressType: mainAddressType,
isFavourite: isFavourite ?? this.isFavourite,
favouriteOrderIndex: favouriteOrderIndex ?? this.favouriteOrderIndex,
cachedChainHeight: cachedChainHeight ?? this.cachedChainHeight,
creationHeight: creationHeight ?? this.creationHeight,
restoreHeight: restoreHeight ?? this.restoreHeight,
isMnemonicVerified: isMnemonicVerified ?? this.isMnemonicVerified,
cachedBalanceString: cachedBalanceString ?? this.cachedBalanceString,
otherDataJsonString:
otherData == null ? otherDataJsonString : jsonEncode(otherData),
)..id = id;
}
@ -140,10 +167,17 @@ class WalletInfo {
}
}
abstract class WalletInfoKeys {
static const String tokenContractAddresses = "tokenContractAddressesKey";
static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey";
static const String epiccashData = "epiccashDataKey";
}
// Used in Isar db and stored there as int indexes so adding/removing values
// in this definition should be done extremely carefully in production
enum WalletType {
bip39,
bip39HD,
cryptonote,
privateKeyBased;
}

View file

@ -32,41 +32,41 @@ const WalletInfoSchema = CollectionSchema(
name: r'coinName',
type: IsarType.string,
),
r'creationHeight': PropertySchema(
id: 3,
name: r'creationHeight',
type: IsarType.long,
),
r'favouriteOrderIndex': PropertySchema(
id: 4,
id: 3,
name: r'favouriteOrderIndex',
type: IsarType.long,
),
r'isFavourite': PropertySchema(
id: 5,
id: 4,
name: r'isFavourite',
type: IsarType.bool,
),
r'isMnemonicVerified': PropertySchema(
id: 6,
id: 5,
name: r'isMnemonicVerified',
type: IsarType.bool,
),
r'mainAddressType': PropertySchema(
id: 7,
id: 6,
name: r'mainAddressType',
type: IsarType.byte,
enumMap: _WalletInfomainAddressTypeEnumValueMap,
),
r'name': PropertySchema(
id: 8,
id: 7,
name: r'name',
type: IsarType.string,
),
r'restoreHeight': PropertySchema(
r'otherDataJsonString': PropertySchema(
id: 8,
name: r'otherDataJsonString',
type: IsarType.string,
),
r'tokenContractAddresses': PropertySchema(
id: 9,
name: r'restoreHeight',
type: IsarType.long,
name: r'tokenContractAddresses',
type: IsarType.stringList,
),
r'walletId': PropertySchema(
id: 10,
@ -122,6 +122,19 @@ int _walletInfoEstimateSize(
}
bytesCount += 3 + object.coinName.length * 3;
bytesCount += 3 + object.name.length * 3;
{
final value = object.otherDataJsonString;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
bytesCount += 3 + object.tokenContractAddresses.length * 3;
{
for (var i = 0; i < object.tokenContractAddresses.length; i++) {
final value = object.tokenContractAddresses[i];
bytesCount += value.length * 3;
}
}
bytesCount += 3 + object.walletId.length * 3;
return bytesCount;
}
@ -135,13 +148,13 @@ void _walletInfoSerialize(
writer.writeString(offsets[0], object.cachedBalanceString);
writer.writeLong(offsets[1], object.cachedChainHeight);
writer.writeString(offsets[2], object.coinName);
writer.writeLong(offsets[3], object.creationHeight);
writer.writeLong(offsets[4], object.favouriteOrderIndex);
writer.writeBool(offsets[5], object.isFavourite);
writer.writeBool(offsets[6], object.isMnemonicVerified);
writer.writeByte(offsets[7], object.mainAddressType.index);
writer.writeString(offsets[8], object.name);
writer.writeLong(offsets[9], object.restoreHeight);
writer.writeLong(offsets[3], object.favouriteOrderIndex);
writer.writeBool(offsets[4], object.isFavourite);
writer.writeBool(offsets[5], object.isMnemonicVerified);
writer.writeByte(offsets[6], object.mainAddressType.index);
writer.writeString(offsets[7], object.name);
writer.writeString(offsets[8], object.otherDataJsonString);
writer.writeStringList(offsets[9], object.tokenContractAddresses);
writer.writeString(offsets[10], object.walletId);
writer.writeByte(offsets[11], object.walletType.index);
}
@ -156,15 +169,13 @@ WalletInfo _walletInfoDeserialize(
cachedBalanceString: reader.readStringOrNull(offsets[0]),
cachedChainHeight: reader.readLongOrNull(offsets[1]) ?? 0,
coinName: reader.readString(offsets[2]),
creationHeight: reader.readLongOrNull(offsets[3]) ?? 0,
favouriteOrderIndex: reader.readLongOrNull(offsets[4]) ?? 0,
isFavourite: reader.readBoolOrNull(offsets[5]) ?? false,
isMnemonicVerified: reader.readBoolOrNull(offsets[6]) ?? false,
favouriteOrderIndex: reader.readLongOrNull(offsets[3]) ?? 0,
isMnemonicVerified: reader.readBoolOrNull(offsets[5]) ?? false,
mainAddressType: _WalletInfomainAddressTypeValueEnumMap[
reader.readByteOrNull(offsets[7])] ??
reader.readByteOrNull(offsets[6])] ??
AddressType.p2pkh,
name: reader.readString(offsets[8]),
restoreHeight: reader.readLongOrNull(offsets[9]) ?? 0,
name: reader.readString(offsets[7]),
otherDataJsonString: reader.readStringOrNull(offsets[8]),
walletId: reader.readString(offsets[10]),
walletType:
_WalletInfowalletTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ??
@ -190,19 +201,19 @@ P _walletInfoDeserializeProp<P>(
case 3:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 4:
return (reader.readLongOrNull(offset) ?? 0) as P;
return (reader.readBool(offset)) as P;
case 5:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 6:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 7:
return (_WalletInfomainAddressTypeValueEnumMap[
reader.readByteOrNull(offset)] ??
AddressType.p2pkh) as P;
case 8:
case 7:
return (reader.readString(offset)) as P;
case 8:
return (reader.readStringOrNull(offset)) as P;
case 9:
return (reader.readLongOrNull(offset) ?? 0) as P;
return (reader.readStringList(offset) ?? []) as P;
case 10:
return (reader.readString(offset)) as P;
case 11:
@ -240,13 +251,15 @@ const _WalletInfomainAddressTypeValueEnumMap = {
};
const _WalletInfowalletTypeEnumValueMap = {
'bip39': 0,
'cryptonote': 1,
'privateKeyBased': 2,
'bip39HD': 1,
'cryptonote': 2,
'privateKeyBased': 3,
};
const _WalletInfowalletTypeValueEnumMap = {
0: WalletType.bip39,
1: WalletType.cryptonote,
2: WalletType.privateKeyBased,
1: WalletType.bip39HD,
2: WalletType.cryptonote,
3: WalletType.privateKeyBased,
};
Id _walletInfoGetId(WalletInfo object) {
@ -784,62 +797,6 @@ extension WalletInfoQueryFilter
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
creationHeightEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'creationHeight',
value: value,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
creationHeightGreaterThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'creationHeight',
value: value,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
creationHeightLessThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'creationHeight',
value: value,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
creationHeightBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'creationHeight',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
favouriteOrderIndexEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
@ -1156,58 +1113,383 @@ extension WalletInfoQueryFilter
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightEqualTo(int value) {
otherDataJsonStringIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'restoreHeight',
value: value,
return query.addFilterCondition(const FilterCondition.isNull(
property: r'otherDataJsonString',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightGreaterThan(
int value, {
otherDataJsonStringIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'otherDataJsonString',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'restoreHeight',
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightLessThan(
int value, {
otherDataJsonStringLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'restoreHeight',
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightBetween(
otherDataJsonStringBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'otherDataJsonString',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'otherDataJsonString',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'otherDataJsonString',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'otherDataJsonString',
value: '',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
otherDataJsonStringIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'otherDataJsonString',
value: '',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'tokenContractAddresses',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'tokenContractAddresses',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'tokenContractAddresses',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'tokenContractAddresses',
value: '',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesElementIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'tokenContractAddresses',
value: '',
));
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tokenContractAddresses',
length,
true,
length,
true,
);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tokenContractAddresses',
0,
true,
0,
true,
);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tokenContractAddresses',
0,
false,
999999,
true,
);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tokenContractAddresses',
0,
true,
length,
include,
);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'tokenContractAddresses',
length,
include,
999999,
true,
);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
tokenContractAddressesLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'restoreHeight',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
return query.listLength(
r'tokenContractAddresses',
lower,
includeLower,
upper,
includeUpper,
);
});
}
@ -1448,19 +1730,6 @@ extension WalletInfoQuerySortBy
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> sortByCreationHeight() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'creationHeight', Sort.asc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
sortByCreationHeightDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'creationHeight', Sort.desc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
sortByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) {
@ -1526,15 +1795,17 @@ extension WalletInfoQuerySortBy
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> sortByRestoreHeight() {
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
sortByOtherDataJsonString() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'restoreHeight', Sort.asc);
return query.addSortBy(r'otherDataJsonString', Sort.asc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> sortByRestoreHeightDesc() {
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
sortByOtherDataJsonStringDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'restoreHeight', Sort.desc);
return query.addSortBy(r'otherDataJsonString', Sort.desc);
});
}
@ -1604,19 +1875,6 @@ extension WalletInfoQuerySortThenBy
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> thenByCreationHeight() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'creationHeight', Sort.asc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
thenByCreationHeightDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'creationHeight', Sort.desc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
thenByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) {
@ -1694,15 +1952,17 @@ extension WalletInfoQuerySortThenBy
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> thenByRestoreHeight() {
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
thenByOtherDataJsonString() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'restoreHeight', Sort.asc);
return query.addSortBy(r'otherDataJsonString', Sort.asc);
});
}
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy> thenByRestoreHeightDesc() {
QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
thenByOtherDataJsonStringDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'restoreHeight', Sort.desc);
return query.addSortBy(r'otherDataJsonString', Sort.desc);
});
}
@ -1755,12 +2015,6 @@ extension WalletInfoQueryWhereDistinct
});
}
QueryBuilder<WalletInfo, WalletInfo, QDistinct> distinctByCreationHeight() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'creationHeight');
});
}
QueryBuilder<WalletInfo, WalletInfo, QDistinct>
distinctByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) {
@ -1794,9 +2048,18 @@ extension WalletInfoQueryWhereDistinct
});
}
QueryBuilder<WalletInfo, WalletInfo, QDistinct> distinctByRestoreHeight() {
QueryBuilder<WalletInfo, WalletInfo, QDistinct> distinctByOtherDataJsonString(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'restoreHeight');
return query.addDistinctBy(r'otherDataJsonString',
caseSensitive: caseSensitive);
});
}
QueryBuilder<WalletInfo, WalletInfo, QDistinct>
distinctByTokenContractAddresses() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'tokenContractAddresses');
});
}
@ -1841,12 +2104,6 @@ extension WalletInfoQueryProperty
});
}
QueryBuilder<WalletInfo, int, QQueryOperations> creationHeightProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'creationHeight');
});
}
QueryBuilder<WalletInfo, int, QQueryOperations>
favouriteOrderIndexProperty() {
return QueryBuilder.apply(this, (query) {
@ -1880,9 +2137,17 @@ extension WalletInfoQueryProperty
});
}
QueryBuilder<WalletInfo, int, QQueryOperations> restoreHeightProperty() {
QueryBuilder<WalletInfo, String?, QQueryOperations>
otherDataJsonStringProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'restoreHeight');
return query.addPropertyName(r'otherDataJsonString');
});
}
QueryBuilder<WalletInfo, List<String>, QQueryOperations>
tokenContractAddressesProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'tokenContractAddresses');
});
}

View file

@ -0,0 +1,214 @@
import 'dart:convert';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
void migrateWallets({
required SecureStorageInterface secureStore,
}) async {
final allWalletsBox = await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
final names = DB.instance
.get<dynamic>(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?;
if (names == null) {
// no wallets to migrate
return;
}
//
// Parse the old data from the Hive map into a nice list
//
final List<
({
Coin coin,
String name,
String walletId,
})> oldInfo = Map<String, dynamic>.from(names).values.map((e) {
final map = e as Map;
return (
coin: Coin.values.byName(map["coin"] as String),
walletId: map["id"] as String,
name: map["name"] as String,
);
}).toList();
//
// Get current ordered list of favourite wallet Ids
//
final List<String> favourites =
(await Hive.openBox<String>(DB.boxNameFavoriteWallets)).values.toList();
final List<WalletInfo> newInfo = [];
//
// Convert each old info into the new Isar WalletInfo
//
for (final old in oldInfo) {
final walletBox = await Hive.openBox<dynamic>(old.walletId);
//
// Set other data values
//
Map<String, dynamic> otherData = {};
otherData[WalletInfoKeys.cachedSecondaryBalance] = walletBox.get(
DBKeys.cachedBalanceSecondary,
) as String?;
otherData[WalletInfoKeys.tokenContractAddresses] = walletBox.get(
DBKeys.ethTokenContracts,
) as List<String>?;
// epiccash specifics
if (old.coin == Coin.epicCash) {
final epicWalletInfo = ExtraEpiccashWalletInfo.fromMap({
"receivingIndex": walletBox.get("receivingIndex") as int? ?? 0,
"changeIndex": walletBox.get("changeIndex") as int? ?? 0,
"slate_to_address": walletBox.get("slate_to_address") as Map? ?? {},
"slatesToCommits": walletBox.get("slatesToCommits") as Map? ?? {},
"lastScannedBlock": walletBox.get("lastScannedBlock") as int? ?? 0,
"restoreHeight": walletBox.get("restoreHeight") as int? ?? 0,
"creationHeight": walletBox.get("creationHeight") as int? ?? 0,
});
otherData[WalletInfoKeys.epiccashData] = jsonEncode(
epicWalletInfo.toMap(),
);
}
//
// Clear out any keys with null values as they are not needed
//
otherData.removeWhere((key, value) => value == null);
final info = WalletInfo(
coinName: old.coin.name,
walletId: old.walletId,
name: old.name,
walletType: _walletTypeForCoin(old.coin),
mainAddressType: _addressTypeForCoin(old.coin),
favouriteOrderIndex: favourites.indexOf(old.walletId),
cachedChainHeight: walletBox.get(
DBKeys.storedChainHeight,
) as int? ??
0,
cachedBalanceString: walletBox.get(
DBKeys.cachedBalance,
) as String?,
otherDataJsonString: jsonEncode(otherData),
);
newInfo.add(info);
}
}
void _cleanupOnSuccess({required List<String> walletIds}) async {
await Hive.deleteBoxFromDisk(DB.boxNameFavoriteWallets);
await Hive.deleteBoxFromDisk(DB.boxNameAllWalletsData);
for (final walletId in walletIds) {
await Hive.deleteBoxFromDisk(walletId);
}
}
WalletType _walletTypeForCoin(Coin coin) {
WalletType walletType;
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.bitcoincash:
case Coin.bitcoincashTestnet:
case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
case Coin.litecoinTestNet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
case Coin.eCash:
walletType = WalletType.bip39HD;
break;
case Coin.monero:
case Coin.wownero:
walletType = WalletType.cryptonote;
break;
case Coin.epicCash:
case Coin.ethereum:
case Coin.tezos:
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
walletType = WalletType.bip39;
break;
}
return walletType;
}
AddressType _addressTypeForCoin(Coin coin) {
AddressType addressType;
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.litecoin:
case Coin.litecoinTestNet:
addressType = AddressType.p2wpkh;
break;
case Coin.eCash:
case Coin.bitcoincash:
case Coin.bitcoincashTestnet:
case Coin.dogecoin:
case Coin.firo:
case Coin.firoTestNet:
case Coin.namecoin:
case Coin.particl:
case Coin.dogecoinTestNet:
addressType = AddressType.p2pkh;
break;
case Coin.monero:
case Coin.wownero:
addressType = AddressType.cryptonote;
break;
case Coin.epicCash:
addressType = AddressType.mimbleWimble;
break;
case Coin.ethereum:
addressType = AddressType.ethereum;
break;
case Coin.tezos:
// should not be unknown but since already used in prod changing
// this requires a migrate
addressType = AddressType.unknown;
break;
case Coin.nano:
addressType = AddressType.nano;
break;
case Coin.banano:
addressType = AddressType.banano;
break;
case Coin.stellar:
case Coin.stellarTestnet:
// should not be unknown but since already used in prod changing
// this requires a migrate
addressType = AddressType.unknown;
break;
}
return addressType;
}

View file

@ -1,14 +1,13 @@
import 'package:bip39/bip39.dart' as bip39;
import 'package:coinlib/coinlib.dart' as coinlib;
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/wallets/coin/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/bip39_wallet.dart';
class Bip39HDWallet<T extends Bip39HDCurrency> extends Wallet<T> {
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> {
Bip39HDWallet(super.cryptoCurrency);
/// Generates a receiving address of [walletInfo.mainAddressType]. If none
@ -50,30 +49,6 @@ class Bip39HDWallet<T extends Bip39HDCurrency> extends Wallet<T> {
return address;
}
Future<String> getMnemonic() async {
final mnemonic = await secureStorageInterface.read(
key: Wallet.mnemonicKey(walletId: walletInfo.walletId),
);
if (mnemonic == null) {
throw SWException("mnemonic has not been set");
}
return mnemonic;
}
Future<String> getMnemonicPassphrase() async {
final mnemonicPassphrase = await secureStorageInterface.read(
key: Wallet.mnemonicPassphraseKey(walletId: walletInfo.walletId),
);
if (mnemonicPassphrase == null) {
throw SWException("mnemonicPassphrase has not been set");
}
return mnemonicPassphrase;
}
// ========== Private ========================================================
Future<Address?> get _currentReceivingAddress async =>
@ -149,4 +124,10 @@ class Bip39HDWallet<T extends Bip39HDCurrency> extends Wallet<T> {
// TODO: implement prepareSend
throw UnimplementedError();
}
@override
Future<void> recover({required bool isRescan}) {
// TODO: implement recover
throw UnimplementedError();
}
}

View file

@ -0,0 +1,53 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/wallets/crypto_currency/bip39_currency.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
abstract class Bip39Wallet<T extends Bip39Currency> extends Wallet<T> {
Bip39Wallet(super.cryptoCurrency);
Future<String> getMnemonic() async {
final mnemonic = await secureStorageInterface.read(
key: Wallet.mnemonicKey(walletId: walletInfo.walletId),
);
if (mnemonic == null) {
throw SWException("mnemonic has not been set");
}
return mnemonic;
}
Future<String> getMnemonicPassphrase() async {
final mnemonicPassphrase = await secureStorageInterface.read(
key: Wallet.mnemonicPassphraseKey(walletId: walletInfo.walletId),
);
if (mnemonicPassphrase == null) {
throw SWException("mnemonicPassphrase has not been set");
}
return mnemonicPassphrase;
}
// ========== Private ========================================================
// ========== Overrides ======================================================
// @override
// Future<TxData> confirmSend({required TxData txData}) {
// // TODO: implement confirmSend
// throw UnimplementedError();
// }
//
// @override
// Future<TxData> prepareSend({required TxData txData}) {
// // TODO: implement prepareSend
// throw UnimplementedError();
// }
//
// @override
// Future<void> recover({required bool isRescan}) {
// // TODO: implement recover
// throw UnimplementedError();
// }
}

View file

@ -2,7 +2,7 @@ import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class CryptonoteWallet extends Wallet {
abstract class CryptonoteWallet extends Wallet {
CryptonoteWallet(super.cryptoCurrency);
Future<String> getMnemonic() async {
@ -30,4 +30,10 @@ class CryptonoteWallet extends Wallet {
// TODO: implement prepareSend
throw UnimplementedError();
}
@override
Future<void> recover({required bool isRescan}) {
// TODO: implement recover
throw UnimplementedError();
}
}

View file

@ -0,0 +1,95 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
import 'package:tuple/tuple.dart';
class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
BitcoinWallet(
super.cryptoCurrency, {
required NodeService nodeService,
required Prefs prefs,
}) {
// TODO: [prio=low] ensure this hack isn't needed
assert(cryptoCurrency is Bitcoin);
this.prefs = prefs;
this.nodeService = nodeService;
}
// ===========================================================================
Future<List<Address>> _fetchAllOwnAddresses() async {
final allAddresses = await mainDB
.getAddresses(walletId)
.filter()
.not()
.group(
(q) => q
.typeEqualTo(AddressType.nonWallet)
.or()
.subTypeEqualTo(AddressSubType.nonWallet),
)
.findAll();
return allAddresses;
}
// ===========================================================================
@override
Future<void> refresh() {
// TODO: implement refresh
throw UnimplementedError();
}
@override
Future<void> updateBalance() {
// TODO: implement updateBalance
throw UnimplementedError();
}
@override
Future<void> updateTransactions() async {
final currentChainHeight = await fetchChainHeight();
final data = await fetchTransactions(
addresses: await _fetchAllOwnAddresses(),
currentChainHeight: currentChainHeight,
);
await mainDB.addNewTransactionData(
data
.map(
(e) => Tuple2(
e.transaction,
e.address,
),
)
.toList(),
walletId,
);
// TODO: [prio=med] get rid of this and watch isar instead
// quick hack to notify manager to call notifyListeners if
// transactions changed
if (data.isNotEmpty) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Transactions updated/added for: $walletId ${walletInfo.name}",
walletId,
),
);
}
}
@override
Future<void> updateUTXOs() {
// TODO: implement updateUTXOs
throw UnimplementedError();
}
}

View file

@ -0,0 +1,54 @@
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/bip39_wallet.dart';
class EpiccashWallet extends Bip39Wallet {
EpiccashWallet(super.cryptoCurrency);
@override
Future<TxData> confirmSend({required TxData txData}) {
// TODO: implement confirmSend
throw UnimplementedError();
}
@override
Future<TxData> prepareSend({required TxData txData}) {
// TODO: implement prepareSend
throw UnimplementedError();
}
@override
Future<void> recover({required bool isRescan}) {
// TODO: implement recover
throw UnimplementedError();
}
@override
Future<void> refresh() {
// TODO: implement refresh
throw UnimplementedError();
}
@override
Future<void> updateBalance() {
// TODO: implement updateBalance
throw UnimplementedError();
}
@override
Future<void> updateNode() {
// TODO: implement updateNode
throw UnimplementedError();
}
@override
Future<void> updateTransactions() {
// TODO: implement updateTransactions
throw UnimplementedError();
}
@override
Future<void> updateUTXOs() {
// TODO: implement updateUTXOs
throw UnimplementedError();
}
}

View file

@ -0,0 +1,431 @@
import 'dart:convert';
import 'package:bip47/src/util.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/input.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
import 'package:uuid/uuid.dart';
mixin ElectrumXMixin on Bip39HDWallet {
late ElectrumX electrumX;
late CachedElectrumX electrumXCached;
late NodeService nodeService;
Future<int> fetchChainHeight() async {
try {
final result = await electrumX.getBlockHeadTip();
return result["height"] as int;
} catch (e) {
rethrow;
}
}
Future<List<({Transaction transaction, Address address})>> fetchTransactions({
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 electrumXCached.getTransaction(
txHash: data.txHash,
verbose: true,
coin: cryptoCurrency.coin,
);
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
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 parseTransaction(
txObject,
addresses,
);
txnsData.add(data);
}
return txnsData;
}
Future<ElectrumXNode> getCurrentNode() async {
final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin) ??
DefaultNodes.getNodeFor(cryptoCurrency.coin);
return ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
useSSL: node.useSSL,
id: node.id,
);
}
Future<void> updateElectrumX({required ElectrumXNode newNode}) async {
final failovers = nodeService
.failoverNodesFor(coin: cryptoCurrency.coin)
.map((e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
))
.toList();
final newNode = await getCurrentNode();
electrumX = ElectrumX.from(
node: newNode,
prefs: prefs,
failovers: failovers,
);
electrumXCached = CachedElectrumX.from(
electrumXClient: electrumX,
);
}
//============================================================================
bool _duplicateTxCheck(
List<Map<String, dynamic>> allTransactions, String txid) {
for (int i = 0; i < allTransactions.length; i++) {
if (allTransactions[i]["txid"] == txid) {
return true;
}
}
return false;
}
Future<List<Map<String, dynamic>>> _fetchHistory(
List<String> allAddresses,
) async {
try {
List<Map<String, dynamic>> allTxHashes = [];
final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {};
const batchSizeMax = 100;
int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) {
batches[batchNumber] = {};
}
final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i],
);
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
requestIdToAddressMap[id] = allAddresses[i];
batches[batchNumber]!.addAll({
id: [scriptHash]
});
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
}
}
for (int i = 0; i < batches.length; i++) {
final response = await electrumX.getBatchHistory(args: batches[i]!);
for (final entry in response.entries) {
for (int j = 0; j < entry.value.length; j++) {
entry.value[j]["address"] = requestIdToAddressMap[entry.key];
if (!allTxHashes.contains(entry.value[j])) {
allTxHashes.add(entry.value[j]);
}
}
}
}
return allTxHashes;
} catch (e, s) {
Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Future<({Transaction transaction, Address address})> parseTransaction(
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 electrumXCached.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 PaynymWalletInterface && 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
Future<void> updateNode() async {
final node = await getCurrentNode();
await updateElectrumX(newNode: node);
}
}

View file

@ -2,7 +2,7 @@ import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class PrivateKeyBasedWallet extends Wallet {
abstract class PrivateKeyBasedWallet extends Wallet {
PrivateKeyBasedWallet(super.cryptoCurrency);
Future<String> getPrivateKey() async {
@ -30,4 +30,10 @@ class PrivateKeyBasedWallet extends Wallet {
// TODO: implement prepareSend
throw UnimplementedError();
}
@override
Future<void> recover({required bool isRescan}) {
// TODO: implement recover
throw UnimplementedError();
}
}

View file

@ -0,0 +1,97 @@
import 'dart:convert';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
extension EpiccashWalletInfoExtension on WalletInfo {
ExtraEpiccashWalletInfo? get epicData {
final String? data = otherData[WalletInfoKeys.epiccashData] as String?;
if (data == null) {
return null;
}
try {
return ExtraEpiccashWalletInfo.fromMap(
Map<String, dynamic>.from(
jsonDecode(data) as Map,
),
);
} catch (e, s) {
Logging.instance.log(
"ExtraEpiccashWalletInfo.fromMap failed: $e\n$s",
level: LogLevel.Error,
);
return null;
}
}
}
/// Holds data previously stored in hive
class ExtraEpiccashWalletInfo {
final int receivingIndex;
final int changeIndex;
// TODO [prio=low] strongly type these maps at some point
final Map<dynamic, dynamic> slatesToAddresses;
final Map<dynamic, dynamic> slatesToCommits;
final int lastScannedBlock;
final int restoreHeight;
final int creationHeight;
ExtraEpiccashWalletInfo({
required this.receivingIndex,
required this.changeIndex,
required this.slatesToAddresses,
required this.slatesToCommits,
required this.lastScannedBlock,
required this.restoreHeight,
required this.creationHeight,
});
// Convert the object to JSON
Map<String, dynamic> toMap() {
return {
'receivingIndex': receivingIndex,
'changeIndex': changeIndex,
'slatesToAddresses': slatesToAddresses,
'slatesToCommits': slatesToCommits,
'lastScannedBlock': lastScannedBlock,
'restoreHeight': restoreHeight,
'creationHeight': creationHeight,
};
}
ExtraEpiccashWalletInfo.fromMap(Map<String, dynamic> json)
: receivingIndex = json['receivingIndex'] as int,
changeIndex = json['changeIndex'] as int,
slatesToAddresses = json['slatesToAddresses'] as Map,
slatesToCommits = json['slatesToCommits'] as Map,
lastScannedBlock = json['lastScannedBlock'] as int,
restoreHeight = json['restoreHeight'] as int,
creationHeight = json['creationHeight'] as int;
ExtraEpiccashWalletInfo copyWith({
int? receivingIndex,
int? changeIndex,
Map<dynamic, dynamic>? slatesToAddresses,
Map<dynamic, dynamic>? slatesToCommits,
int? lastScannedBlock,
int? restoreHeight,
int? creationHeight,
}) {
return ExtraEpiccashWalletInfo(
receivingIndex: receivingIndex ?? this.receivingIndex,
changeIndex: changeIndex ?? this.changeIndex,
slatesToAddresses: slatesToAddresses ?? this.slatesToAddresses,
slatesToCommits: slatesToCommits ?? this.slatesToCommits,
lastScannedBlock: lastScannedBlock ?? this.lastScannedBlock,
restoreHeight: restoreHeight ?? this.restoreHeight,
creationHeight: creationHeight ?? this.creationHeight,
);
}
@override
String toString() {
return toMap().toString();
}
}

View file

@ -1,14 +1,15 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/wallets/coin/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/coin/coins/bitcoin.dart';
import 'package:stackwallet/wallets/coin/crypto_currency.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/private_key_based_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
abstract class Wallet<T extends CryptoCurrency> {
Wallet(this.cryptoCurrency);
@ -21,6 +22,7 @@ abstract class Wallet<T extends CryptoCurrency> {
late final MainDB mainDB;
late final SecureStorageInterface secureStorageInterface;
late final WalletInfo walletInfo;
late final Prefs prefs;
//============================================================================
// ========== Wallet Info Convenience Getters ================================
@ -36,19 +38,23 @@ abstract class Wallet<T extends CryptoCurrency> {
required WalletInfo walletInfo,
required MainDB mainDB,
required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
String? mnemonic,
String? mnemonicPassphrase,
String? privateKey,
int? startDate,
}) async {
final Wallet wallet = await _construct(
walletInfo: walletInfo,
mainDB: mainDB,
secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
);
switch (walletInfo.walletType) {
case WalletType.bip39:
case WalletType.bip39HD:
await secureStorageInterface.write(
key: mnemonicKey(walletId: walletInfo.walletId),
value: mnemonic,
@ -77,6 +83,8 @@ abstract class Wallet<T extends CryptoCurrency> {
required String walletId,
required MainDB mainDB,
required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async {
final walletInfo = await mainDB.isar.walletInfo
.where()
@ -93,22 +101,27 @@ abstract class Wallet<T extends CryptoCurrency> {
walletInfo: walletInfo!,
mainDB: mainDB,
secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
);
}
//============================================================================
// ========== Static Util ====================================================
// secure storage key
static String mnemonicKey({
required String walletId,
}) =>
"${walletId}_mnemonic";
// secure storage key
static String mnemonicPassphraseKey({
required String walletId,
}) =>
"${walletId}_mnemonicPassphrase";
// secure storage key
static String privateKeyKey({
required String walletId,
}) =>
@ -122,23 +135,18 @@ abstract class Wallet<T extends CryptoCurrency> {
required WalletInfo walletInfo,
required MainDB mainDB,
required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async {
final Wallet wallet;
final Wallet wallet = _loadWallet(
walletInfo: walletInfo,
nodeService: nodeService,
prefs: prefs,
);
final cryptoCurrency = _loadCurrency(walletInfo: walletInfo);
switch (walletInfo.walletType) {
case WalletType.bip39:
wallet = Bip39HDWallet(cryptoCurrency as Bip39HDCurrency);
break;
case WalletType.cryptonote:
wallet = PrivateKeyBasedWallet(cryptoCurrency);
break;
case WalletType.privateKeyBased:
wallet = PrivateKeyBasedWallet(cryptoCurrency);
break;
if (wallet is ElectrumXMixin) {
// initialize electrumx instance
await wallet.updateNode();
}
return wallet
@ -147,18 +155,28 @@ abstract class Wallet<T extends CryptoCurrency> {
..walletInfo = walletInfo;
}
static CryptoCurrency _loadCurrency({
static Wallet _loadWallet({
required WalletInfo walletInfo,
required NodeService nodeService,
required Prefs prefs,
}) {
switch (walletInfo.coin) {
case Coin.bitcoin:
return Bitcoin(CryptoCurrencyNetwork.main);
return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.main),
nodeService: nodeService,
prefs: prefs,
);
case Coin.bitcoinTestNet:
return Bitcoin(CryptoCurrencyNetwork.test);
return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.test),
nodeService: nodeService,
prefs: prefs,
);
default:
// should never hit in reality
throw Exception("Unknown cryupto currency");
throw Exception("Unknown crypto currency");
}
}
@ -171,4 +189,22 @@ abstract class Wallet<T extends CryptoCurrency> {
/// Broadcast transaction to network. On success update local wallet state to
/// reflect updated balance, transactions, utxos, etc.
Future<TxData> confirmSend({required TxData txData});
/// Recover a wallet by scanning the blockchain. If called on a new wallet a
/// normal recovery should occur. When called on an existing wallet and
/// [isRescan] is false then it should throw. Otherwise this function should
/// delete all locally stored blockchain data and refetch it.
Future<void> recover({required bool isRescan});
Future<void> updateTransactions();
Future<void> updateUTXOs();
Future<void> updateBalance();
// Should probably call the above 3 functions
// Should fire events
Future<void> refresh();
//===========================================
Future<void> updateNode();
}

View file

@ -1,3 +0,0 @@
mixin ElectrumXMixin {
//
}