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") @Deprecated("Left over for migration from old versions of Stack Wallet")
static const String boxNameAddressBook = "addressBook"; static const String boxNameAddressBook = "addressBook";
static const String boxNameTrades = "exchangeTransactionsBox"; static const String boxNameTrades = "exchangeTransactionsBox";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameFavoriteWallets = "favoriteWallets";
// in use // in use
// TODO: migrate // TODO: migrate
static const String boxNameNodeModels = "nodeModels"; static const String boxNameNodeModels = "nodeModels";
static const String boxNamePrimaryNodes = "primaryNodes"; static const String boxNamePrimaryNodes = "primaryNodes";
static const String boxNameAllWalletsData = "wallets";
static const String boxNameNotifications = "notificationModels"; static const String boxNameNotifications = "notificationModels";
static const String boxNameWatchedTransactions = static const String boxNameWatchedTransactions =
"watchedTxNotificationModels"; "watchedTxNotificationModels";
@ -39,7 +40,6 @@ class DB {
static const String boxNameTradesV2 = "exchangeTradesBox"; static const String boxNameTradesV2 = "exchangeTradesBox";
static const String boxNameTradeNotes = "tradeNotesBox"; static const String boxNameTradeNotes = "tradeNotesBox";
static const String boxNameTradeLookup = "tradeToTxidLookUpBox"; static const String boxNameTradeLookup = "tradeToTxidLookUpBox";
static const String boxNameFavoriteWallets = "favoriteWallets";
static const String boxNameWalletsToDeleteOnStart = "walletsToDeleteOnStart"; static const String boxNameWalletsToDeleteOnStart = "walletsToDeleteOnStart";
static const String boxNamePriceCache = "priceAPIPrice24hCache"; 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:coinlib/coinlib.dart' as coinlib;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; 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/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_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/crypto_currency/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/coin/coin_params.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/coin/crypto_currency.dart';
class Bitcoin extends Bip39HDCurrency { class Bitcoin extends Bip39HDCurrency {
Bitcoin(super.network) { 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 @override
coinlib.NetworkParams get networkParams { coinlib.NetworkParams get networkParams {
switch (network) { switch (network) {
case CryptoCurrencyNetwork.main: 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: 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: default:
throw Exception("Unsupported network: $network"); throw Exception("Unsupported network: $network");
} }
@ -39,13 +66,15 @@ class Bitcoin extends Bip39HDCurrency {
}) { }) {
String coinType; String coinType;
if (networkParams.wifPrefix == CoinParams.bitcoin.mainNet.wifPrefix) { switch (networkParams.wifPrefix) {
coinType = "0"; // btc mainnet case 0x80:
} else if (networkParams.wifPrefix == coinType = "0"; // btc mainnet
CoinParams.bitcoin.testNet.wifPrefix) { break;
coinType = "1"; // btc testnet case 0xef:
} else { coinType = "1"; // btc testnet
throw Exception("Invalid Bitcoin network wif used!"); break;
default:
throw Exception("Invalid Bitcoin network wif used!");
} }
int purpose; int purpose;
@ -66,6 +95,7 @@ class Bitcoin extends Bip39HDCurrency {
return "m/$purpose'/$coinType'/$account'/$chain/$index"; return "m/$purpose'/$coinType'/$account'/$chain/$index";
} }
@override
({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({
required coinlib.ECPublicKey publicKey, required coinlib.ECPublicKey publicKey,
required DerivePathType derivePathType, required DerivePathType derivePathType,
@ -112,4 +142,14 @@ class Bitcoin extends Bip39HDCurrency {
throw Exception("DerivePathType $derivePathType not supported"); 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'; part 'wallet_info.g.dart';
@Collection(accessor: "walletInfo") @Collection(accessor: "walletInfo", inheritance: false)
class WalletInfo { class WalletInfo {
Id id = Isar.autoIncrement; Id id = Isar.autoIncrement;
@ -30,10 +30,11 @@ class WalletInfo {
// Only exposed for isar to avoid dealing with storing enums as Coin can change // Only exposed for isar to avoid dealing with storing enums as Coin can change
final String coinName; final String coinName;
final bool isFavourite;
/// User set favourites ordering. No restrictions are placed on uniqueness. /// User set favourites ordering. No restrictions are placed on uniqueness.
/// Reordering logic in the ui code should ensure this is unique. /// 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; final int favouriteOrderIndex;
/// Wallets without this flag set to true should be deleted on next app run /// 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. /// The highest block height the wallet has scanned.
final int cachedChainHeight; final int cachedChainHeight;
/// Wallet creation chain height. Applies to select coin only. // TODO: store these in other data s
final int creationHeight; // Should contain specific things based on certain coins only
/// Wallet restore chain height. Applies to select coin only. // /// Wallet creation chain height. Applies to select coin only.
final int restoreHeight; // 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 @ignore
Coin get coin => Coin.values.byName(coinName); 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({ WalletInfo({
required this.coinName, required this.coinName,
required this.walletId, required this.walletId,
required this.name, required this.name,
required this.walletType, required this.walletType,
required this.mainAddressType, required this.mainAddressType,
this.isFavourite = false,
this.favouriteOrderIndex = 0, this.favouriteOrderIndex = 0,
this.cachedChainHeight = 0, this.cachedChainHeight = 0,
this.creationHeight = 0,
this.restoreHeight = 0,
this.isMnemonicVerified = false, this.isMnemonicVerified = false,
this.cachedBalanceString, this.cachedBalanceString,
this.otherDataJsonString,
}) : assert( }) : assert(
Coin.values.map((e) => e.name).contains(coinName), Coin.values.map((e) => e.name).contains(coinName),
); );
@ -83,13 +113,11 @@ class WalletInfo {
WalletInfo copyWith({ WalletInfo copyWith({
String? coinName, String? coinName,
String? name, String? name,
bool? isFavourite,
int? favouriteOrderIndex, int? favouriteOrderIndex,
int? cachedChainHeight, int? cachedChainHeight,
int? creationHeight,
int? restoreHeight,
bool? isMnemonicVerified, bool? isMnemonicVerified,
String? cachedBalanceString, String? cachedBalanceString,
Map<String, dynamic>? otherData,
}) { }) {
return WalletInfo( return WalletInfo(
coinName: coinName ?? this.coinName, coinName: coinName ?? this.coinName,
@ -97,13 +125,12 @@ class WalletInfo {
name: name ?? this.name, name: name ?? this.name,
walletType: walletType, walletType: walletType,
mainAddressType: mainAddressType, mainAddressType: mainAddressType,
isFavourite: isFavourite ?? this.isFavourite,
favouriteOrderIndex: favouriteOrderIndex ?? this.favouriteOrderIndex, favouriteOrderIndex: favouriteOrderIndex ?? this.favouriteOrderIndex,
cachedChainHeight: cachedChainHeight ?? this.cachedChainHeight, cachedChainHeight: cachedChainHeight ?? this.cachedChainHeight,
creationHeight: creationHeight ?? this.creationHeight,
restoreHeight: restoreHeight ?? this.restoreHeight,
isMnemonicVerified: isMnemonicVerified ?? this.isMnemonicVerified, isMnemonicVerified: isMnemonicVerified ?? this.isMnemonicVerified,
cachedBalanceString: cachedBalanceString ?? this.cachedBalanceString, cachedBalanceString: cachedBalanceString ?? this.cachedBalanceString,
otherDataJsonString:
otherData == null ? otherDataJsonString : jsonEncode(otherData),
)..id = id; )..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 // Used in Isar db and stored there as int indexes so adding/removing values
// in this definition should be done extremely carefully in production // in this definition should be done extremely carefully in production
enum WalletType { enum WalletType {
bip39, bip39,
bip39HD,
cryptonote, cryptonote,
privateKeyBased; privateKeyBased;
} }

View file

@ -32,41 +32,41 @@ const WalletInfoSchema = CollectionSchema(
name: r'coinName', name: r'coinName',
type: IsarType.string, type: IsarType.string,
), ),
r'creationHeight': PropertySchema(
id: 3,
name: r'creationHeight',
type: IsarType.long,
),
r'favouriteOrderIndex': PropertySchema( r'favouriteOrderIndex': PropertySchema(
id: 4, id: 3,
name: r'favouriteOrderIndex', name: r'favouriteOrderIndex',
type: IsarType.long, type: IsarType.long,
), ),
r'isFavourite': PropertySchema( r'isFavourite': PropertySchema(
id: 5, id: 4,
name: r'isFavourite', name: r'isFavourite',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isMnemonicVerified': PropertySchema( r'isMnemonicVerified': PropertySchema(
id: 6, id: 5,
name: r'isMnemonicVerified', name: r'isMnemonicVerified',
type: IsarType.bool, type: IsarType.bool,
), ),
r'mainAddressType': PropertySchema( r'mainAddressType': PropertySchema(
id: 7, id: 6,
name: r'mainAddressType', name: r'mainAddressType',
type: IsarType.byte, type: IsarType.byte,
enumMap: _WalletInfomainAddressTypeEnumValueMap, enumMap: _WalletInfomainAddressTypeEnumValueMap,
), ),
r'name': PropertySchema( r'name': PropertySchema(
id: 8, id: 7,
name: r'name', name: r'name',
type: IsarType.string, type: IsarType.string,
), ),
r'restoreHeight': PropertySchema( r'otherDataJsonString': PropertySchema(
id: 8,
name: r'otherDataJsonString',
type: IsarType.string,
),
r'tokenContractAddresses': PropertySchema(
id: 9, id: 9,
name: r'restoreHeight', name: r'tokenContractAddresses',
type: IsarType.long, type: IsarType.stringList,
), ),
r'walletId': PropertySchema( r'walletId': PropertySchema(
id: 10, id: 10,
@ -122,6 +122,19 @@ int _walletInfoEstimateSize(
} }
bytesCount += 3 + object.coinName.length * 3; bytesCount += 3 + object.coinName.length * 3;
bytesCount += 3 + object.name.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; bytesCount += 3 + object.walletId.length * 3;
return bytesCount; return bytesCount;
} }
@ -135,13 +148,13 @@ void _walletInfoSerialize(
writer.writeString(offsets[0], object.cachedBalanceString); writer.writeString(offsets[0], object.cachedBalanceString);
writer.writeLong(offsets[1], object.cachedChainHeight); writer.writeLong(offsets[1], object.cachedChainHeight);
writer.writeString(offsets[2], object.coinName); writer.writeString(offsets[2], object.coinName);
writer.writeLong(offsets[3], object.creationHeight); writer.writeLong(offsets[3], object.favouriteOrderIndex);
writer.writeLong(offsets[4], object.favouriteOrderIndex); writer.writeBool(offsets[4], object.isFavourite);
writer.writeBool(offsets[5], object.isFavourite); writer.writeBool(offsets[5], object.isMnemonicVerified);
writer.writeBool(offsets[6], object.isMnemonicVerified); writer.writeByte(offsets[6], object.mainAddressType.index);
writer.writeByte(offsets[7], object.mainAddressType.index); writer.writeString(offsets[7], object.name);
writer.writeString(offsets[8], object.name); writer.writeString(offsets[8], object.otherDataJsonString);
writer.writeLong(offsets[9], object.restoreHeight); writer.writeStringList(offsets[9], object.tokenContractAddresses);
writer.writeString(offsets[10], object.walletId); writer.writeString(offsets[10], object.walletId);
writer.writeByte(offsets[11], object.walletType.index); writer.writeByte(offsets[11], object.walletType.index);
} }
@ -156,15 +169,13 @@ WalletInfo _walletInfoDeserialize(
cachedBalanceString: reader.readStringOrNull(offsets[0]), cachedBalanceString: reader.readStringOrNull(offsets[0]),
cachedChainHeight: reader.readLongOrNull(offsets[1]) ?? 0, cachedChainHeight: reader.readLongOrNull(offsets[1]) ?? 0,
coinName: reader.readString(offsets[2]), coinName: reader.readString(offsets[2]),
creationHeight: reader.readLongOrNull(offsets[3]) ?? 0, favouriteOrderIndex: reader.readLongOrNull(offsets[3]) ?? 0,
favouriteOrderIndex: reader.readLongOrNull(offsets[4]) ?? 0, isMnemonicVerified: reader.readBoolOrNull(offsets[5]) ?? false,
isFavourite: reader.readBoolOrNull(offsets[5]) ?? false,
isMnemonicVerified: reader.readBoolOrNull(offsets[6]) ?? false,
mainAddressType: _WalletInfomainAddressTypeValueEnumMap[ mainAddressType: _WalletInfomainAddressTypeValueEnumMap[
reader.readByteOrNull(offsets[7])] ?? reader.readByteOrNull(offsets[6])] ??
AddressType.p2pkh, AddressType.p2pkh,
name: reader.readString(offsets[8]), name: reader.readString(offsets[7]),
restoreHeight: reader.readLongOrNull(offsets[9]) ?? 0, otherDataJsonString: reader.readStringOrNull(offsets[8]),
walletId: reader.readString(offsets[10]), walletId: reader.readString(offsets[10]),
walletType: walletType:
_WalletInfowalletTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? _WalletInfowalletTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ??
@ -190,19 +201,19 @@ P _walletInfoDeserializeProp<P>(
case 3: case 3:
return (reader.readLongOrNull(offset) ?? 0) as P; return (reader.readLongOrNull(offset) ?? 0) as P;
case 4: case 4:
return (reader.readLongOrNull(offset) ?? 0) as P; return (reader.readBool(offset)) as P;
case 5: case 5:
return (reader.readBoolOrNull(offset) ?? false) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 6: case 6:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 7:
return (_WalletInfomainAddressTypeValueEnumMap[ return (_WalletInfomainAddressTypeValueEnumMap[
reader.readByteOrNull(offset)] ?? reader.readByteOrNull(offset)] ??
AddressType.p2pkh) as P; AddressType.p2pkh) as P;
case 8: case 7:
return (reader.readString(offset)) as P; return (reader.readString(offset)) as P;
case 8:
return (reader.readStringOrNull(offset)) as P;
case 9: case 9:
return (reader.readLongOrNull(offset) ?? 0) as P; return (reader.readStringList(offset) ?? []) as P;
case 10: case 10:
return (reader.readString(offset)) as P; return (reader.readString(offset)) as P;
case 11: case 11:
@ -240,13 +251,15 @@ const _WalletInfomainAddressTypeValueEnumMap = {
}; };
const _WalletInfowalletTypeEnumValueMap = { const _WalletInfowalletTypeEnumValueMap = {
'bip39': 0, 'bip39': 0,
'cryptonote': 1, 'bip39HD': 1,
'privateKeyBased': 2, 'cryptonote': 2,
'privateKeyBased': 3,
}; };
const _WalletInfowalletTypeValueEnumMap = { const _WalletInfowalletTypeValueEnumMap = {
0: WalletType.bip39, 0: WalletType.bip39,
1: WalletType.cryptonote, 1: WalletType.bip39HD,
2: WalletType.privateKeyBased, 2: WalletType.cryptonote,
3: WalletType.privateKeyBased,
}; };
Id _walletInfoGetId(WalletInfo object) { 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> QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
favouriteOrderIndexEqualTo(int value) { favouriteOrderIndexEqualTo(int value) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -1156,58 +1113,383 @@ extension WalletInfoQueryFilter
} }
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition> QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightEqualTo(int value) { otherDataJsonStringIsNull() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo( return query.addFilterCondition(const FilterCondition.isNull(
property: r'restoreHeight', property: r'otherDataJsonString',
value: value,
)); ));
}); });
} }
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition> QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightGreaterThan( otherDataJsonStringIsNotNull() {
int value, { 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 include = false,
bool caseSensitive = true,
}) { }) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan( return query.addFilterCondition(FilterCondition.greaterThan(
include: include, include: include,
property: r'restoreHeight', property: r'otherDataJsonString',
value: value, value: value,
caseSensitive: caseSensitive,
)); ));
}); });
} }
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition> QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition>
restoreHeightLessThan( otherDataJsonStringLessThan(
int value, { String? value, {
bool include = false, bool include = false,
bool caseSensitive = true,
}) { }) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan( return query.addFilterCondition(FilterCondition.lessThan(
include: include, include: include,
property: r'restoreHeight', property: r'otherDataJsonString',
value: value, value: value,
caseSensitive: caseSensitive,
)); ));
}); });
} }
QueryBuilder<WalletInfo, WalletInfo, QAfterFilterCondition> 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 lower,
int upper, { int upper, {
bool includeLower = true, bool includeLower = true,
bool includeUpper = true, bool includeUpper = true,
}) { }) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between( return query.listLength(
property: r'restoreHeight', r'tokenContractAddresses',
lower: lower, lower,
includeLower: includeLower, includeLower,
upper: upper, upper,
includeUpper: includeUpper, 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> QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
sortByFavouriteOrderIndex() { sortByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) { 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 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 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> QueryBuilder<WalletInfo, WalletInfo, QAfterSortBy>
thenByFavouriteOrderIndex() { thenByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) { 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 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 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> QueryBuilder<WalletInfo, WalletInfo, QDistinct>
distinctByFavouriteOrderIndex() { distinctByFavouriteOrderIndex() {
return QueryBuilder.apply(this, (query) { 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 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> QueryBuilder<WalletInfo, int, QQueryOperations>
favouriteOrderIndexProperty() { favouriteOrderIndexProperty() {
return QueryBuilder.apply(this, (query) { 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 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:bip39/bip39.dart' as bip39;
import 'package:coinlib/coinlib.dart' as coinlib; import 'package:coinlib/coinlib.dart' as coinlib;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/sw_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_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/crypto_currency/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.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); Bip39HDWallet(super.cryptoCurrency);
/// Generates a receiving address of [walletInfo.mainAddressType]. If none /// Generates a receiving address of [walletInfo.mainAddressType]. If none
@ -50,30 +49,6 @@ class Bip39HDWallet<T extends Bip39HDCurrency> extends Wallet<T> {
return address; 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 ======================================================== // ========== Private ========================================================
Future<Address?> get _currentReceivingAddress async => Future<Address?> get _currentReceivingAddress async =>
@ -149,4 +124,10 @@ class Bip39HDWallet<T extends Bip39HDCurrency> extends Wallet<T> {
// TODO: implement prepareSend // TODO: implement prepareSend
throw UnimplementedError(); 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/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart';
class CryptonoteWallet extends Wallet { abstract class CryptonoteWallet extends Wallet {
CryptonoteWallet(super.cryptoCurrency); CryptonoteWallet(super.cryptoCurrency);
Future<String> getMnemonic() async { Future<String> getMnemonic() async {
@ -30,4 +30,10 @@ class CryptonoteWallet extends Wallet {
// TODO: implement prepareSend // TODO: implement prepareSend
throw UnimplementedError(); 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/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart';
class PrivateKeyBasedWallet extends Wallet { abstract class PrivateKeyBasedWallet extends Wallet {
PrivateKeyBasedWallet(super.cryptoCurrency); PrivateKeyBasedWallet(super.cryptoCurrency);
Future<String> getPrivateKey() async { Future<String> getPrivateKey() async {
@ -30,4 +30,10 @@ class PrivateKeyBasedWallet extends Wallet {
// TODO: implement prepareSend // TODO: implement prepareSend
throw UnimplementedError(); 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:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.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/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/wallets/coin/bip39_hd_currency.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/coin/coins/bitcoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/coin/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/isar_models/wallet_info.dart'; import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/private_key_based_wallet.dart'; import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
abstract class Wallet<T extends CryptoCurrency> { abstract class Wallet<T extends CryptoCurrency> {
Wallet(this.cryptoCurrency); Wallet(this.cryptoCurrency);
@ -21,6 +22,7 @@ abstract class Wallet<T extends CryptoCurrency> {
late final MainDB mainDB; late final MainDB mainDB;
late final SecureStorageInterface secureStorageInterface; late final SecureStorageInterface secureStorageInterface;
late final WalletInfo walletInfo; late final WalletInfo walletInfo;
late final Prefs prefs;
//============================================================================ //============================================================================
// ========== Wallet Info Convenience Getters ================================ // ========== Wallet Info Convenience Getters ================================
@ -36,19 +38,23 @@ abstract class Wallet<T extends CryptoCurrency> {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required MainDB mainDB, required MainDB mainDB,
required SecureStorageInterface secureStorageInterface, required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
String? mnemonic, String? mnemonic,
String? mnemonicPassphrase, String? mnemonicPassphrase,
String? privateKey, String? privateKey,
int? startDate,
}) async { }) async {
final Wallet wallet = await _construct( final Wallet wallet = await _construct(
walletInfo: walletInfo, walletInfo: walletInfo,
mainDB: mainDB, mainDB: mainDB,
secureStorageInterface: secureStorageInterface, secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
); );
switch (walletInfo.walletType) { switch (walletInfo.walletType) {
case WalletType.bip39: case WalletType.bip39:
case WalletType.bip39HD:
await secureStorageInterface.write( await secureStorageInterface.write(
key: mnemonicKey(walletId: walletInfo.walletId), key: mnemonicKey(walletId: walletInfo.walletId),
value: mnemonic, value: mnemonic,
@ -77,6 +83,8 @@ abstract class Wallet<T extends CryptoCurrency> {
required String walletId, required String walletId,
required MainDB mainDB, required MainDB mainDB,
required SecureStorageInterface secureStorageInterface, required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async { }) async {
final walletInfo = await mainDB.isar.walletInfo final walletInfo = await mainDB.isar.walletInfo
.where() .where()
@ -93,22 +101,27 @@ abstract class Wallet<T extends CryptoCurrency> {
walletInfo: walletInfo!, walletInfo: walletInfo!,
mainDB: mainDB, mainDB: mainDB,
secureStorageInterface: secureStorageInterface, secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
); );
} }
//============================================================================ //============================================================================
// ========== Static Util ==================================================== // ========== Static Util ====================================================
// secure storage key
static String mnemonicKey({ static String mnemonicKey({
required String walletId, required String walletId,
}) => }) =>
"${walletId}_mnemonic"; "${walletId}_mnemonic";
// secure storage key
static String mnemonicPassphraseKey({ static String mnemonicPassphraseKey({
required String walletId, required String walletId,
}) => }) =>
"${walletId}_mnemonicPassphrase"; "${walletId}_mnemonicPassphrase";
// secure storage key
static String privateKeyKey({ static String privateKeyKey({
required String walletId, required String walletId,
}) => }) =>
@ -122,23 +135,18 @@ abstract class Wallet<T extends CryptoCurrency> {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required MainDB mainDB, required MainDB mainDB,
required SecureStorageInterface secureStorageInterface, required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async { }) async {
final Wallet wallet; final Wallet wallet = _loadWallet(
walletInfo: walletInfo,
nodeService: nodeService,
prefs: prefs,
);
final cryptoCurrency = _loadCurrency(walletInfo: walletInfo); if (wallet is ElectrumXMixin) {
// initialize electrumx instance
switch (walletInfo.walletType) { await wallet.updateNode();
case WalletType.bip39:
wallet = Bip39HDWallet(cryptoCurrency as Bip39HDCurrency);
break;
case WalletType.cryptonote:
wallet = PrivateKeyBasedWallet(cryptoCurrency);
break;
case WalletType.privateKeyBased:
wallet = PrivateKeyBasedWallet(cryptoCurrency);
break;
} }
return wallet return wallet
@ -147,18 +155,28 @@ abstract class Wallet<T extends CryptoCurrency> {
..walletInfo = walletInfo; ..walletInfo = walletInfo;
} }
static CryptoCurrency _loadCurrency({ static Wallet _loadWallet({
required WalletInfo walletInfo, required WalletInfo walletInfo,
required NodeService nodeService,
required Prefs prefs,
}) { }) {
switch (walletInfo.coin) { switch (walletInfo.coin) {
case Coin.bitcoin: case Coin.bitcoin:
return Bitcoin(CryptoCurrencyNetwork.main); return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.main),
nodeService: nodeService,
prefs: prefs,
);
case Coin.bitcoinTestNet: case Coin.bitcoinTestNet:
return Bitcoin(CryptoCurrencyNetwork.test); return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.test),
nodeService: nodeService,
prefs: prefs,
);
default: default:
// should never hit in reality // 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 /// Broadcast transaction to network. On success update local wallet state to
/// reflect updated balance, transactions, utxos, etc. /// reflect updated balance, transactions, utxos, etc.
Future<TxData> confirmSend({required TxData txData}); 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 {
//
}