Merge branch 'isar-transactions' into isar_migrate

# Conflicts:
#	lib/services/coins/bitcoin/bitcoin_wallet.dart
#	lib/services/coins/bitcoincash/bitcoincash_wallet.dart
#	lib/services/coins/coin_service.dart
#	lib/services/coins/dogecoin/dogecoin_wallet.dart
#	lib/services/coins/firo/firo_wallet.dart
#	lib/services/coins/litecoin/litecoin_wallet.dart
#	lib/services/coins/monero/monero_wallet.dart
#	lib/services/coins/namecoin/namecoin_wallet.dart
#	lib/services/coins/particl/particl_wallet.dart
#	lib/services/coins/wownero/wownero_wallet.dart
#	pubspec.yaml
This commit is contained in:
julian 2023-01-20 15:30:48 -06:00
commit 830fd58754
184 changed files with 37377 additions and 26934 deletions

1
.gitignore vendored
View file

@ -55,3 +55,4 @@ libcw_monero.dll
libcw_wownero.dll
libepic_cash_wallet.dll
libmobileliblelantus.dll
/libisar.so

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10C0 4.47656 4.47656 0 10 0C15.5234 0 20 4.47656 20 10C20 15.5234 15.5234 20 10 20C4.47656 20 0 15.5234 0 10ZM10 14.375C10.5195 14.375 10.9375 13.957 10.9375 13.4375V10.9375H13.4375C13.957 10.9375 14.375 10.5195 14.375 10C14.375 9.48047 13.957 9.0625 13.4375 9.0625H10.9375V6.5625C10.9375 6.04297 10.5195 5.625 10 5.625C9.48047 5.625 9.0625 6.04297 9.0625 6.5625V9.0625H6.5625C6.04297 9.0625 5.625 9.48047 5.625 10C5.625 10.5195 6.04297 10.9375 6.5625 10.9375H9.0625V13.4375C9.0625 13.957 9.48047 14.375 10 14.375Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View file

@ -0,0 +1,3 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.29219 6.25098H2.70781C1.21266 6.25098 0 7.46348 0 8.95879C0 9.25879 0.2425 9.50098 0.541562 9.50098H6.45875C6.75781 9.50098 7 9.25879 7 8.95879C7 7.46348 5.7875 6.25098 4.29219 6.25098ZM3.5 5.50098C4.60469 5.50098 5.5 4.60551 5.5 3.50098C5.5 2.39645 4.60469 1.50098 3.5 1.50098C2.39531 1.50098 1.5 2.39645 1.5 3.50098C1.5 4.60551 2.39531 5.50098 3.5 5.50098ZM9.625 4.62598H7.375C7.16875 4.62598 7 4.79473 7 5.00098C7 5.20723 7.16797 5.37598 7.375 5.37598H9.625C9.83281 5.37598 10 5.20879 10 5.00098C10 4.79316 9.83281 4.62598 9.625 4.62598Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 674 B

3
assets/svg/user-plus.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.4 12.0004C11.0513 12.0004 13.2 9.85127 13.2 7.20039C13.2 4.54952 11.0513 2.40039 8.4 2.40039C5.74875 2.40039 3.6 4.54952 3.6 7.20039C3.6 9.85127 5.74875 12.0004 8.4 12.0004ZM10.3013 13.8004H6.49875C2.91038 13.8004 0 16.7104 0 20.2991C0 21.0191 0.582 21.6004 1.29975 21.6004H15.501C16.2188 21.6004 16.8 21.0191 16.8 20.2991C16.8 16.7104 13.89 13.8004 10.3013 13.8004ZM23.1 9.90039H21.3V8.10039C21.3 7.60539 20.8988 7.20039 20.4 7.20039C19.9013 7.20039 19.5 7.60352 19.5 8.10039V9.90039H17.7C17.205 9.90039 16.8 10.3054 16.8 10.8004C16.8 11.2954 17.2031 11.7004 17.7 11.7004H19.5V13.5004C19.5 13.9991 19.905 14.4004 20.4 14.4004C20.895 14.4004 21.3 13.9973 21.3 13.5004V11.7004H23.1C23.5988 11.7004 24 11.2991 24 10.8004C24 10.3016 23.5988 9.90039 23.1 9.90039Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 893 B

@ -1 +1 @@
Subproject commit 3aff42511ec8107163a3d1e0b42f2c83c21f6896
Subproject commit f1031db5bb67b38d028187f0ead192acb3e9ba55

236
lib/db/main_db.dart Normal file
View file

@ -0,0 +1,236 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
class MainDB {
MainDB._();
static MainDB? _instance;
static MainDB get instance => _instance ??= MainDB._();
Isar? _isar;
Isar get isar => _isar!;
Future<bool> isarInit({Isar? mock}) async {
if (mock != null) {
_isar = mock;
return true;
}
if (_isar != null && isar.isOpen) return false;
_isar = await Isar.open(
[
TransactionSchema,
TransactionNoteSchema,
InputSchema,
OutputSchema,
UTXOSchema,
AddressSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
inspector: true,
name: "wallet_data",
);
return true;
}
// addresses
QueryBuilder<Address, Address, QAfterWhereClause> getAddresses(
String walletId) =>
isar.addresses.where().walletIdEqualTo(walletId);
Future<void> putAddress(Address address) => isar.writeTxn(() async {
await isar.addresses.put(address);
});
Future<void> putAddresses(List<Address> addresses) => isar.writeTxn(() async {
await isar.addresses.putAll(addresses);
});
Future<void> updateAddress(Address oldAddress, Address newAddress) =>
isar.writeTxn(() async {
newAddress.id = oldAddress.id;
await oldAddress.transactions.load();
final txns = oldAddress.transactions.toList();
await isar.addresses.delete(oldAddress.id);
await isar.addresses.put(newAddress);
newAddress.transactions.addAll(txns);
await newAddress.transactions.save();
});
// transactions
QueryBuilder<Transaction, Transaction, QAfterWhereClause> getTransactions(
String walletId) =>
isar.transactions.where().walletIdEqualTo(walletId);
Future<void> putTransaction(Transaction transaction) =>
isar.writeTxn(() async {
await isar.transactions.put(transaction);
});
Future<void> putTransactions(List<Transaction> transactions) =>
isar.writeTxn(() async {
await isar.transactions.putAll(transactions);
});
// utxos
QueryBuilder<UTXO, UTXO, QAfterWhereClause> getUTXOs(String walletId) =>
isar.utxos.where().walletIdEqualTo(walletId);
Future<void> putUTXO(UTXO utxo) => isar.writeTxn(() async {
await isar.utxos.put(utxo);
});
Future<void> putUTXOs(List<UTXO> utxos) => isar.writeTxn(() async {
await isar.utxos.putAll(utxos);
});
// inputs
QueryBuilder<Input, Input, QAfterWhereClause> getInputs(String walletId) =>
isar.inputs.where().walletIdEqualTo(walletId);
Future<void> putInput(Input input) => isar.writeTxn(() async {
await isar.inputs.put(input);
});
Future<void> putInputs(List<Input> inputs) => isar.writeTxn(() async {
await isar.inputs.putAll(inputs);
});
// outputs
QueryBuilder<Output, Output, QAfterWhereClause> getOutputs(String walletId) =>
isar.outputs.where().walletIdEqualTo(walletId);
Future<void> putOutput(Output output) => isar.writeTxn(() async {
await isar.outputs.put(output);
});
Future<void> putOutputs(List<Output> outputs) => isar.writeTxn(() async {
await isar.outputs.putAll(outputs);
});
// transaction notes
QueryBuilder<TransactionNote, TransactionNote, QAfterWhereClause>
getTransactionNotes(String walletId) =>
isar.transactionNotes.where().walletIdEqualTo(walletId);
Future<void> putTransactionNote(TransactionNote transactionNote) =>
isar.writeTxn(() async {
await isar.transactionNotes.put(transactionNote);
});
Future<void> putTransactionNotes(List<TransactionNote> transactionNotes) =>
isar.writeTxn(() async {
await isar.transactionNotes.putAll(transactionNotes);
});
//
Future<void> deleteWalletBlockchainData(String walletId) async {
final transactionCount = await getTransactions(walletId).count();
final addressCount = await getAddresses(walletId).count();
final utxoCount = await getUTXOs(walletId).count();
final inputCount = await getInputs(walletId).count();
final outputCount = await getOutputs(walletId).count();
await isar.writeTxn(() async {
const paginateLimit = 50;
// transactions
for (int i = 0; i < transactionCount; i += paginateLimit) {
final txns = await getTransactions(walletId)
.offset(i)
.limit(paginateLimit)
.findAll();
await isar.transactions
.deleteAll(txns.map((e) => e.id).toList(growable: false));
}
// addresses
for (int i = 0; i < addressCount; i += paginateLimit) {
final addresses = await getAddresses(walletId)
.offset(i)
.limit(paginateLimit)
.findAll();
await isar.addresses
.deleteAll(addresses.map((e) => e.id).toList(growable: false));
}
// utxos
for (int i = 0; i < utxoCount; i += paginateLimit) {
final utxos =
await getUTXOs(walletId).offset(i).limit(paginateLimit).findAll();
await isar.utxos
.deleteAll(utxos.map((e) => e.id).toList(growable: false));
}
// inputs
for (int i = 0; i < inputCount; i += paginateLimit) {
final inputs =
await getInputs(walletId).offset(i).limit(paginateLimit).findAll();
await isar.inputs
.deleteAll(inputs.map((e) => e.id).toList(growable: false));
}
// outputs
for (int i = 0; i < outputCount; i += paginateLimit) {
final outputs =
await getOutputs(walletId).offset(i).limit(paginateLimit).findAll();
await isar.outputs
.deleteAll(outputs.map((e) => e.id).toList(growable: false));
}
});
}
Future<void> addNewTransactionData(
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId) async {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
final potentiallyUnconfirmedTx = await getTransactions(walletId)
.filter()
.txidEqualTo(tx.txid)
.findFirst();
if (potentiallyUnconfirmedTx != null) {
// update use id to replace tx
tx.id = potentiallyUnconfirmedTx.id;
await isar.transactions.delete(potentiallyUnconfirmedTx.id);
}
// save transaction
await isar.transactions.put(tx);
// link and save outputs
if (data.item2.isNotEmpty) {
await isar.outputs.putAll(data.item2);
tx.outputs.addAll(data.item2);
await tx.outputs.save();
}
// link and save inputs
if (data.item3.isNotEmpty) {
await isar.inputs.putAll(data.item3);
tx.inputs.addAll(data.item3);
await tx.inputs.save();
}
if (data.item4 != null) {
final address = await getAddresses(walletId)
.filter()
.valueEqualTo(data.item4!.value)
.findFirst();
// check if address exists in db and add if it does not
if (address == null) {
await isar.addresses.put(data.item4!);
}
// link and save address
tx.address.value = address ?? data.item4!;
await tx.address.save();
}
}
});
}
}

View file

@ -245,3 +245,11 @@ class DB {
Future<void> deleteBoxFromDisk({required String boxName}) async =>
await mutex.protect(() async => await Hive.deleteBoxFromDisk(boxName));
}
abstract class DBKeys {
static const String cachedBalance = "cachedBalance";
static const String cachedBalanceSecondary = "cachedBalanceSecondary";
static const String isFavorite = "isFavorite";
static const String id = "id";
static const String storedChainHeight = "storedChainHeight";
}

View file

@ -64,6 +64,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:window_size/window_size.dart';
import 'db/main_db.dart';
final openedFromSWBFileStringStateProvider =
StateProvider<String?>((ref) => null);
@ -155,7 +157,7 @@ void main() async {
await Hive.openBox<dynamic>(DB.boxNameDBInfo);
// todo: db migrate stuff for desktop needs to be handled eventually
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
if (!Util.isDesktop) {
int dbVersion = DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ??
@ -170,7 +172,7 @@ void main() async {
),
);
} catch (e, s) {
Logging.instance.log("Cannot migrate database\n$e $s",
Logging.instance.log("Cannot migrate mobile database\n$e $s",
level: LogLevel.Error, printFullLength: true);
}
}
@ -263,6 +265,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
await loadShared();
}
await MainDB.instance.isarInit();
_notificationsService = ref.read(notificationsProvider);
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
_tradesService = ref.read(tradesServiceProvider);

59
lib/models/balance.dart Normal file
View file

@ -0,0 +1,59 @@
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
class Balance {
final Coin coin;
final int total;
final int spendable;
final int blockedTotal;
final int pendingSpendable;
Balance({
required this.coin,
required this.total,
required this.spendable,
required this.blockedTotal,
required this.pendingSpendable,
});
Decimal getTotal({bool includeBlocked = false}) => Format.satoshisToAmount(
includeBlocked ? total : total - blockedTotal,
coin: coin,
);
Decimal getSpendable() => Format.satoshisToAmount(
spendable,
coin: coin,
);
Decimal getPending() => Format.satoshisToAmount(
pendingSpendable,
coin: coin,
);
Decimal getBlocked() => Format.satoshisToAmount(
blockedTotal,
coin: coin,
);
String toJsonIgnoreCoin() => jsonEncode({
"total": total,
"spendable": spendable,
"blockedTotal": blockedTotal,
"pendingSpendable": pendingSpendable,
});
factory Balance.fromJson(String json, Coin coin) {
final decoded = jsonDecode(json);
return Balance(
coin: coin,
total: decoded["total"] as int,
spendable: decoded["spendable"] as int,
blockedTotal: decoded["blockedTotal"] as int,
pendingSpendable: decoded["pendingSpendable"] as int,
);
}
}

View file

@ -0,0 +1,94 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/address/crypto_currency_address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
part 'address.g.dart';
class AddressException extends SWException {
AddressException(super.message);
}
@Collection(accessor: "addresses")
class Address extends CryptoCurrencyAddress {
Address({
required this.walletId,
required this.value,
required this.publicKey,
required this.derivationIndex,
required this.type,
required this.subType,
this.otherData,
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
late final String value;
late final List<byte> publicKey;
@Index()
late final int derivationIndex; // -1 generally means unknown
@enumerated
late final AddressType type;
@enumerated
late final AddressSubType subType;
late final String? otherData;
final transactions = IsarLinks<Transaction>();
int derivationChain() {
if (subType == AddressSubType.receiving) {
return 0; // 0 for receiving (external)
} else if (subType == AddressSubType.change) {
return 1; // 1 for change (internal)
} else {
throw AddressException("Could not imply derivation chain value");
}
}
bool isPaynymAddress() =>
subType == AddressSubType.paynymNotification ||
subType == AddressSubType.paynymSend ||
subType == AddressSubType.paynymReceive;
@override
String toString() => "{ "
"id: $id, "
"walletId: $walletId, "
"value: $value, "
"publicKey: $publicKey, "
"derivationIndex: $derivationIndex, "
"type: ${type.name}, "
"subType: ${subType.name}, "
"transactionsLength: ${transactions.length} "
"otherData: $otherData, "
"}";
}
enum AddressType {
p2pkh,
p2sh,
p2wpkh,
cryptonote,
mimbleWimble,
unknown,
nonWallet,
}
enum AddressSubType {
receiving,
change,
paynymNotification,
paynymSend,
paynymReceive,
unknown,
nonWallet,
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
abstract class CryptoCurrencyAddress {
// future use?
}

View file

@ -0,0 +1,46 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
part 'input.g.dart';
@Collection()
class Input {
Input({
required this.walletId,
required this.txid,
required this.vout,
required this.scriptSig,
required this.scriptSigAsm,
required this.isCoinbase,
required this.sequence,
required this.innerRedeemScriptAsm,
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
late final String txid;
late final int vout;
late final String? scriptSig;
late final String? scriptSigAsm;
// TODO: find witness type // is it even used?
// late List<dynamic>? witness;
late final bool? isCoinbase;
late final int? sequence;
late final String? innerRedeemScriptAsm;
final prevOut = IsarLink<Output>();
@Backlink(to: 'inputs')
final transaction = IsarLink<Transaction>();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
part 'output.g.dart';
@Collection()
class Output {
Output({
required this.walletId,
required this.scriptPubKey,
required this.scriptPubKeyAsm,
required this.scriptPubKeyType,
required this.scriptPubKeyAddress,
required this.value,
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
late final String? scriptPubKey;
late final String? scriptPubKeyAsm;
late final String? scriptPubKeyType;
late final String scriptPubKeyAddress;
late final int value;
@Backlink(to: 'outputs')
final transaction = IsarLink<Transaction>();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,114 @@
import 'dart:math';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/address/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/input.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
part 'transaction.g.dart';
@Collection()
class Transaction {
Transaction({
required this.walletId,
required this.txid,
required this.timestamp,
required this.type,
required this.subType,
required this.amount,
required this.fee,
required this.height,
required this.isCancelled,
required this.isLelantus,
required this.slateId,
required this.otherData,
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
late final String txid;
@Index()
late final int timestamp;
@enumerated
late final TransactionType type;
@enumerated
late final TransactionSubType subType;
late final int amount;
late final int fee;
late final int? height;
late final bool isCancelled;
late bool? isLelantus;
late final String? slateId;
late final String? otherData;
@Backlink(to: "transactions")
final address = IsarLink<Address>();
final inputs = IsarLinks<Input>();
final outputs = IsarLinks<Output>();
int getConfirmations(int currentChainHeight) {
if (height == null || height! <= 0) return 0;
return max(0, currentChainHeight - (height! - 1));
}
bool isConfirmed(int currentChainHeight, int minimumConfirms) {
final confirmations = getConfirmations(currentChainHeight);
return confirmations >= minimumConfirms;
}
@override
toString() => "{ "
"id: $id, "
"walletId: $walletId, "
"txid: $txid, "
"timestamp: $timestamp, "
"type: ${type.name}, "
"subType: ${subType.name}, "
"amount: $amount, "
"fee: $fee, "
"height: $height, "
"isCancelled: $isCancelled, "
"isLelantus: $isLelantus, "
"slateId: $slateId, "
"otherData: $otherData, "
"address: ${address.value}, "
"inputsLength: ${inputs.length}, "
"outputsLength: ${outputs.length}, "
"}";
}
// 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 TransactionType {
// TODO: add more types before prod release?
outgoing,
incoming,
sentToSelf, // should we keep this?
unknown;
}
// 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 TransactionSubType {
// TODO: add more types before prod release?
none,
bip47Notification, // bip47 payment code notification transaction flag
mint, // firo specific
join; // firo specific
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,79 @@
import 'dart:math';
import 'package:isar/isar.dart';
part 'utxo.g.dart';
@Collection(accessor: "utxos")
class UTXO {
UTXO({
required this.walletId,
required this.txid,
required this.vout,
required this.value,
required this.name,
required this.isBlocked,
required this.blockedReason,
required this.isCoinbase,
required this.blockHash,
required this.blockHeight,
required this.blockTime,
this.otherData,
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
@Index(unique: true, replace: true, composite: [CompositeIndex("walletId")])
late final String txid;
late final int vout;
late final int value;
late final String name;
@Index()
late final bool isBlocked;
late final String? blockedReason;
late final bool isCoinbase;
late final String? blockHash;
late final int? blockHeight;
late final int? blockTime;
late final String? otherData;
int getConfirmations(int currentChainHeight) {
if (blockTime == null || blockHash == null) return 0;
if (blockHeight == null || blockHeight! <= 0) return 0;
return max(0, currentChainHeight - (blockHeight! - 1));
}
bool isConfirmed(int currentChainHeight, int minimumConfirms) {
final confirmations = getConfirmations(currentChainHeight);
return confirmations >= minimumConfirms;
}
@override
String toString() => "{ "
"id: $id, "
"walletId: $walletId, "
"txid: $txid, "
"vout: $vout, "
"value: $value, "
"name: $name, "
"isBlocked: $isBlocked, "
"blockedReason: $blockedReason, "
"isCoinbase: $isCoinbase, "
"blockHash: $blockHash, "
"blockHeight: $blockHeight, "
"blockTime: $blockTime, "
"}";
}

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ part of 'encrypted_string_value.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, join_return_with_assignment, avoid_js_rounded_ints, prefer_final_locals
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
extension GetEncryptedStringValueCollection on Isar {
IsarCollection<EncryptedStringValue> get encryptedStringValues =>
@ -30,12 +30,9 @@ const EncryptedStringValueSchema = CollectionSchema(
)
},
estimateSize: _encryptedStringValueEstimateSize,
serializeNative: _encryptedStringValueSerializeNative,
deserializeNative: _encryptedStringValueDeserializeNative,
deserializePropNative: _encryptedStringValueDeserializePropNative,
serializeWeb: _encryptedStringValueSerializeWeb,
deserializeWeb: _encryptedStringValueDeserializeWeb,
deserializePropWeb: _encryptedStringValueDeserializePropWeb,
serialize: _encryptedStringValueSerialize,
deserialize: _encryptedStringValueDeserialize,
deserializeProp: _encryptedStringValueDeserializeProp,
idName: r'id',
indexes: {
r'key': IndexSchema(
@ -57,7 +54,7 @@ const EncryptedStringValueSchema = CollectionSchema(
getId: _encryptedStringValueGetId,
getLinks: _encryptedStringValueGetLinks,
attach: _encryptedStringValueAttach,
version: 5,
version: '3.0.5',
);
int _encryptedStringValueEstimateSize(
@ -71,20 +68,19 @@ int _encryptedStringValueEstimateSize(
return bytesCount;
}
int _encryptedStringValueSerializeNative(
void _encryptedStringValueSerialize(
EncryptedStringValue object,
IsarBinaryWriter writer,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.key);
writer.writeString(offsets[1], object.value);
return writer.usedBytes;
}
EncryptedStringValue _encryptedStringValueDeserializeNative(
int id,
IsarBinaryReader reader,
EncryptedStringValue _encryptedStringValueDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
@ -95,9 +91,8 @@ EncryptedStringValue _encryptedStringValueDeserializeNative(
return object;
}
P _encryptedStringValueDeserializePropNative<P>(
Id id,
IsarBinaryReader reader,
P _encryptedStringValueDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
@ -112,34 +107,9 @@ P _encryptedStringValueDeserializePropNative<P>(
}
}
Object _encryptedStringValueSerializeWeb(
IsarCollection<EncryptedStringValue> collection,
EncryptedStringValue object) {
/*final jsObj = IsarNative.newJsObject();*/ throw UnimplementedError();
}
EncryptedStringValue _encryptedStringValueDeserializeWeb(
IsarCollection<EncryptedStringValue> collection, Object jsObj) {
/*final object = EncryptedStringValue();object.id = IsarNative.jsObjectGet(jsObj, r'id') ?? (double.negativeInfinity as int);object.key = IsarNative.jsObjectGet(jsObj, r'key') ?? '';object.value = IsarNative.jsObjectGet(jsObj, r'value') ?? '';*/
//return object;
throw UnimplementedError();
}
P _encryptedStringValueDeserializePropWeb<P>(
Object jsObj, String propertyName) {
switch (propertyName) {
default:
throw IsarError('Illegal propertyName');
}
}
int? _encryptedStringValueGetId(EncryptedStringValue object) {
if (object.id == Isar.autoIncrement) {
return null;
} else {
Id _encryptedStringValueGetId(EncryptedStringValue object) {
return object.id;
}
}
List<IsarLinkBase<dynamic>> _encryptedStringValueGetLinks(
EncryptedStringValue object) {
@ -188,19 +158,19 @@ extension EncryptedStringValueByIndex on IsarCollection<EncryptedStringValue> {
return deleteAllByIndexSync(r'key', values);
}
Future<int> putByKey(EncryptedStringValue object) {
Future<Id> putByKey(EncryptedStringValue object) {
return putByIndex(r'key', object);
}
int putByKeySync(EncryptedStringValue object, {bool saveLinks = true}) {
Id putByKeySync(EncryptedStringValue object, {bool saveLinks = true}) {
return putByIndexSync(r'key', object, saveLinks: saveLinks);
}
Future<List<int>> putAllByKey(List<EncryptedStringValue> objects) {
Future<List<Id>> putAllByKey(List<EncryptedStringValue> objects) {
return putAllByIndex(r'key', objects);
}
List<int> putAllByKeySync(List<EncryptedStringValue> objects,
List<Id> putAllByKeySync(List<EncryptedStringValue> objects,
{bool saveLinks = true}) {
return putAllByIndexSync(r'key', objects, saveLinks: saveLinks);
}
@ -219,7 +189,7 @@ extension EncryptedStringValueQueryWhereSort
extension EncryptedStringValueQueryWhere
on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QWhereClause> {
QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause>
idEqualTo(int id) {
idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
@ -229,7 +199,7 @@ extension EncryptedStringValueQueryWhere
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause>
idNotEqualTo(int id) {
idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
@ -252,7 +222,7 @@ extension EncryptedStringValueQueryWhere
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause>
idGreaterThan(int id, {bool include = false}) {
idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
@ -261,7 +231,7 @@ extension EncryptedStringValueQueryWhere
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause>
idLessThan(int id, {bool include = false}) {
idLessThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
@ -271,8 +241,8 @@ extension EncryptedStringValueQueryWhere
QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause>
idBetween(
int lowerId,
int upperId, {
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
@ -335,7 +305,7 @@ extension EncryptedStringValueQueryWhere
extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
EncryptedStringValue, QFilterCondition> {
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> idEqualTo(int value) {
QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
@ -346,7 +316,7 @@ extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> idGreaterThan(
int value, {
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@ -360,7 +330,7 @@ extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> idLessThan(
int value, {
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@ -374,8 +344,8 @@ extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> idBetween(
int lower,
int upper, {
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
@ -508,6 +478,26 @@ extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
});
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> keyIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'key',
value: '',
));
});
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> keyIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'key',
value: '',
));
});
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> valueEqualTo(
String value, {
@ -625,6 +615,26 @@ extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue,
));
});
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> valueIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'value',
value: '',
));
});
}
QueryBuilder<EncryptedStringValue, EncryptedStringValue,
QAfterFilterCondition> valueIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'value',
value: '',
));
});
}
}
extension EncryptedStringValueQueryObject on QueryBuilder<EncryptedStringValue,

View file

@ -0,0 +1,7 @@
export 'address/address.dart';
export 'blockchain_data/input.dart';
export 'blockchain_data/output.dart';
export 'blockchain_data/transaction.dart';
export 'blockchain_data/utxo.dart';
export 'log.dart';
export 'transaction_note.dart';

View file

@ -13,6 +13,7 @@ class Log {
@Index()
late int timestampInMillisUTC;
@Enumerated(EnumType.name)
late LogLevel logLevel;
@override

View file

@ -7,7 +7,7 @@ part of 'log.dart';
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, join_return_with_assignment, avoid_js_rounded_ints, prefer_final_locals
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
extension GetLogCollection on Isar {
IsarCollection<Log> get logs => this.collection();
@ -21,6 +21,7 @@ const LogSchema = CollectionSchema(
id: 0,
name: r'logLevel',
type: IsarType.string,
enumMap: _LoglogLevelEnumValueMap,
),
r'message': PropertySchema(
id: 1,
@ -34,12 +35,9 @@ const LogSchema = CollectionSchema(
)
},
estimateSize: _logEstimateSize,
serializeNative: _logSerializeNative,
deserializeNative: _logDeserializeNative,
deserializePropNative: _logDeserializePropNative,
serializeWeb: _logSerializeWeb,
deserializeWeb: _logDeserializeWeb,
deserializePropWeb: _logDeserializePropWeb,
serialize: _logSerialize,
deserialize: _logDeserialize,
deserializeProp: _logDeserializeProp,
idName: r'id',
indexes: {
r'timestampInMillisUTC': IndexSchema(
@ -61,7 +59,7 @@ const LogSchema = CollectionSchema(
getId: _logGetId,
getLinks: _logGetLinks,
attach: _logAttach,
version: 5,
version: '3.0.5',
);
int _logEstimateSize(
@ -70,49 +68,48 @@ int _logEstimateSize(
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.logLevel.value.length * 3;
bytesCount += 3 + object.logLevel.name.length * 3;
bytesCount += 3 + object.message.length * 3;
return bytesCount;
}
int _logSerializeNative(
void _logSerialize(
Log object,
IsarBinaryWriter writer,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.logLevel.value);
writer.writeString(offsets[0], object.logLevel.name);
writer.writeString(offsets[1], object.message);
writer.writeLong(offsets[2], object.timestampInMillisUTC);
return writer.usedBytes;
}
Log _logDeserializeNative(
int id,
IsarBinaryReader reader,
Log _logDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Log();
object.id = id;
object.logLevel =
_LogLogLevelMap[reader.readStringOrNull(offsets[0])] ?? LogLevel.Info;
_LoglogLevelValueEnumMap[reader.readStringOrNull(offsets[0])] ??
LogLevel.Info;
object.message = reader.readString(offsets[1]);
object.timestampInMillisUTC = reader.readLong(offsets[2]);
return object;
}
P _logDeserializePropNative<P>(
Id id,
IsarBinaryReader reader,
P _logDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (_LogLogLevelMap[reader.readStringOrNull(offset)] ?? LogLevel.Info)
as P;
return (_LoglogLevelValueEnumMap[reader.readStringOrNull(offset)] ??
LogLevel.Info) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
@ -122,37 +119,22 @@ P _logDeserializePropNative<P>(
}
}
Object _logSerializeWeb(IsarCollection<Log> collection, Log object) {
/*final jsObj = IsarNative.newJsObject();*/ throw UnimplementedError();
}
Log _logDeserializeWeb(IsarCollection<Log> collection, Object jsObj) {
/*final object = Log();object.id = IsarNative.jsObjectGet(jsObj, r'id') ?? (double.negativeInfinity as int);object.logLevel = IsarNative.jsObjectGet(jsObj, r'logLevel') ?? LogLevel.Info;object.message = IsarNative.jsObjectGet(jsObj, r'message') ?? '';object.timestampInMillisUTC = IsarNative.jsObjectGet(jsObj, r'timestampInMillisUTC') ?? (double.negativeInfinity as int);*/
//return object;
throw UnimplementedError();
}
P _logDeserializePropWeb<P>(Object jsObj, String propertyName) {
switch (propertyName) {
default:
throw IsarError('Illegal propertyName');
}
}
final _LogLogLevelMap = {
LogLevel.Info.value: LogLevel.Info,
LogLevel.Warning.value: LogLevel.Warning,
LogLevel.Error.value: LogLevel.Error,
LogLevel.Fatal.value: LogLevel.Fatal,
const _LoglogLevelEnumValueMap = {
r'Info': r'Info',
r'Warning': r'Warning',
r'Error': r'Error',
r'Fatal': r'Fatal',
};
const _LoglogLevelValueEnumMap = {
r'Info': LogLevel.Info,
r'Warning': LogLevel.Warning,
r'Error': LogLevel.Error,
r'Fatal': LogLevel.Fatal,
};
int? _logGetId(Log object) {
if (object.id == Isar.autoIncrement) {
return null;
} else {
Id _logGetId(Log object) {
return object.id;
}
}
List<IsarLinkBase<dynamic>> _logGetLinks(Log object) {
return [];
@ -179,7 +161,7 @@ extension LogQueryWhereSort on QueryBuilder<Log, Log, QWhere> {
}
extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
QueryBuilder<Log, Log, QAfterWhereClause> idEqualTo(int id) {
QueryBuilder<Log, Log, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
@ -188,7 +170,7 @@ extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
});
}
QueryBuilder<Log, Log, QAfterWhereClause> idNotEqualTo(int id) {
QueryBuilder<Log, Log, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
@ -210,7 +192,7 @@ extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
});
}
QueryBuilder<Log, Log, QAfterWhereClause> idGreaterThan(int id,
QueryBuilder<Log, Log, QAfterWhereClause> idGreaterThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
@ -219,7 +201,7 @@ extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
});
}
QueryBuilder<Log, Log, QAfterWhereClause> idLessThan(int id,
QueryBuilder<Log, Log, QAfterWhereClause> idLessThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
@ -229,8 +211,8 @@ extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
}
QueryBuilder<Log, Log, QAfterWhereClause> idBetween(
int lowerId,
int upperId, {
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
@ -336,7 +318,7 @@ extension LogQueryWhere on QueryBuilder<Log, Log, QWhereClause> {
}
extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
QueryBuilder<Log, Log, QAfterFilterCondition> idEqualTo(int value) {
QueryBuilder<Log, Log, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
@ -346,7 +328,7 @@ extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
}
QueryBuilder<Log, Log, QAfterFilterCondition> idGreaterThan(
int value, {
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@ -359,7 +341,7 @@ extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
}
QueryBuilder<Log, Log, QAfterFilterCondition> idLessThan(
int value, {
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@ -372,8 +354,8 @@ extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
}
QueryBuilder<Log, Log, QAfterFilterCondition> idBetween(
int lower,
int upper, {
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
@ -498,6 +480,24 @@ extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> logLevelIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'logLevel',
value: '',
));
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> logLevelIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'logLevel',
value: '',
));
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> messageEqualTo(
String value, {
bool caseSensitive = true,
@ -608,6 +608,24 @@ extension LogQueryFilter on QueryBuilder<Log, Log, QFilterCondition> {
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> messageIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'message',
value: '',
));
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> messageIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'message',
value: '',
));
});
}
QueryBuilder<Log, Log, QAfterFilterCondition> timestampInMillisUTCEqualTo(
int value) {
return QueryBuilder.apply(this, (query) {

View file

@ -0,0 +1,22 @@
import 'package:isar/isar.dart';
part 'transaction_note.g.dart';
@Collection()
class TransactionNote {
TransactionNote({
required this.walletId,
required this.txid,
required this.value,
});
Id id = Isar.autoIncrement;
@Index()
late String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
late String txid;
late String value;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
class CreatedPaynym {
final bool claimed;
final String? nymAvatar;
final String? nymId;
final String? nymName;
final String? token;
CreatedPaynym(
this.claimed,
this.nymAvatar,
this.nymId,
this.nymName,
this.token,
);
CreatedPaynym.fromMap(Map<String, dynamic> map)
: claimed = map["claimed"] as bool,
nymAvatar = map["nymAvatar"] as String?,
nymId = map["nymID"] as String?,
nymName = map["nymName"] as String?,
token = map["token"] as String?;
Map<String, dynamic> toMap() => {
"claimed": claimed,
"nymAvatar": nymAvatar,
"nymId": nymId,
"nymName": nymName,
"token": token,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,67 @@
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/paynym/paynym_code.dart';
class PaynymAccount {
final String nymID;
final String nymName;
final List<PaynymCode> codes;
/// list of nymId
final List<PaynymAccountLite> followers;
/// list of nymId
final List<PaynymAccountLite> following;
PaynymAccount(
this.nymID,
this.nymName,
this.codes,
this.followers,
this.following,
);
PaynymAccount.fromMap(Map<String, dynamic> map)
: nymID = map["nymID"] as String,
nymName = map["nymName"] as String,
codes = (map["codes"] as List<dynamic>)
.map((e) => PaynymCode.fromMap(Map<String, dynamic>.from(e as Map)))
.toList(),
followers = (map["followers"] as List<dynamic>)
.map((e) =>
PaynymAccountLite.fromMap(Map<String, dynamic>.from(e as Map)))
.toList(),
following = (map["following"] as List<dynamic>)
.map((e) =>
PaynymAccountLite.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
PaynymAccount copyWith({
String? nymID,
String? nymName,
List<PaynymCode>? codes,
List<PaynymAccountLite>? followers,
List<PaynymAccountLite>? following,
}) {
return PaynymAccount(
nymID ?? this.nymID,
nymName ?? this.nymName,
codes ?? this.codes,
followers ?? this.followers,
following ?? this.following,
);
}
Map<String, dynamic> toMap() => {
"nymID": nymID,
"nymName": nymName,
"codes": codes.map((e) => e.toMap()),
"followers": followers.map((e) => e.toMap()),
"following": followers.map((e) => e.toMap()),
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,31 @@
class PaynymAccountLite {
final String nymId;
final String nymName;
final String code;
final bool segwit;
PaynymAccountLite(
this.nymId,
this.nymName,
this.code,
this.segwit,
);
PaynymAccountLite.fromMap(Map<String, dynamic> map)
: nymId = map["nymId"] as String,
nymName = map["nymName"] as String,
code = map["code"] as String,
segwit = map["segwit"] as bool;
Map<String, dynamic> toMap() => {
"nymId": nymId,
"nymName": nymName,
"code": code,
"segwit": segwit,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,20 @@
class PaynymClaim {
final String claimed;
final String token;
PaynymClaim(this.claimed, this.token);
PaynymClaim.fromMap(Map<String, dynamic> map)
: claimed = map["claimed"] as String,
token = map["token"] as String;
Map<String, dynamic> toMap() => {
"claimed": claimed,
"token": token,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,27 @@
class PaynymCode {
final bool claimed;
final bool segwit;
final String code;
PaynymCode(
this.claimed,
this.segwit,
this.code,
);
PaynymCode.fromMap(Map<String, dynamic> map)
: claimed = map["claimed"] as bool,
segwit = map["segwit"] as bool,
code = map["code"] as String;
Map<String, dynamic> toMap() => {
"claimed": claimed,
"segwit": segwit,
"code": code,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,23 @@
class PaynymFollow {
final String follower;
final String following;
final String token;
PaynymFollow(this.follower, this.following, this.token);
PaynymFollow.fromMap(Map<String, dynamic> map)
: follower = map["follower"] as String,
following = map["following"] as String,
token = map["token"] as String;
Map<String, dynamic> toMap() => {
"follower": follower,
"following": following,
"token": token,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -0,0 +1,12 @@
class PaynymResponse<T> {
final T? value;
final int statusCode;
final String message;
PaynymResponse(this.value, this.statusCode, this.message);
@override
String toString() {
return "PaynymResponse: value=$value, statusCode=$statusCode, message=$message";
}
}

View file

@ -0,0 +1,23 @@
class PaynymUnfollow {
final String follower;
final String unfollowing;
final String token;
PaynymUnfollow(this.follower, this.unfollowing, this.token);
PaynymUnfollow.fromMap(Map<String, dynamic> map)
: follower = map["follower"] as String,
unfollowing = map["unfollowing"] as String,
token = map["token"] as String;
Map<String, dynamic> toMap() => {
"follower": follower,
"unfollowing": unfollowing,
"token": token,
};
@override
String toString() {
return toMap().toString();
}
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
@ -160,8 +161,9 @@ class _VerifyRecoveryPhraseViewState
result.insert(random.nextInt(wordsToShow), chosenWord);
//todo: this prints sensitive info
debugPrint("Mnemonic game correct word: $chosenWord");
if (kDebugMode) {
print("Mnemonic game correct word: $chosenWord");
}
return Tuple2(result, chosenWord);
}

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart';
import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart';
@ -15,7 +15,6 @@ import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
@ -28,6 +27,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
import 'package:tuple/tuple.dart';
import '../../../db/main_db.dart';
class ContactDetailsView extends ConsumerStatefulWidget {
const ContactDetailsView({
Key? key,
@ -50,15 +51,6 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
List<Tuple2<String, Transaction>> _cachedTransactions = [];
bool _contactHasAddress(String address, Contact contact) {
for (final entry in contact.addresses) {
if (entry.address == address) {
return true;
}
}
return false;
}
Future<List<Tuple2<String, Transaction>>> _filteredTransactionsByContact(
List<Manager> managers,
) async {
@ -69,18 +61,18 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
List<Tuple2<String, Transaction>> result = [];
for (final manager in managers) {
final transactions = (await manager.transactionData)
.getAllTransactions()
.values
.toList()
.where((e) => _contactHasAddress(e.address, contact));
final transactions = await MainDB.instance
.getTransactions(manager.walletId)
.filter()
.anyOf(contact.addresses.map((e) => e.address),
(q, String e) => q.address((q) => q.valueEqualTo(e)))
.sortByTimestampDesc()
.findAll();
for (final tx in transactions) {
result.add(Tuple2(manager.walletId, tx));
}
}
// sort by date
result.sort((a, b) => b.item2.timestamp - a.item2.timestamp);
return result;
}

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/pages/exchange_view/exchange_form.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
@ -12,6 +14,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/trade_card.dart';
import 'package:tuple/tuple.dart';
import '../../db/main_db.dart';
class ExchangeView extends ConsumerStatefulWidget {
const ExchangeView({Key? key}) : super(key: key);
@ -129,10 +133,11 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> {
//todo: check if print needed
// debugPrint("name: ${manager.walletName}");
// TODO store tx data completely locally in isar so we don't lock up ui here when querying txData
final txData = await manager.transactionData;
final tx = txData.getAllTransactions()[txid];
final tx = await MainDB.instance
.getTransactions(walletIds.first)
.filter()
.txidEqualTo(txid)
.findFirst();
if (mounted) {
unawaited(Navigator.of(context).pushNamed(

View file

@ -438,8 +438,10 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
style: STextStyles.itemSubtitle(context),
),
FutureBuilder(
future: (manager.wallet as FiroWallet)
.availablePrivateBalance(),
// TODO redo this widget now that its not actually a future
future: Future(() =>
(manager.wallet as FiroWallet)
.availablePrivateBalance()),
builder: (builderContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
@ -524,8 +526,10 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
style: STextStyles.itemSubtitle(context),
),
FutureBuilder(
future: (manager.wallet as FiroWallet)
.availablePublicBalance(),
// TODO redo this widget now that its not actually a future
future: Future(() =>
(manager.wallet as FiroWallet)
.availablePublicBalance()),
builder: (builderContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
@ -634,7 +638,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
),
if (!isFiro)
FutureBuilder(
future: manager.totalBalance,
// TODO redo this widget now that its not actually a future
future: Future(() => manager.balance.getTotal()),
builder:
(builderContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==

View file

@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart';
import 'package:stackwallet/pages/exchange_view/send_from_view.dart';
@ -23,7 +23,6 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';

View file

@ -0,0 +1,458 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paynym/paynym_account.dart';
import 'package:stackwallet/pages/paynym/subwidgets/featured_paynyms_widget.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/paynym_search_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class AddNewPaynymFollowView extends ConsumerStatefulWidget {
const AddNewPaynymFollowView({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
static const String routeName = "/addNewPaynymFollow";
@override
ConsumerState<AddNewPaynymFollowView> createState() =>
_AddNewPaynymFollowViewState();
}
class _AddNewPaynymFollowViewState
extends ConsumerState<AddNewPaynymFollowView> {
late final TextEditingController _searchController;
late final FocusNode searchFieldFocusNode;
String _searchString = "";
bool _didSearch = false;
PaynymAccount? _searchResult;
final isDesktop = Util.isDesktop;
Future<void> _search() async {
_didSearch = true;
bool didPopLoading = false;
unawaited(
showDialog<void>(
barrierDismissible: false,
context: context,
builder: (context) => const LoadingIndicator(
width: 200,
),
).then((_) => didPopLoading = true),
);
final paynymAccount = await ref.read(paynymAPIProvider).nym(_searchString);
if (mounted) {
if (!didPopLoading) {
Navigator.of(context).pop();
}
setState(() {
_searchResult = paynymAccount.value;
});
}
}
Future<void> _clear() async {
_searchString = "";
setState(() {
_searchController.text = "";
});
}
Future<void> _paste() async {
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null && data!.text!.isNotEmpty) {
String content = data.text!.trim();
if (content.contains("\n")) {
content = content.substring(
0,
content.indexOf(
"\n",
),
);
}
_searchString = content;
setState(() {
_searchController.text = content;
_searchController.selection = TextSelection.collapsed(
offset: content.length,
);
});
}
}
Future<void> _scanQr() async {
try {
if (!isDesktop && FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
final qrResult = await const BarcodeScannerWrapper().scan();
final pCodeString = qrResult.rawContent;
_searchString = pCodeString;
setState(() {
_searchController.text = pCodeString;
_searchController.selection = TextSelection.collapsed(
offset: pCodeString.length,
);
});
} catch (_) {
// scan failed
}
}
@override
void initState() {
_searchController = TextEditingController();
searchFieldFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_searchController.dispose();
searchFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return ConditionalParent(
condition: !isDesktop,
builder: (child) => MasterScaffold(
isDesktop: isDesktop,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
titleSpacing: 0,
title: Text(
"New follow",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
),
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"New follow",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: child,
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
Text(
"Featured PayNyms",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.sectionLabelMedium12(context),
),
const SizedBox(
height: 12,
),
FeaturedPaynymsWidget(
walletId: widget.walletId,
),
const SizedBox(
height: 24,
),
Text(
"Add new",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.sectionLabelMedium12(context),
),
const SizedBox(
height: 12,
),
if (isDesktop)
Row(
children: [
Expanded(
child: Stack(
children: [
RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
height: 56,
child: Center(
child: TextField(
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchString = value;
});
},
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
// height: 1.8,
),
decoration: InputDecoration(
hintText: "Paste payment code",
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.all(16),
hintStyle:
STextStyles.desktopTextFieldLabel(context)
.copyWith(
fontSize: 14,
),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8),
child: UnconstrainedBox(
child: Row(
children: [
_searchController.text.isNotEmpty
? TextFieldIconButton(
onTap: _clear,
child: RoundedContainer(
padding:
const EdgeInsets.all(8),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: const XIcon(),
),
)
: TextFieldIconButton(
key: const Key(
"paynymPasteAddressFieldButtonKey"),
onTap: _paste,
child: RoundedContainer(
padding:
const EdgeInsets.all(8),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: const ClipboardIcon(),
),
),
TextFieldIconButton(
key: const Key(
"paynymScanQrButtonKey"),
onTap: _scanQr,
child: RoundedContainer(
padding: const EdgeInsets.all(8),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: const QrCodeIcon(),
),
)
],
),
),
),
),
),
),
),
],
),
),
const SizedBox(
width: 10,
),
PaynymSearchButton(onPressed: _search),
],
),
if (!isDesktop)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchString = value;
});
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Paste payment code",
searchFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8),
child: UnconstrainedBox(
child: Row(
children: [
_searchController.text.isNotEmpty
? TextFieldIconButton(
onTap: _clear,
child: const XIcon(),
)
: TextFieldIconButton(
key: const Key(
"paynymPasteAddressFieldButtonKey"),
onTap: _paste,
child: const ClipboardIcon(),
),
TextFieldIconButton(
key: const Key("paynymScanQrButtonKey"),
onTap: _scanQr,
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
if (!isDesktop)
const SizedBox(
height: 12,
),
if (!isDesktop)
SecondaryButton(
label: "Search",
onPressed: _search,
),
if (_didSearch)
const SizedBox(
height: 20,
),
if (_didSearch && _searchResult == null)
RoundedWhiteContainer(
borderColor: isDesktop
? Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Nothing found. Please check the payment code.",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.label(context),
),
],
),
),
if (_didSearch && _searchResult != null)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor: isDesktop
? Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar
: null,
child: PaynymCard(
label: _searchResult!.nymName,
paymentCodeString: _searchResult!.codes.first.code,
walletId: widget.walletId,
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class ClaimingPaynymDialog extends StatefulWidget {
const ClaimingPaynymDialog({
Key? key,
}) : super(key: key);
@override
State<ClaimingPaynymDialog> createState() => _RestoringDialogState();
}
class _RestoringDialogState extends State<ClaimingPaynymDialog>
with TickerProviderStateMixin {
late AnimationController? _spinController;
late Animation<double> _spinAnimation;
@override
void initState() {
_spinController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
_spinAnimation = CurvedAnimation(
parent: _spinController!,
curve: Curves.linear,
);
super.initState();
}
@override
void dispose() {
_spinController?.dispose();
_spinController = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return DesktopDialog(
maxWidth: 580,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DesktopDialogCloseButton(
onPressedOverride: () => Navigator.of(context).pop(true),
),
],
),
RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
width: 40,
height: 40,
),
),
Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Claiming PayNym",
style: STextStyles.desktopH2(context),
),
const SizedBox(
height: 20,
),
Text(
"We are generating your PayNym",
style: STextStyles.desktopTextMedium(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
const SizedBox(
height: 40,
),
SecondaryButton(
label: "Cancel",
width: 272,
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
),
),
],
),
);
} else {
return WillPopScope(
onWillPop: () async {
return false;
},
child: StackDialog(
title: "Claiming PayNym",
message: "We are generating your PayNym",
icon: RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
width: 24,
height: 24,
),
),
rightButton: SecondaryButton(
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(true);
},
),
),
);
}
}
}

View file

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class ConfirmPaynymConnectDialog extends StatelessWidget {
const ConfirmPaynymConnectDialog({
Key? key,
required this.nymName,
required this.onConfirmPressed,
required this.amount,
required this.coin,
}) : super(key: key);
final String nymName;
final VoidCallback onConfirmPressed;
final int amount;
final Coin coin;
String get title => "Connect to $nymName";
String get message => "A one-time connection fee of "
"${Format.satoshisToAmount(amount, coin: coin)} ${coin.ticker} "
"will be charged to connect to this PayNym.\n\nThis fee "
"covers the cost of creating a one-time transaction to create a "
"record on the blockchain. This keeps PayNyms decentralized.";
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return DesktopDialog(
maxHeight: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 40),
child: SvgPicture.asset(
Assets.svg.userPlus,
color: Theme.of(context).extension<StackColors>()!.textDark,
width: 32,
height: 32,
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
left: 40,
bottom: 32,
right: 40,
),
child: Text(
title,
style: STextStyles.desktopH3(context),
),
),
Padding(
padding: const EdgeInsets.only(
left: 40,
right: 40,
),
child: Text(
message,
style: STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 40,
bottom: 40,
right: 40,
top: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Cancel",
onPressed: Navigator.of(context).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Connect",
onPressed: onConfirmPressed,
),
),
],
),
)
],
),
);
} else {
return StackDialog(
title: title,
icon: SvgPicture.asset(
Assets.svg.userPlus,
color: Theme.of(context).extension<StackColors>()!.textDark,
width: 24,
height: 24,
),
message: message,
leftButton: SecondaryButton(
buttonHeight: ButtonHeight.xl,
label: "Cancel",
onPressed: Navigator.of(context).pop,
),
rightButton: PrimaryButton(
buttonHeight: ButtonHeight.xl,
label: "Connect",
onPressed: onConfirmPressed,
),
);
}
}
}

View file

@ -0,0 +1,332 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/paynym_follow_toggle_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class PaynymDetailsPopup extends ConsumerStatefulWidget {
const PaynymDetailsPopup({
Key? key,
required this.walletId,
required this.accountLite,
}) : super(key: key);
final String walletId;
final PaynymAccountLite accountLite;
@override
ConsumerState<PaynymDetailsPopup> createState() => _PaynymDetailsPopupState();
}
class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
bool _showInsufficientFundsInfo = false;
Future<void> _onConnectPressed() async {
bool canPop = false;
unawaited(
showDialog<void>(
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => canPop,
child: const LoadingIndicator(
width: 200,
),
),
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
// sanity check to prevent second notifcation tx
if (wallet.hasConnectedConfirmed(widget.accountLite.code)) {
canPop = true;
Navigator.of(context).pop();
// TODO show info popup
return;
} else if (wallet.hasConnected(widget.accountLite.code)) {
canPop = true;
Navigator.of(context).pop();
// TODO show info popup
return;
}
final rates = await wallet.fees;
Map<String, dynamic> preparedTx;
try {
preparedTx = await wallet.buildNotificationTx(
selectedTxFeeRate: rates.medium,
targetPaymentCodeString: widget.accountLite.code,
);
} on InsufficientBalanceException catch (_) {
if (mounted) {
canPop = true;
Navigator.of(context).pop();
}
setState(() {
_showInsufficientFundsInfo = true;
});
return;
}
if (mounted) {
// We have enough balance and prepared tx should be good to go.
canPop = true;
// close loading
Navigator.of(context).pop();
// Close details
Navigator.of(context).pop();
// show info pop up
await showDialog<void>(
context: context,
builder: (context) => ConfirmPaynymConnectDialog(
nymName: widget.accountLite.nymName,
onConfirmPressed: () {
//
print("CONFIRM NOTIF TX: $preparedTx");
Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => ConfirmTransactionView(
walletId: wallet.walletId,
routeOnSuccessName: PaynymHomeView.routeName,
isPaynymNotificationTransaction: true,
transactionInfo: {
"hex": preparedTx["hex"],
"address": preparedTx["recipientPaynym"],
"recipientAmt": preparedTx["amount"],
"fee": preparedTx["fee"],
"vSize": preparedTx["vSize"],
"note": "PayNym connect"
},
),
),
);
},
amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int),
coin: wallet.coin,
),
);
}
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: MediaQuery.of(context).size.width - 32,
maxHeight: double.infinity,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
top: 24,
right: 24,
bottom: 16,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.w600_12(context),
),
],
),
PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.l,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
onPressed: _onConnectPressed,
),
],
),
if (_showInsufficientFundsInfo)
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 24,
),
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.warningBackground,
child: Text(
"Adding a PayNym to your contacts requires a one-time "
"transaction fee for creating the record on the "
"blockchain. Please deposit more "
"${ref.read(walletsChangeNotifierProvider).getManager(widget.walletId).wallet.coin.ticker} "
"into your wallet and try again.",
style: STextStyles.infoSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
),
),
),
],
),
],
),
),
Container(
color: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
height: 1,
),
Padding(
padding: const EdgeInsets.only(
left: 24,
top: 16,
right: 24,
bottom: 16,
),
child: Row(
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 86),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"PayNym address",
style: STextStyles.infoSmall(context),
),
const SizedBox(
height: 6,
),
Text(
widget.accountLite.code,
style: STextStyles.infoSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
const SizedBox(
height: 6,
),
],
),
),
),
const SizedBox(
width: 20,
),
QrImage(
padding: const EdgeInsets.all(0),
size: 86,
data: widget.accountLite.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 24,
right: 24,
bottom: 24,
),
child: Row(
children: [
Expanded(
child: PaynymFollowToggleButton(
walletId: widget.walletId,
paymentCodeStringToFollow: widget.accountLite.code,
style: PaynymFollowToggleButtonStyle.detailsPopup,
),
),
const SizedBox(
width: 12,
),
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
icon: SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: () async {
await Clipboard.setData(
ClipboardData(
text: widget.accountLite.code,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,161 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/models/paynym/paynym_account.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
class PaynymQrPopup extends StatelessWidget {
const PaynymQrPopup({
Key? key,
required this.paynymAccount,
}) : super(key: key);
final PaynymAccount paynymAccount;
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return DesktopDialog(
maxWidth: isDesktop ? 580 : MediaQuery.of(context).size.width - 32,
maxHeight: double.infinity,
child: Column(
children: [
if (isDesktop)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Address details",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: EdgeInsets.only(
left: isDesktop ? 32 : 24,
top: isDesktop ? 16 : 24,
right: 24,
bottom: 16,
),
child: Row(
children: [
PayNymBot(
paymentCodeString: paynymAccount.codes.first.code,
size: isDesktop ? 56 : 32,
),
const SizedBox(
width: 12,
),
Text(
paynymAccount.nymName,
style: isDesktop
? STextStyles.w500_24(context)
: STextStyles.w600_12(context),
),
],
),
),
if (!isDesktop)
Container(
color:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
height: 1,
),
Padding(
padding: const EdgeInsets.only(
left: 24,
top: 16,
right: 24,
bottom: 24,
),
child: Row(
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 107),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isDesktop ? "PayNym address" : "Your PayNym address",
style: isDesktop
? STextStyles.desktopTextSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.infoSmall(context),
),
const SizedBox(
height: 6,
),
Text(
paynymAccount.codes.first.code,
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.infoSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
const SizedBox(
height: 6,
),
BlueTextButton(
text: "Copy",
textSize: isDesktop ? 18 : 10,
onTap: () async {
await Clipboard.setData(
ClipboardData(
text: paynymAccount.codes.first.code,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
],
),
),
),
const SizedBox(
width: 20,
),
QrImage(
padding: const EdgeInsets.all(0),
size: 107,
data: paynymAccount.codes.first.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
),
],
),
)
],
),
);
}
}

View file

@ -0,0 +1,258 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/paynym/dialogs/claiming_paynym_dialog.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
class PaynymClaimView extends ConsumerStatefulWidget {
const PaynymClaimView({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
static const String routeName = "/claimPaynym";
@override
ConsumerState<PaynymClaimView> createState() => _PaynymClaimViewState();
}
class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final isDesktop = Util.isDesktop;
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
? DesktopAppBar(
isCompactHeight: true,
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
right: 20,
),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
),
SvgPicture.asset(
Assets.svg.user,
width: 42,
height: 42,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
const SizedBox(
width: 10,
),
Text(
"PayNym",
style: STextStyles.desktopH3(context),
)
],
),
)
: AppBar(
leading: const AppBarBackButton(),
titleSpacing: 0,
title: Text(
"PayNym",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
body: ConditionalParent(
condition: !isDesktop,
builder: (child) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(
width: 328,
child: child,
),
child: Column(
children: [
const Spacer(
flex: 1,
),
Image(
image: AssetImage(
Assets.png.stack,
),
width: MediaQuery.of(context).size.width / 2,
),
const SizedBox(
height: 20,
),
Text(
"You do not have a PayNym yet.\nClaim yours now!",
style: isDesktop
? STextStyles.desktopSubtitleH2(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.baseXS(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
textAlign: TextAlign.center,
),
if (isDesktop)
const SizedBox(
height: 30,
),
if (!isDesktop)
const Spacer(
flex: 2,
),
PrimaryButton(
label: "Claim",
onPressed: () async {
bool shouldCancel = false;
unawaited(
showDialog<bool?>(
context: context,
barrierDismissible: false,
builder: (context) => const ClaimingPaynymDialog(),
).then((value) => shouldCancel = value == true),
);
// get wallet to access paynym calls
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
if (shouldCancel) return;
// get payment code
final pCode = await wallet.getPaymentCode();
if (shouldCancel) return;
// attempt to create new entry in paynym.is db
final created = await ref
.read(paynymAPIProvider)
.create(pCode.toString());
debugPrint("created:$created");
if (shouldCancel) return;
if (created.value!.claimed) {
// payment code already claimed
debugPrint("pcode already claimed!!");
if (mounted) {
if (isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pop();
} else {
Navigator.of(context).popUntil(
ModalRoute.withName(
WalletView.routeName,
),
);
}
}
return;
}
if (shouldCancel) return;
final token =
await ref.read(paynymAPIProvider).token(pCode.toString());
if (shouldCancel) return;
// sign token with notification private key
final signature =
await wallet.signStringWithNotificationKey(token.value!);
if (shouldCancel) return;
// claim paynym account
final claim = await ref
.read(paynymAPIProvider)
.claim(token.value!, signature);
if (shouldCancel) return;
if (claim.value?.claimed == pCode.toString()) {
final account =
await ref.read(paynymAPIProvider).nym(pCode.toString());
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
if (mounted) {
if (isDesktop) {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pop();
} else {
Navigator.of(context).popUntil(
ModalRoute.withName(
WalletView.routeName,
),
);
}
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
}
} else if (mounted && !shouldCancel) {
Navigator.of(context, rootNavigator: isDesktop).pop();
}
},
),
if (isDesktop)
const Spacer(
flex: 2,
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,636 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/dialogs/paynym_qr_popup.dart';
import 'package:stackwallet/pages/paynym/subwidgets/desktop_paynym_details.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_followers_list.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_following_list.dart';
import 'package:stackwallet/providers/ui/selected_paynym_details_item_Provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/copy_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/share_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/toggle.dart';
class PaynymHomeView extends ConsumerStatefulWidget {
const PaynymHomeView({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
static const String routeName = "/paynymHome";
@override
ConsumerState<PaynymHomeView> createState() => _PaynymHomeViewState();
}
class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
bool showFollowers = false;
int secretCount = 0;
Timer? timer;
bool _followButtonHoverState = false;
@override
void dispose() {
timer?.cancel();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final isDesktop = Util.isDesktop;
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
? DesktopAppBar(
isCompactHeight: true,
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 24,
right: 20,
),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
),
SvgPicture.asset(
Assets.svg.user,
width: 32,
height: 32,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
const SizedBox(
width: 10,
),
Text(
"PayNym",
style: STextStyles.desktopH3(context),
)
],
),
trailing: Padding(
padding: const EdgeInsets.only(right: 12),
child: SizedBox(
height: 56,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() {
_followButtonHoverState = true;
}),
onExit: (_) => setState(() {
_followButtonHoverState = false;
}),
child: GestureDetector(
onTap: () {
showDialog<void>(
context: context,
builder: (context) => AddNewPaynymFollowView(
walletId: widget.walletId,
),
);
},
child: RoundedContainer(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
color: _followButtonHoverState
? Theme.of(context)
.extension<StackColors>()!
.highlight
: Colors.transparent,
radiusMultiplier: 100,
child: Row(
children: [
SvgPicture.asset(
Assets.svg.plus,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
const SizedBox(
width: 8,
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Follow",
style:
STextStyles.desktopButtonSecondaryEnabled(
context)
.copyWith(
fontSize: 16,
),
),
const SizedBox(
height: 2,
),
],
),
],
),
),
),
),
),
),
)
: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
titleSpacing: 0,
title: Text(
"PayNym",
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
actions: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
Navigator.of(context).pushNamed(
AddNewPaynymFollowView.routeName,
arguments: widget.walletId,
);
},
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
icon: SvgPicture.asset(
Assets.svg.circleQuestion,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
// todo info ?
},
),
),
),
const SizedBox(
width: 4,
),
],
),
body: ConditionalParent(
condition: !isDesktop,
builder: (child) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
child: Column(
crossAxisAlignment:
isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
if (!isDesktop)
Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
secretCount++;
if (secretCount > 5) {
debugPrint(
"My Account: ${ref.read(myPaynymAccountStateProvider.state).state}");
debugPrint(
"My Account: ${ref.read(myPaynymAccountStateProvider.state).state!.following}");
secretCount = 0;
}
timer ??= Timer(
const Duration(milliseconds: 1500),
() {
secretCount = 0;
timer = null;
},
);
},
child: PayNymBot(
paymentCodeString: ref
.watch(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
),
),
const SizedBox(
height: 10,
),
Text(
ref
.watch(myPaynymAccountStateProvider.state)
.state!
.nymName,
style: STextStyles.desktopMenuItemSelected(context),
),
const SizedBox(
height: 4,
),
Text(
Format.shorten(
ref
.watch(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
12,
5),
style: STextStyles.label(context),
),
const SizedBox(
height: 11,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
icon: CopyIcon(
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () async {
await Clipboard.setData(
ClipboardData(
text: ref
.read(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
),
const SizedBox(
width: 13,
),
Expanded(
child: SecondaryButton(
label: "Share",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
icon: ShareIcon(
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
// copy to clipboard
},
),
),
const SizedBox(
width: 13,
),
Expanded(
child: SecondaryButton(
label: "Address",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
icon: QrCodeIcon(
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
showDialog<void>(
context: context,
builder: (context) => PaynymQrPopup(
paynymAccount: ref
.read(myPaynymAccountStateProvider.state)
.state!,
),
);
},
),
),
],
),
],
),
if (isDesktop)
Padding(
padding: const EdgeInsets.all(24),
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const SizedBox(
width: 4,
),
GestureDetector(
onTap: () {
secretCount++;
if (secretCount > 5) {
debugPrint(
"My Account: ${ref.read(myPaynymAccountStateProvider.state).state}");
debugPrint(
"My Account: ${ref.read(myPaynymAccountStateProvider.state).state!.following}");
secretCount = 0;
}
timer ??= Timer(
const Duration(milliseconds: 1500),
() {
secretCount = 0;
timer = null;
},
);
},
child: PayNymBot(
paymentCodeString: ref
.watch(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
),
),
const SizedBox(
width: 16,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ref
.watch(myPaynymAccountStateProvider.state)
.state!
.nymName,
style: STextStyles.desktopH3(context),
),
const SizedBox(
height: 4,
),
Text(
Format.shorten(
ref
.watch(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
12,
5),
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
],
),
const Spacer(),
SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
width: 160,
icon: CopyIcon(
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () async {
await Clipboard.setData(
ClipboardData(
text: ref
.read(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
const SizedBox(
width: 16,
),
SecondaryButton(
label: "Address",
width: 160,
buttonHeight: ButtonHeight.l,
icon: QrCodeIcon(
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
showDialog<void>(
context: context,
builder: (context) => PaynymQrPopup(
paynymAccount: ref
.read(myPaynymAccountStateProvider.state)
.state!,
),
);
},
),
],
),
),
),
if (!isDesktop)
const SizedBox(
height: 24,
),
ConditionalParent(
condition: isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.only(left: 24),
child: child,
),
child: SizedBox(
height: isDesktop ? 56 : 40,
width: isDesktop ? 490 : null,
child: Toggle(
onColor: Theme.of(context).extension<StackColors>()!.popupBG,
onText:
"Following (${ref.watch(myPaynymAccountStateProvider.state).state?.following.length ?? 0})",
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
offText:
"Followers (${ref.watch(myPaynymAccountStateProvider.state).state?.followers.length ?? 0})",
isOn: showFollowers,
onValueChanged: (value) {
setState(() {
showFollowers = value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
),
SizedBox(
height: isDesktop ? 20 : 16,
),
Expanded(
child: ConditionalParent(
condition: isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.only(left: 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 490,
child: child,
),
const SizedBox(
width: 24,
),
if (ref
.watch(selectedPaynymDetailsItemProvider.state)
.state !=
null)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 600,
),
child: DesktopPaynymDetails(
walletId: widget.walletId,
accountLite: ref
.watch(selectedPaynymDetailsItemProvider
.state)
.state!,
),
),
],
),
),
if (ref
.watch(selectedPaynymDetailsItemProvider.state)
.state !=
null)
const SizedBox(
width: 24,
),
],
),
),
child: ConditionalParent(
condition: !isDesktop,
builder: (child) => Container(
child: child,
),
child: !showFollowers
? PaynymFollowingList(
walletId: widget.walletId,
)
: PaynymFollowersList(
walletId: widget.walletId,
),
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,344 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_buttons/paynym_follow_toggle_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class DesktopPaynymDetails extends ConsumerStatefulWidget {
const DesktopPaynymDetails({
Key? key,
required this.walletId,
required this.accountLite,
}) : super(key: key);
final String walletId;
final PaynymAccountLite accountLite;
@override
ConsumerState<DesktopPaynymDetails> createState() =>
_PaynymDetailsPopupState();
}
class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
bool _showInsufficientFundsInfo = false;
Future<void> _onConnectPressed() async {
bool canPop = false;
unawaited(
showDialog<void>(
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => canPop,
child: const LoadingIndicator(
width: 200,
),
),
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
// sanity check to prevent second notification tx
if (wallet.hasConnectedConfirmed(widget.accountLite.code)) {
canPop = true;
Navigator.of(context, rootNavigator: true).pop();
// TODO show info popup
return;
} else if (wallet.hasConnected(widget.accountLite.code)) {
canPop = true;
Navigator.of(context, rootNavigator: true).pop();
// TODO show info popup
return;
}
final rates = await wallet.fees;
Map<String, dynamic> preparedTx;
try {
preparedTx = await wallet.buildNotificationTx(
selectedTxFeeRate: rates.medium,
targetPaymentCodeString: widget.accountLite.code,
);
} on InsufficientBalanceException catch (e) {
if (mounted) {
canPop = true;
Navigator.of(context, rootNavigator: true).pop();
}
setState(() {
_showInsufficientFundsInfo = true;
});
return;
}
if (mounted) {
// We have enough balance and prepared tx should be good to go.
canPop = true;
// close loading
Navigator.of(context, rootNavigator: true).pop();
// show info pop up
await showDialog<void>(
context: context,
builder: (context) => ConfirmPaynymConnectDialog(
nymName: widget.accountLite.nymName,
onConfirmPressed: () {
//
print("CONFIRM NOTIF TX: $preparedTx");
Navigator.of(context, rootNavigator: true).pop();
unawaited(
showDialog(
context: context,
builder: (context) => DesktopDialog(
maxHeight: double.infinity,
maxWidth: 580,
child: ConfirmTransactionView(
walletId: wallet.walletId,
isPaynymNotificationTransaction: true,
transactionInfo: {
"hex": preparedTx["hex"],
"address": preparedTx["recipientPaynym"],
"recipientAmt": preparedTx["amount"],
"fee": preparedTx["fee"],
"vSize": preparedTx["vSize"],
"note": "PayNym connect"
},
onSuccessInsteadOfRouteOnSuccess: () {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context, rootNavigator: true).pop();
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message:
"Connection initiated to ${widget.accountLite.nymName}",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
),
),
);
},
amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int),
coin: wallet.coin,
),
);
}
}
Future<void> _onSend() async {
print("sned");
}
@override
Widget build(BuildContext context) {
final wallet = ref
.watch(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.desktopTextSmall(context),
),
],
),
const SizedBox(
height: 20,
),
Row(
children: [
if (!wallet.hasConnected(widget.accountLite.code))
Expanded(
child: PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onConnectPressed,
),
),
if (wallet.hasConnected(widget.accountLite.code))
Expanded(
child: PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onSend,
),
),
const SizedBox(
width: 20,
),
Expanded(
child: PaynymFollowToggleButton(
walletId: widget.walletId,
paymentCodeStringToFollow: widget.accountLite.code,
style: PaynymFollowToggleButtonStyle.detailsDesktop,
),
),
],
),
if (_showInsufficientFundsInfo)
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 24,
),
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.warningBackground,
child: Text(
"Adding a PayNym to your contacts requires a one-time "
"transaction fee for creating the record on the "
"blockchain. Please deposit more "
"${ref.read(walletsChangeNotifierProvider).getManager(widget.walletId).wallet.coin.ticker} "
"into your wallet and try again.",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
),
),
),
],
),
],
),
),
Container(
color: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
height: 1,
),
Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"PayNym address",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 100),
child: Text(
widget.accountLite.code,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
),
const SizedBox(
width: 20,
),
QrImage(
padding: const EdgeInsets.all(0),
size: 100,
data: widget.accountLite.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
),
],
),
const SizedBox(
height: 8,
),
BlueTextButton(
text: "Copy",
onTap: () async {
await Clipboard.setData(
ClipboardData(
text: widget.accountLite.code,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
},
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card.dart';
import 'package:stackwallet/utilities/featured_paynyms.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FeaturedPaynymsWidget extends StatelessWidget {
const FeaturedPaynymsWidget({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
Widget build(BuildContext context) {
final entries = FeaturedPaynyms.featured.entries.toList(growable: false);
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) => RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: child,
),
child: Column(
children: [
for (int i = 0; i < entries.length; i++)
Column(
children: [
if (i > 0)
isDesktop
? const SizedBox(
height: 10,
)
: Container(
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
height: 1,
),
ConditionalParent(
condition: isDesktop,
builder: (child) => RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
child: child,
),
child: PaynymCard(
walletId: walletId,
label: entries[i].key,
paymentCodeString: entries[i].value,
),
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
class PayNymBot extends StatelessWidget {
const PayNymBot({
Key? key,
required this.paymentCodeString,
this.size = 60.0,
}) : super(key: key);
final String paymentCodeString;
final double size;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(size / 2),
child: SizedBox(
width: size,
height: size,
child: Image.network(
"https://paynym.is/$paymentCodeString/avatar",
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null
? child
: const Center(
child: LoadingIndicator(),
),
),
),
);
}
}

View file

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/paynym_follow_toggle_button.dart';
class PaynymCard extends StatefulWidget {
const PaynymCard({
Key? key,
required this.walletId,
required this.label,
required this.paymentCodeString,
}) : super(key: key);
final String walletId;
final String label;
final String paymentCodeString;
@override
State<PaynymCard> createState() => _PaynymCardState();
}
class _PaynymCardState extends State<PaynymCard> {
final isDesktop = Util.isDesktop;
@override
Widget build(BuildContext context) {
return Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
vertical: 16,
horizontal: 20,
)
: const EdgeInsets.all(12),
child: Row(
children: [
PayNymBot(
size: 32,
paymentCodeString: widget.paymentCodeString,
),
const SizedBox(
width: 12,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.label,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
),
const SizedBox(
height: 2,
),
Text(
Format.shorten(widget.paymentCodeString, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
PaynymFollowToggleButton(
walletId: widget.walletId,
paymentCodeStringToFollow: widget.paymentCodeString,
),
],
),
);
}
}

View file

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/pages/paynym/dialogs/paynym_details_popup.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/providers/ui/selected_paynym_details_item_Provider.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class PaynymCardButton extends ConsumerStatefulWidget {
const PaynymCardButton({
Key? key,
required this.walletId,
required this.accountLite,
}) : super(key: key);
final String walletId;
final PaynymAccountLite accountLite;
@override
ConsumerState<PaynymCardButton> createState() => _PaynymCardButtonState();
}
class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
final isDesktop = Util.isDesktop;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4),
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: isDesktop &&
ref
.watch(selectedPaynymDetailsItemProvider.state)
.state
?.nymId ==
widget.accountLite.nymId
? Theme.of(context)
.extension<StackColors>()!
.accentColorDark
.withOpacity(0.08)
: Colors.transparent,
child: RawMaterialButton(
padding: const EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
if (isDesktop) {
ref.read(selectedPaynymDetailsItemProvider.state).state =
widget.accountLite;
} else {
showDialog<void>(
context: context,
builder: (context) => PaynymDetailsPopup(
accountLite: widget.accountLite,
walletId: widget.walletId,
),
);
}
},
child: Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
)
: const EdgeInsets.all(8.0),
child: Row(
children: [
PayNymBot(
size: 32,
paymentCodeString: widget.accountLite.code,
),
const SizedBox(
width: 12,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.accountLite.nymName,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
),
const SizedBox(
height: 2,
),
Text(
Format.shorten(widget.accountLite.code, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,114 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card_button.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class PaynymFollowersList extends ConsumerStatefulWidget {
const PaynymFollowersList({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
ConsumerState<PaynymFollowersList> createState() =>
_PaynymFollowersListState();
}
class _PaynymFollowersListState extends ConsumerState<PaynymFollowersList> {
final isDesktop = Util.isDesktop;
BorderRadius get _borderRadiusFirst {
return BorderRadius.only(
topLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
topRight: Radius.circular(
Constants.size.circularBorderRadius,
),
);
}
BorderRadius get _borderRadiusLast {
return BorderRadius.only(
bottomLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
bottomRight: Radius.circular(
Constants.size.circularBorderRadius,
),
);
}
@override
Widget build(BuildContext context) {
final followers =
ref.watch(myPaynymAccountStateProvider.state).state?.followers;
final count = followers?.length ?? 0;
return ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym followers will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return Container(
key: Key("paynymCardKey_${followers![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers[index],
),
);
}
},
);
}
}

View file

@ -0,0 +1,114 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card_button.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class PaynymFollowingList extends ConsumerStatefulWidget {
const PaynymFollowingList({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
ConsumerState<PaynymFollowingList> createState() =>
_PaynymFollowingListState();
}
class _PaynymFollowingListState extends ConsumerState<PaynymFollowingList> {
final isDesktop = Util.isDesktop;
BorderRadius get _borderRadiusFirst {
return BorderRadius.only(
topLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
topRight: Radius.circular(
Constants.size.circularBorderRadius,
),
);
}
BorderRadius get _borderRadiusLast {
return BorderRadius.only(
bottomLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
bottomRight: Radius.circular(
Constants.size.circularBorderRadius,
),
);
}
@override
Widget build(BuildContext context) {
final following =
ref.watch(myPaynymAccountStateProvider.state).state?.following;
final count = following?.length ?? 0;
return ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym contacts will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return Container(
key: Key("paynymCardKey_${following![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following[index],
),
);
}
},
);
}
}

View file

@ -12,6 +12,8 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -41,6 +43,9 @@ class ConfirmTransactionView extends ConsumerStatefulWidget {
required this.walletId,
this.routeOnSuccessName = WalletView.routeName,
this.isTradeTransaction = false,
this.isPaynymTransaction = false,
this.isPaynymNotificationTransaction = false,
this.onSuccessInsteadOfRouteOnSuccess,
}) : super(key: key);
static const String routeName = "/confirmTransactionView";
@ -49,6 +54,9 @@ class ConfirmTransactionView extends ConsumerStatefulWidget {
final String walletId;
final String routeOnSuccessName;
final bool isTradeTransaction;
final bool isPaynymTransaction;
final bool isPaynymNotificationTransaction;
final VoidCallback? onSuccessInsteadOfRouteOnSuccess;
@override
ConsumerState<ConfirmTransactionView> createState() =>
@ -83,6 +91,13 @@ class _ConfirmTransactionViewState
try {
String txid;
if (widget.isPaynymNotificationTransaction) {
txid = await (manager.wallet as DogecoinWallet)
.confirmNotificationTx(preparedTx: transactionInfo);
} else if (widget.isPaynymTransaction) {
//
throw UnimplementedError("paynym send not implemented yet");
} else {
final coin = manager.coin;
if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
@ -92,6 +107,7 @@ class _ConfirmTransactionViewState
} else {
txid = await manager.confirmSend(txData: transactionInfo);
}
}
// save note
await ref
@ -102,7 +118,12 @@ class _ConfirmTransactionViewState
// pop back to wallet
if (mounted) {
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
if (widget.onSuccessInsteadOfRouteOnSuccess == null) {
Navigator.of(context)
.popUntil(ModalRoute.withName(routeOnSuccessName));
} else {
widget.onSuccessInsteadOfRouteOnSuccess!.call();
}
}
} on BadEpicHttpAddressException catch (_) {
if (mounted) {

View file

@ -485,22 +485,22 @@ class _SendViewState extends ConsumerState<SendView> {
coin == Coin.firoTestNet)
const Spacer(),
FutureBuilder(
// TODO redo this widget now that its not actually a future
future: (coin != Coin.firo &&
coin != Coin.firoTestNet)
? ref.watch(provider.select(
(value) => value.availableBalance))
: ref
.watch(
publicPrivateBalanceStateProvider
.state)
.state ==
? Future(() => ref.watch(
provider.select((value) =>
value.balance.getSpendable())))
: ref.watch(publicPrivateBalanceStateProvider.state).state ==
"Private"
? (ref.watch(provider).wallet
as FiroWallet)
.availablePrivateBalance()
: (ref.watch(provider).wallet
as FiroWallet)
.availablePublicBalance(),
? Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePrivateBalance())
: Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePublicBalance()),
builder:
(_, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
@ -1085,9 +1085,10 @@ class _SendViewState extends ConsumerState<SendView> {
.decimalPlacesForCoin(coin));
}
} else {
cryptoAmountController.text = (await ref
cryptoAmountController.text = (ref
.read(provider)
.availableBalance)
.balance
.getSpendable())
.toStringAsFixed(
Constants.decimalPlacesForCoin(
coin));
@ -1523,43 +1524,43 @@ class _SendViewState extends ConsumerState<SendView> {
.read(walletsChangeNotifierProvider)
.getManager(walletId);
// TODO: remove the need for this!!
final bool isOwnAddress =
await manager.isOwnAddress(_address!);
if (isOwnAddress) {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message:
"Sending to self is currently disabled",
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(
context),
child: Text(
"Ok",
style: STextStyles.button(
context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
);
return;
}
// // TODO: remove the need for this!!
// final bool isOwnAddress =
// await manager.isOwnAddress(_address!);
// if (isOwnAddress && coin != Coin.dogecoinTestNet) {
// await showDialog<dynamic>(
// context: context,
// useSafeArea: false,
// barrierDismissible: true,
// builder: (context) {
// return StackDialog(
// title: "Transaction failed",
// message:
// "Sending to self is currently disabled",
// rightButton: TextButton(
// style: Theme.of(context)
// .extension<StackColors>()!
// .getSecondaryEnabledButtonColor(
// context),
// child: Text(
// "Ok",
// style: STextStyles.button(
// context)
// .copyWith(
// color: Theme.of(context)
// .extension<
// StackColors>()!
// .accentColorDark),
// ),
// onPressed: () {
// Navigator.of(context).pop();
// },
// ),
// );
// },
// );
// return;
// }
final amount =
Format.decimalAmountToSatoshis(
@ -1575,22 +1576,20 @@ class _SendViewState extends ConsumerState<SendView> {
"Private") {
availableBalance =
Format.decimalAmountToSatoshis(
await (manager.wallet
as FiroWallet)
(manager.wallet as FiroWallet)
.availablePrivateBalance(),
coin);
} else {
availableBalance =
Format.decimalAmountToSatoshis(
await (manager.wallet
as FiroWallet)
(manager.wallet as FiroWallet)
.availablePublicBalance(),
coin);
}
} else {
availableBalance =
Format.decimalAmountToSatoshis(
await manager.availableBalance,
manager.balance.getSpendable(),
coin);
}

View file

@ -154,7 +154,9 @@ class _FiroBalanceSelectionSheetState
width: 2,
),
FutureBuilder(
future: firoWallet.availablePrivateBalance(),
// TODO redo this widget now that its not actually a future
future: Future(
() => firoWallet.availablePrivateBalance()),
builder:
(context, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
@ -244,7 +246,9 @@ class _FiroBalanceSelectionSheetState
width: 2,
),
FutureBuilder(
future: firoWallet.availablePublicBalance(),
// TODO redo this widget now that its not actually a future
future: Future(
() => firoWallet.availablePublicBalance()),
builder:
(context, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==

View file

@ -145,7 +145,8 @@ class WalletSyncingOptionsView extends ConsumerWidget {
height: 2,
),
FutureBuilder(
future: manager.totalBalance,
future: Future(
() => manager.balance.getTotal()),
builder: (builderContext,
AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==

View file

@ -507,7 +507,7 @@ class _WalletNetworkSettingsViewState
children: [
Text(
"Synchronized",
style: STextStyles.w600_10(context),
style: STextStyles.w600_12(context),
),
Text(
"100%",
@ -581,7 +581,7 @@ class _WalletNetworkSettingsViewState
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AnimatedText(
style: STextStyles.w600_10(context),
style: STextStyles.w600_12(context),
stringsToLoopThrough: const [
"Synchronizing",
"Synchronizing.",
@ -679,7 +679,7 @@ class _WalletNetworkSettingsViewState
children: [
Text(
"Unable to synchronize",
style: STextStyles.w600_10(context).copyWith(
style: STextStyles.w600_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorRed,

View file

@ -2,9 +2,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
@ -37,19 +38,10 @@ class TransactionsList extends ConsumerStatefulWidget {
class _TransactionsListState extends ConsumerState<TransactionsList> {
//
bool _hasLoaded = false;
Map<String, Transaction> _transactions = {};
List<Transaction> _transactions2 = [];
late final ChangeNotifierProvider<Manager> managerProvider;
void updateTransactions(TransactionData newData) {
_transactions = {};
final newTransactions =
newData.txChunks.expand((element) => element.transactions);
for (final tx in newTransactions) {
_transactions[tx.txid] = tx;
}
}
BorderRadius get _borderRadiusFirst {
return BorderRadius.only(
topLeft: Radius.circular(
@ -73,12 +65,15 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
}
Widget itemBuilder(
BuildContext context, Transaction tx, BorderRadius? radius) {
BuildContext context,
Transaction tx,
BorderRadius? radius,
) {
final matchingTrades = ref
.read(tradesServiceProvider)
.trades
.where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
if (tx.txType == "Sent" && matchingTrades.isNotEmpty) {
if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
decoration: BoxDecoration(
@ -90,13 +85,16 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
children: [
TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
transaction: tx,
walletId: widget.walletId,
),
TradeCard(
// this may mess with combined firo transactions
key: Key(tx.toString() + trade.uuid), //
key: Key(tx.txid +
tx.type.name +
tx.address.value.toString() +
trade.uuid), //
trade: trade,
onTap: () async {
if (Util.isDesktop) {
@ -182,7 +180,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
),
child: TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
transaction: tx,
walletId: widget.walletId,
),
@ -190,6 +188,13 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
}
}
void updateHeightProvider(Manager manager) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(currentHeightProvider(manager.coin).state).state =
manager.currentHeight;
});
}
@override
void initState() {
managerProvider = widget.managerProvider;
@ -202,13 +207,16 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
// .watch(walletsChangeNotifierProvider)
// .getManagerProvider(widget.walletId);
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
updateHeightProvider(manager);
return FutureBuilder(
future:
ref.watch(managerProvider.select((value) => value.transactionData)),
builder: (fbContext, AsyncSnapshot<TransactionData> snapshot) {
future: manager.transactions,
builder: (fbContext, AsyncSnapshot<List<Transaction>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
updateTransactions(snapshot.data!);
_transactions2 = snapshot.data!;
_hasLoaded = true;
}
if (!_hasLoaded) {
@ -227,11 +235,10 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
],
);
}
if (_transactions.isEmpty) {
if (_transactions2.isEmpty) {
return const NoTransActionsFound();
} else {
final list = _transactions.values.toList(growable: false);
list.sort((a, b) => b.timestamp - a.timestamp);
_transactions2.sort((a, b) => b.timestamp - a.timestamp);
return RefreshIndicator(
onRefresh: () async {
//todo: check if print needed
@ -247,12 +254,16 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
? ListView.separated(
itemBuilder: (context, index) {
BorderRadius? radius;
if (index == list.length - 1) {
if (_transactions2.length == 1) {
radius = BorderRadius.circular(
Constants.size.circularBorderRadius,
);
} else if (index == _transactions2.length - 1) {
radius = _borderRadiusLast;
} else if (index == 0) {
radius = _borderRadiusFirst;
}
final tx = list[index];
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
},
separatorBuilder: (context, index) {
@ -264,18 +275,22 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
.background,
);
},
itemCount: list.length,
itemCount: _transactions2.length,
)
: ListView.builder(
itemCount: list.length,
itemCount: _transactions2.length,
itemBuilder: (context, index) {
BorderRadius? radius;
if (index == list.length - 1) {
if (_transactions2.length == 1) {
radius = BorderRadius.circular(
Constants.size.circularBorderRadius,
);
} else if (index == _transactions2.length - 1) {
radius = _borderRadiusLast;
} else if (index == 0) {
radius = _borderRadiusFirst;
}
final tx = list[index];
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
},
),

View file

@ -1,17 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
class TxIcon extends StatelessWidget {
const TxIcon({Key? key, required this.transaction}) : super(key: key);
const TxIcon({
Key? key,
required this.transaction,
required this.currentHeight,
required this.coin,
}) : super(key: key);
final Transaction transaction;
final int currentHeight;
final Coin coin;
static const Size size = Size(32, 32);
String _getAssetName(
bool isCancelled, bool isReceived, bool isPending, BuildContext context) {
if (!isReceived && transaction.subType == "mint") {
if (!isReceived && transaction.subType == TransactionSubType.mint) {
if (isCancelled) {
return Assets.svg.anonymizeFailed;
}
@ -42,7 +51,7 @@ class TxIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final txIsReceived = transaction.txType == "Received";
final txIsReceived = transaction.type == TransactionType.incoming;
return SizedBox(
width: size.width,
@ -52,7 +61,10 @@ class TxIcon extends StatelessWidget {
_getAssetName(
transaction.isCancelled,
txIsReceived,
!transaction.confirmedStatus,
!transaction.isConfirmed(
currentHeight,
coin.requiredConfirmations,
),
context,
),
width: size.width,

View file

@ -32,13 +32,14 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)))
.wallet as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture = firoWallet.availablePrivateBalance();
totalBalanceFuture = Future(() => firoWallet.balance.getSpendable());
availableBalanceFuture =
Future(() => firoWallet.balancePrivate.getSpendable());
} else {
final wallet = ref.watch(walletsChangeNotifierProvider
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
totalBalanceFuture = wallet.totalBalance;
availableBalanceFuture = wallet.availableBalance;
totalBalanceFuture = Future(() => manager.balance.getTotal());
availableBalanceFuture = Future(() => manager.balance.getSpendable());
}
return Container(

View file

@ -1,10 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
class WalletNavigationBar extends StatelessWidget {
class WalletNavigationBar extends StatefulWidget {
const WalletNavigationBar({
Key? key,
required this.onReceivePressed,
@ -13,6 +26,8 @@ class WalletNavigationBar extends StatelessWidget {
required this.onBuyPressed,
required this.height,
required this.enableExchange,
required this.coin,
required this.walletId,
}) : super(key: key);
final VoidCallback onReceivePressed;
@ -21,18 +36,165 @@ class WalletNavigationBar extends StatelessWidget {
final VoidCallback onBuyPressed;
final double height;
final bool enableExchange;
final Coin coin;
final String walletId;
@override
State<WalletNavigationBar> createState() => _WalletNavigationBarState();
}
class _WalletNavigationBarState extends State<WalletNavigationBar> {
double scale = 0;
final duration = const Duration(milliseconds: 200);
@override
Widget build(BuildContext context) {
return Container(
height: height,
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// const Spacer(),
AnimatedScale(
scale: scale,
duration: duration,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
opacity: scale,
duration: duration,
child: GestureDetector(
onTap: () {},
child: Container(
padding: const EdgeInsets.all(16),
width: 146,
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow
],
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Whirlpool",
style: STextStyles.w600_12(context),
),
],
),
),
),
),
const SizedBox(
height: 8,
),
AnimatedOpacity(
opacity: scale,
duration: duration,
child: Consumer(builder: (context, ref, __) {
return GestureDetector(
onTap: () async {
setState(() {
scale = 0;
});
unawaited(
showDialog(
context: context,
builder: (context) => const LoadingIndicator(
width: 100,
),
),
);
// todo make generic and not doge specific
final wallet = (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet);
final code = await wallet.getPaymentCode();
final account = await ref
.read(paynymAPIProvider)
.nym(code.toString());
Logging.instance.log(
"my nym account: $account",
level: LogLevel.Info,
);
if (mounted) {
Navigator.of(context).pop();
// check if account exists and for matching code to see if claimed
if (account.value != null &&
account.value!.codes.first.claimed) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
} else {
await Navigator.of(context).pushNamed(
PaynymClaimView.routeName,
arguments: widget.walletId,
);
}
}
},
child: Container(
padding: const EdgeInsets.all(16),
width: 146,
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow
],
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Paynym",
style: STextStyles.w600_12(context),
),
],
),
),
);
}),
),
const SizedBox(
height: 8,
),
],
),
),
Container(
height: widget.height,
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.bottomNavBack,
boxShadow: [
Theme.of(context).extension<StackColors>()!.standardBoxShadow
],
borderRadius: BorderRadius.circular(
height / 2.0,
widget.height / 2.0,
),
),
child: Padding(
@ -50,12 +212,12 @@ class WalletNavigationBar extends StatelessWidget {
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: onReceivePressed,
onPressed: widget.onReceivePressed,
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
height / 2.0,
widget.height / 2.0,
),
),
child: Container(
@ -105,12 +267,12 @@ class WalletNavigationBar extends StatelessWidget {
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: onSendPressed,
onPressed: widget.onSendPressed,
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
height / 2.0,
widget.height / 2.0,
),
),
child: Container(
@ -156,17 +318,17 @@ class WalletNavigationBar extends StatelessWidget {
),
),
),
if (enableExchange)
if (widget.enableExchange)
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: onExchangePressed,
onPressed: widget.onExchangePressed,
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
height / 2.0,
widget.height / 2.0,
),
),
child: Container(
@ -195,6 +357,58 @@ class WalletNavigationBar extends StatelessWidget {
),
),
),
if (widget.coin.hasPaynymSupport)
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: () {
if (scale == 0) {
setState(() {
scale = 1;
});
} else if (scale == 1) {
setState(() {
scale = 0;
});
}
},
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const SizedBox(
height: 2,
),
SvgPicture.asset(
Assets.svg.bars,
width: 20,
height: 20,
),
const SizedBox(
height: 6,
),
Text(
"More",
style: STextStyles.buttonSmall(context),
),
const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
@ -234,52 +448,8 @@ class WalletNavigationBar extends StatelessWidget {
],
),
),
),
],
);
}
}
//
// class BarButton extends StatelessWidget {
// const BarButton(
// {Key? key, required this.icon, required this.text, this.onPressed})
// : super(key: key);
//
// final Widget icon;
// final String text;
// final VoidCallback? onPressed;
//
// @override
// Widget build(BuildContext context) {
// return Container(
// child: MaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
// padding: const EdgeInsets.all(0),
// minWidth: 45,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(
// Constants.size.circularBorderRadius,
// ),
// ),
// materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// onPressed: onPressed,
// child: Padding(
// padding: const EdgeInsets.all(4.0),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// icon,
// SizedBox(
// height: 4,
// ),
// Text(
// text,
// style: STextStyles.itemSubtitle12(context).copyWith(
// fontSize: 10,
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// }

View file

@ -79,14 +79,16 @@ class _WalletSummaryInfoState extends State<WalletSummaryInfo> {
final firoWallet =
ref.watch(managerProvider.select((value) => value.wallet))
as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture = firoWallet.availablePrivateBalance();
totalBalanceFuture =
Future(() => firoWallet.balance.getSpendable());
availableBalanceFuture =
Future(() => firoWallet.balancePrivate.getSpendable());
} else {
totalBalanceFuture = ref.watch(
managerProvider.select((value) => value.totalBalance));
availableBalanceFuture = ref.watch(
managerProvider.select((value) => value.availableBalance));
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
totalBalanceFuture = Future(() => manager.balance.getTotal());
availableBalanceFuture =
Future(() => manager.balance.getSpendable());
}
final locale = ref.watch(localeServiceChangeNotifierProvider

View file

@ -4,12 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/transaction_filter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
@ -89,11 +90,15 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
return false;
}
if (filter.received && !filter.sent && tx.txType == "Sent") {
if (filter.received &&
!filter.sent &&
tx.type == TransactionType.outgoing) {
return false;
}
if (filter.sent && !filter.received && tx.txType == "Received") {
if (filter.sent &&
!filter.received &&
tx.type == TransactionType.incoming) {
return false;
}
@ -131,7 +136,8 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
.isNotEmpty;
// check if address contains
contains |= tx.address.toLowerCase().contains(keyword);
contains |=
tx.address.value?.value.toLowerCase().contains(keyword) ?? false;
// check if note contains
contains |= notes[tx.txid] != null &&
@ -141,11 +147,10 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
contains |= tx.txid.toLowerCase().contains(keyword);
// check if subType contains
contains |=
tx.subType.isNotEmpty && tx.subType.toLowerCase().contains(keyword);
contains |= tx.subType.name.toLowerCase().contains(keyword);
// check if txType contains
contains |= tx.txType.toLowerCase().contains(keyword);
contains |= tx.type.name.toLowerCase().contains(keyword);
// check if date contains
contains |=
@ -454,17 +459,13 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
// debugPrint("Consumer build called");
return FutureBuilder(
future: ref.watch(managerProvider
.select((value) => value.transactionData)),
builder: (_, AsyncSnapshot<TransactionData> snapshot) {
future: ref.watch(
managerProvider.select((value) => value.transactions)),
builder: (_, AsyncSnapshot<List<Transaction>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
final filtered = filter(
transactions: snapshot.data!
.getAllTransactions()
.values
.toList(),
filter: criteria);
transactions: snapshot.data!, filter: criteria);
final searched = search(_searchString, filtered);
@ -787,33 +788,33 @@ class _DesktopTransactionCardRowState
late final Transaction _transaction;
late final String walletId;
String whatIsIt(String type, Coin coin) {
String whatIsIt(TransactionType type, Coin coin, int height) {
if (coin == Coin.epicCash && _transaction.slateId == null) {
return "Restored Funds";
}
if (_transaction.subType == "mint") {
if (_transaction.confirmedStatus) {
if (_transaction.subType == TransactionSubType.mint) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Anonymized";
} else {
return "Anonymizing";
}
}
if (type == "Received") {
if (_transaction.confirmedStatus) {
if (type == TransactionType.incoming) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Received";
} else {
return "Receiving";
}
} else if (type == "Sent") {
if (_transaction.confirmedStatus) {
} else if (type == TransactionType.outgoing) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Sent";
} else {
return "Sending";
}
} else {
return type;
return type.name;
}
}
@ -843,15 +844,17 @@ class _DesktopTransactionCardRowState
late final String prefix;
if (Util.isDesktop) {
if (_transaction.txType == "Sent") {
if (_transaction.type == TransactionType.outgoing) {
prefix = "-";
} else if (_transaction.txType == "Received") {
} else if (_transaction.type == TransactionType.incoming) {
prefix = "+";
}
} else {
prefix = "";
}
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,
elevation: 0,
@ -911,7 +914,11 @@ class _DesktopTransactionCardRowState
),
child: Row(
children: [
TxIcon(transaction: _transaction),
TxIcon(
transaction: _transaction,
currentHeight: currentHeight,
coin: coin,
),
const SizedBox(
width: 12,
),
@ -920,7 +927,11 @@ class _DesktopTransactionCardRowState
child: Text(
_transaction.isCancelled
? "Cancelled"
: whatIsIt(_transaction.txType, coin),
: whatIsIt(
_transaction.type,
coin,
currentHeight,
),
style:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,

View file

@ -5,12 +5,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
@ -19,7 +20,6 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/block_explorers.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -80,13 +80,13 @@ class _TransactionDetailsViewState
coin = widget.coin;
amount = Format.satoshisToAmount(_transaction.amount, coin: coin);
fee = Format.satoshisToAmount(_transaction.fees, coin: coin);
fee = Format.satoshisToAmount(_transaction.fee, coin: coin);
if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
_transaction.subType == "mint") {
_transaction.subType == TransactionSubType.mint) {
amountPrefix = "";
} else {
amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "-" : "+";
amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+";
}
// if (coin == Coin.firo || coin == Coin.firoTestNet) {
@ -102,10 +102,10 @@ class _TransactionDetailsViewState
super.dispose();
}
String whatIsIt(String type) {
String whatIsIt(TransactionType type, int height) {
if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (_transaction.subType == "mint") {
if (_transaction.confirmedStatus) {
if (_transaction.subType == TransactionSubType.mint) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Minted";
} else {
return "Minting";
@ -113,23 +113,23 @@ class _TransactionDetailsViewState
}
}
if (type == "Received") {
if (type == TransactionType.incoming) {
// if (_transaction.isMinting) {
// return "Minting";
// } else
if (_transaction.confirmedStatus) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Received";
} else {
return "Receiving";
}
} else if (type == "Sent") {
if (_transaction.confirmedStatus) {
} else if (type == TransactionType.outgoing) {
if (_transaction.isConfirmed(height, coin.requiredConfirmations)) {
return "Sent";
} else {
return "Sending";
}
} else {
return type;
return type.name;
}
}
@ -298,6 +298,8 @@ class _TransactionDetailsViewState
@override
Widget build(BuildContext context) {
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
@ -403,6 +405,8 @@ class _TransactionDetailsViewState
children: [
TxIcon(
transaction: _transaction,
currentHeight: currentHeight,
coin: coin,
),
const SizedBox(
width: 16,
@ -411,7 +415,9 @@ class _TransactionDetailsViewState
_transaction.isCancelled
? "Cancelled"
: whatIsIt(
_transaction.txType),
_transaction.type,
currentHeight,
),
style:
STextStyles.desktopTextMedium(
context),
@ -489,6 +495,8 @@ class _TransactionDetailsViewState
if (!isDesktop)
TxIcon(
transaction: _transaction,
currentHeight: currentHeight,
coin: coin,
),
],
),
@ -523,13 +531,17 @@ class _TransactionDetailsViewState
SelectableText(
_transaction.isCancelled
? "Cancelled"
: whatIsIt(_transaction.txType),
: whatIsIt(
_transaction.type,
currentHeight,
),
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: _transaction.txType == "Sent"
color: _transaction.type ==
TransactionType.outgoing
? Theme.of(context)
.extension<StackColors>()!
.accentColorOrange
@ -546,11 +558,12 @@ class _TransactionDetailsViewState
),
if (!((coin == Coin.monero ||
coin == Coin.wownero) &&
_transaction.txType.toLowerCase() ==
"sent") &&
_transaction.type ==
TransactionType.outgoing) &&
!((coin == Coin.firo ||
coin == Coin.firoTestNet) &&
_transaction.subType == "mint"))
_transaction.subType ==
TransactionSubType.mint))
isDesktop
? const _Divider()
: const SizedBox(
@ -558,11 +571,12 @@ class _TransactionDetailsViewState
),
if (!((coin == Coin.monero ||
coin == Coin.wownero) &&
_transaction.txType.toLowerCase() ==
"sent") &&
_transaction.type ==
TransactionType.outgoing) &&
!((coin == Coin.firo ||
coin == Coin.firoTestNet) &&
_transaction.subType == "mint"))
_transaction.subType ==
TransactionSubType.mint))
RoundedWhiteContainer(
padding: isDesktop
? const EdgeInsets.all(16)
@ -578,8 +592,8 @@ class _TransactionDetailsViewState
CrossAxisAlignment.start,
children: [
Text(
_transaction.txType.toLowerCase() ==
"sent"
_transaction.type ==
TransactionType.outgoing
? "Sent to"
: "Receiving address",
style: isDesktop
@ -592,17 +606,19 @@ class _TransactionDetailsViewState
const SizedBox(
height: 8,
),
_transaction.txType.toLowerCase() ==
"received"
_transaction.type ==
TransactionType.incoming
? FutureBuilder(
future: fetchContactNameFor(
_transaction.address),
_transaction.address
.value!.value),
builder: (builderContext,
AsyncSnapshot<String>
snapshot) {
String
addressOrContactName =
_transaction.address;
_transaction.address
.value!.value;
if (snapshot.connectionState ==
ConnectionState
.done &&
@ -630,7 +646,8 @@ class _TransactionDetailsViewState
},
)
: SelectableText(
_transaction.address,
_transaction
.address.value!.value,
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
@ -651,7 +668,7 @@ class _TransactionDetailsViewState
),
if (isDesktop)
IconCopyButton(
data: _transaction.address,
data: _transaction.address.value!.value,
),
],
),
@ -855,7 +872,10 @@ class _TransactionDetailsViewState
: const EdgeInsets.all(12),
child: Builder(builder: (context) {
final feeString = showFeePending
? _transaction.confirmedStatus
? _transaction.isConfirmed(
currentHeight,
coin.requiredConfirmations,
)
? Format.localizedStringAsFixed(
value: fee,
locale: ref.watch(
@ -947,9 +967,14 @@ class _TransactionDetailsViewState
: const EdgeInsets.all(12),
child: Builder(builder: (context) {
final height = widget.coin != Coin.epicCash &&
_transaction.confirmedStatus
_transaction.isConfirmed(
currentHeight,
coin.requiredConfirmations,
)
? "${_transaction.height == 0 ? "Unknown" : _transaction.height}"
: _transaction.confirmations > 0
: _transaction.getConfirmations(
currentHeight) >
0
? "${_transaction.height}"
: "Pending";
@ -1297,9 +1322,13 @@ class _TransactionDetailsViewState
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: (coin == Coin.epicCash &&
_transaction.confirmedStatus == false &&
_transaction.isConfirmed(
currentHeight,
coin.requiredConfirmations,
) ==
false &&
_transaction.isCancelled == false &&
_transaction.txType == "Sent")
_transaction.type == TransactionType.outgoing)
? SizedBox(
width: MediaQuery.of(context).size.width - 32,
child: TextButton(

View file

@ -35,7 +35,6 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -707,7 +706,6 @@ class _WalletViewState extends ConsumerState<WalletView> {
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -717,11 +715,11 @@ class _WalletViewState extends ConsumerState<WalletView> {
left: 16,
right: 16,
),
child: SizedBox(
height: WalletView.navBarHeight,
child: WalletNavigationBar(
enableExchange:
Constants.enableExchange &&
walletId: widget.walletId,
coin: ref.watch(managerProvider
.select((value) => value.coin)),
enableExchange: Constants.enableExchange &&
ref.watch(managerProvider.select(
(value) => value.coin)) !=
Coin.epicCash,
@ -748,8 +746,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
final coin =
ref.read(managerProvider).coin;
switch (ref
.read(
walletBalanceToggleStateProvider
.read(walletBalanceToggleStateProvider
.state)
.state) {
case WalletBalanceToggleState.full:
@ -759,8 +756,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
.state)
.state = "Public";
break;
case WalletBalanceToggleState
.available:
case WalletBalanceToggleState.available:
ref
.read(
publicPrivateBalanceStateProvider
@ -779,7 +775,6 @@ class _WalletViewState extends ConsumerState<WalletView> {
onBuyPressed: () {},
),
),
),
],
),
],

View file

@ -217,8 +217,8 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
),
),
FutureBuilder(
future: ref.watch(
managerProvider.select((value) => value.totalBalance)),
future: Future(() => ref.watch(managerProvider
.select((value) => value.balance.getTotal()))),
builder: (builderContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {

View file

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart';
import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_card.dart';
import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart';
@ -22,6 +24,8 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
import 'package:tuple/tuple.dart';
import '../../../db/main_db.dart';
class DesktopContactDetails extends ConsumerStatefulWidget {
const DesktopContactDetails({
Key? key,
@ -57,11 +61,13 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> {
List<Tuple2<String, Transaction>> result = [];
for (final manager in managers) {
final transactions = (await manager.transactionData)
.getAllTransactions()
.values
.toList()
.where((e) => _contactHasAddress(e.address, contact));
final transactions = await MainDB.instance
.getTransactions(manager.walletId)
.filter()
.anyOf(contact.addresses.map((e) => e.address),
(q, String e) => q.address((q) => q.valueEqualTo(e)))
.sortByTimestampDesc()
.findAll();
for (final tx in transactions) {
result.add(Tuple2(manager.walletId, tx));

View file

@ -4,12 +4,15 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/format.dart';
@ -26,7 +29,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
import '../../route_generator.dart';
import '../../db/main_db.dart';
class DesktopAllTradesView extends ConsumerStatefulWidget {
const DesktopAllTradesView({Key? key}) : super(key: key);
@ -349,10 +352,12 @@ class _DesktopTradeRowCardState extends ConsumerState<DesktopTradeRowCard> {
//todo: check if print needed
// debugPrint("name: ${manager.walletName}");
// TODO store tx data completely locally in isar so we don't lock up ui here when querying txData
final txData = await manager.transactionData;
final tx = await MainDB.instance
.getTransactions(walletIds.first)
.filter()
.txidEqualTo(txid)
.findFirst();
final tx = txData.getAllTransactions()[txid];
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(

View file

@ -305,8 +305,9 @@ class _BalanceDisplayState extends ConsumerState<BalanceDisplay> {
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
// TODO redo this widget now that its not actually a future
return FutureBuilder(
future: manager.availableBalance,
future: Future(() => manager.balance.getSpendable()),
builder: (context, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart';
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
@ -17,6 +19,8 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/trade_card.dart';
import '../../../db/main_db.dart';
class DesktopTradeHistory extends ConsumerStatefulWidget {
const DesktopTradeHistory({Key? key}) : super(key: key);
@ -126,10 +130,11 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> {
//todo: check if print needed
// debugPrint("name: ${manager.walletName}");
// TODO store tx data completely locally in isar so we don't lock up ui here when querying txData
final txData = await manager.transactionData;
final tx = txData.getAllTransactions()[txid];
final tx = await MainDB.instance
.getTransactions(walletIds.first)
.filter()
.txidEqualTo(txid)
.findFirst();
if (mounted) {
await showDialog<void>(

View file

@ -40,6 +40,7 @@ class CoinWalletsTable extends ConsumerWidget {
children: [
for (int i = 0; i < walletIds.length; i++)
Column(
key: Key("${coin.name}_$runtimeType${walletIds[i]}_key"),
children: [
if (i != 0)
const SizedBox(

View file

@ -37,6 +37,7 @@ class _WalletTableState extends ConsumerState<WalletSummaryTable> {
rows: [
for (int i = 0; i < providersByCoin.length; i++)
Builder(
key: Key("${providersByCoin[i].key.name}_${runtimeType}_key"),
builder: (context) {
final providers = ref.watch(walletsChangeNotifierProvider.select(
(value) => value

View file

@ -8,6 +8,8 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart';
@ -15,8 +17,12 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
@ -36,6 +42,7 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/hover_text_field.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
@ -281,6 +288,51 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
}
}
Future<void> onPaynymButtonPressed() async {
unawaited(
showDialog(
context: context,
builder: (context) => const LoadingIndicator(
width: 100,
),
),
);
// todo make generic and not doge specific
final wallet = (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet);
final code = await wallet.getPaymentCode();
final account = await ref.read(paynymAPIProvider).nym(code.toString());
Logging.instance.log(
"my nym account: $account",
level: LogLevel.Info,
);
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
// check if account exists and for matching code to see if claimed
if (account.value != null && account.value!.codes.first.claimed) {
ref.read(myPaynymAccountStateProvider.state).state = account.value!;
await Navigator.of(context).pushNamed(
PaynymHomeView.routeName,
arguments: widget.walletId,
);
} else {
await Navigator.of(context).pushNamed(
PaynymClaimView.routeName,
arguments: widget.walletId,
);
}
}
}
@override
void initState() {
controller = TextEditingController();
@ -482,6 +534,21 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
);
},
),
if (coin.hasPaynymSupport)
SecondaryButton(
label: "PayNym",
width: 160,
buttonHeight: ButtonHeight.l,
icon: SvgPicture.asset(
Assets.svg.user,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: onPaynymButtonPressed,
),
// if (coin == Coin.firo) const SizedBox(width: 16),
// SecondaryButton(
// width: 180,

View file

@ -97,73 +97,73 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
// TODO: remove the need for this!!
final bool isOwnAddress = await manager.isOwnAddress(_address!);
if (isOwnAddress) {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return DesktopDialog(
maxWidth: 400,
maxHeight: double.infinity,
child: Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 32,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction failed",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
const SizedBox(
height: 12,
),
Text(
"Sending to self is currently disabled",
textAlign: TextAlign.left,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
fontSize: 18,
),
),
const SizedBox(
height: 40,
),
Row(
children: [
Expanded(
child: SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Ok",
onPressed: () {
Navigator.of(context).pop();
},
),
),
const SizedBox(
width: 32,
),
],
),
],
),
),
);
},
);
return;
}
// // TODO: remove the need for this!!
// final bool isOwnAddress = await manager.isOwnAddress(_address!);
// if (isOwnAddress) {
// await showDialog<dynamic>(
// context: context,
// useSafeArea: false,
// barrierDismissible: true,
// builder: (context) {
// return DesktopDialog(
// maxWidth: 400,
// maxHeight: double.infinity,
// child: Padding(
// padding: const EdgeInsets.only(
// left: 32,
// bottom: 32,
// ),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "Transaction failed",
// style: STextStyles.desktopH3(context),
// ),
// const DesktopDialogCloseButton(),
// ],
// ),
// const SizedBox(
// height: 12,
// ),
// Text(
// "Sending to self is currently disabled",
// textAlign: TextAlign.left,
// style: STextStyles.desktopTextExtraExtraSmall(context)
// .copyWith(
// fontSize: 18,
// ),
// ),
// const SizedBox(
// height: 40,
// ),
// Row(
// children: [
// Expanded(
// child: SecondaryButton(
// buttonHeight: ButtonHeight.l,
// label: "Ok",
// onPressed: () {
// Navigator.of(context).pop();
// },
// ),
// ),
// const SizedBox(
// width: 32,
// ),
// ],
// ),
// ],
// ),
// ),
// );
// },
// );
// return;
// }
final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin);
int availableBalance;
@ -171,16 +171,14 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
availableBalance = Format.decimalAmountToSatoshis(
await (manager.wallet as FiroWallet).availablePrivateBalance(),
coin);
(manager.wallet as FiroWallet).availablePrivateBalance(), coin);
} else {
availableBalance = Format.decimalAmountToSatoshis(
await (manager.wallet as FiroWallet).availablePublicBalance(),
coin);
(manager.wallet as FiroWallet).availablePublicBalance(), coin);
}
} else {
availableBalance =
Format.decimalAmountToSatoshis(await manager.availableBalance, coin);
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
}
// confirm send all
@ -568,9 +566,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
if (wallet != null) {
Decimal? balance;
if (private) {
balance = await wallet.availablePrivateBalance();
balance = wallet.availablePrivateBalance();
} else {
balance = await wallet.availablePublicBalance();
balance = wallet.availablePublicBalance();
}
return Format.localizedStringAsFixed(
@ -757,19 +755,18 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
.wallet as FiroWallet;
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
cryptoAmountController.text =
(await firoWallet.availablePrivateBalance())
cryptoAmountController.text = (firoWallet.availablePrivateBalance())
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
} else {
cryptoAmountController.text =
(await firoWallet.availablePublicBalance())
cryptoAmountController.text = (firoWallet.availablePublicBalance())
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
}
} else {
cryptoAmountController.text = (await ref
cryptoAmountController.text = (ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.availableBalance)
.balance
.getSpendable())
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
}
}

View file

@ -68,15 +68,17 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> {
final firoWallet = ref.watch(
managerProvider.select((value) => value.wallet))
as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture =
firoWallet.availablePrivateBalance();
totalBalanceFuture =
Future(() => firoWallet.balance.getSpendable());
availableBalanceFuture = Future(
() => firoWallet.balancePrivate.getSpendable());
} else {
totalBalanceFuture = ref.watch(managerProvider
.select((value) => value.totalBalance));
availableBalanceFuture = ref.watch(managerProvider
.select((value) => value.availableBalance));
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
totalBalanceFuture =
Future(() => manager.balance.getTotal());
availableBalanceFuture =
Future(() => manager.balance.getSpendable());
}
final locale = ref.watch(localeServiceChangeNotifierProvider

View file

@ -13,12 +13,17 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import '../../hive/db.dart';
import '../../utilities/db_version_migration.dart';
import '../../utilities/logger.dart';
class DesktopLoginView extends ConsumerStatefulWidget {
const DesktopLoginView({
Key? key,
@ -43,6 +48,25 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
bool hidePassword = true;
bool _continueEnabled = false;
Future<void> _checkDesktopMigrate() async {
if (Util.isDesktop) {
int dbVersion = DB.instance.get<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ??
0;
if (dbVersion < Constants.currentHiveDbVersion) {
try {
await DbVersionMigrator().migrate(
dbVersion,
secureStore: ref.read(secureStoreProvider),
);
} catch (e, s) {
Logging.instance.log("Cannot migrate desktop database\n$e $s",
level: LogLevel.Error, printFullLength: true);
}
}
}
}
Future<void> login() async {
try {
unawaited(
@ -63,12 +87,18 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
await Future<void>.delayed(const Duration(seconds: 1));
// init security context
await ref
.read(storageCryptoHandlerProvider)
.initFromExisting(passwordController.text);
// init desktop secure storage
await (ref.read(secureStoreProvider).store as DesktopSecureStore).init();
// check and migrate if needed
await _checkDesktopMigrate();
// load data
await widget.load?.call();
// if no errors passphrase is correct

View file

@ -0,0 +1,6 @@
import 'package:dart_numerics/dart_numerics.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
final currentHeightProvider =
StateProvider.family<int, Coin>((ref, coin) => int64MaxValue);

View file

@ -0,0 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/utilities/paynym_is_api.dart';
final paynymAPIProvider = Provider<PaynymIsApi>((_) => PaynymIsApi());

View file

@ -0,0 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
final selectedPaynymDetailsItemProvider =
StateProvider.autoDispose<PaynymAccountLite?>((_) => null);

View file

@ -0,0 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paynym/paynym_account.dart';
final myPaynymAccountStateProvider =
StateProvider<PaynymAccount?>((ref) => null);

View file

@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
@ -36,6 +35,9 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/intro_view.dart';
import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart';
import 'package:stackwallet/pages/notification_views/notifications_view.dart';
import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
@ -119,6 +121,8 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:tuple/tuple.dart';
import 'models/isar/models/blockchain_data/transaction.dart';
class RouteGenerator {
static const bool useMaterialPageRoute = true;
@ -172,7 +176,7 @@ class RouteGenerator {
}
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => StackPrivacyCalls(isSettings: false),
builder: (_) => const StackPrivacyCalls(isSettings: false),
settings: RouteSettings(name: settings.name));
case WalletsView.routeName:
@ -187,6 +191,48 @@ class RouteGenerator {
builder: (_) => const AddWalletView(),
settings: RouteSettings(name: settings.name));
case PaynymClaimView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => PaynymClaimView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case PaynymHomeView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => PaynymHomeView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case AddNewPaynymFollowView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => AddNewPaynymFollowView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case GlobalSettingsView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,756 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bip47/bip47.dart';
import 'package:bip47/src/util.dart';
import 'package:bitcoindart/bitcoindart.dart' as btc_dart;
import 'package:bitcoindart/src/utils/constants/op.dart' as op;
import 'package:bitcoindart/src/utils/script.dart' as bscript;
import 'package:decimal/decimal.dart';
import 'package:isar/isar.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:tuple/tuple.dart';
class SWException with Exception {
SWException(this.message);
final String message;
@override
toString() => message;
}
class InsufficientBalanceException extends SWException {
InsufficientBalanceException(super.message);
}
class PaynymSendException extends SWException {
PaynymSendException(super.message);
}
extension PayNym on DogecoinWallet {
// fetch or generate this wallet's bip47 payment code
Future<PaymentCode> getPaymentCode() async {
final paymentCodeString = DB.instance
.get<dynamic>(boxName: walletId, key: "paymentCodeString") as String?;
PaymentCode paymentCode;
if (paymentCodeString == null) {
final node = getBip32Root((await mnemonic).join(" "), network)
.derivePath("m/47'/0'/0'");
paymentCode =
PaymentCode.initFromPubKey(node.publicKey, node.chainCode, network);
await DB.instance.put<dynamic>(
boxName: walletId,
key: "paymentCodeString",
value: paymentCode.toString());
} else {
paymentCode = PaymentCode.fromPaymentCode(paymentCodeString, network);
}
return paymentCode;
}
Future<Uint8List> signWithNotificationKey(Uint8List data) async {
final node = getBip32Root((await mnemonic).join(" "), network)
.derivePath("m/47'/0'/0'");
final pair =
btc_dart.ECPair.fromPrivateKey(node.privateKey!, network: network);
final signed = pair.sign(SHA256Digest().process(data));
return signed;
}
Future<String> signStringWithNotificationKey(String data) async {
final bytes =
await signWithNotificationKey(Uint8List.fromList(utf8.encode(data)));
return Format.uint8listToString(bytes);
// final bytes =
// await signWithNotificationKey(Uint8List.fromList(utf8.encode(data)));
// return Format.uint8listToString(bytes);
}
/// Update cached lists of notification transaction IDs.
/// Returns true if there are new notification transactions found since last
/// checked.
Future<bool> checkForNotificationTransactions() async {
final myPCode = await getPaymentCode();
final transactionIds = await electrumXClient.getHistory(
scripthash: AddressUtils.convertToScriptHash(
myPCode.notificationAddress(),
network,
),
);
final confirmedNotificationTransactionIds = DB.instance.get<dynamic>(
boxName: walletId,
key: "confirmedNotificationTransactionIds",
) as Set? ??
{};
final unconfirmedNotificationTransactionIds = DB.instance.get<dynamic>(
boxName: walletId,
key: "unconfirmedNotificationTransactionIds",
) as Set? ??
{};
// since we are only checking for newly found transactions here we can use the sum
final totalCount = confirmedNotificationTransactionIds.length +
unconfirmedNotificationTransactionIds.length;
for (final entry in transactionIds) {
final txid = entry["tx_hash"] as String;
final tx = await cachedElectrumXClient.getTransaction(
txHash: txid,
coin: coin,
);
// check if tx is confirmed
if ((tx["confirmations"] as int? ?? 0) > MINIMUM_CONFIRMATIONS) {
// remove it from unconfirmed set
unconfirmedNotificationTransactionIds.remove(txid);
// add it to confirmed set
confirmedNotificationTransactionIds.add(txid);
} else {
// otherwise add it to the unconfirmed set
unconfirmedNotificationTransactionIds.add(txid);
}
}
final newTotalCount = confirmedNotificationTransactionIds.length +
unconfirmedNotificationTransactionIds.length;
return newTotalCount > totalCount;
}
/// return the notification tx sent from my wallet if it exists
Future<Transaction?> hasSentNotificationTx(PaymentCode pCode) async {
final tx = await db
.getTransactions(walletId)
.filter()
.address((q) => q.valueEqualTo(pCode.notificationAddress()))
.findFirst();
return tx;
}
void preparePaymentCodeSend(PaymentCode pCode) async {
final notifTx = await hasSentNotificationTx(pCode);
final currentHeight = await chainHeight;
if (notifTx == null) {
throw PaynymSendException("No notification transaction sent to $pCode");
} else if (!notifTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
throw PaynymSendException(
"Notification transaction sent to $pCode has not confirmed yet");
} else {
final node = getBip32Root((await mnemonic).join(" "), network)
.derivePath("m/47'/0'/0'");
final sendToAddress = await nextUnusedSendAddressFrom(
pCode,
node.derive(0).privateKey!,
);
// todo: Actual transaction build
}
}
/// get the next unused address to send to given the receiver's payment code
/// and your own private key
Future<String> nextUnusedSendAddressFrom(
PaymentCode pCode,
Uint8List privateKey,
) async {
// https://en.bitcoin.it/wiki/BIP_0047#Path_levels
const maxCount = 2147483647;
final paymentAddress = PaymentAddress.initWithPrivateKey(
privateKey,
pCode,
0, // initial index to check
);
for (paymentAddress.index = 0;
paymentAddress.index <= maxCount;
paymentAddress.index++) {
final address = paymentAddress.getSendAddress();
final transactionIds = await electrumXClient.getHistory(
scripthash: AddressUtils.convertToScriptHash(
address,
network,
),
);
if (transactionIds.isEmpty) {
return address;
}
}
throw PaynymSendException("Exhausted unused send addresses!");
}
/// get your receiving addresses given the sender's payment code and your own
/// private key
List<String> deriveReceivingAddressesFor(
PaymentCode pCode,
Uint8List privateKey,
int count,
) {
// https://en.bitcoin.it/wiki/BIP_0047#Path_levels
const maxCount = 2147483647;
assert(count <= maxCount);
final paymentAddress = PaymentAddress.initWithPrivateKey(
privateKey,
pCode,
0, // initial index
);
final List<String> result = [];
for (paymentAddress.index = 0;
paymentAddress.index < count;
paymentAddress.index++) {
final address = paymentAddress.getReceiveAddress();
result.add(address);
}
return result;
}
Future<Map<String, dynamic>> buildNotificationTx({
required int selectedTxFeeRate,
required String targetPaymentCodeString,
int additionalOutputs = 0,
List<UTXO>? utxos,
}) async {
const amountToSend = DUST_LIMIT;
final List<UTXO> availableOutputs = utxos ?? await this.utxos;
final List<UTXO> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i]
.isConfirmed(await chainHeight, MINIMUM_CONFIRMATIONS) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
}
}
if (spendableSatoshiValue < amountToSend) {
// insufficient balance
throw InsufficientBalanceException(
"Spendable balance is less than the minimum required for a notification transaction.");
} else if (spendableSatoshiValue == amountToSend) {
// insufficient balance due to missing amount to cover fee
throw InsufficientBalanceException(
"Remaining balance does not cover the network fee.");
}
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
int satoshisBeingUsed = 0;
int outputsBeingUsed = 0;
List<UTXO> utxoObjectsToUse = [];
for (int i = 0;
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
outputsBeingUsed += 1;
}
// add additional outputs if required
for (int i = 0;
i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
outputsBeingUsed += 1;
}
// gather required signing data
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
final int vSizeForNoChange = (await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0))
.item2;
final int vSizeForWithChange = (await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: satoshisBeingUsed - amountToSend))
.item2;
// Assume 2 outputs, for recipient and payment code script
int feeForNoChange = estimateTxFee(
vSize: vSizeForNoChange,
feeRatePerKB: selectedTxFeeRate,
);
// Assume 3 outputs, for recipient, payment code script, and change
int feeForWithChange = estimateTxFee(
vSize: vSizeForWithChange,
feeRatePerKB: selectedTxFeeRate,
);
if (feeForNoChange < vSizeForNoChange * 1000) {
feeForNoChange = vSizeForNoChange * 1000;
}
if (feeForWithChange < vSizeForWithChange * 1000) {
feeForWithChange = vSizeForWithChange * 1000;
}
if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) {
// try to add change output due to "left over" amount being greater than
// the estimated fee + the dust limit
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
// check estimates are correct and build notification tx
if (changeAmount >= DUST_LIMIT &&
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: changeAmount,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
} else {
// something broke during fee estimation or the change amount is smaller
// than the dust limit. Try without change
final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend;
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
}
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
// since we already checked if we need to add a change output we can just
// build without change here
final txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: 0,
);
int feeBeingPaid = satoshisBeingUsed - amountToSend;
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
"amount": amountToSend,
"fee": feeBeingPaid,
"vSize": txn.item2,
};
return transactionObject;
} else {
// if we get here we do not have enough funds to cover the tx total so we
// check if we have any more available outputs and try again
if (spendableOutputs.length > outputsBeingUsed) {
return buildNotificationTx(
selectedTxFeeRate: selectedTxFeeRate,
targetPaymentCodeString: targetPaymentCodeString,
additionalOutputs: additionalOutputs + 1,
);
} else {
throw InsufficientBalanceException(
"Remaining balance does not cover the network fee.");
}
}
}
// return tuple with string value equal to the raw tx hex and the int value
// equal to its vSize
Future<Tuple2<String, int>> _createNotificationTx({
required String targetPaymentCodeString,
required List<UTXO> utxosToUse,
required Map<String, dynamic> utxoSigningData,
required int change,
}) async {
final targetPaymentCode =
PaymentCode.fromPaymentCode(targetPaymentCodeString, network);
final myCode = await getPaymentCode();
final utxo = utxosToUse.first;
final txPoint = utxo.txid.fromHex.toList();
final txPointIndex = utxo.vout;
final rev = Uint8List(txPoint.length + 4);
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
final buffer = rev.buffer.asByteData();
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
final myKeyPair = utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair;
final S = SecretPoint(
myKeyPair.privateKey!,
targetPaymentCode.notificationPublicKey(),
);
final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev);
final blindedPaymentCode = PaymentCode.blind(
myCode.getPayload(),
blindingMask,
);
final opReturnScript = bscript.compile([
(op.OPS["OP_RETURN"] as int),
blindedPaymentCode,
]);
// build a notification tx
final txb = btc_dart.TransactionBuilder(network: network);
txb.setVersion(1);
txb.addInput(
utxo.txid,
txPointIndex,
);
txb.addOutput(targetPaymentCode.notificationAddress(), DUST_LIMIT);
txb.addOutput(opReturnScript, 0);
// TODO: add possible change output and mark output as dangerous
if (change > 0) {
// generate new change address if current change address has been used
await checkChangeAddressForTransactions();
final String changeAddress = await currentChangeAddress;
txb.addOutput(changeAddress, change);
}
txb.sign(
vin: 0,
keyPair: myKeyPair,
);
// sign rest of possible inputs
for (var i = 1; i < utxosToUse.length - 1; i++) {
final txid = utxosToUse[i].txid;
txb.sign(
vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair,
// witnessValue: utxosToUse[i].value,
);
}
final builtTx = txb.build();
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
}
Future<String> confirmNotificationTx(
{required Map<String, dynamic> preparedTx}) async {
try {
Logging.instance.log("confirmNotificationTx txData: $preparedTx",
level: LogLevel.Info);
final txHash = await electrumXClient.broadcastTransaction(
rawTx: preparedTx["hex"] as String);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
await updatePaynymNotificationInfo(
txid: txHash,
confirmed: false,
paymentCodeString: preparedTx["address"] as String,
);
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
// Future<bool> hasConfirmedNotificationTxSentTo(
// String paymentCodeString) async {
// final targetPaymentCode =
// PaymentCode.fromPaymentCode(paymentCodeString, network);
// final targetNotificationAddress = targetPaymentCode.notificationAddress();
//
// final myTxHistory = (await transactionData)
// .getAllTransactions()
// .entries
// .map((e) => e.value)
// .where((e) =>
// e.txType == "Sent" && e.address == targetNotificationAddress);
//
// return myTxHistory.isNotEmpty;
// }
bool hasConnected(String paymentCodeString) {
return getPaynymNotificationTxInfo()
.values
.where((e) => e["paymentCodeString"] == paymentCodeString)
.isNotEmpty;
}
bool hasConnectedConfirmed(String paymentCodeString) {
return getPaynymNotificationTxInfo()
.values
.where((e) =>
e["paymentCodeString"] == paymentCodeString &&
e["confirmed"] == true)
.isNotEmpty;
}
// fetch paynym notification tx meta data
Map<String, dynamic> getPaynymNotificationTxInfo() {
final map = DB.instance.get<dynamic>(
boxName: walletId, key: "paynymNotificationTxInfo") as Map? ??
{};
return Map<String, dynamic>.from(map);
}
// add/update paynym notification tx meta data entry
Future<void> updatePaynymNotificationInfo({
required String txid,
required bool confirmed,
required String paymentCodeString,
}) async {
final data = getPaynymNotificationTxInfo();
data[txid] = {
"txid": txid,
"confirmed": confirmed,
"paymentCodeString": paymentCodeString,
};
await DB.instance.put<dynamic>(
boxName: walletId,
key: "paynymNotificationTxInfo",
value: data,
);
}
}
Future<Tuple4<Transaction, List<Output>, List<Input>, Address>>
parseTransaction(
Map<String, dynamic> txData,
dynamic electrumxClient,
List<Address> myAddresses,
Coin coin,
int minConfirms,
String walletId,
) async {
Set<String> receivingAddresses = myAddresses
.where((e) => e.subType == AddressSubType.receiving)
.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 = {};
int totalInputValue = 0;
int totalOutputValue = 0;
int amountSentFromWallet = 0;
int amountReceivedInWallet = 0;
int changeAmount = 0;
// 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 electrumxClient.getTransaction(
txHash: prevTxid,
coin: coin,
);
for (final output in inputTx["vout"] as List) {
// check matching output
if (prevOut == output["n"]) {
// get value
final value = Format.decimalAmountToSatoshis(
Decimal.parse(output["value"].toString()),
coin,
);
// 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 = Format.decimalAmountToSatoshis(
Decimal.parse(output["value"].toString()),
coin,
);
// 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;
int 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;
final possible =
outputAddresses.difference(myChangeReceivedOnAddresses).first;
if (transactionAddress.value != possible) {
transactionAddress = Address(
walletId: walletId,
value: possible,
derivationIndex: -1,
subType: AddressSubType.nonWallet,
type: AddressType.nonWallet,
publicKey: [],
);
}
} else {
// incoming tx
type = TransactionType.incoming;
amount = amountReceivedInWallet;
}
final tx = Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: txData["blocktime"] as int? ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
type: type,
subType: TransactionSubType.none,
amount: amount,
fee: fee,
height: txData["height"] as int?,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: null,
);
List<Output> outs = [];
List<Input> ins = [];
for (final json in txData["vin"] as List) {
bool isCoinBase = json['coinbase'] != null;
final input = Input(
walletId: walletId,
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?,
);
ins.add(input);
}
for (final json in txData["vout"] as List) {
final output = Output(
walletId: walletId,
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: Format.decimalAmountToSatoshis(
Decimal.parse(json["value"].toString()),
coin,
),
);
outs.add(output);
}
return Tuple4(tx, outs, ins, transactionAddress);
}

View file

@ -1,13 +1,15 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart';
import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart';
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
@ -17,8 +19,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'litecoin/litecoin_wallet.dart';
abstract class CoinServiceAPI {
CoinServiceAPI();
@ -240,30 +240,15 @@ abstract class CoinServiceAPI {
Future<String> confirmSend({required Map<String, dynamic> txData});
/// create and submit tx to network
///
/// Returns the txid of the sent tx
/// will throw exceptions on failure
Future<String> send(
{required String toAddress,
required int amount,
Map<String, String> args});
Future<FeeObject> get fees;
Future<int> get maxFee;
Future<String> get currentReceivingAddress;
// Future<String> get currentLegacyReceivingAddress;
Future<Decimal> get availableBalance;
Future<Decimal> get pendingBalance;
Future<Decimal> get totalBalance;
Future<Decimal> get balanceMinusMaxFee;
Balance get balance;
Future<List<String>> get allOwnAddresses;
Future<TransactionData> get transactionData;
Future<List<UtxoObject>> get unspentOutputs;
Future<List<isar_models.Transaction>> get transactions;
Future<List<isar_models.UTXO>> get utxos;
Future<void> refresh();
@ -308,6 +293,8 @@ abstract class CoinServiceAPI {
// used for electrumx coins
Future<void> updateSentCachedTxData(Map<String, dynamic> txData);
int get storedChainHeight;
// Certain outputs return address as an array/list of strings like List<String> ["addresses"][0], some return it as a string like String ["address"]
String? getAddress(dynamic output) {
// Julian's code from https://github.com/cypherstack/stack_wallet/blob/35a8172d35f1b5cdbd22f0d56c4db02f795fd032/lib/services/coins/coin_paynym_extension.dart#L170 wins codegolf for this, I'd love to commit it now but need to retest this section ... should make unit tests for this case

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
@ -59,7 +60,6 @@ class Manager with ChangeNotifier {
Future<void> updateNode(bool shouldRefresh) async {
await _currentWallet.updateNode(shouldRefresh);
}
// Function(bool isActive)? onIsActiveWalletChanged;
CoinServiceAPI get wallet => _currentWallet;
@ -120,73 +120,17 @@ class Manager with ChangeNotifier {
}
}
/// create and submit tx to network
///
/// Returns the txid of the sent tx
/// will throw exceptions on failure
Future<String> send({
required String toAddress,
required int amount,
Map<String, String> args = const {},
}) async {
try {
final txid = await _currentWallet.send(
toAddress: toAddress,
amount: amount,
args: args,
);
notifyListeners();
return txid;
} catch (e, s) {
Logging.instance.log("$e\n $s", level: LogLevel.Error);
// rethrow to pass error in alert
rethrow;
}
}
Future<FeeObject> get fees => _currentWallet.fees;
Future<int> get maxFee => _currentWallet.maxFee;
Future<String> get currentReceivingAddress =>
_currentWallet.currentReceivingAddress;
// Future<String> get currentLegacyReceivingAddress =>
// _currentWallet.currentLegacyReceivingAddress;
Future<Decimal> get availableBalance async {
_cachedAvailableBalance = await _currentWallet.availableBalance;
return _cachedAvailableBalance;
}
Balance get balance => _currentWallet.balance;
Decimal _cachedAvailableBalance = Decimal.zero;
Decimal get cachedAvailableBalance => _cachedAvailableBalance;
Future<Decimal> get pendingBalance => _currentWallet.pendingBalance;
Future<Decimal> get balanceMinusMaxFee => _currentWallet.balanceMinusMaxFee;
Future<Decimal> get totalBalance async {
_cachedTotalBalance = await _currentWallet.totalBalance;
return _cachedTotalBalance;
}
Decimal _cachedTotalBalance = Decimal.zero;
Decimal get cachedTotalBalance => _cachedTotalBalance;
// Future<Decimal> get fiatBalance async {
// final balance = await _currentWallet.availableBalance;
// final price = await _currentWallet.basePrice;
// return balance * price;
// }
//
// Future<Decimal> get fiatTotalBalance async {
// final balance = await _currentWallet.totalBalance;
// final price = await _currentWallet.basePrice;
// return balance * price;
// }
Future<List<String>> get allOwnAddresses => _currentWallet.allOwnAddresses;
Future<TransactionData> get transactionData => _currentWallet.transactionData;
Future<List<UtxoObject>> get unspentOutputs => _currentWallet.unspentOutputs;
Future<List<isar_models.Transaction>> get transactions =>
_currentWallet.transactions;
Future<List<isar_models.UTXO>> get utxos => _currentWallet.utxos;
Future<void> refresh() async {
await _currentWallet.refresh();
@ -233,11 +177,6 @@ class Manager with ChangeNotifier {
}
}
// Future<bool> initializeWallet() async {
// final success = await _currentWallet.initializeWallet();
// return success;
// }
Future<void> exitCurrentWallet() async {
final name = _currentWallet.walletName;
final id = _currentWallet.walletId;
@ -260,11 +199,6 @@ class Manager with ChangeNotifier {
}
}
Future<bool> isOwnAddress(String address) async {
final allOwnAddresses = await this.allOwnAddresses;
return allOwnAddresses.contains(address);
}
bool get isConnected => _currentWallet.isConnected;
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
@ -278,4 +212,6 @@ class Manager with ChangeNotifier {
}
return success;
}
int get currentHeight => _currentWallet.storedChainHeight;
}

View file

@ -21,21 +21,23 @@ import 'package:flutter_libmonero/core/key_service.dart';
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
import 'package:flutter_libmonero/monero/monero.dart';
import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output;
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/paymint/utxo_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/price.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -45,17 +47,18 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
const int MINIMUM_CONFIRMATIONS = 10;
class MoneroWallet extends CoinServiceAPI {
final String _walletId;
final Coin _coin;
final PriceAPI _priceAPI;
final SecureStorageInterface _secureStorage;
final Prefs _prefs;
class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
late final String _walletId;
late final Coin _coin;
late final SecureStorageInterface _secureStorage;
late final Prefs _prefs;
late String _walletName;
String _walletName;
bool _shouldAutoSync = false;
bool _isConnected = false;
bool _hasCalledExit = false;
@ -68,9 +71,9 @@ class MoneroWallet extends CoinServiceAPI {
WalletCreationService? _walletCreationService;
Timer? _autoSaveTimer;
Future<String>? _currentReceivingAddress;
Future<isar_models.Address?> get _currentReceivingAddress =>
db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst();
Future<FeeObject>? _feeObject;
Future<TransactionData>? _transactionData;
Mutex prepareSendMutex = Mutex();
Mutex estimateFeeMutex = Mutex();
@ -80,34 +83,29 @@ class MoneroWallet extends CoinServiceAPI {
required String walletName,
required Coin coin,
required SecureStorageInterface secureStorage,
PriceAPI? priceAPI,
Prefs? prefs,
}) : _walletId = walletId,
_walletName = walletName,
_coin = coin,
_priceAPI = priceAPI ?? PriceAPI(Client()),
_secureStorage = secureStorage,
MainDB? mockableOverride,
}) {
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_secureStorage = secureStorage;
_prefs = prefs ?? Prefs.instance;
@override
bool get isFavorite {
try {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
initCache(walletId, coin);
isarInit(mockableOverride: mockableOverride);
}
@override
set isFavorite(bool markFavorite) {
DB.instance.put<dynamic>(
boxName: walletId, key: "isFavorite", value: markFavorite);
_isFavorite = markFavorite;
updateCachedIsFavorite(markFavorite);
}
@override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite;
@override
bool get shouldAutoSync => _shouldAutoSync;
@ -139,23 +137,6 @@ class MoneroWallet extends CoinServiceAPI {
@override
set walletName(String newName) => _walletName = newName;
@override
// not used for monero
Future<List<String>> get allOwnAddresses async => [];
@override
Future<Decimal> get availableBalance async {
int runningBalance = 0;
for (final entry in walletBase!.balance!.entries) {
runningBalance += entry.value.unlockedBalance;
}
return Format.satoshisToAmount(runningBalance, coin: coin);
}
@override
// not used
Future<Decimal> get balanceMinusMaxFee => throw UnimplementedError();
@override
Coin get coin => _coin;
@ -184,8 +165,9 @@ class MoneroWallet extends CoinServiceAPI {
}
@override
Future<String> get currentReceivingAddress =>
_currentReceivingAddress ??= _getCurrentAddressForChain(0);
Future<String> get currentReceivingAddress async =>
(await _currentReceivingAddress)?.value ??
(await _generateAddressForChain(0, 0)).value;
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
@ -236,30 +218,30 @@ class MoneroWallet extends CoinServiceAPI {
int maxUnusedAddressGap,
int maxNumberOfIndexesToCheck,
) async {
// clear blockchain info
await db.deleteWalletBlockchainData(walletId);
var restoreHeight = walletBase?.walletInfo.restoreHeight;
highestPercentCached = 0;
await walletBase?.rescan(height: restoreHeight);
await refresh();
}
@override
Future<bool> generateNewAddress() async {
try {
const String indexKey = "receivingIndex";
// First increment the receiving index
await _incrementAddressIndexForChain(0);
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final currentReceiving = await _currentReceivingAddress;
final newReceivingIndex = currentReceiving!.derivationIndex + 1;
// Use new index to derive a new receiving address
final newReceivingAddress =
await _generateAddressForChain(0, newReceivingIndex);
final newReceivingAddress = await _generateAddressForChain(
0,
newReceivingIndex,
);
// Add that new receiving address to the array of receiving addresses
await _addToAddressesArrayForChain(newReceivingAddress, 0);
// Set the new receiving address that the service
_currentReceivingAddress = Future(() => newReceivingAddress);
// Add that new receiving address
await db.putAddress(newReceivingAddress);
return true;
} catch (e, s) {
@ -280,7 +262,7 @@ class MoneroWallet extends CoinServiceAPI {
level: LogLevel.Info,
);
if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) {
if (getCachedId() == null) {
throw Exception(
"Attempted to initialize an existing wallet using an unknown wallet ID!");
}
@ -290,6 +272,7 @@ class MoneroWallet extends CoinServiceAPI {
keysStorage = KeyService(_secureStorage);
await _prefs.init();
// final data =
// DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model")
// as TransactionData?;
@ -314,14 +297,14 @@ class MoneroWallet extends CoinServiceAPI {
);
// Wallet already exists, triggers for a returning user
String indexKey = "receivingIndex";
final curIndex =
await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(0, curIndex);
Logging.instance.log("xmr address in init existing: $newReceivingAddress",
level: LogLevel.Info);
_currentReceivingAddress = Future(() => newReceivingAddress);
// String indexKey = "receivingIndex";
// final curIndex =
// await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
// // Use new index to derive a new receiving address
// final newReceivingAddress = await _generateAddressForChain(0, curIndex);
// Logging.instance.log("xmr address in init existing: $newReceivingAddress",
// level: LogLevel.Info);
// _currentReceivingAddress = Future(() => newReceivingAddress);
}
@override
@ -398,47 +381,20 @@ class MoneroWallet extends CoinServiceAPI {
final node = await _getCurrentNode();
final host = Uri.parse(node.host).host;
await walletBase!.connectToNode(
node: Node(
uri: "$host:${node.port}",
type: WalletType.monero,
trusted: node.trusted ?? false));
node: Node(uri: "$host:${node.port}", type: WalletType.monero));
await walletBase!.startSync();
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
// Set relevant indexes
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'blocked_tx_hashes',
value: ["0xdefault"],
); // A list of transaction hashes to represent frozen utxos in wallet
// initialize address book entries
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'addressBookEntries',
value: <String, String>{});
await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
// Generate and add addresses to relevant arrays
final initialReceivingAddress = await _generateAddressForChain(0, 0);
// final initialChangeAddress = await _generateAddressForChain(1, 0);
await _addToAddressesArrayForChain(initialReceivingAddress, 0);
// await _addToAddressesArrayForChain(initialChangeAddress, 1);
await db.putAddress(initialReceivingAddress);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: [initialReceivingAddress]);
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
_currentReceivingAddress = Future(() => initialReceivingAddress);
walletBase?.close();
Logging.instance
.log("initializeNew for $walletName $walletId", level: LogLevel.Info);
@ -465,10 +421,6 @@ class MoneroWallet extends CoinServiceAPI {
return data;
}
@override
// not used in xmr
Future<Decimal> get pendingBalance => throw UnimplementedError();
@override
Future<Map<String, dynamic>> prepareSend({
required String address,
@ -496,15 +448,14 @@ class MoneroWallet extends CoinServiceAPI {
try {
// check for send all
bool isSendAll = false;
final balance = await availableBalance;
final satInDecimal =
Format.satoshisToAmount(satoshiAmount, coin: coin);
if (satInDecimal == balance) {
final balance = await _availableBalance;
if (satoshiAmount == balance) {
isSendAll = true;
}
Logging.instance
.log("$toAddress $satoshiAmount $args", level: LogLevel.Info);
String amountToSend = satInDecimal
String amountToSend =
Format.satoshisToAmount(satoshiAmount, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
Logging.instance
.log("$satoshiAmount $amountToSend", level: LogLevel.Info);
@ -640,28 +591,11 @@ class MoneroWallet extends CoinServiceAPI {
// walletBase!.onNewBlock = onNewBlock;
// walletBase!.onNewTransaction = onNewTransaction;
// walletBase!.syncStatusChanged = syncStatusChanged;
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: [walletInfo.address!]);
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'blocked_tx_hashes',
value: ["0xdefault"],
); // A list of transaction hashes to represent frozen utxos in wallet
// initialize address book entries
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'addressBookEntries',
value: <String, String>{});
await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
@ -669,10 +603,7 @@ class MoneroWallet extends CoinServiceAPI {
final node = await _getCurrentNode();
final host = Uri.parse(node.host).host;
await walletBase!.connectToNode(
node: Node(
uri: "$host:${node.port}",
type: WalletType.monero,
trusted: node.trusted ?? false));
node: Node(uri: "$host:${node.port}", type: WalletType.monero));
await walletBase!.rescan(height: credentials.height);
walletBase!.close();
} catch (e, s) {
@ -708,22 +639,10 @@ class MoneroWallet extends CoinServiceAPI {
),
);
final newTxData = await _fetchTransactionData();
_transactionData = Future(() => newTxData);
await _refreshTransactions();
await _updateBalance();
await _checkCurrentReceivingAddressesForTransactions();
String indexKey = "receivingIndex";
final curIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
// Use new index to derive a new receiving address
try {
final newReceivingAddress = await _generateAddressForChain(0, curIndex);
_currentReceivingAddress = Future(() => newReceivingAddress);
} catch (e, s) {
Logging.instance.log(
"Failed to call _generateAddressForChain(0, $curIndex): $e\n$s",
level: LogLevel.Error);
}
if (walletBase?.syncStatus is SyncedSyncStatus) {
refreshMutex = false;
@ -737,16 +656,6 @@ class MoneroWallet extends CoinServiceAPI {
}
}
@override
Future<String> send({
required String toAddress,
required int amount,
Map<String, String> args = const {},
}) {
// not used for xmr
throw UnimplementedError();
}
@override
Future<bool> testNetworkConnection() async {
return await walletBase?.isConnected() ?? false;
@ -781,10 +690,7 @@ class MoneroWallet extends CoinServiceAPI {
final node = await _getCurrentNode();
final host = Uri.parse(node.host).host;
await walletBase?.connectToNode(
node: Node(
uri: "$host:${node.port}",
type: WalletType.monero,
trusted: node.trusted ?? false));
node: Node(uri: "$host:${node.port}", type: WalletType.monero));
}
await walletBase?.startSync();
await refresh();
@ -816,8 +722,32 @@ class MoneroWallet extends CoinServiceAPI {
) as int? ??
0;
@override
Future<Decimal> get totalBalance async {
Future<void> _updateBalance() async {
final total = await _totalBalance;
final available = await _availableBalance;
_balance = Balance(
coin: coin,
total: total,
spendable: available,
blockedTotal: 0,
pendingSpendable: total - available,
);
await updateCachedBalance(_balance!);
}
Future<int> get _availableBalance async {
try {
int runningBalance = 0;
for (final entry in walletBase!.balance!.entries) {
runningBalance += entry.value.unlockedBalance;
}
return runningBalance;
} catch (_) {
return 0;
}
}
Future<int> get _totalBalance async {
try {
final balanceEntries = walletBase?.balance?.entries;
if (balanceEntries != null) {
@ -826,7 +756,7 @@ class MoneroWallet extends CoinServiceAPI {
bal = bal + element.value.fullBalance;
}
await _updateCachedBalance(bal);
return Format.satoshisToAmount(bal, coin: coin);
return bal;
} else {
final transactions = walletBase!.transactionHistory!.transactions;
int transactionBalance = 0;
@ -839,31 +769,20 @@ class MoneroWallet extends CoinServiceAPI {
}
await _updateCachedBalance(transactionBalance);
return Format.satoshisToAmount(transactionBalance, coin: coin);
return transactionBalance;
}
} catch (_) {
return Format.satoshisToAmount(_getCachedBalance(), coin: coin);
return _getCachedBalance();
}
}
@override
Future<TransactionData> get transactionData =>
_transactionData ??= _fetchTransactionData();
@override
// not used for xmr
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
@override
Future<void> updateNode(bool shouldRefresh) async {
final node = await _getCurrentNode();
final host = Uri.parse(node.host).host;
await walletBase?.connectToNode(
node: Node(
uri: "$host:${node.port}",
type: WalletType.monero,
trusted: node.trusted ?? false));
node: Node(uri: "$host:${node.port}", type: WalletType.monero));
// TODO: is this sync call needed? Do we need to notify ui here?
await walletBase?.startSync();
@ -885,66 +804,23 @@ class MoneroWallet extends CoinServiceAPI {
@override
String get walletId => _walletId;
/// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
/// and
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<String> _getCurrentAddressForChain(int chain) async {
// Here, we assume that chain == 1 if it isn't 0
String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
final internalChainArray = (DB.instance
.get<dynamic>(boxName: walletId, key: arrayKey)) as List<dynamic>;
return internalChainArray.last as String;
}
/// Increases the index for either the internal or external chain, depending on [chain].
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<void> _incrementAddressIndexForChain(int chain) async {
// Here we assume chain == 1 if it isn't 0
String indexKey = chain == 0 ? "receivingIndex" : "changeIndex";
final newIndex =
(DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1;
await DB.instance
.put<dynamic>(boxName: walletId, key: indexKey, value: newIndex);
}
Future<String> _generateAddressForChain(int chain, int index) async {
Future<isar_models.Address> _generateAddressForChain(
int chain,
int index,
) async {
//
String address = walletBase!.getTransactionAddress(chain, index);
return address;
}
/// Adds [address] to the relevant chain's address array, which is determined by [chain].
/// [address] - Expects a standard native segwit address
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<void> _addToAddressesArrayForChain(String address, int chain) async {
String chainArray = '';
if (chain == 0) {
chainArray = 'receivingAddresses';
} else {
chainArray = 'changeAddresses';
}
final addressArray =
DB.instance.get<dynamic>(boxName: walletId, key: chainArray);
if (addressArray == null) {
Logging.instance.log(
'Attempting to add the following to $chainArray array for chain $chain:${[
address
]}',
level: LogLevel.Info);
await DB.instance
.put<dynamic>(boxName: walletId, key: chainArray, value: [address]);
} else {
// Make a deep copy of the existing list
final List<String> newArray = [];
addressArray
.forEach((dynamic _address) => newArray.add(_address as String));
newArray.add(address); // Add the address passed into the method
await DB.instance
.put<dynamic>(boxName: walletId, key: chainArray, value: newArray);
}
return isar_models.Address(
walletId: walletId,
derivationIndex: index,
value: address,
publicKey: [],
type: isar_models.AddressType.cryptonote,
subType: chain == 0
? isar_models.AddressSubType.receiving
: isar_models.AddressSubType.change,
);
}
Future<FeeObject> _getFees() async {
@ -959,7 +835,7 @@ class MoneroWallet extends CoinServiceAPI {
);
}
Future<TransactionData> _fetchTransactionData() async {
Future<void> _refreshTransactions() async {
await walletBase!.updateTransactions();
final transactions = walletBase?.transactionHistory!.transactions;
@ -977,123 +853,75 @@ class MoneroWallet extends CoinServiceAPI {
//
// final Set<String> cachedTxids = Set<String>.from(txidsList);
// sort thing stuff
// change to get Monero price
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> midSortedArray = [];
final List<
Tuple4<isar_models.Transaction, List<isar_models.Output>,
List<isar_models.Input>, isar_models.Address?>> txnsData = [];
if (transactions != null) {
for (var tx in transactions.entries) {
// cachedTxids.add(tx.value.id);
Logging.instance.log(
"${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} "
"${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} "
"${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}"
" ${tx.value.keyIndex}",
level: LogLevel.Info);
final worthNow = (currentPrice *
Format.satoshisToAmount(
tx.value.amount!,
coin: coin,
))
.toStringAsFixed(2);
Map<String, dynamic> midSortedTx = {};
// // create final tx map
midSortedTx["txid"] = tx.value.id;
midSortedTx["confirmed_status"] = !tx.value.isPending &&
tx.value.confirmations! >= MINIMUM_CONFIRMATIONS;
midSortedTx["confirmations"] = tx.value.confirmations ?? 0;
midSortedTx["timestamp"] =
(tx.value.date.millisecondsSinceEpoch ~/ 1000);
midSortedTx["txType"] =
tx.value.direction == TransactionDirection.incoming
? "Received"
: "Sent";
midSortedTx["amount"] = tx.value.amount;
midSortedTx["worthNow"] = worthNow;
midSortedTx["worthAtBlockTimestamp"] = worthNow;
midSortedTx["fees"] = tx.value.fee;
// Logging.instance.log(
// "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} "
// "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} "
// "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}"
// " ${tx.value.keyIndex}",
// level: LogLevel.Info);
isar_models.Address? address;
isar_models.TransactionType type;
if (tx.value.direction == TransactionDirection.incoming) {
final addressInfo = tx.value.additionalInfo;
midSortedTx["address"] = walletBase?.getTransactionAddress(
final addressString = walletBase?.getTransactionAddress(
addressInfo!['accountIndex'] as int,
addressInfo['addressIndex'] as int,
);
if (addressString != null) {
address = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(addressString)
.findFirst();
}
type = isar_models.TransactionType.incoming;
} else {
midSortedTx["address"] = "";
// txn.address = "";
type = isar_models.TransactionType.outgoing;
}
final int txHeight = tx.value.height ?? 0;
midSortedTx["height"] = txHeight;
// if (txHeight >= latestTxnBlockHeight) {
// latestTxnBlockHeight = txHeight;
// }
final txn = isar_models.Transaction(
walletId: walletId,
txid: tx.value.id,
timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000),
type: type,
subType: isar_models.TransactionSubType.none,
amount: tx.value.amount ?? 0,
fee: tx.value.fee ?? 0,
height: tx.value.height,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: null,
);
midSortedTx["aliens"] = <dynamic>[];
midSortedTx["inputSize"] = 0;
midSortedTx["outputSize"] = 0;
midSortedTx["inputs"] = <dynamic>[];
midSortedTx["outputs"] = <dynamic>[];
midSortedArray.add(midSortedTx);
txnsData.add(Tuple4(txn, [], [], address));
}
}
// sort by date ----
midSortedArray
.sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int));
Logging.instance.log(midSortedArray, level: LogLevel.Info);
await db.addNewTransactionData(txnsData, walletId);
// buildDateTimeChunks
final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]};
final dateArray = <dynamic>[];
for (int i = 0; i < midSortedArray.length; i++) {
final txObject = midSortedArray[i];
final date = extractDateFromTimestamp(txObject["timestamp"] as int);
final txTimeArray = [txObject["timestamp"], date];
if (dateArray.contains(txTimeArray[1])) {
result["dateTimeChunks"].forEach((dynamic chunk) {
if (extractDateFromTimestamp(chunk["timestamp"] as int) ==
txTimeArray[1]) {
if (chunk["transactions"] == null) {
chunk["transactions"] = <Map<String, dynamic>>[];
// quick hack to notify manager to call notifyListeners if
// transactions changed
if (txnsData.isNotEmpty) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Transactions updated/added for: $walletId $walletName ",
walletId,
),
);
}
chunk["transactions"].add(txObject);
}
});
} else {
dateArray.add(txTimeArray[1]);
final chunk = {
"timestamp": txTimeArray[0],
"transactions": [txObject],
};
result["dateTimeChunks"].add(chunk);
}
}
// final transactionsMap = cachedTransactions?.getAllTransactions() ?? {};
final Map<String, Transaction> transactionsMap = {};
transactionsMap
.addAll(TransactionData.fromJson(result).getAllTransactions());
final txModel = TransactionData.fromMap(transactionsMap);
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'storedTxnDataHeight',
// value: latestTxnBlockHeight);
// await DB.instance.put<dynamic>(
// boxName: walletId, key: 'latest_tx_model', value: txModel);
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'cachedTxids',
// value: cachedTxids.toList(growable: false));
return txModel;
}
Future<String> _pathForWalletDir({
@ -1126,11 +954,12 @@ class MoneroWallet extends CoinServiceAPI {
DefaultNodes.getNodeFor(coin);
}
void onNewBlock() {
void onNewBlock({required int height, required int blocksLeft}) {
//
print("=============================");
print("New Block! :: $walletName");
print("=============================");
updateCachedChainHeight(height);
_refreshTxDataHelper();
}
@ -1176,12 +1005,12 @@ class MoneroWallet extends CoinServiceAPI {
}
Future<void> _refreshTxData() async {
final txnData = await _fetchTransactionData();
final count = txnData.getAllTransactions().length;
await _refreshTransactions();
final count = await db.getTransactions(walletId).count();
if (count > _txCount) {
_txCount = count;
_transactionData = Future(() => txnData);
await _updateBalance();
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"New transaction data found in $walletId $walletName!",
@ -1217,6 +1046,7 @@ class MoneroWallet extends CoinServiceAPI {
if (highestPercentCached < percent) {
highestPercentCached = percent;
}
await updateCachedChainHeight(height);
GlobalEventBus.instance.fire(
RefreshPercentChangedEvent(
@ -1305,23 +1135,34 @@ class MoneroWallet extends CoinServiceAPI {
}
// Check the new receiving index
String indexKey = "receivingIndex";
final curIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final currentReceiving = await _currentReceivingAddress;
final curIndex = currentReceiving?.derivationIndex ?? -1;
if (highestIndex >= curIndex) {
// First increment the receiving index
await _incrementAddressIndexForChain(0);
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final newReceivingIndex = curIndex + 1;
// Use new index to derive a new receiving address
final newReceivingAddress =
await _generateAddressForChain(0, newReceivingIndex);
// Add that new receiving address to the array of receiving addresses
await _addToAddressesArrayForChain(newReceivingAddress, 0);
final existing = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(newReceivingAddress.value)
.findFirst();
if (existing == null) {
// Add that new change address
await db.putAddress(newReceivingAddress);
} else {
// we need to update the address
await db.updateAddress(existing, newReceivingAddress);
_currentReceivingAddress = Future(() => newReceivingAddress);
// since we updated an existing address there is a chance it has
// some tx history. To prevent address reuse we will call check again
// recursively
await _checkReceivingAddressForTransactions();
}
}
} on SocketException catch (se, s) {
Logging.instance.log(
@ -1346,4 +1187,19 @@ class MoneroWallet extends CoinServiceAPI {
key: "highestPercentCached",
value: value,
);
@override
int get storedChainHeight => getCachedChainHeight();
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@override
Future<List<isar_models.Transaction>> get transactions =>
db.getTransactions(walletId).sortByTimestampDesc().findAll();
@override
// TODO: implement utxos
Future<List<isar_models.UTXO>> get utxos => throw UnimplementedError();
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,6 @@ import 'package:cw_core/wallet_type.dart';
import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
import 'package:cw_wownero/api/wallet.dart';
import 'package:cw_wownero/pending_wownero_transaction.dart';
import 'package:cw_wownero/wownero_amount_format.dart';
import 'package:cw_wownero/wownero_wallet.dart';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
@ -24,21 +23,23 @@ import 'package:flutter_libmonero/core/wallet_creation_service.dart';
import 'package:flutter_libmonero/view_model/send/output.dart'
as wownero_output;
import 'package:flutter_libmonero/wownero/wownero.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/paymint/utxo_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/price.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -48,17 +49,18 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
const int MINIMUM_CONFIRMATIONS = 10;
class WowneroWallet extends CoinServiceAPI {
final String _walletId;
final Coin _coin;
final PriceAPI _priceAPI;
final SecureStorageInterface _secureStorage;
final Prefs _prefs;
class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
late final String _walletId;
late final Coin _coin;
late final SecureStorageInterface _secureStorage;
late final Prefs _prefs;
late String _walletName;
String _walletName;
bool _shouldAutoSync = false;
bool _isConnected = false;
bool _hasCalledExit = false;
@ -71,9 +73,9 @@ class WowneroWallet extends CoinServiceAPI {
WalletCreationService? _walletCreationService;
Timer? _autoSaveTimer;
Future<String>? _currentReceivingAddress;
Future<isar_models.Address?> get _currentReceivingAddress =>
db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst();
Future<FeeObject>? _feeObject;
Future<TransactionData>? _transactionData;
Mutex prepareSendMutex = Mutex();
Mutex estimateFeeMutex = Mutex();
@ -83,34 +85,29 @@ class WowneroWallet extends CoinServiceAPI {
required String walletName,
required Coin coin,
required SecureStorageInterface secureStorage,
PriceAPI? priceAPI,
Prefs? prefs,
}) : _walletId = walletId,
_walletName = walletName,
_coin = coin,
_priceAPI = priceAPI ?? PriceAPI(Client()),
_secureStorage = secureStorage,
MainDB? mockableOverride,
}) {
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_secureStorage = secureStorage;
_prefs = prefs ?? Prefs.instance;
@override
bool get isFavorite {
try {
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
as bool;
} catch (e, s) {
Logging.instance.log(
"isFavorite fetch failed (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
initCache(walletId, coin);
isarInit(mockableOverride: mockableOverride);
}
@override
set isFavorite(bool markFavorite) {
DB.instance.put<dynamic>(
boxName: walletId, key: "isFavorite", value: markFavorite);
_isFavorite = markFavorite;
updateCachedIsFavorite(markFavorite);
}
@override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite;
@override
bool get shouldAutoSync => _shouldAutoSync;
@ -142,23 +139,6 @@ class WowneroWallet extends CoinServiceAPI {
@override
set walletName(String newName) => _walletName = newName;
@override
// not really used for wownero
Future<List<String>> get allOwnAddresses async => [];
@override
Future<Decimal> get availableBalance async {
int runningBalance = 0;
for (final entry in walletBase!.balance!.entries) {
runningBalance += entry.value.unlockedBalance;
}
return Format.satoshisToAmount(runningBalance, coin: coin);
}
@override
// not used
Future<Decimal> get balanceMinusMaxFee => throw UnimplementedError();
@override
Coin get coin => _coin;
@ -187,8 +167,9 @@ class WowneroWallet extends CoinServiceAPI {
}
@override
Future<String> get currentReceivingAddress =>
_currentReceivingAddress ??= _getCurrentAddressForChain(0);
Future<String> get currentReceivingAddress async =>
(await _currentReceivingAddress)?.value ??
(await _generateAddressForChain(0, 0)).value;
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
@ -260,30 +241,30 @@ class WowneroWallet extends CoinServiceAPI {
int maxUnusedAddressGap,
int maxNumberOfIndexesToCheck,
) async {
// clear blockchain info
await db.deleteWalletBlockchainData(walletId);
var restoreHeight = walletBase?.walletInfo.restoreHeight;
highestPercentCached = 0;
await walletBase?.rescan(height: restoreHeight);
await refresh();
}
@override
Future<bool> generateNewAddress() async {
try {
const String indexKey = "receivingIndex";
// First increment the receiving index
await _incrementAddressIndexForChain(0);
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final currentReceiving = await _currentReceivingAddress;
final newReceivingIndex = currentReceiving!.derivationIndex + 1;
// Use new index to derive a new receiving address
final newReceivingAddress =
await _generateAddressForChain(0, newReceivingIndex);
final newReceivingAddress = await _generateAddressForChain(
0,
newReceivingIndex,
);
// Add that new receiving address to the array of receiving addresses
await _addToAddressesArrayForChain(newReceivingAddress, 0);
// Set the new receiving address that the service
_currentReceivingAddress = Future(() => newReceivingAddress);
// Add that new receiving address
await db.putAddress(newReceivingAddress);
return true;
} catch (e, s) {
@ -303,7 +284,7 @@ class WowneroWallet extends CoinServiceAPI {
"Opening existing ${coin.prettyName} wallet $walletName...",
level: LogLevel.Info);
if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) {
if (getCachedId() == null) {
//todo: check if print needed
// debugPrint("Exception was thrown");
throw Exception(
@ -315,12 +296,6 @@ class WowneroWallet extends CoinServiceAPI {
keysStorage = KeyService(_secureStorage);
await _prefs.init();
// final data =
// DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model")
// as TransactionData?;
// if (data != null) {
// _transactionData = Future(() => data);
// }
String? password;
try {
@ -335,17 +310,6 @@ class WowneroWallet extends CoinServiceAPI {
"Opened existing ${coin.prettyName} wallet $walletName",
level: LogLevel.Info,
);
// Wallet already exists, triggers for a returning user
String indexKey = "receivingIndex";
final curIndex =
await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(0, curIndex);
Logging.instance.log(
"wownero address in init existing: $newReceivingAddress",
level: LogLevel.Info);
_currentReceivingAddress = Future(() => newReceivingAddress);
}
@override
@ -427,42 +391,18 @@ class WowneroWallet extends CoinServiceAPI {
await walletBase?.connectToNode(
node: Node(uri: "$host:${node.port}", type: WalletType.wownero));
await walletBase?.startSync();
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
// Set relevant indexes
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'blocked_tx_hashes',
value: ["0xdefault"],
); // A list of transaction hashes to represent frozen utxos in wallet
// initialize address book entries
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'addressBookEntries',
value: <String, String>{});
await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
// Generate and add addresses to relevant arrays
final initialReceivingAddress = await _generateAddressForChain(0, 0);
// final initialChangeAddress = await _generateAddressForChain(1, 0);
await _addToAddressesArrayForChain(initialReceivingAddress, 0);
// await _addToAddressesArrayForChain(initialChangeAddress, 1);
await db.putAddress(initialReceivingAddress);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: [initialReceivingAddress]);
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
walletBase?.close();
_currentReceivingAddress = Future(() => initialReceivingAddress);
Logging.instance
.log("initializeNew for $walletName $walletId", level: LogLevel.Info);
@ -489,10 +429,6 @@ class WowneroWallet extends CoinServiceAPI {
return data;
}
@override
// not used in wow
Future<Decimal> get pendingBalance => throw UnimplementedError();
@override
Future<Map<String, dynamic>> prepareSend({
required String address,
@ -519,16 +455,14 @@ class WowneroWallet extends CoinServiceAPI {
try {
// check for send all
bool isSendAll = false;
final balance = await availableBalance;
final satInDecimal =
Format.satoshisToAmount(satoshiAmount, coin: coin);
if (satInDecimal == balance) {
final balance = await _availableBalance;
if (satoshiAmount == balance) {
isSendAll = true;
}
Logging.instance
.log("$address $satoshiAmount $args", level: LogLevel.Info);
String amountToSend = satInDecimal
String amountToSend =
Format.satoshisToAmount(satoshiAmount, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
Logging.instance
.log("$satoshiAmount $amountToSend", level: LogLevel.Info);
@ -667,28 +601,11 @@ class WowneroWallet extends CoinServiceAPI {
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
walletBase?.close();
walletBase = wallet as WowneroWalletBase;
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: [walletInfo.address!]);
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'blocked_tx_hashes',
value: ["0xdefault"],
); // A list of transaction hashes to represent frozen utxos in wallet
// initialize address book entries
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'addressBookEntries',
value: <String, String>{});
await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
} catch (e, s) {
//todo: come back to this
debugPrint(e.toString());
@ -733,22 +650,10 @@ class WowneroWallet extends CoinServiceAPI {
),
);
final newTxData = await _fetchTransactionData();
_transactionData = Future(() => newTxData);
await _refreshTransactions();
await _updateBalance();
await _checkCurrentReceivingAddressesForTransactions();
String indexKey = "receivingIndex";
final curIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
// Use new index to derive a new receiving address
try {
final newReceivingAddress = await _generateAddressForChain(0, curIndex);
_currentReceivingAddress = Future(() => newReceivingAddress);
} catch (e, s) {
Logging.instance.log(
"Failed to call _generateAddressForChain(0, $curIndex): $e\n$s",
level: LogLevel.Error);
}
if (walletBase?.syncStatus is SyncedSyncStatus) {
refreshMutex = false;
@ -762,16 +667,6 @@ class WowneroWallet extends CoinServiceAPI {
}
}
@override
Future<String> send({
required String toAddress,
required int amount,
Map<String, String> args = const {},
}) {
// not used for xmr
throw UnimplementedError();
}
@override
Future<bool> testNetworkConnection() async {
return await walletBase?.isConnected() ?? false;
@ -835,8 +730,32 @@ class WowneroWallet extends CoinServiceAPI {
) as int? ??
0;
@override
Future<Decimal> get totalBalance async {
Future<void> _updateBalance() async {
final total = await _totalBalance;
final available = await _availableBalance;
_balance = Balance(
coin: coin,
total: total,
spendable: available,
blockedTotal: 0,
pendingSpendable: total - available,
);
await updateCachedBalance(_balance!);
}
Future<int> get _availableBalance async {
try {
int runningBalance = 0;
for (final entry in walletBase!.balance!.entries) {
runningBalance += entry.value.unlockedBalance;
}
return runningBalance;
} catch (_) {
return 0;
}
}
Future<int> get _totalBalance async {
try {
final balanceEntries = walletBase?.balance?.entries;
if (balanceEntries != null) {
@ -845,7 +764,7 @@ class WowneroWallet extends CoinServiceAPI {
bal = bal + element.value.fullBalance;
}
await _updateCachedBalance(bal);
return Format.satoshisToAmount(bal, coin: coin);
return bal;
} else {
final transactions = walletBase!.transactionHistory!.transactions;
int transactionBalance = 0;
@ -858,21 +777,13 @@ class WowneroWallet extends CoinServiceAPI {
}
await _updateCachedBalance(transactionBalance);
return Format.satoshisToAmount(transactionBalance, coin: coin);
return transactionBalance;
}
} catch (_) {
return Format.satoshisToAmount(_getCachedBalance(), coin: coin);
return _getCachedBalance();
}
}
@override
Future<TransactionData> get transactionData =>
_transactionData ??= _fetchTransactionData();
@override
// not used for xmr
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
@override
Future<void> updateNode(bool shouldRefresh) async {
final node = await _getCurrentNode();
@ -901,66 +812,23 @@ class WowneroWallet extends CoinServiceAPI {
@override
String get walletId => _walletId;
/// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
/// and
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<String> _getCurrentAddressForChain(int chain) async {
// Here, we assume that chain == 1 if it isn't 0
String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
final internalChainArray = (DB.instance
.get<dynamic>(boxName: walletId, key: arrayKey)) as List<dynamic>;
return internalChainArray.last as String;
}
/// Increases the index for either the internal or external chain, depending on [chain].
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<void> _incrementAddressIndexForChain(int chain) async {
// Here we assume chain == 1 if it isn't 0
String indexKey = chain == 0 ? "receivingIndex" : "changeIndex";
final newIndex =
(DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1;
await DB.instance
.put<dynamic>(boxName: walletId, key: indexKey, value: newIndex);
}
Future<String> _generateAddressForChain(int chain, int index) async {
Future<isar_models.Address> _generateAddressForChain(
int chain,
int index,
) async {
//
String address = walletBase!.getTransactionAddress(chain, index);
return address;
}
/// Adds [address] to the relevant chain's address array, which is determined by [chain].
/// [address] - Expects a standard native segwit address
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future<void> _addToAddressesArrayForChain(String address, int chain) async {
String chainArray = '';
if (chain == 0) {
chainArray = 'receivingAddresses';
} else {
chainArray = 'changeAddresses';
}
final addressArray =
DB.instance.get<dynamic>(boxName: walletId, key: chainArray);
if (addressArray == null) {
Logging.instance.log(
'Attempting to add the following to $chainArray array for chain $chain:${[
address
]}',
level: LogLevel.Info);
await DB.instance
.put<dynamic>(boxName: walletId, key: chainArray, value: [address]);
} else {
// Make a deep copy of the existing list
final List<String> newArray = [];
addressArray
.forEach((dynamic _address) => newArray.add(_address as String));
newArray.add(address); // Add the address passed into the method
await DB.instance
.put<dynamic>(boxName: walletId, key: chainArray, value: newArray);
}
return isar_models.Address(
walletId: walletId,
derivationIndex: index,
value: address,
publicKey: [],
type: isar_models.AddressType.cryptonote,
subType: chain == 0
? isar_models.AddressSubType.receiving
: isar_models.AddressSubType.change,
);
}
Future<FeeObject> _getFees() async {
@ -975,7 +843,7 @@ class WowneroWallet extends CoinServiceAPI {
);
}
Future<TransactionData> _fetchTransactionData() async {
Future<void> _refreshTransactions() async {
await walletBase!.updateTransactions();
final transactions = walletBase?.transactionHistory!.transactions;
@ -1011,121 +879,117 @@ class WowneroWallet extends CoinServiceAPI {
// }
// }
// sort thing stuff
// change to get Wownero price
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> midSortedArray = [];
final List<
Tuple4<isar_models.Transaction, List<isar_models.Output>,
List<isar_models.Input>, isar_models.Address?>> txnsData = [];
if (transactions != null) {
for (var tx in transactions.entries) {
// cachedTxids.add(tx.value.id);
Logging.instance.log(
"${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} "
"${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} "
"${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}"
" ${tx.value.keyIndex}",
level: LogLevel.Info);
String am = wowneroAmountToString(amount: tx.value.amount!);
final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2);
Map<String, dynamic> midSortedTx = {};
// // create final tx map
midSortedTx["txid"] = tx.value.id;
midSortedTx["confirmed_status"] = !tx.value.isPending &&
tx.value.confirmations != null &&
tx.value.confirmations! >= MINIMUM_CONFIRMATIONS;
midSortedTx["confirmations"] = tx.value.confirmations ?? 0;
midSortedTx["timestamp"] =
(tx.value.date.millisecondsSinceEpoch ~/ 1000);
midSortedTx["txType"] =
tx.value.direction == TransactionDirection.incoming
? "Received"
: "Sent";
midSortedTx["amount"] = tx.value.amount;
midSortedTx["worthNow"] = worthNow;
midSortedTx["worthAtBlockTimestamp"] = worthNow;
midSortedTx["fees"] = tx.value.fee;
// TODO: shouldn't wownero have an address I can grab
// Logging.instance.log(
// "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} "
// "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} "
// "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}"
// " ${tx.value.keyIndex}",
// level: LogLevel.Info);
// String am = wowneroAmountToString(amount: tx.value.amount!);
// final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2);
// Map<String, dynamic> midSortedTx = {};
// // // create final tx map
// midSortedTx["txid"] = tx.value.id;
// midSortedTx["confirmed_status"] = !tx.value.isPending &&
// tx.value.confirmations != null &&
// tx.value.confirmations! >= MINIMUM_CONFIRMATIONS;
// midSortedTx["confirmations"] = tx.value.confirmations ?? 0;
// midSortedTx["timestamp"] =
// (tx.value.date.millisecondsSinceEpoch ~/ 1000);
// midSortedTx["txType"] =
// tx.value.direction == TransactionDirection.incoming
// ? "Received"
// : "Sent";
// midSortedTx["amount"] = tx.value.amount;
// midSortedTx["worthNow"] = worthNow;
// midSortedTx["worthAtBlockTimestamp"] = worthNow;
// midSortedTx["fees"] = tx.value.fee;
// if (tx.value.direction == TransactionDirection.incoming) {
// final addressInfo = tx.value.additionalInfo;
//
// midSortedTx["address"] = walletBase?.getTransactionAddress(
// addressInfo!['accountIndex'] as int,
// addressInfo['addressIndex'] as int,
// );
// } else {
// midSortedTx["address"] = "";
// }
//
// final int txHeight = tx.value.height ?? 0;
// midSortedTx["height"] = txHeight;
// // if (txHeight >= latestTxnBlockHeight) {
// // latestTxnBlockHeight = txHeight;
// // }
//
// midSortedTx["aliens"] = <dynamic>[];
// midSortedTx["inputSize"] = 0;
// midSortedTx["outputSize"] = 0;
// midSortedTx["inputs"] = <dynamic>[];
// midSortedTx["outputs"] = <dynamic>[];
// midSortedArray.add(midSortedTx);
isar_models.Address? address;
isar_models.TransactionType type;
if (tx.value.direction == TransactionDirection.incoming) {
final addressInfo = tx.value.additionalInfo;
midSortedTx["address"] = walletBase?.getTransactionAddress(
final addressString = walletBase?.getTransactionAddress(
addressInfo!['accountIndex'] as int,
addressInfo['addressIndex'] as int,
);
if (addressString != null) {
address = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(addressString)
.findFirst();
}
type = isar_models.TransactionType.incoming;
} else {
midSortedTx["address"] = "";
// txn.address = "";
type = isar_models.TransactionType.outgoing;
}
final int txHeight = tx.value.height ?? 0;
midSortedTx["height"] = txHeight;
// if (txHeight >= latestTxnBlockHeight) {
// latestTxnBlockHeight = txHeight;
// }
final txn = isar_models.Transaction(
walletId: walletId,
txid: tx.value.id,
timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000),
type: type,
subType: isar_models.TransactionSubType.none,
amount: tx.value.amount ?? 0,
fee: tx.value.fee ?? 0,
height: tx.value.height,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: null,
);
midSortedTx["aliens"] = <dynamic>[];
midSortedTx["inputSize"] = 0;
midSortedTx["outputSize"] = 0;
midSortedTx["inputs"] = <dynamic>[];
midSortedTx["outputs"] = <dynamic>[];
midSortedArray.add(midSortedTx);
txnsData.add(Tuple4(txn, [], [], address));
}
}
// sort by date ----
midSortedArray
.sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int));
Logging.instance.log(midSortedArray, level: LogLevel.Info);
await db.addNewTransactionData(txnsData, walletId);
// buildDateTimeChunks
final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]};
final dateArray = <dynamic>[];
for (int i = 0; i < midSortedArray.length; i++) {
final txObject = midSortedArray[i];
final date = extractDateFromTimestamp(txObject["timestamp"] as int);
final txTimeArray = [txObject["timestamp"], date];
if (dateArray.contains(txTimeArray[1])) {
result["dateTimeChunks"].forEach((dynamic chunk) {
if (extractDateFromTimestamp(chunk["timestamp"] as int) ==
txTimeArray[1]) {
if (chunk["transactions"] == null) {
chunk["transactions"] = <Map<String, dynamic>>[];
// quick hack to notify manager to call notifyListeners if
// transactions changed
if (txnsData.isNotEmpty) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Transactions updated/added for: $walletId $walletName ",
walletId,
),
);
}
chunk["transactions"].add(txObject);
}
});
} else {
dateArray.add(txTimeArray[1]);
final chunk = {
"timestamp": txTimeArray[0],
"transactions": [txObject],
};
result["dateTimeChunks"].add(chunk);
}
}
// final transactionsMap = cachedTransactions?.getAllTransactions() ?? {};
final Map<String, Transaction> transactionsMap = {};
transactionsMap
.addAll(TransactionData.fromJson(result).getAllTransactions());
final txModel = TransactionData.fromMap(transactionsMap);
//
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'storedTxnDataHeight',
// value: latestTxnBlockHeight);
// await DB.instance.put<dynamic>(
// boxName: walletId, key: 'latest_tx_model', value: txModel);
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'cachedTxids',
// value: cachedTxids.toList(growable: false));
return txModel;
}
Future<String> _pathForWalletDir({
@ -1158,11 +1022,12 @@ class WowneroWallet extends CoinServiceAPI {
DefaultNodes.getNodeFor(coin);
}
void onNewBlock() {
void onNewBlock({required int height, required int blocksLeft}) {
//
print("=============================");
print("New Wownero Block! :: $walletName");
print("=============================");
updateCachedChainHeight(height);
_refreshTxDataHelper();
}
@ -1193,12 +1058,12 @@ class WowneroWallet extends CoinServiceAPI {
}
Future<void> _refreshTxData() async {
final txnData = await _fetchTransactionData();
final count = txnData.getAllTransactions().length;
await _refreshTransactions();
final count = await db.getTransactions(walletId).count();
if (count > _txCount) {
_txCount = count;
_transactionData = Future(() => txnData);
await _updateBalance();
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"New transaction data found in $walletId $walletName!",
@ -1249,6 +1114,7 @@ class WowneroWallet extends CoinServiceAPI {
if (highestPercentCached < percent) {
highestPercentCached = percent;
}
await updateCachedChainHeight(height);
GlobalEventBus.instance.fire(
RefreshPercentChangedEvent(
@ -1337,23 +1203,34 @@ class WowneroWallet extends CoinServiceAPI {
}
// Check the new receiving index
String indexKey = "receivingIndex";
final curIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final currentReceiving = await _currentReceivingAddress;
final curIndex = currentReceiving?.derivationIndex ?? -1;
if (highestIndex >= curIndex) {
// First increment the receiving index
await _incrementAddressIndexForChain(0);
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
final newReceivingIndex = curIndex + 1;
// Use new index to derive a new receiving address
final newReceivingAddress =
await _generateAddressForChain(0, newReceivingIndex);
// Add that new receiving address to the array of receiving addresses
await _addToAddressesArrayForChain(newReceivingAddress, 0);
final existing = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(newReceivingAddress.value)
.findFirst();
if (existing == null) {
// Add that new change address
await db.putAddress(newReceivingAddress);
} else {
// we need to update the address
await db.updateAddress(existing, newReceivingAddress);
_currentReceivingAddress = Future(() => newReceivingAddress);
// since we updated an existing address there is a chance it has
// some tx history. To prevent address reuse we will call check again
// recursively
await _checkReceivingAddressForTransactions();
}
}
} on SocketException catch (se, s) {
Logging.instance.log(
@ -1378,4 +1255,19 @@ class WowneroWallet extends CoinServiceAPI {
key: "highestPercentCached",
value: value,
);
@override
int get storedChainHeight => getCachedChainHeight();
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@override
Future<List<isar_models.Transaction>> get transactions =>
db.getTransactions(walletId).sortByTimestampDesc().findAll();
@override
// TODO: implement utxos
Future<List<isar_models.UTXO>> get utxos => throw UnimplementedError();
}

View file

@ -0,0 +1,112 @@
import 'package:stackwallet/hive/db.dart';
mixin EpicCashHive {
late final String _walletId;
void initEpicCashHive(String walletId) {
_walletId = walletId;
}
// receiving index
int? epicGetReceivingIndex() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "receivingIndex")
as int?;
}
Future<void> epicUpdateReceivingIndex(int index) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "receivingIndex",
value: index,
);
}
// change index
int? epicGetChangeIndex() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "changeIndex")
as int?;
}
Future<void> epicUpdateChangeIndex(int index) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "changeIndex",
value: index,
);
}
// slateToAddresses
Map epicGetSlatesToAddresses() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: "slate_to_address",
) as Map? ??
{};
}
Future<void> epicUpdateSlatesToAddresses(Map map) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "slate_to_address",
value: map,
);
}
// slatesToCommits
Map? epicGetSlatesToCommits() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: "slatesToCommits",
) as Map?;
}
Future<void> epicUpdateSlatesToCommits(Map map) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "slatesToCommits",
value: map,
);
}
// last scanned block
int? epicGetLastScannedBlock() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "lastScannedBlock")
as int?;
}
Future<void> epicUpdateLastScannedBlock(int blockHeight) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "lastScannedBlock",
value: blockHeight,
);
}
// epic restore height
int? epicGetRestoreHeight() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "restoreHeight")
as int?;
}
Future<void> epicUpdateRestoreHeight(int height) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "restoreHeight",
value: height,
);
}
// epic creation height
int? epicGetCreationHeight() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "creationHeight")
as int?;
}
Future<void> epicUpdateCreationHeight(int height) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "creationHeight",
value: height,
);
}
}

View file

@ -0,0 +1,50 @@
import 'package:stackwallet/hive/db.dart';
mixin FiroHive {
late final String _walletId;
void initFiroHive(String walletId) {
_walletId = walletId;
}
// jindex
List? firoGetJIndex() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "jindex") as List?;
}
Future<void> firoUpdateJIndex(List jIndex) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "jindex",
value: jIndex,
);
}
// _lelantus_coins
List? firoGetLelantusCoins() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "_lelantus_coins")
as List?;
}
Future<void> firoUpdateLelantusCoins(List lelantusCoins) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "_lelantus_coins",
value: lelantusCoins,
);
}
// mintIndex
int? firoGetMintIndex() {
return DB.instance.get<dynamic>(boxName: _walletId, key: "mintIndex")
as int?;
}
Future<void> firoUpdateMintIndex(int mintIndex) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "mintIndex",
value: mintIndex,
);
}
}

View file

@ -0,0 +1,115 @@
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
mixin WalletCache {
late final String _walletId;
late final Coin _coin;
void initCache(String walletId, Coin coin) {
_walletId = walletId;
_coin = coin;
}
// cached wallet id
String? getCachedId() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: DBKeys.id,
) as String?;
}
Future<void> updateCachedId(String? id) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: DBKeys.id,
value: id,
);
}
// cached Chain Height
int getCachedChainHeight() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: DBKeys.storedChainHeight,
) as int? ??
0;
}
Future<void> updateCachedChainHeight(int height) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: DBKeys.storedChainHeight,
value: height,
);
}
// wallet favorite flag
bool getCachedIsFavorite() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: DBKeys.isFavorite,
) as bool? ??
false;
}
Future<void> updateCachedIsFavorite(bool isFavorite) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: DBKeys.isFavorite,
value: isFavorite,
);
}
// main balance cache
Balance getCachedBalance() {
final jsonString = DB.instance.get<dynamic>(
boxName: _walletId,
key: DBKeys.cachedBalance,
) as String?;
if (jsonString == null) {
return Balance(
coin: _coin,
total: 0,
spendable: 0,
blockedTotal: 0,
pendingSpendable: 0,
);
}
return Balance.fromJson(jsonString, _coin);
}
Future<void> updateCachedBalance(Balance balance) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: DBKeys.cachedBalance,
value: balance.toJsonIgnoreCoin(),
);
}
// secondary balance cache for coins such as firo
Balance getCachedBalanceSecondary() {
final jsonString = DB.instance.get<dynamic>(
boxName: _walletId,
key: DBKeys.cachedBalanceSecondary,
) as String?;
if (jsonString == null) {
return Balance(
coin: _coin,
total: 0,
spendable: 0,
blockedTotal: 0,
pendingSpendable: 0,
);
}
return Balance.fromJson(jsonString, _coin);
}
Future<void> updateCachedBalanceSecondary(Balance balance) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: DBKeys.cachedBalanceSecondary,
value: balance.toJsonIgnoreCoin(),
);
}
}

View file

@ -0,0 +1,10 @@
import 'package:stackwallet/db/main_db.dart';
mixin WalletDB {
MainDB? _db;
MainDB get db => _db!;
void isarInit({MainDB? mockableOverride}) async {
_db = mockableOverride ?? MainDB.instance;
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_libmonero/monero/monero.dart';
import 'package:flutter_libmonero/wownero/wownero.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/notifications_service.dart';
@ -385,6 +386,9 @@ class WalletsService extends ChangeNotifier {
level: LogLevel.Info);
}
// delete wallet data in main db
await MainDB.instance.deleteWalletBlockchainData(walletId);
// box data may currently still be read/written to if wallet was refreshing
// when delete was requested so instead of deleting now we mark the wallet
// as needs delete by adding it's id to a list which gets checked on app start

Some files were not shown because too many files have changed in this diff Show more