Merge remote-tracking branch 'origin_SW/staging' into tor

# Conflicts:
#	pubspec.lock
This commit is contained in:
julian 2023-08-07 09:19:14 -06:00
commit 18e1a10ba1
197 changed files with 12141 additions and 2186 deletions
.metadata
assets
crypto_plugins
dockerfile.linux
lib
db
dto
electrumx_rpc
models
pages
pages_desktop_specific
providers/exchange
route_generator.dart
services
themes
utilities

View file

@ -4,7 +4,7 @@
# This file should be version controlled.
version:
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
revision: f92f44110e87bad5ff168335c36da6f6053036e6
channel: stable
project_type: app
@ -13,17 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: linux
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
- platform: macos
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: windows
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
create_revision: f92f44110e87bad5ff168335c36da6f6053036e6
base_revision: f92f44110e87bad5ff168335c36da6f6053036e6
# User provided section

Binary file not shown.

Binary file not shown.

BIN
assets/icon/macos-icon.png Normal file

Binary file not shown.

After

(image error) Size: 15 KiB

3
assets/svg/monkey.svg Normal file
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="M6.32099 5.11654C6.36635 4.1732 5.61228 3.38665 4.66934 3.38665H4.33832C3.26477 3.38665 2.34238 4.25531 2.34238 5.35395V13.0533C2.34238 15.0164 3.93487 16.6089 5.89793 16.6089H6.38407L6.39773 16.1229L6.42638 15.1044L6.42658 15.1044V15.0904C6.42658 12.5838 8.06723 10.4614 10.3343 9.74146C11.2194 10.8231 12.5669 11.5163 14.0747 11.5163C14.341 11.5163 14.5977 11.4793 14.8307 11.4349L17.2447 13.7633L17.2463 13.7649C18.1535 14.6318 18.6673 15.8317 18.6673 17.0892V17.1274C18.6673 17.4146 18.436 17.6459 18.1488 17.6459C17.8616 17.6459 17.6303 17.4146 17.6303 17.1274V17.0892C17.6303 16.1162 17.2343 15.1861 16.5298 14.5151L14.4197 12.5036L13.5747 11.6981V12.8655V16.1089V16.6089H14.0747H15.0932C15.3805 16.6089 15.6118 16.8402 15.6118 17.1274C15.6118 17.4146 15.3805 17.6459 15.0932 17.6459H5.92658C3.38996 17.6459 1.33398 15.5908 1.33398 13.0533V5.35395C1.33398 3.6952 2.67931 2.34962 4.33832 2.34962H4.66934C6.20478 2.34962 7.42841 3.6318 7.35595 5.16484L7.35592 5.16564C7.29694 6.45768 6.32086 7.52368 5.03701 7.70095L5.03698 7.70076L5.0232 7.70306L4.97418 7.71122C4.69124 7.74677 4.43295 7.54849 4.39441 7.26866C4.35516 6.98361 4.55436 6.72184 4.83595 6.68323L4.83596 6.68333L4.84599 6.68174L4.90097 6.67306C5.6898 6.56499 6.28431 5.90776 6.32099 5.11654ZM6.32099 5.11654C6.32098 5.11674 6.32097 5.11695 6.32096 5.11715L5.82154 5.09295L6.32101 5.11592C6.321 5.11612 6.32099 5.11633 6.32099 5.11654ZM11.4399 7.74158L11.3069 7.46881L11.0036 7.46089C10.1647 7.43901 9.48213 6.76462 9.48213 5.92368C9.48213 5.07034 10.1733 4.38712 10.9908 4.38665L11.308 4.38646C11.1303 4.68787 11.0284 5.03924 11.0284 5.41442C11.0284 6.26965 11.5569 6.99893 12.3044 7.29712C12.3563 8.22999 13.1288 8.96998 14.0747 8.96998C15.0206 8.96998 15.7931 8.22999 15.8451 7.29712C16.5926 6.99893 17.121 6.26965 17.121 5.41442C17.121 5.03925 17.0191 4.68789 16.8415 4.38648L17.13 4.38665C17.9808 4.38713 18.6673 5.07495 18.6673 5.92368C18.6673 6.76031 17.9892 7.43872 17.1176 7.46088L16.7929 7.46914L16.6685 7.76911C16.247 8.78503 15.2445 9.49776 14.0747 9.49776C13.2838 9.49776 12.5655 9.16901 12.0541 8.64197C11.812 8.38874 11.6082 8.08661 11.4399 7.74158ZM13.0562 3.38665C12.3341 3.38665 11.6999 3.76432 11.3406 4.33302L11.4483 4.08766C11.8986 3.0621 12.9068 2.34961 14.0747 2.34961C15.2444 2.34961 16.2469 3.06287 16.6685 4.07834L16.7212 4.20536C16.3516 3.7085 15.7599 3.38665 15.0932 3.38665H13.0562ZM13.0469 5.41442C13.0469 5.41314 13.0471 5.41269 13.0473 5.4122C13.0477 5.41135 13.0485 5.40988 13.0501 5.40831C13.0517 5.40674 13.0531 5.4059 13.054 5.40554C13.0542 5.40545 13.0544 5.40538 13.0546 5.40531C13.055 5.40522 13.0555 5.40517 13.0562 5.40517C13.0575 5.40517 13.0579 5.40533 13.0584 5.40554C13.0593 5.4059 13.0607 5.40674 13.0623 5.40831C13.0639 5.40988 13.0647 5.41135 13.0651 5.4122C13.0653 5.41269 13.0655 5.41314 13.0655 5.41442C13.0655 5.415 13.0654 5.41541 13.0654 5.41573C13.0653 5.41612 13.0652 5.41638 13.0651 5.41665C13.0647 5.41749 13.0639 5.41897 13.0623 5.42054C13.0607 5.4221 13.0593 5.42295 13.0584 5.42331C13.0579 5.42352 13.0575 5.42368 13.0562 5.42368C13.0556 5.42368 13.0551 5.42364 13.0548 5.42358C13.0545 5.42351 13.0542 5.42341 13.054 5.42331C13.0531 5.42295 13.0517 5.4221 13.0501 5.42054C13.0485 5.41897 13.0477 5.41749 13.0473 5.41665C13.0471 5.41616 13.0469 5.41571 13.0469 5.41442ZM15.1025 5.41442C15.1025 5.41571 15.1023 5.41616 15.1021 5.41665C15.1018 5.41749 15.1009 5.41897 15.0994 5.42054C15.0978 5.4221 15.0963 5.42295 15.0955 5.42331C15.095 5.42352 15.0945 5.42368 15.0932 5.42368C15.092 5.42368 15.0915 5.42352 15.091 5.42331C15.0902 5.42295 15.0887 5.4221 15.0871 5.42054C15.0856 5.41897 15.0847 5.41749 15.0844 5.41665C15.0843 5.41643 15.0842 5.41621 15.0841 5.41592C15.084 5.41557 15.084 5.41512 15.084 5.41442C15.084 5.41314 15.0842 5.41269 15.0844 5.4122C15.0845 5.41178 15.0848 5.41121 15.0853 5.41054C15.0857 5.40987 15.0863 5.4091 15.0871 5.40831C15.0887 5.40674 15.0902 5.4059 15.091 5.40554C15.0915 5.40533 15.092 5.40517 15.0932 5.40517C15.0939 5.40517 15.0943 5.4052 15.0946 5.40527C15.095 5.40534 15.0952 5.40543 15.0955 5.40554C15.0963 5.4059 15.0978 5.40674 15.0994 5.40831C15.1009 5.40988 15.1018 5.41135 15.1021 5.4122C15.1023 5.41269 15.1025 5.41314 15.1025 5.41442Z" fill="#8E9192" stroke="#8E9192"/>
</svg>

After

(image error) Size: 4.3 KiB

12
assets/svg/ordinal.svg Normal file
View file

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ordinal">
<path id="Rectangle 43 (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M4.99935 3.33366C4.07887 3.33366 3.33268 4.07985 3.33268 5.00033V15.0003C3.33268 15.9208 4.07887 16.667 4.99935 16.667H14.9993C15.9198 16.667 16.666 15.9208 16.666 15.0003V5.00033C16.666 4.07985 15.9198 3.33366 14.9993 3.33366H4.99935ZM1.66602 5.00033C1.66602 3.15938 3.1584 1.66699 4.99935 1.66699H14.9993C16.8403 1.66699 18.3327 3.15938 18.3327 5.00033V15.0003C18.3327 16.8413 16.8403 18.3337 14.9993 18.3337H4.99935C3.1584 18.3337 1.66602 16.8413 1.66602 15.0003V5.00033Z" fill="#8E9192"/>
<circle id="Ellipse 76" cx="6.25" cy="6.25" r="1.25" fill="#8E9192"/>
<g id="Vector">
<path d="M6.89025 11.0396L5.11402 13.704C4.74482 14.2578 5.14181 14.9996 5.80739 14.9996H9.35986C10.0254 14.9996 10.4224 14.2578 10.0532 13.704L8.277 11.0396C7.94715 10.5449 7.2201 10.5449 6.89025 11.0396Z" fill="#8E9192"/>
<path d="M10.5883 8.15695L7.76997 13.7936C7.49292 14.3476 7.89584 14.9996 8.51532 14.9996H14.1519C14.7714 14.9996 15.1743 14.3476 14.8973 13.7936L12.079 8.15694C11.7719 7.54274 10.8954 7.54274 10.5883 8.15695Z" fill="#8E9192"/>
<path d="M6.89025 11.0396L5.11402 13.704C4.74482 14.2578 5.14181 14.9996 5.80739 14.9996H9.35986C10.0254 14.9996 10.4224 14.2578 10.0532 13.704L8.277 11.0396C7.94715 10.5449 7.2201 10.5449 6.89025 11.0396Z" stroke="#8E9192"/>
<path d="M10.5883 8.15695L7.76997 13.7936C7.49292 14.3476 7.89584 14.9996 8.51532 14.9996H14.1519C14.7714 14.9996 15.1743 14.3476 14.8973 13.7936L12.079 8.15694C11.7719 7.54274 10.8954 7.54274 10.5883 8.15695Z" stroke="#8E9192"/>
</g>
</g>
</svg>

After

(image error) Size: 1.7 KiB

5
assets/svg/send.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="send">
<path id="Vector" d="M9.16537 4.9832C9.16537 5.1919 9.04104 5.3807 8.84889 5.46272L1.55925 8.60772C1.49302 8.63625 1.4232 8.65001 1.35397 8.65001C1.21334 8.65001 1.07532 8.59295 0.974398 8.48615C0.824086 8.32692 0.789503 8.09076 0.887266 7.89478L2.08827 5.49311L6.03562 4.99794L2.08801 4.50221L0.887005 2.10053C0.789292 1.90488 0.823936 1.66862 0.974235 1.50949C1.12505 1.35026 1.35837 1.30161 1.55908 1.38797L8.86373 4.51851C9.05612 4.60004 9.16537 4.78917 9.16537 4.9832Z" fill="white"/>
</g>
</svg>

After

(image error) Size: 612 B

@ -1 +1 @@
Subproject commit cd12741de19e4faef39a23b7d543a2452524990a
Subproject commit f677dec0b34d3f9fe8fce2bc8ff5c508c3f3bb9a

@ -1 +1 @@
Subproject commit ec3cf5e8e1b90e006188aa8c323d4cd52dbfa9b9
Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287

@ -1 +1 @@
Subproject commit 13f19022e8d929b215a65f775b0dda7a5881608c
Subproject commit e48952185556a10f182184fd572bcb04365f5831

View file

@ -1,17 +0,0 @@
FROM ubuntu:20.04 as base
COPY . /stack_wallet
WORKDIR /stack_wallet/scripts/linux
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git=1:2.25.1-1ubuntu3.6 make=4.2.1-1.2 curl=7.68.0-1ubuntu2.14 cargo=0.62.0ubuntu0libgit2-0ubuntu0.20.04.1 \
file=1:5.38-4 ca-certificates=20211016ubuntu0.20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 \
libclang-dev=1:10.0-50~exp1 unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 \
libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 \
libgtk-3-dev=3.24.20-0ubuntu1.1 libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 && cd .. && ./prebuild.sh && cd linux && ./build_all.sh
WORKDIR /
RUN git clone https://github.com/flutter/flutter.git -b 3.3.4
ENV PATH "$PATH:/flutter/bin"
WORKDIR /stack_wallet
RUN flutter pub get Linux && flutter build linux
ENTRYPOINT ["/bin/bash"]

View file

@ -177,6 +177,9 @@ class DB {
}
Future<Box<dynamic>> getTxCacheBox({required Coin coin}) async {
if (_txCacheBoxes[coin]?.isOpen != true) {
_txCacheBoxes.remove(coin);
}
return _txCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameTxCache(coin: coin));
}
@ -186,6 +189,9 @@ class DB {
}
Future<Box<dynamic>> getAnonymitySetCacheBox({required Coin coin}) async {
if (_setCacheBoxes[coin]?.isOpen != true) {
_setCacheBoxes.remove(coin);
}
return _setCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameSetCache(coin: coin));
}
@ -195,6 +201,9 @@ class DB {
}
Future<Box<dynamic>> getUsedSerialsCacheBox({required Coin coin}) async {
if (_usedSerialsCacheBoxes[coin]?.isOpen != true) {
_usedSerialsCacheBoxes.remove(coin);
}
return _usedSerialsCacheBoxes[coin] ??=
await Hive.openBox<dynamic>(_boxNameUsedSerialsCache(coin: coin));
}
@ -265,8 +274,12 @@ class DB {
{required dynamic key, required String boxName}) async =>
await mutex.protect(() async => await Hive.box<T>(boxName).delete(key));
Future<void> deleteAll<T>({required String boxName}) async =>
await mutex.protect(() async => await Hive.box<T>(boxName).clear());
Future<void> deleteAll<T>({required String boxName}) async {
await mutex.protect(() async {
final box = await Hive.openBox<T>(boxName);
await box.clear();
});
}
Future<void> deleteBoxFromDisk({required String boxName}) async =>
await mutex.protect(() async => await Hive.deleteBoxFromDisk(boxName));

View file

@ -15,6 +15,7 @@ import 'package:stackwallet/exceptions/main_db/main_db_exception.dart';
import 'package:stackwallet/models/isar/models/block_explorer.dart';
import 'package:stackwallet/models/isar/models/contact_entry.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/models/isar/stack_theme.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -54,6 +55,8 @@ class MainDB {
TransactionBlockExplorerSchema,
StackThemeSchema,
ContactEntrySchema,
OrdinalSchema,
LelantusCoinSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
// inspector: kDebugMode,
@ -246,7 +249,8 @@ class MainDB {
await isar.utxos.putAll(utxos);
});
Future<void> updateUTXOs(String walletId, List<UTXO> utxos) async {
Future<bool> updateUTXOs(String walletId, List<UTXO> utxos) async {
bool newUTXO = false;
await isar.writeTxn(() async {
final set = utxos.toSet();
for (final utxo in utxos) {
@ -268,12 +272,16 @@ class MainDB {
blockHash: utxo.blockHash,
),
);
} else {
newUTXO = true;
}
}
await isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await isar.utxos.putAll(set.toList());
});
return newUTXO;
}
Stream<UTXO?> watchUTXO({
@ -370,6 +378,8 @@ class MainDB {
final transactionCount = await getTransactions(walletId).count();
final addressCount = await getAddresses(walletId).count();
final utxoCount = await getUTXOs(walletId).count();
final lelantusCoinCount =
await isar.lelantusCoins.where().walletIdEqualTo(walletId).count();
await isar.writeTxn(() async {
const paginateLimit = 50;
@ -403,6 +413,18 @@ class MainDB {
.findAll();
await isar.utxos.deleteAll(utxoIds);
}
// lelantusCoins
for (int i = 0; i < lelantusCoinCount; i += paginateLimit) {
final lelantusCoinIds = await isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.offset(i)
.limit(paginateLimit)
.idProperty()
.findAll();
await isar.lelantusCoins.deleteAll(lelantusCoinIds);
}
});
}
@ -497,4 +519,15 @@ class MainDB {
isar.writeTxn(() async {
await isar.ethContracts.putAll(contracts);
});
// ========== Lelantus =======================================================
Future<int?> getHighestUsedMintIndex({required String walletId}) async {
return await isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.sortByMintIndexDesc()
.mintIndexProperty()
.findFirst();
}
}

View file

@ -46,7 +46,7 @@ class EthTokenTxExtraDTO {
),
gas: _amountFromJsonNum(map['gas']),
gasPrice: _amountFromJsonNum(map['gasPrice']),
nonce: map['nonce'] as int,
nonce: map['nonce'] as int?,
input: map['input'] as String,
gasCost: _amountFromJsonNum(map['gasCost']),
gasUsed: _amountFromJsonNum(map['gasUsed']),
@ -63,7 +63,7 @@ class EthTokenTxExtraDTO {
final Amount gas;
final Amount gasPrice;
final String input;
final int nonce;
final int? nonce;
final Amount gasCost;
final Amount gasUsed;

View file

@ -127,16 +127,16 @@ class EthTxDTO {
map['timestamp'] = timestamp;
map['from'] = from;
map['to'] = to;
map['value'] = value;
map['gas'] = gas;
map['gasPrice'] = gasPrice;
map['maxFeePerGas'] = maxFeePerGas;
map['maxPriorityFeePerGas'] = maxPriorityFeePerGas;
map['value'] = value.toString();
map['gas'] = gas.toString();
map['gasPrice'] = gasPrice.toString();
map['maxFeePerGas'] = maxFeePerGas.toString();
map['maxPriorityFeePerGas'] = maxPriorityFeePerGas.toString();
map['isError'] = isError;
map['hasToken'] = hasToken;
map['compressedTx'] = compressedTx;
map['gasCost'] = gasCost;
map['gasUsed'] = gasUsed;
map['gasCost'] = gasCost.toString();
map['gasUsed'] = gasUsed.toString();
return map;
}

View file

@ -0,0 +1,39 @@
import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
class AddressInscriptionResponse extends LitescribeResponse<AddressInscriptionResponse> {
final int status;
final String message;
final AddressInscriptionResult result;
AddressInscriptionResponse({
required this.status,
required this.message,
required this.result,
});
factory AddressInscriptionResponse.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResponse(
status: json['status'] as int,
message: json['message'] as String,
result: AddressInscriptionResult.fromJson(json['result'] as Map<String, dynamic>),
);
}
}
class AddressInscriptionResult {
final List<InscriptionData> list;
final int total;
AddressInscriptionResult({
required this.list,
required this.total,
});
factory AddressInscriptionResult.fromJson(Map<String, dynamic> json) {
return AddressInscriptionResult(
list: (json['list'] as List).map((item) => InscriptionData.fromJson(item as Map<String, dynamic>)).toList(),
total: json['total'] as int,
);
}
}

View file

@ -0,0 +1,73 @@
// inscription data from litescribe /address/inscriptions endpoint
class InscriptionData {
final String inscriptionId;
final int inscriptionNumber;
final String address;
final String preview;
final String content;
final int contentLength;
final String contentType;
final String contentBody;
final int timestamp;
final String genesisTransaction;
final String location;
final String output;
final int outputValue;
final int offset;
InscriptionData({
required this.inscriptionId,
required this.inscriptionNumber,
required this.address,
required this.preview,
required this.content,
required this.contentLength,
required this.contentType,
required this.contentBody,
required this.timestamp,
required this.genesisTransaction,
required this.location,
required this.output,
required this.outputValue,
required this.offset,
});
factory InscriptionData.fromJson(Map<String, dynamic> json) {
return InscriptionData(
inscriptionId: json['inscriptionId'] as String,
inscriptionNumber: json['inscriptionNumber'] as int,
address: json['address'] as String,
preview: json['preview'] as String,
content: json['content'] as String,
contentLength: json['contentLength'] as int,
contentType: json['contentType'] as String,
contentBody: json['contentBody'] as String,
timestamp: json['timestamp'] as int,
genesisTransaction: json['genesisTransaction'] as String,
location: json['location'] as String,
output: json['output'] as String,
outputValue: json['outputValue'] as int,
offset: json['offset'] as int,
);
}
@override
String toString() {
return 'InscriptionData {'
' inscriptionId: $inscriptionId,'
' inscriptionNumber: $inscriptionNumber,'
' address: $address,'
' preview: $preview,'
' content: $content,'
' contentLength: $contentLength,'
' contentType: $contentType,'
' contentBody: $contentBody,'
' timestamp: $timestamp,'
' genesisTransaction: $genesisTransaction,'
' location: $location,'
' output: $output,'
' outputValue: $outputValue,'
' offset: $offset'
' }';
}
}

View file

@ -0,0 +1,6 @@
class LitescribeResponse<T> {
final T? data;
final String? error;
LitescribeResponse({this.data, this.error});
}

View file

@ -164,14 +164,16 @@ class CachedElectrumX {
final _list = box.get("serials") as List?;
List<String> cachedSerials =
_list == null ? [] : List<String>.from(_list);
Set<String> cachedSerials =
_list == null ? {} : List<String>.from(_list).toSet();
final startNumber = cachedSerials.length;
final startNumber =
cachedSerials.length - 10; // 10 being some arbitrary buffer
final serials =
await electrumXClient.getUsedCoinSerials(startNumber: startNumber);
List<String> newSerials = [];
final serials = await electrumXClient.getUsedCoinSerials(
startNumber: startNumber,
);
Set<String> newSerials = {};
for (final element in (serials["serials"] as List)) {
if (!isHexadecimal(element as String)) {
@ -182,12 +184,14 @@ class CachedElectrumX {
}
cachedSerials.addAll(newSerials);
final resultingList = cachedSerials.toList();
await box.put(
"serials",
cachedSerials,
resultingList,
);
return cachedSerials;
return resultingList;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getTransaction(): $e\n$s",

View file

@ -69,13 +69,18 @@ class ElectrumX {
List<ElectrumXNode>? failovers;
int currentFailoverIndex = -1;
ElectrumX(
{required String host,
required int port,
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
JsonRPC? client}) {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
ElectrumX({
required String host,
required int port,
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
}) {
_prefs = prefs;
_host = host;
_port = port;
@ -108,9 +113,9 @@ class ElectrumX {
Future<dynamic> request({
required String command,
List<dynamic> args = const [],
Duration connectionTimeout = const Duration(seconds: 60),
String? requestID,
int retries = 2,
Duration requestTimeout = const Duration(seconds: 60),
}) async {
if (!(await _allow())) {
throw WifiOnlyException();
@ -121,26 +126,31 @@ class ElectrumX {
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} else {
_rpcClient = JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
}
try {
final requestId = requestID ?? const Uuid().v1();
final jsonArgs = json.encode(args);
final jsonRequestString =
'{"jsonrpc": "2.0", "id": "$requestId","method": "$command","params": $jsonArgs}';
final jsonRequestString = '{"jsonrpc": "2.0", '
'"id": "$requestId",'
'"method": "$command",'
'"params": $jsonArgs}';
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
final response = await _rpcClient!.request(jsonRequestString);
final response = await _rpcClient!.request(
jsonRequestString,
requestTimeout,
);
if (response.exception != null) {
throw response.exception!;
@ -159,8 +169,8 @@ class ElectrumX {
throw Exception(
"JSONRPC response\n"
" command: $command\n"
" args: $args\n"
" error: ${response.data}",
" error: ${response.data}"
" args: $args\n",
);
}
@ -174,7 +184,7 @@ class ElectrumX {
return request(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
requestID: requestID,
retries: retries - 1,
);
@ -187,7 +197,7 @@ class ElectrumX {
return request(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
requestID: requestID,
);
} else {
@ -204,7 +214,7 @@ class ElectrumX {
Future<List<Map<String, dynamic>>> batchRequest({
required String command,
required Map<String, List<dynamic>> args,
Duration connectionTimeout = const Duration(seconds: 60),
Duration requestTimeout = const Duration(seconds: 60),
int retries = 2,
}) async {
if (!(await _allow())) {
@ -216,14 +226,14 @@ class ElectrumX {
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} else {
_rpcClient = JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeout,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
}
@ -246,7 +256,8 @@ class ElectrumX {
// Logging.instance.log("batch request: $request");
// send batch request
final jsonRpcResponse = (await _rpcClient!.request(request));
final jsonRpcResponse =
(await _rpcClient!.request(request, requestTimeout));
if (jsonRpcResponse.exception != null) {
throw jsonRpcResponse.exception!;
@ -281,7 +292,7 @@ class ElectrumX {
return batchRequest(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
retries: retries - 1,
);
} else {
@ -293,7 +304,7 @@ class ElectrumX {
return batchRequest(
command: command,
args: args,
connectionTimeout: connectionTimeout,
requestTimeout: requestTimeout,
);
} else {
currentFailoverIndex = -1;
@ -310,7 +321,7 @@ class ElectrumX {
final response = await request(
requestID: requestID,
command: 'server.ping',
connectionTimeout: const Duration(seconds: 2),
requestTimeout: const Duration(seconds: 2),
retries: retryCount,
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>;
return response.keys.contains("result") && response["result"] == null;
@ -442,7 +453,7 @@ class ElectrumX {
final response = await request(
requestID: requestID,
command: 'blockchain.scripthash.get_history',
connectionTimeout: const Duration(minutes: 5),
requestTimeout: const Duration(minutes: 5),
args: [
scripthash,
],
@ -666,11 +677,13 @@ class ElectrumX {
}) async {
try {
final response = await request(
requestID: requestID,
command: 'lelantus.getusedcoinserials',
args: [
"$startNumber",
]);
requestID: requestID,
command: 'lelantus.getusedcoinserials',
args: [
"$startNumber",
],
requestTimeout: const Duration(minutes: 2),
);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);

View file

@ -79,7 +79,6 @@ class JsonRPC {
// TODO different timeout length?
req.initiateTimeout(
const Duration(seconds: 10),
onTimedOut: () {
_requestQueue.remove(req);
},
@ -88,7 +87,10 @@ class JsonRPC {
});
}
Future<JsonRPCResponse> request(String jsonRpcRequest) async {
Future<JsonRPCResponse> request(
String jsonRpcRequest,
Duration requestTimeout,
) async {
await _requestMutex.protect(() async {
if (_socket == null) {
Logging.instance.log(
@ -101,6 +103,7 @@ class JsonRPC {
final req = _JsonRPCRequest(
jsonRequest: jsonRpcRequest,
requestTimeout: requestTimeout,
completer: Completer<JsonRPCResponse>(),
);
@ -243,9 +246,14 @@ class _JsonRPCRequest {
final String jsonRequest;
final Completer<JsonRPCResponse> completer;
final Duration requestTimeout;
final List<int> _responseData = [];
_JsonRPCRequest({required this.jsonRequest, required this.completer});
_JsonRPCRequest({
required this.jsonRequest,
required this.completer,
required this.requestTimeout,
});
void appendDataAndCheckIfComplete(List<int> data) {
_responseData.addAll(data);
@ -263,11 +271,10 @@ class _JsonRPCRequest {
}
}
void initiateTimeout(
Duration timeout, {
void initiateTimeout({
VoidCallback? onTimedOut,
}) {
Future<void>.delayed(timeout).then((_) {
Future<void>.delayed(requestTimeout).then((_) {
if (!isComplete) {
try {
throw Exception("_JsonRPCRequest timed out: $jsonRequest");

View file

@ -0,0 +1,81 @@
import 'package:isar/isar.dart';
part 'lelantus_coin.g.dart';
@collection
class LelantusCoin {
Id id = Isar.autoIncrement;
@Index()
final String walletId;
final String txid;
final String value; // can't use BigInt in isar :shrug:
@Index(
unique: true,
replace: false,
composite: [
CompositeIndex("walletId"),
],
)
final int mintIndex;
final int anonymitySetId;
final bool isUsed;
final bool isJMint;
final String? otherData;
LelantusCoin({
required this.walletId,
required this.txid,
required this.value,
required this.mintIndex,
required this.anonymitySetId,
required this.isUsed,
required this.isJMint,
required this.otherData,
});
LelantusCoin copyWith({
String? walletId,
String? publicCoin,
String? txid,
String? value,
int? mintIndex,
int? anonymitySetId,
bool? isUsed,
bool? isJMint,
String? otherData,
}) {
return LelantusCoin(
walletId: walletId ?? this.walletId,
txid: txid ?? this.txid,
value: value ?? this.value,
mintIndex: mintIndex ?? this.mintIndex,
anonymitySetId: anonymitySetId ?? this.anonymitySetId,
isUsed: isUsed ?? this.isUsed,
isJMint: isJMint ?? this.isJMint,
otherData: otherData ?? this.otherData,
);
}
@override
String toString() {
return 'LelantusCoin{'
'id: $id, '
'walletId: $walletId, '
'txid: $txid, '
'value: $value, '
'mintIndex: $mintIndex, '
'anonymitySetId: $anonymitySetId, '
'otherData: $otherData, '
'isJMint: $isJMint, '
'isUsed: $isUsed'
'}';
}
}

File diff suppressed because it is too large Load diff

View file

@ -15,5 +15,6 @@ export 'blockchain_data/output.dart';
export 'blockchain_data/transaction.dart';
export 'blockchain_data/utxo.dart';
export 'ethereum/eth_contract.dart';
export 'firo_specific/lelantus_coin.dart';
export 'log.dart';
export 'transaction_note.dart';

View file

@ -0,0 +1,89 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
part 'ordinal.g.dart';
@collection
class Ordinal {
Id id = Isar.autoIncrement;
final String walletId;
@Index(unique: true, replace: true, composite: [
CompositeIndex("utxoTXID"),
CompositeIndex("utxoVOUT"),
])
final String inscriptionId;
final int inscriptionNumber;
final String content;
// following two are used to look up the UTXO object in isar combined w/ walletId
final String utxoTXID;
final int utxoVOUT;
Ordinal({
required this.walletId,
required this.inscriptionId,
required this.inscriptionNumber,
required this.content,
required this.utxoTXID,
required this.utxoVOUT,
});
factory Ordinal.fromInscriptionData(InscriptionData data, String walletId) {
return Ordinal(
walletId: walletId,
inscriptionId: data.inscriptionId,
inscriptionNumber: data.inscriptionNumber,
content: data.content,
utxoTXID: data.output.split(':')[
0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0"
utxoVOUT: int.parse(data.output.split(':')[1]),
);
}
Ordinal copyWith({
String? walletId,
String? inscriptionId,
int? inscriptionNumber,
String? content,
String? utxoTXID,
int? utxoVOUT,
}) {
return Ordinal(
walletId: walletId ?? this.walletId,
inscriptionId: inscriptionId ?? this.inscriptionId,
inscriptionNumber: inscriptionNumber ?? this.inscriptionNumber,
content: content ?? this.content,
utxoTXID: utxoTXID ?? this.utxoTXID,
utxoVOUT: utxoVOUT ?? this.utxoVOUT,
);
}
UTXO? getUTXO(MainDB db) {
return db.isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(utxoTXID)
.and()
.voutEqualTo(utxoVOUT)
.findFirstSync();
}
@override
String toString() {
return 'Ordinal {'
' walletId: $walletId,'
' inscriptionId: $inscriptionId,'
' inscriptionNumber: $inscriptionNumber,'
' content: $content,'
' utxoTXID: $utxoTXID,'
' utxoVOUT: $utxoVOUT'
' }';
}
}

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,7 @@ class StackTheme {
case "dark":
return Brightness.dark;
default:
// just return light instead of a possible crash causing error
// just return light instead of a possible crash causing error
return Brightness.light;
}
}
@ -131,8 +131,8 @@ class StackTheme {
@ignore
Color get accentColorBlue => _accentColorBlue ??= Color(
accentColorBlueInt,
);
accentColorBlueInt,
);
@ignore
Color? _accentColorBlue;
late final int accentColorBlueInt;
@ -141,8 +141,8 @@ class StackTheme {
@ignore
Color get accentColorGreen => _accentColorGreen ??= Color(
accentColorGreenInt,
);
accentColorGreenInt,
);
@ignore
Color? _accentColorGreen;
late final int accentColorGreenInt;
@ -151,8 +151,8 @@ class StackTheme {
@ignore
Color get accentColorYellow => _accentColorYellow ??= Color(
accentColorYellowInt,
);
accentColorYellowInt,
);
@ignore
Color? _accentColorYellow;
late final int accentColorYellowInt;
@ -161,8 +161,8 @@ class StackTheme {
@ignore
Color get accentColorRed => _accentColorRed ??= Color(
accentColorRedInt,
);
accentColorRedInt,
);
@ignore
Color? _accentColorRed;
late final int accentColorRedInt;
@ -171,8 +171,8 @@ class StackTheme {
@ignore
Color get accentColorOrange => _accentColorOrange ??= Color(
accentColorOrangeInt,
);
accentColorOrangeInt,
);
@ignore
Color? _accentColorOrange;
late final int accentColorOrangeInt;
@ -181,8 +181,8 @@ class StackTheme {
@ignore
Color get accentColorDark => _accentColorDark ??= Color(
accentColorDarkInt,
);
accentColorDarkInt,
);
@ignore
Color? _accentColorDark;
late final int accentColorDarkInt;
@ -191,8 +191,8 @@ class StackTheme {
@ignore
Color get shadow => _shadow ??= Color(
shadowInt,
);
shadowInt,
);
@ignore
Color? _shadow;
late final int shadowInt;
@ -201,8 +201,8 @@ class StackTheme {
@ignore
Color get textDark => _textDark ??= Color(
textDarkInt,
);
textDarkInt,
);
@ignore
Color? _textDark;
late final int textDarkInt;
@ -211,8 +211,8 @@ class StackTheme {
@ignore
Color get textDark2 => _textDark2 ??= Color(
textDark2Int,
);
textDark2Int,
);
@ignore
Color? _textDark2;
late final int textDark2Int;
@ -221,8 +221,8 @@ class StackTheme {
@ignore
Color get textDark3 => _textDark3 ??= Color(
textDark3Int,
);
textDark3Int,
);
@ignore
Color? _textDark3;
late final int textDark3Int;
@ -231,8 +231,8 @@ class StackTheme {
@ignore
Color get textSubtitle1 => _textSubtitle1 ??= Color(
textSubtitle1Int,
);
textSubtitle1Int,
);
@ignore
Color? _textSubtitle1;
late final int textSubtitle1Int;
@ -241,8 +241,8 @@ class StackTheme {
@ignore
Color get textSubtitle2 => _textSubtitle2 ??= Color(
textSubtitle2Int,
);
textSubtitle2Int,
);
@ignore
Color? _textSubtitle2;
late final int textSubtitle2Int;
@ -251,8 +251,8 @@ class StackTheme {
@ignore
Color get textSubtitle3 => _textSubtitle3 ??= Color(
textSubtitle3Int,
);
textSubtitle3Int,
);
@ignore
Color? _textSubtitle3;
late final int textSubtitle3Int;
@ -261,8 +261,8 @@ class StackTheme {
@ignore
Color get textSubtitle4 => _textSubtitle4 ??= Color(
textSubtitle4Int,
);
textSubtitle4Int,
);
@ignore
Color? _textSubtitle4;
late final int textSubtitle4Int;
@ -271,8 +271,8 @@ class StackTheme {
@ignore
Color get textSubtitle5 => _textSubtitle5 ??= Color(
textSubtitle5Int,
);
textSubtitle5Int,
);
@ignore
Color? _textSubtitle5;
late final int textSubtitle5Int;
@ -281,8 +281,8 @@ class StackTheme {
@ignore
Color get textSubtitle6 => _textSubtitle6 ??= Color(
textSubtitle6Int,
);
textSubtitle6Int,
);
@ignore
Color? _textSubtitle6;
late final int textSubtitle6Int;
@ -291,8 +291,8 @@ class StackTheme {
@ignore
Color get textWhite => _textWhite ??= Color(
textWhiteInt,
);
textWhiteInt,
);
@ignore
Color? _textWhite;
late final int textWhiteInt;
@ -301,8 +301,8 @@ class StackTheme {
@ignore
Color get textFavoriteCard => _textFavoriteCard ??= Color(
textFavoriteCardInt,
);
textFavoriteCardInt,
);
@ignore
Color? _textFavoriteCard;
late final int textFavoriteCardInt;
@ -311,8 +311,8 @@ class StackTheme {
@ignore
Color get textError => _textError ??= Color(
textErrorInt,
);
textErrorInt,
);
@ignore
Color? _textError;
late final int textErrorInt;
@ -321,8 +321,8 @@ class StackTheme {
@ignore
Color get textRestore => _textRestore ??= Color(
textRestoreInt,
);
textRestoreInt,
);
@ignore
Color? _textRestore;
late final int textRestoreInt;
@ -331,8 +331,8 @@ class StackTheme {
@ignore
Color get buttonBackPrimary => _buttonBackPrimary ??= Color(
buttonBackPrimaryInt,
);
buttonBackPrimaryInt,
);
@ignore
Color? _buttonBackPrimary;
late final int buttonBackPrimaryInt;
@ -341,8 +341,8 @@ class StackTheme {
@ignore
Color get buttonBackSecondary => _buttonBackSecondary ??= Color(
buttonBackSecondaryInt,
);
buttonBackSecondaryInt,
);
@ignore
Color? _buttonBackSecondary;
late final int buttonBackSecondaryInt;
@ -351,8 +351,8 @@ class StackTheme {
@ignore
Color get buttonBackPrimaryDisabled => _buttonBackPrimaryDisabled ??= Color(
buttonBackPrimaryDisabledInt,
);
buttonBackPrimaryDisabledInt,
);
@ignore
Color? _buttonBackPrimaryDisabled;
late final int buttonBackPrimaryDisabledInt;
@ -372,8 +372,8 @@ class StackTheme {
@ignore
Color get buttonBackBorder => _buttonBackBorder ??= Color(
buttonBackBorderInt,
);
buttonBackBorderInt,
);
@ignore
Color? _buttonBackBorder;
late final int buttonBackBorderInt;
@ -382,8 +382,8 @@ class StackTheme {
@ignore
Color get buttonBackBorderDisabled => _buttonBackBorderDisabled ??= Color(
buttonBackBorderDisabledInt,
);
buttonBackBorderDisabledInt,
);
@ignore
Color? _buttonBackBorderDisabled;
late final int buttonBackBorderDisabledInt;
@ -392,8 +392,8 @@ class StackTheme {
@ignore
Color get buttonBackBorderSecondary => _buttonBackBorderSecondary ??= Color(
buttonBackBorderSecondaryInt,
);
buttonBackBorderSecondaryInt,
);
@ignore
Color? _buttonBackBorderSecondary;
late final int buttonBackBorderSecondaryInt;
@ -413,8 +413,8 @@ class StackTheme {
@ignore
Color get numberBackDefault => _numberBackDefault ??= Color(
numberBackDefaultInt,
);
numberBackDefaultInt,
);
@ignore
Color? _numberBackDefault;
late final int numberBackDefaultInt;
@ -423,8 +423,8 @@ class StackTheme {
@ignore
Color get numpadBackDefault => _numpadBackDefault ??= Color(
numpadBackDefaultInt,
);
numpadBackDefaultInt,
);
@ignore
Color? _numpadBackDefault;
late final int numpadBackDefaultInt;
@ -433,8 +433,8 @@ class StackTheme {
@ignore
Color get bottomNavBack => _bottomNavBack ??= Color(
bottomNavBackInt,
);
bottomNavBackInt,
);
@ignore
Color? _bottomNavBack;
late final int bottomNavBackInt;
@ -443,8 +443,8 @@ class StackTheme {
@ignore
Color get buttonTextPrimary => _buttonTextPrimary ??= Color(
buttonTextPrimaryInt,
);
buttonTextPrimaryInt,
);
@ignore
Color? _buttonTextPrimary;
late final int buttonTextPrimaryInt;
@ -453,8 +453,8 @@ class StackTheme {
@ignore
Color get buttonTextSecondary => _buttonTextSecondary ??= Color(
buttonTextSecondaryInt,
);
buttonTextSecondaryInt,
);
@ignore
Color? _buttonTextSecondary;
late final int buttonTextSecondaryInt;
@ -463,8 +463,8 @@ class StackTheme {
@ignore
Color get buttonTextPrimaryDisabled => _buttonTextPrimaryDisabled ??= Color(
buttonTextPrimaryDisabledInt,
);
buttonTextPrimaryDisabledInt,
);
@ignore
Color? _buttonTextPrimaryDisabled;
late final int buttonTextPrimaryDisabledInt;
@ -1517,117 +1517,117 @@ class StackTheme {
..version = version
..assetsV1 = version == 1
? ThemeAssets.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..assetsV2 = version == 2
? ThemeAssetsV2.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..assetsV3 = version >= 3
? ThemeAssetsV3.fromJson(
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
json: Map<String, dynamic>.from(json["assets"] as Map),
themeId: json["id"] as String,
)
: null
..themeId = json["id"] as String
..name = json["name"] as String
..brightnessString = json["brightness"] as String
..backgroundInt = parseColor(json["colors"]["background"] as String)
..backgroundAppBarInt =
parseColor(json["colors"]["background_app_bar"] as String)
parseColor(json["colors"]["background_app_bar"] as String)
..gradientBackgroundString = json["colors"]["gradients"] != null
? jsonEncode(json["colors"]["gradients"])
: null
..standardBoxShadowString =
jsonEncode(json["colors"]["box_shadows"]["standard"] as Map)
jsonEncode(json["colors"]["box_shadows"]["standard"] as Map)
..homeViewButtonBarBoxShadowString =
json["colors"]["box_shadows"]["home_view_button_bar"] == null
? null
: jsonEncode(
json["colors"]["box_shadows"]["home_view_button_bar"] as Map)
json["colors"]["box_shadows"]["home_view_button_bar"] == null
? null
: jsonEncode(
json["colors"]["box_shadows"]["home_view_button_bar"] as Map)
..coinColorsJsonString = jsonEncode(json["colors"]['coin'] as Map)
..overlayInt = parseColor(json["colors"]["overlay"] as String)
..accentColorBlueInt =
parseColor(json["colors"]["accent_color_blue"] as String)
parseColor(json["colors"]["accent_color_blue"] as String)
..accentColorGreenInt =
parseColor(json["colors"]["accent_color_green"] as String)
parseColor(json["colors"]["accent_color_green"] as String)
..accentColorYellowInt =
parseColor(json["colors"]["accent_color_yellow"] as String)
parseColor(json["colors"]["accent_color_yellow"] as String)
..accentColorRedInt =
parseColor(json["colors"]["accent_color_red"] as String)
parseColor(json["colors"]["accent_color_red"] as String)
..accentColorOrangeInt =
parseColor(json["colors"]["accent_color_orange"] as String)
parseColor(json["colors"]["accent_color_orange"] as String)
..accentColorDarkInt =
parseColor(json["colors"]["accent_color_dark"] as String)
parseColor(json["colors"]["accent_color_dark"] as String)
..shadowInt = parseColor(json["colors"]["shadow"] as String)
..textDarkInt = parseColor(json["colors"]["text_dark_one"] as String)
..textDark2Int = parseColor(json["colors"]["text_dark_two"] as String)
..textDark3Int = parseColor(json["colors"]["text_dark_three"] as String)
..textWhiteInt = parseColor(json["colors"]["text_white"] as String)
..textFavoriteCardInt =
parseColor(json["colors"]["text_favorite"] as String)
parseColor(json["colors"]["text_favorite"] as String)
..textErrorInt = parseColor(json["colors"]["text_error"] as String)
..textRestoreInt = parseColor(json["colors"]["text_restore"] as String)
..buttonBackPrimaryInt =
parseColor(json["colors"]["button_back_primary"] as String)
parseColor(json["colors"]["button_back_primary"] as String)
..buttonBackSecondaryInt =
parseColor(json["colors"]["button_back_secondary"] as String)
parseColor(json["colors"]["button_back_secondary"] as String)
..buttonBackPrimaryDisabledInt =
parseColor(json["colors"]["button_back_primary_disabled"] as String)
parseColor(json["colors"]["button_back_primary_disabled"] as String)
..buttonBackSecondaryDisabledInt =
parseColor(json["colors"]["button_back_secondary_disabled"] as String)
parseColor(json["colors"]["button_back_secondary_disabled"] as String)
..buttonBackBorderInt =
parseColor(json["colors"]["button_back_border"] as String)
parseColor(json["colors"]["button_back_border"] as String)
..buttonBackBorderDisabledInt =
parseColor(json["colors"]["button_back_border_disabled"] as String)
parseColor(json["colors"]["button_back_border_disabled"] as String)
..buttonBackBorderSecondaryInt =
parseColor(json["colors"]["button_back_border_secondary"] as String)
parseColor(json["colors"]["button_back_border_secondary"] as String)
..buttonBackBorderSecondaryDisabledInt = parseColor(
json["colors"]["button_back_border_secondary_disabled"] as String)
..numberBackDefaultInt =
parseColor(json["colors"]["number_back_default"] as String)
parseColor(json["colors"]["number_back_default"] as String)
..numpadBackDefaultInt =
parseColor(json["colors"]["numpad_back_default"] as String)
parseColor(json["colors"]["numpad_back_default"] as String)
..bottomNavBackInt =
parseColor(json["colors"]["bottom_nav_back"] as String)
parseColor(json["colors"]["bottom_nav_back"] as String)
..textSubtitle1Int =
parseColor(json["colors"]["text_subtitle_one"] as String)
parseColor(json["colors"]["text_subtitle_one"] as String)
..textSubtitle2Int =
parseColor(json["colors"]["text_subtitle_two"] as String)
parseColor(json["colors"]["text_subtitle_two"] as String)
..textSubtitle3Int =
parseColor(json["colors"]["text_subtitle_three"] as String)
parseColor(json["colors"]["text_subtitle_three"] as String)
..textSubtitle4Int =
parseColor(json["colors"]["text_subtitle_four"] as String)
parseColor(json["colors"]["text_subtitle_four"] as String)
..textSubtitle5Int =
parseColor(json["colors"]["text_subtitle_five"] as String)
parseColor(json["colors"]["text_subtitle_five"] as String)
..textSubtitle6Int =
parseColor(json["colors"]["text_subtitle_six"] as String)
parseColor(json["colors"]["text_subtitle_six"] as String)
..buttonTextPrimaryInt =
parseColor(json["colors"]["button_text_primary"] as String)
parseColor(json["colors"]["button_text_primary"] as String)
..buttonTextSecondaryInt =
parseColor(json["colors"]["button_text_secondary"] as String)
parseColor(json["colors"]["button_text_secondary"] as String)
..buttonTextPrimaryDisabledInt =
parseColor(json["colors"]["button_text_primary_disabled"] as String)
parseColor(json["colors"]["button_text_primary_disabled"] as String)
..buttonTextSecondaryDisabledInt =
parseColor(json["colors"]["button_text_secondary_disabled"] as String)
parseColor(json["colors"]["button_text_secondary_disabled"] as String)
..buttonTextBorderInt =
parseColor(json["colors"]["button_text_border"] as String)
parseColor(json["colors"]["button_text_border"] as String)
..buttonTextDisabledInt =
parseColor(json["colors"]["button_text_disabled"] as String)
parseColor(json["colors"]["button_text_disabled"] as String)
..buttonTextBorderlessInt =
parseColor(json["colors"]["button_text_borderless"] as String)
parseColor(json["colors"]["button_text_borderless"] as String)
..buttonTextBorderlessDisabledInt = parseColor(
json["colors"]["button_text_borderless_disabled"] as String)
..numberTextDefaultInt =
parseColor(json["colors"]["number_text_default"] as String)
parseColor(json["colors"]["number_text_default"] as String)
..numpadTextDefaultInt =
parseColor(json["colors"]["numpad_text_default"] as String)
parseColor(json["colors"]["numpad_text_default"] as String)
..bottomNavTextInt =
parseColor(json["colors"]["bottom_nav_text"] as String)
parseColor(json["colors"]["bottom_nav_text"] as String)
..customTextButtonEnabledTextInt = parseColor(
json["colors"]["custom_text_button_enabled_text"] as String)
..customTextButtonDisabledTextInt = parseColor(
@ -1635,87 +1635,87 @@ class StackTheme {
..switchBGOnInt = parseColor(json["colors"]["switch_bg_on"] as String)
..switchBGOffInt = parseColor(json["colors"]["switch_bg_off"] as String)
..switchBGDisabledInt =
parseColor(json["colors"]["switch_bg_disabled"] as String)
parseColor(json["colors"]["switch_bg_disabled"] as String)
..switchCircleOnInt =
parseColor(json["colors"]["switch_circle_on"] as String)
parseColor(json["colors"]["switch_circle_on"] as String)
..switchCircleOffInt =
parseColor(json["colors"]["switch_circle_off"] as String)
parseColor(json["colors"]["switch_circle_off"] as String)
..switchCircleDisabledInt =
parseColor(json["colors"]["switch_circle_disabled"] as String)
parseColor(json["colors"]["switch_circle_disabled"] as String)
..stepIndicatorBGCheckInt =
parseColor(json["colors"]["step_indicator_bg_check"] as String)
parseColor(json["colors"]["step_indicator_bg_check"] as String)
..stepIndicatorBGNumberInt =
parseColor(json["colors"]["step_indicator_bg_number"] as String)
parseColor(json["colors"]["step_indicator_bg_number"] as String)
..stepIndicatorBGInactiveInt =
parseColor(json["colors"]["step_indicator_bg_inactive"] as String)
parseColor(json["colors"]["step_indicator_bg_inactive"] as String)
..stepIndicatorBGLinesInt =
parseColor(json["colors"]["step_indicator_bg_lines"] as String)
parseColor(json["colors"]["step_indicator_bg_lines"] as String)
..stepIndicatorBGLinesInactiveInt = parseColor(
json["colors"]["step_indicator_bg_lines_inactive"] as String)
..stepIndicatorIconTextInt =
parseColor(json["colors"]["step_indicator_icon_text"] as String)
parseColor(json["colors"]["step_indicator_icon_text"] as String)
..stepIndicatorIconNumberInt =
parseColor(json["colors"]["step_indicator_icon_number"] as String)
parseColor(json["colors"]["step_indicator_icon_number"] as String)
..stepIndicatorIconInactiveInt =
parseColor(json["colors"]["step_indicator_icon_inactive"] as String)
parseColor(json["colors"]["step_indicator_icon_inactive"] as String)
..checkboxBGCheckedInt =
parseColor(json["colors"]["checkbox_bg_checked"] as String)
parseColor(json["colors"]["checkbox_bg_checked"] as String)
..checkboxBorderEmptyInt =
parseColor(json["colors"]["checkbox_border_empty"] as String)
parseColor(json["colors"]["checkbox_border_empty"] as String)
..checkboxBGDisabledInt =
parseColor(json["colors"]["checkbox_bg_disabled"] as String)
parseColor(json["colors"]["checkbox_bg_disabled"] as String)
..checkboxIconCheckedInt =
parseColor(json["colors"]["checkbox_icon_checked"] as String)
parseColor(json["colors"]["checkbox_icon_checked"] as String)
..checkboxIconDisabledInt =
parseColor(json["colors"]["checkbox_icon_disabled"] as String)
parseColor(json["colors"]["checkbox_icon_disabled"] as String)
..checkboxTextLabelInt =
parseColor(json["colors"]["checkbox_text_label"] as String)
parseColor(json["colors"]["checkbox_text_label"] as String)
..snackBarBackSuccessInt =
parseColor(json["colors"]["snack_bar_back_success"] as String)
parseColor(json["colors"]["snack_bar_back_success"] as String)
..snackBarBackErrorInt =
parseColor(json["colors"]["snack_bar_back_error"] as String)
parseColor(json["colors"]["snack_bar_back_error"] as String)
..snackBarBackInfoInt =
parseColor(json["colors"]["snack_bar_back_info"] as String)
parseColor(json["colors"]["snack_bar_back_info"] as String)
..snackBarTextSuccessInt =
parseColor(json["colors"]["snack_bar_text_success"] as String)
parseColor(json["colors"]["snack_bar_text_success"] as String)
..snackBarTextErrorInt =
parseColor(json["colors"]["snack_bar_text_error"] as String)
parseColor(json["colors"]["snack_bar_text_error"] as String)
..snackBarTextInfoInt =
parseColor(json["colors"]["snack_bar_text_info"] as String)
parseColor(json["colors"]["snack_bar_text_info"] as String)
..bottomNavIconBackInt =
parseColor(json["colors"]["bottom_nav_icon_back"] as String)
parseColor(json["colors"]["bottom_nav_icon_back"] as String)
..bottomNavIconIconInt =
parseColor(json["colors"]["bottom_nav_icon_icon"] as String)
parseColor(json["colors"]["bottom_nav_icon_icon"] as String)
..bottomNavIconIconHighlightedInt = parseColor(
json["colors"]["bottom_nav_icon_icon_highlighted"] as String)
..topNavIconPrimaryInt =
parseColor(json["colors"]["top_nav_icon_primary"] as String)
parseColor(json["colors"]["top_nav_icon_primary"] as String)
..topNavIconGreenInt =
parseColor(json["colors"]["top_nav_icon_green"] as String)
parseColor(json["colors"]["top_nav_icon_green"] as String)
..topNavIconYellowInt =
parseColor(json["colors"]["top_nav_icon_yellow"] as String)
parseColor(json["colors"]["top_nav_icon_yellow"] as String)
..topNavIconRedInt =
parseColor(json["colors"]["top_nav_icon_red"] as String)
parseColor(json["colors"]["top_nav_icon_red"] as String)
..settingsIconBackInt =
parseColor(json["colors"]["settings_icon_back"] as String)
parseColor(json["colors"]["settings_icon_back"] as String)
..settingsIconIconInt =
parseColor(json["colors"]["settings_icon_icon"] as String)
parseColor(json["colors"]["settings_icon_icon"] as String)
..settingsIconBack2Int =
parseColor(json["colors"]["settings_icon_back_two"] as String)
parseColor(json["colors"]["settings_icon_back_two"] as String)
..settingsIconElementInt =
parseColor(json["colors"]["settings_icon_element"] as String)
parseColor(json["colors"]["settings_icon_element"] as String)
..textFieldActiveBGInt =
parseColor(json["colors"]["text_field_active_bg"] as String)
parseColor(json["colors"]["text_field_active_bg"] as String)
..textFieldDefaultBGInt =
parseColor(json["colors"]["text_field_default_bg"] as String)
parseColor(json["colors"]["text_field_default_bg"] as String)
..textFieldErrorBGInt =
parseColor(json["colors"]["text_field_error_bg"] as String)
parseColor(json["colors"]["text_field_error_bg"] as String)
..textFieldSuccessBGInt =
parseColor(json["colors"]["text_field_success_bg"] as String)
parseColor(json["colors"]["text_field_success_bg"] as String)
..textFieldErrorBorderInt =
parseColor(json["colors"]["text_field_error_border"] as String)
parseColor(json["colors"]["text_field_error_border"] as String)
..textFieldSuccessBorderInt =
parseColor(json["colors"]["text_field_success_border"] as String)
parseColor(json["colors"]["text_field_success_border"] as String)
..textFieldActiveSearchIconLeftInt = parseColor(
json["colors"]["text_field_active_search_icon_left"] as String)
..textFieldDefaultSearchIconLeftInt = parseColor(
@ -1725,19 +1725,19 @@ class StackTheme {
..textFieldSuccessSearchIconLeftInt = parseColor(
json["colors"]["text_field_success_search_icon_left"] as String)
..textFieldActiveTextInt =
parseColor(json["colors"]["text_field_active_text"] as String)
parseColor(json["colors"]["text_field_active_text"] as String)
..textFieldDefaultTextInt =
parseColor(json["colors"]["text_field_default_text"] as String)
parseColor(json["colors"]["text_field_default_text"] as String)
..textFieldErrorTextInt =
parseColor(json["colors"]["text_field_error_text"] as String)
parseColor(json["colors"]["text_field_error_text"] as String)
..textFieldSuccessTextInt =
parseColor(json["colors"]["text_field_success_text"] as String)
parseColor(json["colors"]["text_field_success_text"] as String)
..textFieldActiveLabelInt =
parseColor(json["colors"]["text_field_active_label"] as String)
parseColor(json["colors"]["text_field_active_label"] as String)
..textFieldErrorLabelInt =
parseColor(json["colors"]["text_field_error_label"] as String)
parseColor(json["colors"]["text_field_error_label"] as String)
..textFieldSuccessLabelInt =
parseColor(json["colors"]["text_field_success_label"] as String)
parseColor(json["colors"]["text_field_success_label"] as String)
..textFieldActiveSearchIconRightInt = parseColor(
json["colors"]["text_field_active_search_icon_right"] as String)
..textFieldDefaultSearchIconRightInt = parseColor(
@ -1753,61 +1753,61 @@ class StackTheme {
..settingsItem2ActiveSubInt = parseColor(
json["colors"]["settings_item_level_two_active_sub"] as String)
..radioButtonIconBorderInt =
parseColor(json["colors"]["radio_button_icon_border"] as String)
parseColor(json["colors"]["radio_button_icon_border"] as String)
..radioButtonIconBorderDisabledInt = parseColor(
json["colors"]["radio_button_icon_border_disabled"] as String)
..radioButtonBorderEnabledInt =
parseColor(json["colors"]["radio_button_border_enabled"] as String)
parseColor(json["colors"]["radio_button_border_enabled"] as String)
..radioButtonBorderDisabledInt =
parseColor(json["colors"]["radio_button_border_disabled"] as String)
parseColor(json["colors"]["radio_button_border_disabled"] as String)
..radioButtonIconCircleInt =
parseColor(json["colors"]["radio_button_icon_circle"] as String)
parseColor(json["colors"]["radio_button_icon_circle"] as String)
..radioButtonIconEnabledInt =
parseColor(json["colors"]["radio_button_icon_enabled"] as String)
parseColor(json["colors"]["radio_button_icon_enabled"] as String)
..radioButtonTextEnabledInt =
parseColor(json["colors"]["radio_button_text_enabled"] as String)
parseColor(json["colors"]["radio_button_text_enabled"] as String)
..radioButtonTextDisabledInt =
parseColor(json["colors"]["radio_button_text_disabled"] as String)
parseColor(json["colors"]["radio_button_text_disabled"] as String)
..radioButtonLabelEnabledInt =
parseColor(json["colors"]["radio_button_label_enabled"] as String)
parseColor(json["colors"]["radio_button_label_enabled"] as String)
..radioButtonLabelDisabledInt =
parseColor(json["colors"]["radio_button_label_disabled"] as String)
parseColor(json["colors"]["radio_button_label_disabled"] as String)
..infoItemBGInt = parseColor(json["colors"]["info_item_bg"] as String)
..infoItemLabelInt =
parseColor(json["colors"]["info_item_label"] as String)
parseColor(json["colors"]["info_item_label"] as String)
..infoItemTextInt = parseColor(json["colors"]["info_item_text"] as String)
..infoItemIconsInt =
parseColor(json["colors"]["info_item_icons"] as String)
parseColor(json["colors"]["info_item_icons"] as String)
..popupBGInt = parseColor(json["colors"]["popup_bg"] as String)
..currencyListItemBGInt =
parseColor(json["colors"]["currency_list_item_bg"] as String)
parseColor(json["colors"]["currency_list_item_bg"] as String)
..stackWalletBGInt = parseColor(json["colors"]["sw_bg"] as String)
..stackWalletMidInt = parseColor(json["colors"]["sw_mid"] as String)
..stackWalletBottomInt = parseColor(json["colors"]["sw_bottom"] as String)
..bottomNavShadowInt =
parseColor(json["colors"]["bottom_nav_shadow"] as String)
parseColor(json["colors"]["bottom_nav_shadow"] as String)
..splashInt = parseColor(json["colors"]["splash"] as String)
..highlightInt = parseColor(json["colors"]["highlight"] as String)
..warningForegroundInt =
parseColor(json["colors"]["warning_foreground"] as String)
parseColor(json["colors"]["warning_foreground"] as String)
..warningBackgroundInt =
parseColor(json["colors"]["warning_background"] as String)
parseColor(json["colors"]["warning_background"] as String)
..loadingOverlayTextColorInt =
parseColor(json["colors"]["loading_overlay_text_color"] as String)
parseColor(json["colors"]["loading_overlay_text_color"] as String)
..myStackContactIconBGInt =
parseColor(json["colors"]["my_stack_contact_icon_bg"] as String)
parseColor(json["colors"]["my_stack_contact_icon_bg"] as String)
..textConfirmTotalAmountInt =
parseColor(json["colors"]["text_confirm_total_amount"] as String)
parseColor(json["colors"]["text_confirm_total_amount"] as String)
..textSelectedWordTableItemInt =
parseColor(json["colors"]["text_selected_word_table_iterm"] as String)
parseColor(json["colors"]["text_selected_word_table_iterm"] as String)
..favoriteStarActiveInt =
parseColor(json["colors"]["favorite_star_active"] as String)
parseColor(json["colors"]["favorite_star_active"] as String)
..favoriteStarInactiveInt =
parseColor(json["colors"]["favorite_star_inactive"] as String)
parseColor(json["colors"]["favorite_star_inactive"] as String)
..rateTypeToggleColorOnInt =
parseColor(json["colors"]["rate_type_toggle_color_on"] as String)
parseColor(json["colors"]["rate_type_toggle_color_on"] as String)
..rateTypeToggleColorOffInt =
parseColor(json["colors"]["rate_type_toggle_color_off"] as String)
parseColor(json["colors"]["rate_type_toggle_color_off"] as String)
..rateTypeToggleDesktopColorOnInt = parseColor(
json["colors"]["rate_type_toggle_desktop_color_on"] as String)
..rateTypeToggleDesktopColorOffInt = parseColor(
@ -1815,19 +1815,19 @@ class StackTheme {
..ethTagTextInt = parseColor(json["colors"]["eth_tag_text"] as String)
..ethTagBGInt = parseColor(json["colors"]["eth_tag_bg"] as String)
..ethWalletTagTextInt =
parseColor(json["colors"]["eth_wallet_tag_text"] as String)
parseColor(json["colors"]["eth_wallet_tag_text"] as String)
..ethWalletTagBGInt =
parseColor(json["colors"]["eth_wallet_tag_bg"] as String)
parseColor(json["colors"]["eth_wallet_tag_bg"] as String)
..tokenSummaryTextPrimaryInt =
parseColor(json["colors"]["token_summary_text_primary"] as String)
parseColor(json["colors"]["token_summary_text_primary"] as String)
..tokenSummaryTextSecondaryInt =
parseColor(json["colors"]["token_summary_text_secondary"] as String)
parseColor(json["colors"]["token_summary_text_secondary"] as String)
..tokenSummaryBGInt =
parseColor(json["colors"]["token_summary_bg"] as String)
parseColor(json["colors"]["token_summary_bg"] as String)
..tokenSummaryButtonBGInt =
parseColor(json["colors"]["token_summary_button_bg"] as String)
parseColor(json["colors"]["token_summary_button_bg"] as String)
..tokenSummaryIconInt =
parseColor(json["colors"]["token_summary_icon"] as String);
parseColor(json["colors"]["token_summary_icon"] as String);
}
/// Grab the int value of the hex color string.
@ -1840,7 +1840,7 @@ class StackTheme {
} else {
throw ArgumentError(
'"$colorHex" and corresponding int '
'value "$colorValue" is not a valid color.',
'value "$colorValue" is not a valid color.',
);
}
} catch (_) {
@ -2078,18 +2078,18 @@ class ThemeAssetsV2 implements IThemeAssets {
@ignore
Map<Coin, String> get coinIcons => _coinIcons ??= parseCoinAssetsString(
coinIconsString,
placeHolder: coinPlaceholder,
);
coinIconsString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinIcons;
late final String coinIconsString;
@ignore
Map<Coin, String> get coinImages => _coinImages ??= parseCoinAssetsString(
coinImagesString,
placeHolder: coinPlaceholder,
);
coinImagesString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinImages;
late final String coinImagesString;
@ -2164,9 +2164,9 @@ class ThemeAssetsV2 implements IThemeAssets {
}
static Map<Coin, String> parseCoinAssetsString(
String jsonString, {
required String placeHolder,
}) {
String jsonString, {
required String placeHolder,
}) {
final json = jsonDecode(jsonString) as Map;
final map = Map<String, dynamic>.from(json);
@ -2341,8 +2341,6 @@ class ThemeAssetsV3 implements IThemeAssets {
// Added some future proof params in case we want to add anything else
// This should provide some buffer in stead of creating assetsV4 etc
@Name("otherStringParam1")
late final String? dummy1;
@Name("otherStringParam2")
late final String? dummy2;
@Name("otherStringParam3")
@ -2350,18 +2348,18 @@ class ThemeAssetsV3 implements IThemeAssets {
@ignore
Map<Coin, String> get coinIcons => _coinIcons ??= parseCoinAssetsString(
coinIconsString,
placeHolder: coinPlaceholder,
);
coinIconsString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinIcons;
late final String coinIconsString;
@ignore
Map<Coin, String> get coinImages => _coinImages ??= parseCoinAssetsString(
coinImagesString,
placeHolder: coinPlaceholder,
);
coinImagesString,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinImages;
late final String coinImagesString;
@ -2381,13 +2379,26 @@ class ThemeAssetsV3 implements IThemeAssets {
_coinCardImages ??= coinCardImagesString == null
? null
: parseCoinAssetsString(
coinCardImagesString!,
placeHolder: coinPlaceholder,
);
coinCardImagesString!,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinCardImages;
late final String? coinCardImagesString;
@ignore
Map<Coin, String>? get coinCardFavoritesImages =>
_coinCardFavoritesImages ??= coinCardFavoritesImagesString == null
? null
: parseCoinAssetsString(
coinCardFavoritesImagesString!,
placeHolder: coinPlaceholder,
);
@ignore
Map<Coin, String>? _coinCardFavoritesImages;
@Name("otherStringParam1")
late final String? coinCardFavoritesImagesString;
ThemeAssetsV3();
factory ThemeAssetsV3.fromJson({
@ -2439,9 +2450,15 @@ class ThemeAssetsV3 implements IThemeAssets {
)
..coinCardImagesString = json["coins"]["cards"] is Map
? createCoinAssetsString(
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["cards"] as Map),
)
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["cards"] as Map),
)
: null
..coinCardFavoritesImagesString = json["coins"]["favoriteCards"] is Map
? createCoinAssetsString(
"$themeId/assets",
Map<String, dynamic>.from(json["coins"]["favoriteCards"] as Map),
)
: null
..loadingGifRelative = json["loading_gif"] is String
? "$themeId/assets/${json["loading_gif"] as String}"
@ -2449,7 +2466,6 @@ class ThemeAssetsV3 implements IThemeAssets {
..backgroundRelative = json["background"] is String
? "$themeId/assets/${json["background"] as String}"
: null
..dummy1 = null
..dummy2 = null
..dummy3 = null;
}
@ -2483,9 +2499,9 @@ class ThemeAssetsV3 implements IThemeAssets {
}
static Map<Coin, String> parseCoinAssetsString(
String jsonString, {
required String placeHolder,
}) {
String jsonString, {
required String placeHolder,
}) {
final json = jsonDecode(jsonString) as Map;
final map = Map<String, dynamic>.from(json);
@ -2528,6 +2544,7 @@ class ThemeAssetsV3 implements IThemeAssets {
'coinImages: $coinImages, '
'coinSecondaryImages: $coinSecondaryImages, '
'coinCardImages: $coinCardImages'
'coinCardFavoritesImages: $coinCardFavoritesImages'
')';
}
}
@ -2554,4 +2571,4 @@ abstract class IThemeAssets {
String? get loadingGif;
String? get background;
}
}

View file

@ -29626,7 +29626,7 @@ int _themeAssetsV3EstimateSize(
}
}
{
final value = object.dummy1;
final value = object.coinCardFavoritesImagesString;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
@ -29677,7 +29677,7 @@ void _themeAssetsV3Serialize(
writer.writeString(offsets[7], object.coinSecondaryImagesString);
writer.writeString(offsets[8], object.exchangeRelative);
writer.writeString(offsets[9], object.loadingGifRelative);
writer.writeString(offsets[10], object.dummy1);
writer.writeString(offsets[10], object.coinCardFavoritesImagesString);
writer.writeString(offsets[11], object.dummy2);
writer.writeString(offsets[12], object.dummy3);
writer.writeString(offsets[13], object.personaEasyRelative);
@ -29714,7 +29714,7 @@ ThemeAssetsV3 _themeAssetsV3Deserialize(
object.coinSecondaryImagesString = reader.readString(offsets[7]);
object.exchangeRelative = reader.readString(offsets[8]);
object.loadingGifRelative = reader.readStringOrNull(offsets[9]);
object.dummy1 = reader.readStringOrNull(offsets[10]);
object.coinCardFavoritesImagesString = reader.readStringOrNull(offsets[10]);
object.dummy2 = reader.readStringOrNull(offsets[11]);
object.dummy3 = reader.readStringOrNull(offsets[12]);
object.personaEasyRelative = reader.readString(offsets[13]);
@ -31224,7 +31224,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1IsNull() {
coinCardFavoritesImagesStringIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'otherStringParam1',
@ -31233,7 +31233,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1IsNotNull() {
coinCardFavoritesImagesStringIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'otherStringParam1',
@ -31242,7 +31242,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1EqualTo(
coinCardFavoritesImagesStringEqualTo(
String? value, {
bool caseSensitive = true,
}) {
@ -31256,7 +31256,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1GreaterThan(
coinCardFavoritesImagesStringGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
@ -31272,7 +31272,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1LessThan(
coinCardFavoritesImagesStringLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
@ -31288,7 +31288,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1Between(
coinCardFavoritesImagesStringBetween(
String? lower,
String? upper, {
bool includeLower = true,
@ -31308,7 +31308,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1StartsWith(
coinCardFavoritesImagesStringStartsWith(
String value, {
bool caseSensitive = true,
}) {
@ -31322,7 +31322,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1EndsWith(
coinCardFavoritesImagesStringEndsWith(
String value, {
bool caseSensitive = true,
}) {
@ -31336,7 +31336,8 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1Contains(String value, {bool caseSensitive = true}) {
coinCardFavoritesImagesStringContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'otherStringParam1',
@ -31347,7 +31348,8 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1Matches(String pattern, {bool caseSensitive = true}) {
coinCardFavoritesImagesStringMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'otherStringParam1',
@ -31358,7 +31360,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1IsEmpty() {
coinCardFavoritesImagesStringIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'otherStringParam1',
@ -31368,7 +31370,7 @@ extension ThemeAssetsV3QueryFilter
}
QueryBuilder<ThemeAssetsV3, ThemeAssetsV3, QAfterFilterCondition>
dummy1IsNotEmpty() {
coinCardFavoritesImagesStringIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'otherStringParam1',

View file

@ -12,6 +12,7 @@ import 'package:hive/hive.dart';
part 'type_adaptors/lelantus_coin.g.dart';
@Deprecated("Use Isar object instead")
// @HiveType(typeId: 9)
class LelantusCoin {
// @HiveField(0)
@ -27,6 +28,7 @@ class LelantusCoin {
// @HiveField(5)
bool isUsed;
@Deprecated("Use Isar object instead")
LelantusCoin(
this.index,
this.value,

View file

@ -142,7 +142,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context,
message: "Looking up contract",
);
currentToken = response.value;
currentToken = response!.value;
if (currentToken != null) {
nameController.text = currentToken!.name;
symbolController.text = currentToken!.symbol;
@ -157,7 +157,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context,
builder: (context) => StackOkDialog(
title: "Failed to look up token",
message: response.exception?.message,
message: response!.exception?.message,
),
),
);

View file

@ -8,6 +8,8 @@
*
*/
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
import 'package:stackwallet/themes/stack_colors.dart';
@ -32,35 +34,37 @@ class CreateWalletButtonGroup extends StatelessWidget {
crossAxisAlignment:
isDesktop ? CrossAxisAlignment.center : CrossAxisAlignment.stretch,
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: isDesktop ? 70 : 0,
minWidth: isDesktop ? 480 : 0,
),
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
Navigator.of(context).pushNamed(
NameYourWalletView.routeName,
arguments: Tuple2(
AddWalletType.New,
coin,
),
);
},
child: Text(
"Create new wallet",
style: isDesktop
? STextStyles.desktopButtonEnabled(context)
: STextStyles.button(context),
if (Platform.isAndroid || coin != Coin.wownero)
ConstrainedBox(
constraints: BoxConstraints(
minHeight: isDesktop ? 70 : 0,
minWidth: isDesktop ? 480 : 0,
),
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
Navigator.of(context).pushNamed(
NameYourWalletView.routeName,
arguments: Tuple2(
AddWalletType.New,
coin,
),
);
},
child: Text(
"Create new wallet",
style: isDesktop
? STextStyles.desktopButtonEnabled(context)
: STextStyles.button(context),
),
),
),
),
SizedBox(
height: isDesktop ? 16 : 12,
),
if (Platform.isAndroid || coin != Coin.wownero)
SizedBox(
height: isDesktop ? 16 : 12,
),
ConstrainedBox(
constraints: BoxConstraints(
minHeight: isDesktop ? 70 : 0,

View file

@ -35,6 +35,7 @@ import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -160,26 +161,15 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
if (value == null) {
return null;
}
try {
// wtf Dart?????
// This turns "99999999999999999999" into 100000000000000000000.0
// final numFromLocalised = NumberFormat.decimalPattern(
// ref.read(localeServiceChangeNotifierProvider).locale)
// .parse(value);
// return Decimal.tryParse(numFromLocalised.toString());
try {
return Decimal.parse(value);
} catch (_) {
try {
return Decimal.parse(value.replaceAll(",", "."));
} catch (_) {
rethrow;
}
}
} catch (_) {
return null;
}
return AmountUnit.normal
.tryParse(
value,
locale: ref.read(localeServiceChangeNotifierProvider).locale,
coin: Coin.bitcoin, // dummy value (not used due to override)
overrideWithDecimalPlacesFromString: true,
)
?.decimal;
}
Future<AggregateCurrency> _getAggregateCurrency(Currency currency) async {
@ -809,6 +799,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
// if (_swapLock) {
_sendController.text = ref.read(efSendAmountStringProvider);
// }
if (_sendFocusNode.hasFocus) {
_sendController.selection = TextSelection.fromPosition(
TextPosition(
offset: _sendController.text.length,
),
);
}
}
});
ref.listen(efSendAmountStringProvider, (previous, String next) {
@ -820,11 +818,19 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
? "-"
: ref.read(efReceiveAmountStringProvider);
// }
if (_receiveFocusNode.hasFocus) {
_receiveController.selection = TextSelection.fromPosition(
TextPosition(
offset: _receiveController.text.length,
),
);
}
}
});
ref.listen(efEstimateProvider.notifier, (previous, next) {
final estimate = (next as StateController<Estimate?>).state;
final estimate = (next).state;
if (ref.read(efReversedProvider)) {
updateSend(estimate);
} else {

View file

@ -0,0 +1,275 @@
// import 'dart:io';
// import 'dart:typed_data';
//
// import 'package:flutter/material.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:flutter_svg/svg.dart';
// import 'package:http/http.dart' as http;
// import 'package:path_provider/path_provider.dart';
// import 'package:permission_handler/permission_handler.dart';
// import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
// import 'package:stackwallet/providers/global/wallets_provider.dart';
// import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
// import 'package:stackwallet/services/coins/manager.dart';
// import 'package:stackwallet/themes/stack_colors.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/widgets/background.dart';
// import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
// import 'package:stackwallet/widgets/desktop/secondary_button.dart';
//
// class MonkeyLoadedView extends ConsumerStatefulWidget {
// const MonkeyLoadedView({
// Key? key,
// required this.walletId,
// required this.managerProvider,
// }) : super(key: key);
//
// static const String routeName = "/hasMonkey";
// static const double navBarHeight = 65.0;
//
// final String walletId;
// final ChangeNotifierProvider<Manager> managerProvider;
//
// @override
// ConsumerState<MonkeyLoadedView> createState() => _MonkeyLoadedViewState();
// }
//
// class _MonkeyLoadedViewState extends ConsumerState<MonkeyLoadedView> {
// late final String walletId;
// late final ChangeNotifierProvider<Manager> managerProvider;
//
// String receivingAddress = "";
//
// void getMonkeySVG(String address) async {
// if (address.isEmpty) {
// //address shouldn't be empty
// return;
// }
//
// final http.Response response = await http
// .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address'));
//
// if (response.statusCode == 200) {
// final decodedResponse = response.bodyBytes;
// Directory directory = await getApplicationDocumentsDirectory();
// late Directory sampleFolder;
//
// if (Platform.isAndroid) {
// directory = Directory("/storage/emulated/0/");
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isIOS) {
// sampleFolder = Directory(directory!.path);
// } else if (Platform.isLinux) {
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isWindows) {
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isMacOS) {
// sampleFolder = Directory('${directory!.path}Documents');
// }
//
// try {
// if (!sampleFolder.existsSync()) {
// sampleFolder.createSync(recursive: true);
// }
// } catch (e, s) {
// // todo: come back to this
// debugPrint("$e $s");
// }
//
// final docPath = sampleFolder.path;
// final filePath = "$docPath/monkey.svg";
//
// File imgFile = File(filePath);
// await imgFile.writeAsBytes(decodedResponse);
// } else {
// throw Exception("Failed to get MonKey");
// }
// }
//
// void getMonkeyPNG(String address) async {
// if (address.isEmpty) {
// //address shouldn't be empty
// return;
// }
//
// final http.Response response = await http.get(Uri.parse(
// 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false'));
//
// if (response.statusCode == 200) {
// if (Platform.isAndroid) {
// await Permission.storage.request();
// }
//
// final decodedResponse = response.bodyBytes;
// Directory directory = await getApplicationDocumentsDirectory();
// late Directory sampleFolder;
//
// if (Platform.isAndroid) {
// directory = Directory("/storage/emulated/0/");
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isIOS) {
// sampleFolder = Directory(directory!.path);
// } else if (Platform.isLinux) {
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isWindows) {
// sampleFolder = Directory('${directory!.path}Documents');
// } else if (Platform.isMacOS) {
// sampleFolder = Directory('${directory!.path}Documents');
// }
//
// try {
// if (!sampleFolder.existsSync()) {
// sampleFolder.createSync(recursive: true);
// }
// } catch (e, s) {
// // todo: come back to this
// debugPrint("$e $s");
// }
//
// final docPath = sampleFolder.path;
// final filePath = "$docPath/monkey.png";
//
// File imgFile = File(filePath);
// await imgFile.writeAsBytes(decodedResponse);
// } else {
// throw Exception("Failed to get MonKey");
// }
// }
//
// @override
// void initState() {
// walletId = widget.walletId;
// managerProvider = widget.managerProvider;
//
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
// final address = await ref
// .read(walletsChangeNotifierProvider)
// .getManager(walletId)
// .currentReceivingAddress;
// setState(() {
// receivingAddress = address;
// });
// });
//
// super.initState();
// }
//
// @override
// void dispose() {
// super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
// final Coin coin = ref.watch(managerProvider.select((value) => value.coin));
// final manager = ref.watch(walletsChangeNotifierProvider
// .select((value) => value.getManager(widget.walletId)));
//
// List<int>? imageBytes;
// imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes();
//
// return Background(
// child: Stack(
// children: [
// Scaffold(
// appBar: AppBar(
// leading: AppBarBackButton(
// onPressed: () {
// Navigator.of(context).popUntil(
// ModalRoute.withName(WalletView.routeName),
// );
// },
// ),
// title: Text(
// "MonKey",
// style: STextStyles.navBarTitle(context),
// ),
// actions: [
// AspectRatio(
// aspectRatio: 1,
// child: AppBarIconButton(
// icon: SvgPicture.asset(Assets.svg.circleQuestion),
// onPressed: () {
// showDialog<dynamic>(
// context: context,
// useSafeArea: false,
// barrierDismissible: true,
// builder: (context) {
// return Dialog(
// child: Material(
// borderRadius: BorderRadius.circular(
// 20,
// ),
// child: Container(
// height: 200,
// decoration: BoxDecoration(
// color: Theme.of(context)
// .extension<StackColors>()!
// .popupBG,
// borderRadius: BorderRadius.circular(
// 20,
// ),
// ),
// child: Column(
// children: [
// Center(
// child: Text(
// "Help",
// style: STextStyles.pageTitleH2(
// context),
// ),
// )
// ],
// ),
// ),
// ),
// );
// });
// }),
// )
// ],
// ),
// body: Column(
// children: [
// const Spacer(
// flex: 1,
// ),
// if (imageBytes != null)
// Container(
// child: SvgPicture.memory(Uint8List.fromList(imageBytes!)),
// width: 300,
// height: 300,
// ),
// const Spacer(
// flex: 1,
// ),
// Padding(
// padding: const EdgeInsets.all(16.0),
// child: Column(
// children: [
// SecondaryButton(
// label: "Download as SVG",
// onPressed: () async {
// getMonkeySVG(receivingAddress);
// },
// ),
// const SizedBox(height: 12),
// SecondaryButton(
// label: "Download as PNG",
// onPressed: () {
// getMonkeyPNG(receivingAddress);
// },
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// );
// }
// }

View file

@ -0,0 +1,540 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/monkey_service.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.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_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/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class MonkeyView extends ConsumerStatefulWidget {
const MonkeyView({
Key? key,
required this.walletId,
}) : super(key: key);
static const String routeName = "/monkey";
static const double navBarHeight = 65.0;
final String walletId;
@override
ConsumerState<MonkeyView> createState() => _MonkeyViewState();
}
class _MonkeyViewState extends ConsumerState<MonkeyView> {
late final String walletId;
List<int>? imageBytes;
Future<void> _updateWalletMonKey(Uint8List monKeyBytes) async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
await (manager.wallet as BananoWallet)
.updateMonkeyImageBytes(monKeyBytes.toList());
}
Future<Directory?> _getDocsDir() async {
try {
if (Platform.isAndroid) {
return Directory("/storage/emulated/0/Documents");
}
return await getApplicationDocumentsDirectory();
} catch (_) {
return null;
}
}
String _monkeyPath = "";
Future<void> _saveMonKeyToFile({
required Uint8List bytes,
bool isPNG = false,
bool overwrite = false,
}) async {
if (Platform.isAndroid) {
await Permission.storage.request();
}
final dir = await _getDocsDir();
if (dir == null) {
throw Exception("Failed to get documents directory to save monKey image");
}
final address = await ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.currentReceivingAddress;
final docPath = dir.path;
String filePath = "$docPath/monkey_$address";
filePath += isPNG ? ".png" : ".svg";
File imgFile = File(filePath);
if (imgFile.existsSync() && !overwrite) {
throw Exception("File already exists");
}
await imgFile.writeAsBytes(bytes);
_monkeyPath = filePath;
}
@override
void initState() {
walletId = widget.walletId;
super.initState();
}
@override
Widget build(BuildContext context) {
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
final Coin coin = manager.coin;
final bool isDesktop = Util.isDesktop;
imageBytes ??= (manager.wallet as BananoWallet).getMonkeyImageBytes();
return Background(
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Expanded(
child: Row(
children: [
const SizedBox(
width: 32,
),
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,
),
const SizedBox(
width: 15,
),
SvgPicture.asset(
Assets.svg.monkey,
width: 32,
height: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
const SizedBox(
width: 12,
),
Text(
"MonKey",
style: STextStyles.desktopH3(context),
),
],
),
),
trailing: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(1000),
),
onPressed: () {
showDialog<void>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return DesktopDialog(
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"About MonKeys",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Text(
"A MonKey is a visual representation of your Banano address.",
style:
STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.all(
32,
),
child: PrimaryButton(
width: 272.5,
label: "OK",
onPressed: () {
Navigator.of(context).pop();
},
),
),
],
),
],
),
);
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 19,
horizontal: 32,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.circleQuestion,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
const SizedBox(
width: 8,
),
Text(
"What is MonKey?",
style:
STextStyles.desktopMenuItemSelected(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
)
],
),
),
),
useSpacers: false,
isCompactHeight: true,
),
body: child,
),
child: ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"MonKey",
style: STextStyles.navBarTitle(context),
),
actions: [
AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
icon: SvgPicture.asset(
Assets.svg.circleQuestion,
),
onPressed: () {
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const StackOkDialog(
title: "About MonKeys",
message:
"A MonKey is a visual representation of your Banano address.",
);
},
);
},
),
),
],
),
body: child,
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(
width: 318,
child: child,
),
child: ConditionalParent(
condition: imageBytes != null,
builder: (_) => Column(
children: [
isDesktop
? const SizedBox(
height: 50,
)
: const Spacer(
flex: 1,
),
if (imageBytes != null)
SizedBox(
width: 300,
height: 300,
child: SvgPicture.memory(Uint8List.fromList(imageBytes!)),
),
isDesktop
? const SizedBox(
height: 50,
)
: const Spacer(
flex: 1,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SecondaryButton(
label: "Save as SVG",
onPressed: () async {
bool didError = false;
await showLoading(
whileFuture: Future.wait([
_saveMonKeyToFile(
bytes: Uint8List.fromList(
(manager.wallet as BananoWallet)
.getMonkeyImageBytes()!),
),
Future<void>.delayed(
const Duration(seconds: 2),
),
]),
context: context,
isDesktop: Util.isDesktop,
message: "Saving MonKey svg",
onException: (e) {
didError = true;
String msg = e.toString();
while (msg.isNotEmpty &&
msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
},
);
if (!didError && mounted) {
await showFloatingFlushBar(
type: FlushBarType.success,
message:
"SVG MonKey image saved to $_monkeyPath",
context: context,
);
}
},
),
const SizedBox(height: 12),
SecondaryButton(
label: "Download as PNG",
onPressed: () async {
bool didError = false;
await showLoading(
whileFuture: Future.wait([
manager.currentReceivingAddress.then(
(address) async => await ref
.read(pMonKeyService)
.fetchMonKey(
address: address,
png: true,
)
.then(
(monKeyBytes) async =>
await _saveMonKeyToFile(
bytes: monKeyBytes,
isPNG: true,
),
),
),
Future<void>.delayed(
const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
message: "Downloading MonKey png",
onException: (e) {
didError = true;
String msg = e.toString();
while (msg.isNotEmpty &&
msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
},
);
if (!didError && mounted) {
await showFloatingFlushBar(
type: FlushBarType.success,
message:
"PNG MonKey image saved to $_monkeyPath",
context: context,
);
}
},
),
],
),
),
// child,
],
),
child: Column(
children: [
isDesktop
? const SizedBox(
height: 100,
)
: const Spacer(
flex: 4,
),
Center(
child: Column(
children: [
Opacity(
opacity: 0.2,
child: SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
),
width: 200,
height: 200,
),
),
const SizedBox(
height: 70,
),
Text(
"You do not have a MonKey yet. \nFetch yours now!",
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
textAlign: TextAlign.center,
),
],
),
),
isDesktop
? const SizedBox(
height: 50,
)
: const Spacer(
flex: 6,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: PrimaryButton(
label: "Fetch MonKey",
onPressed: () async {
await showLoading(
whileFuture: Future.wait([
manager.currentReceivingAddress.then(
(address) async => await ref
.read(pMonKeyService)
.fetchMonKey(address: address)
.then(
(monKeyBytes) async =>
await _updateWalletMonKey(
monKeyBytes,
),
),
),
Future<void>.delayed(const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
message: "Fetching MonKey",
subMessage: "We are fetching your MonKey",
onException: (e) {
String msg = e.toString();
while (msg.isNotEmpty &&
msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
},
);
imageBytes = (manager.wallet as BananoWallet)
.getMonkeyImageBytes();
if (imageBytes != null) {
setState(() {});
}
},
),
),
],
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,135 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_widgets/rotating_arrows.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 FetchMonkeyDialog extends StatefulWidget {
const FetchMonkeyDialog({
Key? key,
required this.onCancel,
}) : super(key: key);
final Future<void> Function() onCancel;
@override
State<FetchMonkeyDialog> createState() => _FetchMonkeyDialogState();
}
class _FetchMonkeyDialogState extends State<FetchMonkeyDialog> {
late final Future<void> Function() onCancel;
@override
void initState() {
onCancel = widget.onCancel;
super.initState();
}
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return DesktopDialog(
child: Column(
children: [
DesktopDialogCloseButton(
onPressedOverride: () async {
await onCancel.call();
if (mounted) {
Navigator.of(context).pop();
}
},
),
const Spacer(
flex: 1,
),
const RotatingArrows(
width: 40,
height: 40,
),
const Spacer(
flex: 2,
),
Text(
"Fetching MonKey",
style: STextStyles.desktopH2(context),
textAlign: TextAlign.center,
),
const SizedBox(
height: 16,
),
Text(
"We are fetching your MonKey",
style: STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
textAlign: TextAlign.center,
),
const Spacer(
flex: 2,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: SecondaryButton(
label: "Cancel",
width: 272.5,
onPressed: () async {
await onCancel.call();
if (mounted) {
Navigator.of(context).pop();
}
},
),
),
],
),
);
} else {
return WillPopScope(
onWillPop: () async {
return false;
},
child: StackDialog(
title: "Fetching MonKey",
message: "We are fetching your MonKey",
icon: const RotatingArrows(
width: 24,
height: 24,
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () async {
await onCancel.call();
if (mounted) {
Navigator.of(context).pop();
}
},
),
),
);
}
}
}

View file

@ -0,0 +1,374 @@
import 'dart:async';
import 'dart:io';
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:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class OrdinalDetailsView extends ConsumerStatefulWidget {
const OrdinalDetailsView({
Key? key,
required this.walletId,
required this.ordinal,
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
static const routeName = "/ordinalDetailsView";
@override
ConsumerState<OrdinalDetailsView> createState() => _OrdinalDetailsViewState();
}
class _OrdinalDetailsViewState extends ConsumerState<OrdinalDetailsView> {
static const _spacing = 12.0;
late final UTXO? utxo;
@override
void initState() {
utxo = widget.ordinal.getUTXO(ref.read(mainDBProvider));
super.initState();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
return Background(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: const AppBarBackButton(),
title: Text(
"Ordinal details",
style: STextStyles.navBarTitle(context),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 39,
),
child: _OrdinalImageGroup(
ordinal: widget.ordinal,
walletId: widget.walletId,
),
),
_DetailsItemWCopy(
title: "Inscription number",
data: widget.ordinal.inscriptionNumber.toString(),
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Inscription ID",
data: widget.ordinal.inscriptionId,
),
// const SizedBox(
// height: _spacing,
// ),
// // todo: add utxo status
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Amount",
data: utxo == null
? "ERROR"
: ref.watch(pAmountFormatter(coin)).format(
Amount(
rawValue: BigInt.from(utxo!.value),
fractionDigits: coin.decimals,
),
),
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Owner address",
data: utxo?.address ?? "ERROR",
),
const SizedBox(
height: _spacing,
),
_DetailsItemWCopy(
title: "Transaction ID",
data: widget.ordinal.utxoTXID,
),
const SizedBox(
height: _spacing,
),
],
),
),
),
),
),
);
}
}
class _DetailsItemWCopy extends StatelessWidget {
const _DetailsItemWCopy({
Key? key,
required this.title,
required this.data,
}) : super(key: key);
final String title;
final String data;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: STextStyles.itemSubtitle(context),
),
GestureDetector(
onTap: () async {
await Clipboard.setData(ClipboardData(text: data));
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
width: 12,
),
const SizedBox(
width: 6,
),
Text(
"Copy",
style: STextStyles.infoSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
),
],
),
),
],
),
const SizedBox(
height: 4,
),
SelectableText(
data,
style: STextStyles.itemSubtitle12(context),
),
],
),
);
}
}
class _OrdinalImageGroup extends StatelessWidget {
const _OrdinalImageGroup({
Key? key,
required this.walletId,
required this.ordinal,
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
static const _spacing = 12.0;
Future<String> _savePngToFile() async {
final response = await get(Uri.parse(ordinal.content));
if (response.statusCode != 200) {
throw Exception(
"statusCode=${response.statusCode} body=${response.bodyBytes}");
}
final bytes = response.bodyBytes;
if (Platform.isAndroid) {
await Permission.storage.request();
}
final dir = Platform.isAndroid
? Directory("/storage/emulated/0/Documents")
: await getApplicationDocumentsDirectory();
final docPath = dir.path;
final filePath = "$docPath/ordinal_${ordinal.inscriptionNumber}.png";
File imgFile = File(filePath);
if (imgFile.existsSync()) {
throw Exception("File already exists");
}
await imgFile.writeAsBytes(bytes);
return filePath;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Text(
// "${ordinal.inscriptionId}", // Use any other property you want
// style: STextStyles.w600_16(context),
// ),
// const SizedBox(
// height: _spacing,
// ),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: AspectRatio(
aspectRatio: 1,
child: Container(
color: Colors.transparent,
child: Image.network(
ordinal.content, // Use the preview URL as the image source
fit: BoxFit.cover,
filterQuality:
FilterQuality.none, // Set the filter mode to nearest
),
),
),
),
const SizedBox(
height: _spacing,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Download",
icon: SvgPicture.asset(
Assets.svg.arrowDown,
width: 10,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
onPressed: () async {
bool didError = false;
final filePath = await showLoading<String>(
whileFuture: _savePngToFile(),
context: context,
isDesktop: true,
message: "Saving ordinal image",
onException: (e) {
didError = true;
String msg = e.toString();
while (msg.isNotEmpty && msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
},
);
if (!didError && context.mounted) {
await showFloatingFlushBar(
type: FlushBarType.success,
message: "Image saved to $filePath",
context: context,
);
}
},
),
),
// const SizedBox(
// width: _spacing,
// ),
// Expanded(
// child: PrimaryButton(
// label: "Send",
// icon: SvgPicture.asset(
// Assets.svg.send,
// width: 10,
// height: 10,
// color: Theme.of(context)
// .extension<StackColors>()!
// .buttonTextPrimary,
// ),
// buttonHeight: ButtonHeight.l,
// iconSpacing: 4,
// onPressed: () async {
// final response = await showDialog<String?>(
// context: context,
// builder: (_) => const SendOrdinalUnfreezeDialog(),
// );
// if (response == "unfreeze") {
// // TODO: unfreeze and go to send ord screen
// }
// },
// ),
// ),
],
),
],
);
}
}

View file

@ -0,0 +1,889 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.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/util.dart';
import 'package:stackwallet/widgets/background.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/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class OrdinalFilter {
// final bool isMoonbird;
// final bool isPunk;
final DateTime? from;
final DateTime? to;
final String? inscription;
final String keyword;
OrdinalFilter({
// required this.isMoonbird,
// required this.isPunk,
required this.from,
required this.to,
required this.inscription,
required this.keyword,
});
OrdinalFilter copyWith({
// bool? isMoonbird,
// bool? isPunk,
DateTime? from,
DateTime? to,
String? inscription,
String? keyword,
}) {
return OrdinalFilter(
// isMoonbird: isMoonbird ?? this.isMoonbird,
// isPunk: isPunk ?? this.isPunk,
from: from ?? this.from,
to: to ?? this.to,
inscription: inscription ?? this.inscription,
keyword: keyword ?? this.keyword,
);
}
}
final ordinalFilterProvider = StateProvider<OrdinalFilter?>((_) => null);
class OrdinalsFilterView extends ConsumerStatefulWidget {
const OrdinalsFilterView({
Key? key,
}) : super(key: key);
static const String routeName = "/ordinalsFilterView";
@override
ConsumerState<OrdinalsFilterView> createState() => _OrdinalsFilterViewState();
}
class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
final _inscriptionTextEditingController = TextEditingController();
final _keywordTextEditingController = TextEditingController();
// bool _isPunk = false;
// bool _isMoonbird = false;
String _fromDateString = "";
String _toDateString = "";
final keywordTextFieldFocusNode = FocusNode();
final inscriptionTextFieldFocusNode = FocusNode();
late Color baseColor;
@override
initState() {
baseColor = ref.read(themeProvider.state).state.textSubtitle2;
final filterState = ref.read(ordinalFilterProvider.state).state;
if (filterState != null) {
// _isMoonbird = filterState.isMoonbird;
// _isPunk = filterState.isPunk;
_selectedToDate = filterState.to;
_selectedFromDate = filterState.from;
_keywordTextEditingController.text = filterState.keyword;
_inscriptionTextEditingController.text = filterState.inscription ?? "";
}
super.initState();
}
@override
dispose() {
_inscriptionTextEditingController.dispose();
_keywordTextEditingController.dispose();
keywordTextFieldFocusNode.dispose();
inscriptionTextFieldFocusNode.dispose();
super.dispose();
}
// The following two getters are not required if the
// date fields are to remain unclearable.
Widget get _dateFromText {
final isDateSelected = _fromDateString.isEmpty;
return Text(
isDateSelected ? "From..." : _fromDateString,
style: STextStyles.fieldLabel(context).copyWith(
color: isDateSelected
? Theme.of(context).extension<StackColors>()!.textSubtitle2
: Theme.of(context).extension<StackColors>()!.accentColorDark),
);
}
Widget get _dateToText {
final isDateSelected = _toDateString.isEmpty;
return Text(
isDateSelected ? "To..." : _toDateString,
style: STextStyles.fieldLabel(context).copyWith(
color: isDateSelected
? Theme.of(context).extension<StackColors>()!.textSubtitle2
: Theme.of(context).extension<StackColors>()!.accentColorDark),
);
}
DateTime? _selectedFromDate = DateTime(2007);
DateTime? _selectedToDate = DateTime.now();
MaterialRoundedDatePickerStyle _buildDatePickerStyle() {
return MaterialRoundedDatePickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
// backgroundHeader: Theme.of(context).extension<StackColors>()!.textSubtitle2,
paddingMonthHeader: const EdgeInsets.only(top: 11),
colorArrowNext: Theme.of(context).extension<StackColors>()!.textSubtitle1,
colorArrowPrevious:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
textStyleButtonNegative: STextStyles.datePicker600(context).copyWith(
color: baseColor,
),
textStyleButtonPositive: STextStyles.datePicker600(context).copyWith(
color: baseColor,
),
textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context),
textStyleDayHeader: STextStyles.datePicker600(context),
textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith(
color: baseColor,
),
textStyleDayOnCalendarDisabled:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle3,
),
textStyleDayOnCalendarSelected:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textStyleYearButton: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
// textStyleButtonAction: GoogleFonts.inter(),
);
}
MaterialRoundedYearPickerStyle _buildYearPickerStyle() {
return MaterialRoundedYearPickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
textStyleYear: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle2,
fontSize: 16,
),
textStyleYearSelected: STextStyles.datePicker600(context).copyWith(
fontSize: 18,
),
);
}
Widget _buildDateRangePicker() {
const middleSeparatorPadding = 2.0;
const middleSeparatorWidth = 12.0;
final isDesktop = Util.isDesktop;
final width = isDesktop
? null
: (MediaQuery.of(context).size.width -
(middleSeparatorWidth +
(2 * middleSeparatorPadding) +
(2 * Constants.size.standardPadding))) /
2;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
key: const Key("OrdinalsViewFromDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
initialDate: DateTime.now(),
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedFromDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedToDate != null &&
!_selectedFromDate!.isBefore(_selectedToDate!);
if (flag) {
_selectedToDate = DateTime.fromMillisecondsSinceEpoch(
_selectedFromDate!.millisecondsSinceEpoch);
}
setState(() {
if (flag) {
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
}
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
});
}
}
},
child: Container(
width: width,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
width: 1,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: isDesktop ? 17 : 12,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.calendar,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
const SizedBox(
width: 10,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: _dateFromText,
),
)
],
),
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: middleSeparatorPadding),
child: Container(
width: middleSeparatorWidth,
// height: 1,
// color: CFColors.smoke,
),
),
Expanded(
child: GestureDetector(
key: const Key("OrdinalsViewToDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
initialDate: DateTime.now(),
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedToDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedFromDate != null &&
!_selectedToDate!.isAfter(_selectedFromDate!);
if (flag) {
_selectedFromDate = DateTime.fromMillisecondsSinceEpoch(
_selectedToDate!.millisecondsSinceEpoch);
}
setState(() {
if (flag) {
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
}
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
});
}
}
},
child: Container(
width: width,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
width: 1,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: isDesktop ? 17 : 12,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.calendar,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
const SizedBox(
width: 10,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: _dateToText,
),
)
],
),
),
),
),
),
if (isDesktop)
const SizedBox(
width: 24,
),
],
);
}
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return DesktopDialog(
maxWidth: 576,
maxHeight: double.infinity,
child: Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 32,
),
child: _buildContent(context),
),
);
} else {
return Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Ordinals filter",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: EdgeInsets.symmetric(
horizontal: Constants.size.standardPadding,
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: _buildContent(context),
),
),
);
},
),
),
),
);
}
}
Widget _buildContent(BuildContext context) {
final isDesktop = Util.isDesktop;
return Column(
children: [
if (isDesktop)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Ordinals filter",
style: STextStyles.desktopH3(context),
textAlign: TextAlign.center,
),
const DesktopDialogCloseButton(),
],
),
SizedBox(
height: isDesktop ? 14 : 10,
),
// if (!isDesktop)
// Align(
// alignment: Alignment.centerLeft,
// child: FittedBox(
// child: Text(
// "Collection",
// style: STextStyles.smallMed12(context),
// ),
// ),
// ),
// if (!isDesktop)
// const SizedBox(
// height: 12,
// ),
// RoundedWhiteContainer(
// padding: EdgeInsets.all(isDesktop ? 0 : 12),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// GestureDetector(
// onTap: () {
// setState(() {
// _isPunk = !_isPunk;
// });
// },
// child: Container(
// color: Colors.transparent,
// child: Row(
// children: [
// SizedBox(
// height: 20,
// width: 20,
// child: Checkbox(
// key: const Key("OrdinalsPunkCheckboxKey"),
// materialTapTargetSize:
// MaterialTapTargetSize.shrinkWrap,
// value: _isPunk,
// onChanged: (newValue) {
// setState(() {
// _isPunk = newValue!;
// });
// },
// ),
// ),
// const SizedBox(
// width: 14,
// ),
// Align(
// alignment: Alignment.centerLeft,
// child: FittedBox(
// child: Column(
// children: [
// Text(
// "Punks",
// style: isDesktop
// ? STextStyles.desktopTextSmall(context)
// : STextStyles.itemSubtitle12(context),
// ),
// if (isDesktop)
// const SizedBox(
// height: 4,
// ),
// ],
// ),
// ),
// )
// ],
// ),
// ),
// ),
// ],
// ),
// SizedBox(
// height: isDesktop ? 4 : 10,
// ),
// Row(
// children: [
// GestureDetector(
// onTap: () {
// setState(() {
// _isMoonbird = !_isMoonbird;
// });
// },
// child: Container(
// color: Colors.transparent,
// child: Row(
// children: [
// SizedBox(
// height: 20,
// width: 20,
// child: Checkbox(
// key: const Key(
// "OrdinalsFilterMoonbirdCheckboxKey",
// ),
// materialTapTargetSize:
// MaterialTapTargetSize.shrinkWrap,
// value: _isMoonbird,
// onChanged: (newValue) {
// setState(() {
// _isMoonbird = newValue!;
// });
// },
// ),
// ),
// const SizedBox(
// width: 14,
// ),
// Align(
// alignment: Alignment.centerLeft,
// child: FittedBox(
// child: Column(
// children: [
// Text(
// "Moonbirds",
// style: isDesktop
// ? STextStyles.desktopTextSmall(context)
// : STextStyles.itemSubtitle12(context),
// ),
// if (isDesktop)
// const SizedBox(
// height: 4,
// ),
// ],
// ),
// ),
// )
// ],
// ),
// ),
// ),
// ],
// ),
// ],
// ),
// ),
// SizedBox(
// height: isDesktop ? 32 : 24,
// ),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Date",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
_buildDateRangePicker(),
SizedBox(
height: isDesktop ? 32 : 24,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Inscription",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
Padding(
padding: EdgeInsets.only(right: isDesktop ? 32 : 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("OrdinalsInscriptionFieldKey"),
controller: _inscriptionTextEditingController,
focusNode: inscriptionTextFieldFocusNode,
onChanged: (_) => setState(() {}),
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark,
height: 1.8,
)
: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter inscription number...",
keywordTextFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
)
: null,
suffixIcon: _inscriptionTextEditingController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_inscriptionTextEditingController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
SizedBox(
height: isDesktop ? 32 : 24,
),
Align(
alignment: Alignment.centerLeft,
child: FittedBox(
child: Text(
"Keyword",
style: isDesktop
? STextStyles.labelExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
),
),
SizedBox(
height: isDesktop ? 10 : 8,
),
Padding(
padding: EdgeInsets.only(right: isDesktop ? 32 : 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("OrdinalsViewKeywordFieldKey"),
controller: _keywordTextEditingController,
focusNode: keywordTextFieldFocusNode,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark,
height: 1.8,
)
: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type keyword...",
keywordTextFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
)
: null,
suffixIcon: _keywordTextEditingController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_keywordTextEditingController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
if (!isDesktop) const Spacer(),
SizedBox(
height: isDesktop ? 32 : 20,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
if (!isDesktop) {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
await _onApplyPressed();
},
label: "Save",
),
),
if (isDesktop)
const SizedBox(
width: 32,
),
],
),
if (!isDesktop)
const SizedBox(
height: 20,
),
],
);
}
Future<void> _onApplyPressed() async {
final filter = OrdinalFilter(
// isPunk: _isPunk,
// isMoonbird: _isMoonbird,
from: _selectedFromDate,
to: _selectedToDate,
inscription: _inscriptionTextEditingController.text,
keyword: _keywordTextEditingController.text,
);
ref.read(ordinalFilterProvider.state).state = filter;
Navigator.of(context).pop();
}
}

View file

@ -0,0 +1,202 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/mixins/ordinals_interface.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
class OrdinalsView extends ConsumerStatefulWidget {
const OrdinalsView({
super.key,
required this.walletId,
});
static const routeName = "/ordinalsView";
final String walletId;
@override
ConsumerState<OrdinalsView> createState() => _OrdinalsViewState();
}
class _OrdinalsViewState extends ConsumerState<OrdinalsView> {
late final TextEditingController searchController;
late final FocusNode searchFocus;
String _searchTerm = "";
@override
void initState() {
searchController = TextEditingController();
searchFocus = FocusNode();
super.initState();
}
@override
void dispose() {
searchController.dispose();
searchFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Background(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
automaticallyImplyLeading: false,
leading: const AppBarBackButton(),
title: Text(
"Ordinals",
style: STextStyles.navBarTitle(context),
),
titleSpacing: 0,
actions: [
AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
size: 36,
icon: SvgPicture.asset(
Assets.svg.arrowRotate,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () async {
// show loading for a minimum of 2 seconds on refreshing
await showLoading(
whileFuture: Future.wait<void>([
Future.delayed(const Duration(seconds: 2)),
(ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as OrdinalsInterface)
.refreshInscriptions()
]),
context: context,
message: "Refreshing...",
);
},
),
),
// AspectRatio(
// aspectRatio: 1,
// child: AppBarIconButton(
// size: 36,
// icon: SvgPicture.asset(
// Assets.svg.filter,
// width: 20,
// height: 20,
// color: Theme.of(context)
// .extension<StackColors>()!
// .topNavIconPrimary,
// ),
// onPressed: () {
// Navigator.of(context).pushNamed(
// OrdinalsFilterView.routeName,
// );
// },
// ),
// ),
],
),
body: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
),
child: Column(
children: [
// ClipRRect(
// borderRadius: BorderRadius.circular(
// Constants.size.circularBorderRadius,
// ),
// child: TextField(
// autocorrect: Util.isDesktop ? false : true,
// enableSuggestions: Util.isDesktop ? false : true,
// controller: searchController,
// focusNode: searchFocus,
// onChanged: (value) {
// setState(() {
// _searchTerm = value;
// });
// },
// style: STextStyles.field(context),
// decoration: standardInputDecoration(
// "Search",
// searchFocus,
// context,
// ).copyWith(
// prefixIcon: Padding(
// padding: const EdgeInsets.symmetric(
// horizontal: 10,
// vertical: 16,
// ),
// child: SvgPicture.asset(
// Assets.svg.search,
// width: 16,
// height: 16,
// ),
// ),
// suffixIcon: searchController.text.isNotEmpty
// ? Padding(
// padding: const EdgeInsets.only(right: 0),
// child: UnconstrainedBox(
// child: Row(
// children: [
// TextFieldIconButton(
// child: const XIcon(),
// onTap: () async {
// setState(() {
// searchController.text = "";
// _searchTerm = "";
// });
// },
// ),
// ],
// ),
// ),
// )
// : null,
// ),
// ),
// ),
// const SizedBox(
// height: 16,
// ),
Expanded(
child: OrdinalsList(
walletId: widget.walletId,
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.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 SendOrdinalUnfreezeDialog extends StatelessWidget {
const SendOrdinalUnfreezeDialog({super.key});
@override
Widget build(BuildContext context) {
return StackDialog(
title: "This ordinal is frozen",
icon: SvgPicture.asset(
Assets.svg.coinControl.blocked,
width: 24,
height: 24,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
message: "To send this ordinal, you must unfreeze it first.",
leftButton: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(context).pop,
),
rightButton: PrimaryButton(
label: "Unfreeze",
onPressed: () {
Navigator.of(context).pop("unfreeze");
},
),
);
}
}
class UnfreezeOrdinalDialog extends StatelessWidget {
const UnfreezeOrdinalDialog({super.key});
@override
Widget build(BuildContext context) {
return StackDialog(
title: "Are you sure you want to unfreeze this ordinal?",
icon: SvgPicture.asset(
Assets.svg.coinControl.blocked,
width: 24,
height: 24,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
leftButton: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(context).pop,
),
rightButton: PrimaryButton(
label: "Unfreeze",
onPressed: () {
Navigator.of(context).pop("unfreeze");
},
),
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class OrdinalCard extends StatelessWidget {
const OrdinalCard({
Key? key,
required this.walletId,
required this.ordinal,
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
radiusMultiplier: 2,
onPressed: () {
Navigator.of(context).pushNamed(
Util.isDesktop
? DesktopOrdinalDetailsView.routeName
: OrdinalDetailsView.routeName,
arguments: (walletId: walletId, ordinal: ordinal),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AspectRatio(
aspectRatio: 1,
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: Image.network(
ordinal.content, // Use the preview URL as the image source
fit: BoxFit.cover,
filterQuality:
FilterQuality.none, // Set the filter mode to nearest
),
),
),
const Spacer(),
Text(
'INSC. ${ordinal.inscriptionNumber}', // infer from address associated with utxoTXID
style: STextStyles.w500_12(context),
),
// const Spacer(),
// Text(
// "ID ${ordinal.inscriptionId}",
// style: STextStyles.w500_8(context),
// ),
],
),
);
}
}

View file

@ -0,0 +1,119 @@
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/ordinal.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class OrdinalsList extends ConsumerStatefulWidget {
const OrdinalsList({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
ConsumerState<OrdinalsList> createState() => _OrdinalsListState();
}
class _OrdinalsListState extends ConsumerState<OrdinalsList> {
final double _spacing = Util.isDesktop ? 16 : 10;
late List<Ordinal> _data;
late final Stream<List<Ordinal>?> _stream;
@override
void initState() {
_stream = ref
.read(mainDBProvider)
.isar
.ordinals
.where()
.filter()
.walletIdEqualTo(widget.walletId)
.watch();
_data = ref
.read(mainDBProvider)
.isar
.ordinals
.where()
.filter()
.walletIdEqualTo(widget.walletId)
.findAllSync();
super.initState();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Ordinal>?>(
stream: _stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
_data = snapshot.data!;
}
if (_data.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Center(
child: Text(
"Your ordinals will appear here",
style: Util.isDesktop
? STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1)
: STextStyles.label(context),
),
),
),
],
);
}
if (Util.isDesktop) {
return Wrap(
spacing: _spacing,
runSpacing: _spacing,
children: _data
.map((e) => SizedBox(
width: 220,
height: 270,
child: OrdinalCard(
walletId: widget.walletId,
ordinal: e,
)))
.toList(),
);
} else {
return GridView.builder(
shrinkWrap: true,
itemCount: _data.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: _spacing,
mainAxisSpacing: _spacing,
crossAxisCount: Util.isDesktop ? 4 : 2,
childAspectRatio: 6 / 7, // was 3/4, less data displayed now
),
itemBuilder: (_, i) => OrdinalCard(
walletId: widget.walletId,
ordinal: _data[i],
),
);
}
},
);
}
}

View file

@ -89,6 +89,11 @@ class _ConfirmTransactionViewState
late final FocusNode _noteFocusNode;
late final TextEditingController noteController;
late final FocusNode _onChainNoteFocusNode;
late final TextEditingController onChainNoteController;
Future<void> _attemptSend(BuildContext context) async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
@ -138,6 +143,9 @@ class _ConfirmTransactionViewState
txidFuture = (manager.wallet as FiroWallet)
.confirmSendPublic(txData: transactionInfo);
} else {
if (coin == Coin.epicCash) {
transactionInfo["onChainNote"] = onChainNoteController.text;
}
txidFuture = manager.confirmSend(txData: transactionInfo);
}
}
@ -272,14 +280,21 @@ class _ConfirmTransactionViewState
_noteFocusNode = FocusNode();
noteController = TextEditingController();
noteController.text = transactionInfo["note"] as String? ?? "";
_onChainNoteFocusNode = FocusNode();
onChainNoteController = TextEditingController();
onChainNoteController.text = transactionInfo["onChainNote"] as String? ?? "";
super.initState();
}
@override
void dispose() {
noteController.dispose();
onChainNoteController.dispose();
_noteFocusNode.dispose();
_onChainNoteFocusNode.dispose();
super.dispose();
}
@ -493,51 +508,54 @@ class _ConfirmTransactionViewState
],
),
),
if (coin == Coin.epicCash)
if (coin == Coin.epicCash &&
(transactionInfo["onChainNote"] as String).isNotEmpty)
const SizedBox(
height: 12,
),
if (coin == Coin.epicCash)
if (coin == Coin.epicCash &&
(transactionInfo["onChainNote"] as String).isNotEmpty)
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"On chain note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["onChainNote"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"On chain note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["onChainNote"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
(coin == Coin.epicCash) ? "Local Note" :
"Note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["note"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
if ((transactionInfo["note"] as String).isNotEmpty)
const SizedBox(
height: 12,
),
if ((transactionInfo["note"] as String).isNotEmpty)
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
(coin == Coin.epicCash) ? "Local Note" : "Note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["note"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
],
),
if (isDesktop)
@ -837,8 +855,64 @@ class _ConfirmTransactionViewState
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (coin == Coin.epicCash)
Text(
"On chain Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin == Coin.epicCash)
const SizedBox(
height: 8,
),
if (coin == Coin.epicCash)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
maxLength: 256,
controller: onChainNoteController,
focusNode: _onChainNoteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_onChainNoteFocusNode,
context,
).copyWith(
suffixIcon: onChainNoteController.text.isNotEmpty
? Padding(
padding:
const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
onChainNoteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
if (coin == Coin.epicCash)
const SizedBox(
height: 12,
),
Text(
"Note (optional)",
(coin == Coin.epicCash) ? "Local Note (optional)"
: "Note (optional)",
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)

View file

@ -164,7 +164,7 @@ class _InstallThemeFromFileDialogState
);
if (mounted) {
Navigator.of(context).pop();
if (!result) {
if (!result!) {
unawaited(
showDialog(
context: context,

View file

@ -72,11 +72,11 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
}
Future<void> _downloadPressed() async {
final result = await showLoading(
final result = (await showLoading(
whileFuture: _downloadAndInstall(),
context: context,
message: "Downloading and installing theme...",
);
))!;
if (mounted) {
final message = result

View file

@ -196,7 +196,10 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
case Coin.nano:
case Coin.banano:
//TODO: check network/node
case Coin.stellar:
case Coin.stellarTestnet:
throw UnimplementedError();
//TODO: check network/node
}
if (showFlushBar && mounted) {
@ -736,6 +739,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.nano:
case Coin.banano:
case Coin.eCash:
case Coin.stellar:
case Coin.stellarTestnet:
return false;
case Coin.ethereum:

View file

@ -172,7 +172,10 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.nano:
case Coin.banano:
//TODO: check network/node
case Coin.stellar:
case Coin.stellarTestnet:
throw UnimplementedError();
//TODO: check network/node
}
if (testPassed) {

View file

@ -20,11 +20,12 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/route_generator.dart';
@ -231,7 +232,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
.mnemonic;
if (mounted) {
Navigator.push(
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
@ -305,6 +306,25 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
);
},
),
if (coin == Coin.nano || coin == Coin.banano)
const SizedBox(
height: 8,
),
if (coin == Coin.nano || coin == Coin.banano)
Consumer(
builder: (_, ref, __) {
return SettingsListButton(
iconAssetName: Assets.svg.eye,
title: "Change representative",
onPressed: () {
Navigator.of(context).pushNamed(
ChangeRepresentativeView.routeName,
arguments: widget.walletId,
);
},
);
},
),
const SizedBox(
height: 8,
),
@ -434,18 +454,20 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
TextButton(
onPressed: () async {
try {
wallet.updateEpicboxConfig(
await wallet.updateEpicboxConfig(
hostController.text,
int.parse(portController.text),
);
showFloatingFlushBar(
context: context,
message: "Epicbox info saved!",
type: FlushBarType.success,
);
wallet.refresh();
if (mounted) {
await showFloatingFlushBar(
context: context,
message: "Epicbox info saved!",
type: FlushBarType.success,
);
}
unawaited(wallet.refresh());
} catch (e) {
showFloatingFlushBar(
await showFloatingFlushBar(
context: context,
message: "Failed to save epicbox info: $e",
type: FlushBarType.warning,

View file

@ -0,0 +1,402 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
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/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/themes/stack_colors.dart';
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/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.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/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/loading_indicator.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 ChangeRepresentativeView extends ConsumerStatefulWidget {
const ChangeRepresentativeView({
Key? key,
required this.walletId,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
final String walletId;
final ClipboardInterface clipboardInterface;
static const String routeName = "/changeRepresentative";
@override
ConsumerState<ChangeRepresentativeView> createState() => _XPubViewState();
}
class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
final _textController = TextEditingController();
final _textFocusNode = FocusNode();
final bool isDesktop = Util.isDesktop;
late ClipboardInterface _clipboardInterface;
String? representative;
Future<String> loadRepresentative() async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
if (manager.coin == Coin.nano) {
return (manager.wallet as NanoWallet).getCurrentRepresentative();
} else if (manager.coin == Coin.banano) {
return (manager.wallet as BananoWallet).getCurrentRepresentative();
}
throw Exception("Unsupported wallet attempted to show representative!");
}
Future<void> _save() async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
final changeFuture = manager.coin == Coin.nano
? (manager.wallet as NanoWallet).changeRepresentative
: (manager.wallet as BananoWallet).changeRepresentative;
final result = await showLoading(
whileFuture: changeFuture(_textController.text),
context: context,
message: "Updating representative...",
isDesktop: Util.isDesktop,
onException: (ex) {
String msg = ex.toString();
while (msg.isNotEmpty && msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
});
if (mounted) {
if (result != null && result) {
setState(() {
representative = _textController.text;
_textController.text = "";
});
await showFloatingFlushBar(
type: FlushBarType.success,
message: "Representative changed",
context: context,
);
}
}
}
@override
void initState() {
_clipboardInterface = widget.clipboardInterface;
super.initState();
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
super.dispose();
}
Future<void> _copy() async {
await _clipboardInterface
.setData(ClipboardData(text: representative ?? ""));
if (mounted) {
unawaited(showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
));
}
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
"Wallet representative",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.all(10),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
color: Theme.of(context)
.extension<StackColors>()!
.background,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.copy,
width: 24,
height: 24,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
if (representative != null) {
_copy();
}
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: child,
),
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 600,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Change representative",
style: STextStyles.desktopH2(context),
),
),
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
AnimatedSize(
duration: const Duration(
milliseconds: 150,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
child: child,
),
),
],
),
),
child: Column(
children: [
if (isDesktop) const SizedBox(height: 24),
ConditionalParent(
condition: !isDesktop,
builder: (child) => Expanded(
child: child,
),
child: FutureBuilder(
future: loadRepresentative(),
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
representative = snapshot.data!;
}
const height = 600.0;
Widget child;
if (representative == null) {
child = const SizedBox(
key: Key("loadingRepresentative"),
height: height,
child: Center(
child: LoadingIndicator(
width: 100,
),
),
);
} else {
child = Column(
children: [
ConditionalParent(
condition: !isDesktop,
builder: (child) => RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
child,
],
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Current representative",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
const SizedBox(
height: 4,
),
Row(
children: [
child,
],
),
],
),
child: SelectableText(
representative!,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(context),
),
),
),
const SizedBox(
height: 24,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _textController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: _textFocusNode,
decoration: standardInputDecoration(
"Enter new representative",
_textFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _textController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_textController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
if (isDesktop) const SizedBox(height: 60),
if (!isDesktop) const Spacer(),
PrimaryButton(
label: "Save",
onPressed: _save,
),
if (!isDesktop)
const SizedBox(
height: 16,
),
],
);
}
return AnimatedSwitcher(
duration: const Duration(
milliseconds: 200,
),
child: child,
);
},
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_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';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
enum FiroRescanRecoveryErrorViewOption {
retry,
showMnemonic,
deleteWallet;
}
class FiroRescanRecoveryErrorView extends ConsumerStatefulWidget {
const FiroRescanRecoveryErrorView({
super.key,
required this.walletId,
});
static const String routeName = "/firoRescanRecoveryErrorView";
final String walletId;
@override
ConsumerState<FiroRescanRecoveryErrorView> createState() =>
_FiroRescanRecoveryErrorViewState();
}
class _FiroRescanRecoveryErrorViewState
extends ConsumerState<FiroRescanRecoveryErrorView> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) {
return DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
isCompactHeight: true,
// useSpacers: false,
trailing: Padding(
padding: const EdgeInsets.only(right: 16),
child: CustomTextButton(
text: "Delete wallet",
onTap: () async {
final result = await showDialog<bool?>(
context: context,
barrierDismissible: false,
builder: (context) => Navigator(
initialRoute: DesktopDeleteWalletDialog.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
RouteGenerator.generateRoute(
RouteSettings(
name: DesktopDeleteWalletDialog.routeName,
arguments: widget.walletId,
),
),
];
},
),
);
if (result == true) {
if (context.mounted) {
Navigator.of(context).pop();
Navigator.of(context).pop();
}
}
},
),
),
),
body: SizedBox(width: 328, child: child),
);
},
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) {
return Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
automaticallyImplyLeading: false,
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
semanticsLabel: "Delete wallet button. "
"Start process of deleting current wallet.",
key: const Key("walletViewRadioButton"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
icon: SvgPicture.asset(
Assets.svg.trash,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () async {
await showDialog<void>(
barrierDismissible: true,
context: context,
builder: (_) => StackDialog(
title:
"Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(widget.walletId).walletName}?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
onPressed: () {
Navigator.pop(context);
},
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
Navigator.pop(context);
Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
widget.walletId,
showBackButton: true,
routeOnSuccess:
DeleteWalletWarningView.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to delete wallet",
biometricsAuthenticationTitle:
"Delete wallet",
),
settings: const RouteSettings(
name: "/deleteWalletLockscreen"),
),
);
},
child: Text(
"Delete",
style: STextStyles.button(context),
),
),
),
);
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!Util.isDesktop) const Spacer(),
Text(
"Failed to rescan firo wallet",
style: STextStyles.pageTitleH2(context),
),
Util.isDesktop
? const SizedBox(
height: 60,
)
: const Spacer(),
BranchedParent(
condition: Util.isDesktop,
conditionBranchBuilder: (children) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
otherBranchBuilder: (children) => Row(
children: [
Expanded(child: children[0]),
children[1],
Expanded(child: children[2]),
],
),
children: [
SecondaryButton(
label: "Show mnemonic",
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: () async {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => Navigator(
initialRoute: UnlockWalletKeysDesktop.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
RouteGenerator.generateRoute(
RouteSettings(
name: UnlockWalletKeysDesktop.routeName,
arguments: widget.walletId,
),
)
];
},
),
);
} else {
final mnemonic = await ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.mnemonic;
if (mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
Tuple2(widget.walletId, mnemonic),
showBackButton: true,
routeOnSuccess: WalletBackupView.routeName,
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name: "/viewRecoverPhraseLockscreen"),
),
);
}
}
},
),
const SizedBox(
width: 16,
height: 16,
),
PrimaryButton(
label: "Retry",
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
onPressed: () {
Navigator.of(context).pop(
true,
);
},
),
],
),
],
),
),
),
);
}
}

View file

@ -100,7 +100,7 @@ class _MyTokenSelectItemState extends ConsumerState<MyTokenSelectItem> {
message: "Loading ${widget.token.name}",
);
if (!success) {
if (!success!) {
return;
}

View file

@ -121,7 +121,8 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
height: 24,
),
BalanceSelector(
title: "Available balance",
title:
"Available${balanceSecondary != null ? " public" : ""} balance",
coin: coin,
balance: balance.spendable,
onPressed: () {
@ -141,6 +142,31 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
value: _BalanceType.available,
groupValue: _bal,
),
const SizedBox(
height: 12,
),
BalanceSelector(
title:
"Full${balanceSecondary != null ? " public" : ""} balance",
coin: coin,
balance: balance.total,
onPressed: () {
ref.read(walletBalanceToggleStateProvider.state).state =
WalletBalanceToggleState.full;
ref.read(publicPrivateBalanceStateProvider.state).state =
"Public";
Navigator.of(context).pop();
},
onChanged: (_) {
ref.read(walletBalanceToggleStateProvider.state).state =
WalletBalanceToggleState.full;
ref.read(publicPrivateBalanceStateProvider.state).state =
"Public";
Navigator.of(context).pop();
},
value: _BalanceType.full,
groupValue: _bal,
),
if (balanceSecondary != null)
const SizedBox(
height: 12,
@ -167,30 +193,6 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
value: _BalanceType.privateAvailable,
groupValue: _bal,
),
const SizedBox(
height: 12,
),
BalanceSelector(
title: "Full balance",
coin: coin,
balance: balance.total,
onPressed: () {
ref.read(walletBalanceToggleStateProvider.state).state =
WalletBalanceToggleState.full;
ref.read(publicPrivateBalanceStateProvider.state).state =
"Public";
Navigator.of(context).pop();
},
onChanged: (_) {
ref.read(walletBalanceToggleStateProvider.state).state =
WalletBalanceToggleState.full;
ref.read(publicPrivateBalanceStateProvider.state).state =
"Public";
Navigator.of(context).pop();
},
value: _BalanceType.full,
groupValue: _bal,
),
if (balanceSecondary != null)
const SizedBox(
height: 12,

View file

@ -52,6 +52,7 @@ class WalletSummary extends StatelessWidget {
walletId: walletId,
width: constraints.maxWidth,
height: constraints.maxHeight,
isFavorite: false,
),
Positioned.fill(
child: Padding(

View file

@ -10,6 +10,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -19,6 +20,7 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@ -31,6 +33,7 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
class WalletSummaryInfo extends ConsumerStatefulWidget {
const WalletSummaryInfo({
@ -49,6 +52,8 @@ class WalletSummaryInfo extends ConsumerStatefulWidget {
class _WalletSummaryInfoState extends ConsumerState<WalletSummaryInfo> {
late StreamSubscription<BalanceRefreshedEvent> _balanceUpdated;
String receivingAddress = "";
void showSheet() {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
@ -72,6 +77,17 @@ class _WalletSummaryInfoState extends ConsumerState<WalletSummaryInfo> {
}
},
);
// managerProvider = widget.managerProvider;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final address = await ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.currentReceivingAddress;
setState(() {
receivingAddress = address;
});
});
super.initState();
}
@ -85,10 +101,14 @@ class _WalletSummaryInfoState extends ConsumerState<WalletSummaryInfo> {
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
bool isMonkey = true;
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
final externalCalls = ref.watch(
prefsChangeNotifierProvider.select((value) => value.externalCalls));
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
final coin = manager.coin;
final balance = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).balance));
@ -125,84 +145,104 @@ class _WalletSummaryInfoState extends ConsumerState<WalletSummaryInfo> {
title = _showAvailable ? "Available balance" : "Full balance";
}
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
Text(
title,
style: STextStyles.subtitle500(context).copyWith(
List<int>? imageBytes;
if (coin == Coin.banano) {
imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes();
}
return ConditionalParent(
condition: imageBytes != null,
builder: (child) => Stack(
children: [
Positioned.fill(
left: 150.0,
child: SvgPicture.memory(
Uint8List.fromList(imageBytes!),
),
),
child,
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
Text(
title,
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
],
),
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
ref.watch(pAmountFormatter(coin)).format(balanceToShow),
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
],
),
),
if (externalCalls)
Text(
"${(priceTuple.item1 * balanceToShow.decimal).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
)} $baseCurrency",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
),
),
Column(
children: [
SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
),
width: 24,
height: 24,
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
ref.watch(pAmountFormatter(coin)).format(balanceToShow),
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
WalletRefreshButton(
walletId: widget.walletId,
initialSyncStatus: widget.initialSyncStatus,
),
if (externalCalls)
Text(
"${(priceTuple.item1 * balanceToShow.decimal).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
)} $baseCurrency",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
),
),
Column(
children: [
SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
),
width: 24,
height: 24,
),
const Spacer(),
WalletRefreshButton(
walletId: widget.walletId,
initialSyncStatus: widget.initialSyncStatus,
),
],
)
],
)
],
),
);
}
}

View file

@ -358,7 +358,6 @@ class _TransactionDetailsViewState
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
print("THIS TRANSACTION IS $_transaction");
return ConditionalParent(
@ -474,7 +473,9 @@ class _TransactionDetailsViewState
),
SelectableText(
_transaction.isCancelled
? "Cancelled"
? coin == Coin.ethereum
? "Failed"
: "Cancelled"
: whatIsIt(
_transaction,
currentHeight,
@ -585,7 +586,9 @@ class _TransactionDetailsViewState
// child:
SelectableText(
_transaction.isCancelled
? "Cancelled"
? coin == Coin.ethereum
? "Failed"
: "Cancelled"
: whatIsIt(
_transaction,
currentHeight,
@ -781,8 +784,8 @@ class _TransactionDetailsViewState
isDesktop
? const _Divider()
: const SizedBox(
height: 12,
),
height: 12,
),
if (coin == Coin.epicCash)
RoundedWhiteContainer(
padding: isDesktop
@ -790,22 +793,22 @@ class _TransactionDetailsViewState
: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [
Text(
"On chain note",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context)
.desktopTextExtraExtraSmall(
context)
: STextStyles.itemSubtitle(
context),
context),
),
const SizedBox(
height: 8,
@ -814,18 +817,16 @@ class _TransactionDetailsViewState
_transaction.otherData ?? "",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles
.itemSubtitle12(
context),
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(
context),
),
],
),
@ -854,7 +855,9 @@ class _TransactionDetailsViewState
MainAxisAlignment.spaceBetween,
children: [
Text(
(coin == Coin.epicCash) ? "Local Note" : "Note ",
(coin == Coin.epicCash)
? "Local Note"
: "Note ",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
@ -923,7 +926,9 @@ class _TransactionDetailsViewState
notesServiceChangeNotifierProvider(
walletId)
.select((value) => value.getNoteFor(
txid: (coin == Coin.epicCash)? _transaction.slateId! : _transaction.txid ))),
txid: (coin == Coin.epicCash)
? _transaction.slateId!
: _transaction.txid))),
builder: (builderContext,
AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState ==

View file

@ -22,13 +22,16 @@ import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/monkey/monkey_view.dart';
import 'package:stackwallet/pages/notification_views/notifications_view.dart';
import 'package:stackwallet/pages/ordinals/ordinals_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart';
@ -72,6 +75,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart';
@ -116,6 +120,36 @@ class _WalletViewState extends ConsumerState<WalletView> {
late StreamSubscription<dynamic> _nodeStatusSubscription;
bool _rescanningOnOpen = false;
bool _lelantusRescanRecovery = false;
Future<void> _firoRescanRecovery() async {
final success = await (ref.read(managerProvider).wallet as FiroWallet)
.firoRescanRecovery();
if (success) {
// go into wallet
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() {
_rescanningOnOpen = false;
_lelantusRescanRecovery = false;
}),
);
} else {
// show error message dialog w/ options
if (mounted) {
final shouldRetry = await Navigator.of(context).pushNamed(
FiroRescanRecoveryErrorView.routeName,
arguments: walletId,
);
if (shouldRetry is bool && shouldRetry) {
await _firoRescanRecovery();
}
} else {
return await _firoRescanRecovery();
}
}
}
@override
void initState() {
@ -131,7 +165,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
_shouldDisableAutoSyncOnLogOut = false;
}
if (ref.read(managerProvider).rescanOnOpenVersion == Constants.rescanV1) {
if (ref.read(managerProvider).coin == Coin.firo &&
(ref.read(managerProvider).wallet as FiroWallet)
.lelantusCoinIsarRescanRequired) {
_rescanningOnOpen = true;
_lelantusRescanRecovery = true;
_firoRescanRecovery();
} else if (ref.read(managerProvider).rescanOnOpenVersion ==
Constants.rescanV1) {
_rescanningOnOpen = true;
ref.read(managerProvider).fullRescan(20, 1000).then(
(_) => ref.read(managerProvider).resetRescanOnOpen().then(
@ -209,6 +250,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
DateTime? _cachedTime;
Future<bool> _onWillPop() async {
if (_rescanningOnOpen || _lelantusRescanRecovery) {
return false;
}
final now = DateTime.now();
const timeout = Duration(milliseconds: 1500);
if (_cachedTime == null || now.difference(_cachedTime!) > timeout) {
@ -431,33 +476,37 @@ class _WalletViewState extends ConsumerState<WalletView> {
eventBus: null,
textColor:
Theme.of(context).extension<StackColors>()!.textDark,
actionButton: SecondaryButton(
label: "Cancel",
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => StackDialog(
title: "Warning!",
message: "Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
leftButton: SecondaryButton(
label: "Cancel",
onPressed:
Navigator.of(context, rootNavigator: true).pop,
),
rightButton: SecondaryButton(
label: "Ok",
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
setState(() => _rescanningOnOpen = false);
},
),
actionButton: _lelantusRescanRecovery
? null
: SecondaryButton(
label: "Cancel",
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => StackDialog(
title: "Warning!",
message: "Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
leftButton: SecondaryButton(
label: "Cancel",
onPressed:
Navigator.of(context, rootNavigator: true)
.pop,
),
rightButton: SecondaryButton(
label: "Ok",
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop();
setState(() => _rescanningOnOpen = false);
},
),
),
);
},
),
);
},
),
),
)
],
@ -924,6 +973,23 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (coin == Coin.banano)
WalletNavigationBarItemData(
icon: SvgPicture.asset(
Assets.svg.monkey,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
label: "MonKey",
onTap: () {
Navigator.of(context).pushNamed(
MonkeyView.routeName,
arguments: widget.walletId,
);
}),
if (ref.watch(
walletsChangeNotifierProvider.select(
(value) => value
@ -1007,6 +1073,22 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
},
),
if (ref.watch(
walletsChangeNotifierProvider.select(
(value) =>
value.getManager(widget.walletId).hasOrdinalsSupport,
),
))
WalletNavigationBarItemData(
label: "Ordinals",
icon: const OrdinalsNavIcon(),
onTap: () {
Navigator.of(context).pushNamed(
OrdinalsView.routeName,
arguments: widget.walletId,
);
},
),
],
),
],

View file

@ -149,6 +149,7 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
walletId: widget.walletId,
width: widget.width,
height: widget.height,
isFavorite: true,
),
child: Padding(
padding: const EdgeInsets.all(12.0),

View file

@ -10,12 +10,14 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart';
@ -28,6 +30,8 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_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';
import 'package:stackwallet/themes/coin_icon_provider.dart';
@ -76,6 +80,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
late final bool _shouldDisableAutoSyncOnLogOut;
bool _rescanningOnOpen = false;
bool _lelantusRescanRecovery = false;
Future<void> onBackPressed() async {
await _logout();
@ -101,6 +106,38 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
ref.read(managerProvider.notifier).isActiveWallet = false;
}
Future<void> _firoRescanRecovery() async {
final success = await (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as FiroWallet)
.firoRescanRecovery();
if (success) {
// go into wallet
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() {
_rescanningOnOpen = false;
_lelantusRescanRecovery = false;
}),
);
} else {
// show error message dialog w/ options
if (mounted) {
final shouldRetry = await Navigator.of(context).pushNamed(
FiroRescanRecoveryErrorView.routeName,
arguments: widget.walletId,
);
if (shouldRetry is bool && shouldRetry) {
await _firoRescanRecovery();
}
} else {
return await _firoRescanRecovery();
}
}
}
@override
void initState() {
controller = TextEditingController();
@ -122,7 +159,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
_shouldDisableAutoSyncOnLogOut = false;
}
if (ref.read(managerProvider).coin != Coin.ethereum &&
if (ref.read(managerProvider).coin == Coin.firo &&
(ref.read(managerProvider).wallet as FiroWallet)
.lelantusCoinIsarRescanRequired) {
_rescanningOnOpen = true;
_lelantusRescanRecovery = true;
_firoRescanRecovery();
} else if (ref.read(managerProvider).coin != Coin.ethereum &&
ref.read(managerProvider).rescanOnOpenVersion == Constants.rescanV1) {
_rescanningOnOpen = true;
ref.read(managerProvider).fullRescan(20, 1000).then(
@ -153,6 +196,10 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
final managerProvider = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(widget.walletId)));
final monke = coin == Coin.banano
? (manager.wallet as BananoWallet).getMonkeyImageBytes()
: null;
return ConditionalParent(
condition: _rescanningOnOpen,
builder: (child) {
@ -166,83 +213,86 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
subMessage: "This only needs to run once per wallet",
eventBus: null,
textColor: Theme.of(context).extension<StackColors>()!.textDark,
actionButton: SecondaryButton(
label: "Skip",
buttonHeight: ButtonHeight.l,
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(
maxWidth: 500,
maxHeight: double.infinity,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
actionButton: _lelantusRescanRecovery
? null
: SecondaryButton(
label: "Skip",
buttonHeight: ButtonHeight.l,
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(
maxWidth: 500,
maxHeight: double.infinity,
child: Column(
children: [
Text(
"Warning!",
style: STextStyles.desktopH3(context),
Padding(
padding: const EdgeInsets.only(left: 32),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Warning!",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
),
const DesktopDialogCloseButton(),
],
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 32),
child: Text(
"Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
style: STextStyles.desktopTextSmall(context),
),
),
const SizedBox(
height: 32,
),
Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: ButtonHeight.l,
onPressed: Navigator.of(context,
rootNavigator: true)
.pop,
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32),
child: Text(
"Skipping this process can completely"
" break your wallet. It is only meant to be done in"
" emergency situations where the migration fails"
" and will not let you continue. Still skip?",
style:
STextStyles.desktopTextSmall(context),
),
),
const SizedBox(
width: 16,
height: 32,
),
Expanded(
child: PrimaryButton(
label: "Ok",
buttonHeight: ButtonHeight.l,
onPressed: () {
Navigator.of(context,
rootNavigator: true)
.pop();
setState(
() => _rescanningOnOpen = false);
},
Padding(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: ButtonHeight.l,
onPressed: Navigator.of(context,
rootNavigator: true)
.pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Ok",
buttonHeight: ButtonHeight.l,
onPressed: () {
Navigator.of(context,
rootNavigator: true)
.pop();
setState(() =>
_rescanningOnOpen = false);
},
),
),
],
),
),
)
],
),
)
],
),
),
);
},
),
);
},
),
),
)
],
@ -334,13 +384,20 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
padding: const EdgeInsets.all(20),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
if (monke != null)
SvgPicture.memory(
Uint8List.fromList(monke!),
width: 60,
height: 60,
),
if (monke == null)
SvgPicture.file(
File(
ref.watch(coinIconProvider(coin)),
),
width: 40,
height: 40,
),
width: 40,
height: 40,
),
const SizedBox(
width: 10,
),

View file

@ -105,6 +105,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
final _baseFocus = FocusNode();
String? _note;
String? _onChainNote;
Amount? _amountToSend;
Amount? _cachedAmountToSend;
@ -354,6 +355,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} else {
txData["address"] = _address;
txData["note"] = _note ?? "";
if (coin == Coin.epicCash) {
txData['onChainNote'] = _onChainNote ?? "";
}
}
// pop building dialog
Navigator.of(

View file

@ -16,12 +16,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/monkey/monkey_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/coin_control/desktop_coin_control_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart';
import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/providers.dart';
@ -80,6 +82,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
onCoinControlPressed: _onCoinControlPressed,
onAnonymizeAllPressed: _onAnonymizeAllPressed,
onWhirlpoolPressed: _onWhirlpoolPressed,
onOrdinalsPressed: _onOrdinalsPressed,
onMonkeyPressed: _onMonkeyPressed,
),
);
}
@ -313,6 +317,24 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
}
}
Future<void> _onMonkeyPressed() async {
Navigator.of(context, rootNavigator: true).pop();
await (Navigator.of(context).pushNamed(
MonkeyView.routeName,
arguments: widget.walletId,
));
}
void _onOrdinalsPressed() {
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context).pushNamed(
DesktopOrdinalsView.routeName,
arguments: widget.walletId,
);
}
@override
Widget build(BuildContext context) {
final manager = ref.watch(
@ -330,8 +352,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
)) ||
manager.coin == Coin.firo ||
manager.coin == Coin.firoTestNet ||
manager.hasWhirlpoolSupport;
manager.hasWhirlpoolSupport ||
manager.coin == Coin.banano ||
manager.hasOrdinalsSupport;
return Row(
children: [
if (Constants.enableExchange)

View file

@ -29,6 +29,8 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
required this.onCoinControlPressed,
required this.onAnonymizeAllPressed,
required this.onWhirlpoolPressed,
required this.onOrdinalsPressed,
required this.onMonkeyPressed,
}) : super(key: key);
final String walletId;
@ -36,6 +38,8 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget {
final VoidCallback? onCoinControlPressed;
final VoidCallback? onAnonymizeAllPressed;
final VoidCallback? onWhirlpoolPressed;
final VoidCallback? onOrdinalsPressed;
final VoidCallback? onMonkeyPressed;
@override
ConsumerState<MoreFeaturesDialog> createState() => _MoreFeaturesDialogState();
@ -103,6 +107,20 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
iconAsset: Assets.svg.robotHead,
onPressed: () => widget.onPaynymPressed?.call(),
),
if (manager.hasOrdinalsSupport)
_MoreFeaturesItem(
label: "Ordinals",
detail: "View and control your ordinals in Stack",
iconAsset: Assets.svg.ordinal,
onPressed: () => widget.onOrdinalsPressed?.call(),
),
if (manager.coin == Coin.banano)
_MoreFeaturesItem(
label: "MonKey",
detail: "Generate Banano MonKey",
iconAsset: Assets.svg.monkey,
onPressed: () => widget.onMonkeyPressed?.call(),
),
const SizedBox(
height: 28,
),

View file

@ -13,7 +13,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart';
import 'package:stackwallet/providers/providers.dart';
@ -21,11 +22,13 @@ import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
enum _WalletOptions {
addressList,
deleteWallet,
changeRepresentative,
showXpub;
String get prettyName {
@ -34,6 +37,8 @@ enum _WalletOptions {
return "Address list";
case _WalletOptions.deleteWallet:
return "Delete wallet";
case _WalletOptions.changeRepresentative:
return "Change representative";
case _WalletOptions.showXpub:
return "Show xPub";
}
@ -70,6 +75,9 @@ class WalletOptionsButton extends StatelessWidget {
onAddressListPressed: () async {
Navigator.of(context).pop(_WalletOptions.addressList);
},
onChangeRepPressed: () async {
Navigator.of(context).pop(_WalletOptions.changeRepresentative);
},
onShowXpubPressed: () async {
Navigator.of(context).pop(_WalletOptions.showXpub);
},
@ -134,6 +142,32 @@ class WalletOptionsButton extends StatelessWidget {
),
);
if (result == true) {
if (context.mounted) {
Navigator.of(context).pop();
}
}
break;
case _WalletOptions.changeRepresentative:
final result = await showDialog<bool?>(
context: context,
barrierDismissible: false,
builder: (context) => Navigator(
initialRoute: ChangeRepresentativeView.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
RouteGenerator.generateRoute(
RouteSettings(
name: ChangeRepresentativeView.routeName,
arguments: walletId,
),
),
];
},
),
);
if (result == true) {
if (context.mounted) {
Navigator.of(context).pop();
@ -171,18 +205,24 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
required this.onDeletePressed,
required this.onAddressListPressed,
required this.onShowXpubPressed,
required this.onChangeRepPressed,
required this.walletId,
}) : super(key: key);
final VoidCallback onDeletePressed;
final VoidCallback onAddressListPressed;
final VoidCallback onShowXpubPressed;
final VoidCallback onChangeRepPressed;
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool xpubEnabled = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).hasXPub));
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
final bool xpubEnabled = manager.hasXPub;
final bool canChangeRep =
manager.coin == Coin.nano || manager.coin == Coin.banano;
return Stack(
children: [
@ -237,6 +277,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
),
),
),
if (canChangeRep)
const SizedBox(
height: 8,
),
if (canChangeRep)
TransparentButton(
onPressed: onChangeRepPressed,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.eye,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
const SizedBox(width: 14),
Expanded(
child: Text(
_WalletOptions.changeRepresentative.prettyName,
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
),
),
if (xpubEnabled)
const SizedBox(
height: 8,

View file

@ -0,0 +1,372 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.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/rounded_white_container.dart';
class DesktopOrdinalDetailsView extends ConsumerStatefulWidget {
const DesktopOrdinalDetailsView({
Key? key,
required this.walletId,
required this.ordinal,
}) : super(key: key);
final String walletId;
final Ordinal ordinal;
static const routeName = "/desktopOrdinalDetailsView";
@override
ConsumerState<DesktopOrdinalDetailsView> createState() =>
_DesktopOrdinalDetailsViewState();
}
class _DesktopOrdinalDetailsViewState
extends ConsumerState<DesktopOrdinalDetailsView> {
static const _spacing = 12.0;
late final UTXO? utxo;
Future<String> _savePngToFile() async {
final response = await get(Uri.parse(widget.ordinal.content));
if (response.statusCode != 200) {
throw Exception(
"statusCode=${response.statusCode} body=${response.bodyBytes}");
}
final bytes = response.bodyBytes;
if (Platform.isAndroid) {
await Permission.storage.request();
}
final dir = Platform.isAndroid
? Directory("/storage/emulated/0/Documents")
: await getApplicationDocumentsDirectory();
final docPath = dir.path;
final filePath = "$docPath/ordinal_${widget.ordinal.inscriptionNumber}.png";
File imgFile = File(filePath);
if (imgFile.existsSync()) {
throw Exception("File already exists");
}
await imgFile.writeAsBytes(bytes);
return filePath;
}
@override
void initState() {
utxo = widget.ordinal.getUTXO(ref.read(mainDBProvider));
super.initState();
}
@override
Widget build(BuildContext context) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId).coin));
return DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
width: 32,
),
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,
),
const SizedBox(
width: 18,
),
Text(
"Ordinal details",
style: STextStyles.desktopH3(context),
),
],
),
),
useSpacers: false,
isCompactHeight: true,
),
body: Padding(
padding: const EdgeInsets.only(
left: 24,
top: 24,
right: 24,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 300,
height: 300,
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: Image.network(
widget.ordinal
.content, // Use the preview URL as the image source
fit: BoxFit.cover,
filterQuality:
FilterQuality.none, // Set the filter mode to nearest
),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"INSC. ${widget.ordinal.inscriptionNumber}",
style: STextStyles.w600_20(context),
),
],
),
),
const SizedBox(
width: 16,
),
// PrimaryButton(
// width: 150,
// label: "Send",
// icon: SvgPicture.asset(
// Assets.svg.send,
// width: 18,
// height: 18,
// color: Theme.of(context)
// .extension<StackColors>()!
// .buttonTextPrimary,
// ),
// buttonHeight: ButtonHeight.l,
// iconSpacing: 8,
// onPressed: () async {
// final response = await showDialog<String?>(
// context: context,
// builder: (_) =>
// const SendOrdinalUnfreezeDialog(),
// );
// if (response == "unfreeze") {
// // TODO: unfreeze and go to send ord screen
// }
// },
// ),
// const SizedBox(
// width: 16,
// ),
SecondaryButton(
width: 150,
label: "Download",
icon: SvgPicture.asset(
Assets.svg.arrowDown,
width: 13,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
buttonHeight: ButtonHeight.l,
iconSpacing: 8,
onPressed: () async {
bool didError = false;
final path = await showLoading<String>(
whileFuture: _savePngToFile(),
context: context,
isDesktop: true,
message: "Saving ordinal image",
onException: (e) {
didError = true;
String msg = e.toString();
while (msg.isNotEmpty &&
msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
},
);
if (!didError && mounted) {
await showFloatingFlushBar(
type: FlushBarType.success,
message: "Image saved to $path",
context: context,
);
}
},
),
],
),
),
const SizedBox(
height: 16,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_DetailsItemWCopy(
title: "Inscription number",
data: widget.ordinal.inscriptionNumber.toString(),
),
const _Divider(),
_DetailsItemWCopy(
title: "Inscription ID",
data: widget.ordinal.inscriptionId,
),
// const SizedBox(
// height: _spacing,
// ),
// // todo: add utxo status
const _Divider(),
_DetailsItemWCopy(
title: "Amount",
data: utxo == null
? "ERROR"
: ref.watch(pAmountFormatter(coin)).format(
Amount(
rawValue: BigInt.from(utxo!.value),
fractionDigits: coin.decimals,
),
),
),
const _Divider(),
_DetailsItemWCopy(
title: "Owner address",
data: utxo?.address ?? "ERROR",
),
const _Divider(),
_DetailsItemWCopy(
title: "Transaction ID",
data: widget.ordinal.utxoTXID,
),
],
),
),
],
),
),
),
),
],
),
),
);
}
}
class _Divider extends StatelessWidget {
const _Divider({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: Container(
height: 1,
color: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
),
);
}
}
class _DetailsItemWCopy extends StatelessWidget {
const _DetailsItemWCopy({
Key? key,
required this.title,
required this.data,
}) : super(key: key);
final String title;
final String data;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: STextStyles.itemSubtitle(context),
),
IconCopyButton(
data: data,
),
],
),
const SizedBox(
height: 4,
),
SelectableText(
data,
style: STextStyles.itemSubtitle12(context),
),
],
);
}
}

View file

@ -0,0 +1,243 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/mixins/ordinals_interface.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.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';
class DesktopOrdinalsView extends ConsumerStatefulWidget {
const DesktopOrdinalsView({
super.key,
required this.walletId,
});
static const String routeName = "/desktopOrdinalsView";
final String walletId;
@override
ConsumerState<DesktopOrdinalsView> createState() => _DesktopOrdinals();
}
class _DesktopOrdinals extends ConsumerState<DesktopOrdinalsView> {
late final TextEditingController searchController;
late final FocusNode searchFocusNode;
String _searchTerm = "";
@override
void initState() {
searchController = TextEditingController();
searchFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
searchController.dispose();
searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
isCompactHeight: true,
useSpacers: false,
leading: Expanded(
child: Row(
children: [
const SizedBox(
width: 32,
),
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,
),
const SizedBox(
width: 15,
),
SvgPicture.asset(
Assets.svg.ordinal,
width: 32,
height: 32,
color:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
const SizedBox(
width: 12,
),
Text(
"Ordinals",
style: STextStyles.desktopH3(context),
)
],
),
),
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Spacer(),
// Expanded(
// child: ClipRRect(
// borderRadius: BorderRadius.circular(
// Constants.size.circularBorderRadius,
// ),
// child: TextField(
// autocorrect: Util.isDesktop ? false : true,
// enableSuggestions: Util.isDesktop ? false : true,
// controller: searchController,
// focusNode: searchFocusNode,
// onChanged: (value) {
// setState(() {
// _searchTerm = value;
// });
// },
// style: STextStyles.field(context),
// decoration: standardInputDecoration(
// "Search",
// searchFocusNode,
// context,
// ).copyWith(
// prefixIcon: Padding(
// padding: const EdgeInsets.symmetric(
// horizontal: 10,
// vertical: 20,
// ),
// child: SvgPicture.asset(
// Assets.svg.search,
// width: 16,
// height: 16,
// ),
// ),
// suffixIcon: searchController.text.isNotEmpty
// ? Padding(
// padding: const EdgeInsets.only(right: 0),
// child: UnconstrainedBox(
// child: Row(
// children: [
// TextFieldIconButton(
// child: const XIcon(),
// onTap: () async {
// setState(() {
// searchController.text = "";
// _searchTerm = "";
// });
// },
// ),
// ],
// ),
// ),
// )
// : null,
// ),
// ),
// ),
// ),
// const SizedBox(
// width: 16,
// ),
// SecondaryButton(
// width: 184,
// label: "Filter",
// buttonHeight: ButtonHeight.l,
// icon: SvgPicture.asset(
// Assets.svg.filter,
// color: Theme.of(context)
// .extension<StackColors>()!
// .buttonTextSecondary,
// ),
// onPressed: () {
// Navigator.of(context).pushNamed(
// OrdinalsFilterView.routeName,
// );
// },
// ),
const SizedBox(
width: 16,
),
SecondaryButton(
width: 184,
label: "Update",
buttonHeight: ButtonHeight.l,
icon: SvgPicture.asset(
Assets.svg.arrowRotate,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () async {
// show loading for a minimum of 2 seconds on refreshing
await showLoading(
isDesktop: true,
whileFuture: Future.wait<void>([
Future.delayed(const Duration(seconds: 2)),
(ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as OrdinalsInterface)
.refreshInscriptions()
]),
context: context,
message: "Refreshing...");
},
),
],
),
const SizedBox(
height: 16,
),
Expanded(
child: SingleChildScrollView(
child: OrdinalsList(
walletId: widget.walletId,
),
),
),
],
),
),
);
}
}

View file

@ -125,7 +125,7 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
}
}
if (!widget.restoreFromSWB) {
if (!widget.restoreFromSWB && mounted) {
unawaited(showFloatingFlushBar(
type: FlushBarType.success,
message: "Your password is set up",

View file

@ -84,10 +84,10 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
unawaited(
showDialog(
context: context,
builder: (context) => Column(
builder: (context) => const Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
children: [
LoadingIndicator(
width: 200,
height: 200,

View file

@ -13,8 +13,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/exchange/active_pair.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart';
import 'package:tuple/tuple.dart';
@ -44,7 +48,22 @@ final efSendAmountStringProvider = StateProvider<String>((ref) {
if (refreshing && reversed) {
return "-";
} else {
return ref.watch(efSendAmountProvider)?.toString() ?? "";
final decimal = ref.watch(efSendAmountProvider);
String string = "";
if (decimal != null) {
final amount = Amount.fromDecimal(decimal, fractionDigits: decimal.scale);
final locale = ref.watch(localeServiceChangeNotifierProvider).locale;
string = AmountUnit.normal.displayAmount(
amount: amount,
locale: locale,
coin: Coin
.nano, // use nano just to ensure decimal.scale < Coin.value.decimals
withUnitName: false,
maxDecimalPlaces: decimal.scale,
);
}
return string;
}
});
final efReceiveAmountStringProvider = StateProvider<String>((ref) {
@ -54,7 +73,22 @@ final efReceiveAmountStringProvider = StateProvider<String>((ref) {
if (refreshing && reversed == false) {
return "-";
} else {
return ref.watch(efReceiveAmountProvider)?.toString() ?? "";
final decimal = ref.watch(efReceiveAmountProvider);
String string = "";
if (decimal != null) {
final amount = Amount.fromDecimal(decimal, fractionDigits: decimal.scale);
final locale = ref.watch(localeServiceChangeNotifierProvider).locale;
string = AmountUnit.normal.displayAmount(
amount: amount,
locale: locale,
coin: Coin
.nano, // use nano just to ensure decimal.scale < Coin.value.decimals
withUnitName: false,
maxDecimalPlaces: decimal.scale,
);
}
return string;
}
});

View file

@ -17,7 +17,9 @@ import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/contact_entry.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
@ -56,7 +58,11 @@ import 'package:stackwallet/pages/generic/single_field_edit_view.dart';
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/monkey/monkey_view.dart';
import 'package:stackwallet/pages/notification_views/notifications_view.dart';
import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart';
import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart';
import 'package:stackwallet/pages/ordinals/ordinals_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';
@ -103,14 +109,16 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart';
import 'package:stackwallet/pages/stack_privacy_calls.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.dart';
import 'package:stackwallet/pages/token_view/token_contract_details_view.dart';
@ -140,6 +148,8 @@ 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/unlock_wallet_keys_desktop.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart';
import 'package:stackwallet/pages_desktop_specific/notifications/desktop_notifications_view.dart';
import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart';
import 'package:stackwallet/pages_desktop_specific/password/create_password_view.dart';
import 'package:stackwallet/pages_desktop_specific/password/delete_password_warning_view.dart';
import 'package:stackwallet/pages_desktop_specific/password/forgot_password_desktop_view.dart';
@ -164,8 +174,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/choose_coin_view.dart';
import 'package:tuple/tuple.dart';
import 'models/isar/models/contact_entry.dart';
/*
* This file contains all the routes for the app.
* To add a new route, add it to the switch statement in the generateRoute method.
@ -257,6 +265,20 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FiroRescanRecoveryErrorView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FiroRescanRecoveryErrorView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case WalletsView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@ -375,6 +397,35 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case MonkeyView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => MonkeyView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
// case MonkeyLoadedView.routeName:
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
// return getRoute(
// shouldUseMaterialRoute: useMaterialPageRoute,
// builder: (_) => MonkeyLoadedView(
// walletId: args.item1,
// managerProvider: args.item2,
// ),
// settings: RouteSettings(
// name: settings.name,
// ),
// );
// }
// return _routeError("${settings.name} invalid args: ${args.toString()}");
case CoinControlView.routeName:
if (args is Tuple2<String, CoinControlViewType>) {
return getRoute(
@ -404,6 +455,70 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case OrdinalsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => OrdinalsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case DesktopOrdinalsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => DesktopOrdinalsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case OrdinalDetailsView.routeName:
if (args is ({Ordinal ordinal, String walletId})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => OrdinalDetailsView(
walletId: args.walletId,
ordinal: args.ordinal,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case DesktopOrdinalDetailsView.routeName:
if (args is ({Ordinal ordinal, String walletId})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => DesktopOrdinalDetailsView(
walletId: args.walletId,
ordinal: args.ordinal,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case OrdinalsFilterView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const OrdinalsFilterView(),
settings: RouteSettings(name: settings.name));
case UtxoDetailsView.routeName:
if (args is Tuple2<Id, String>) {
return getRoute(
@ -564,6 +679,20 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ChangeRepresentativeView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ChangeRepresentativeView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case AppearanceSettingsView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,

View file

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
@ -14,9 +15,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta
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/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -32,8 +33,7 @@ const int MINIMUM_CONFIRMATIONS = 1;
const String DEFAULT_REPRESENTATIVE =
"ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo";
class BananoWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
BananoWallet({
required String walletId,
required String walletName,
@ -925,4 +925,66 @@ class BananoWallet extends CoinServiceAPI
);
await updateCachedChainHeight(height ?? 0);
}
Future<void> updateMonkeyImageBytes(List<int> bytes) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "monkeyImageBytesKey",
value: bytes,
);
}
List<int>? getMonkeyImageBytes() {
return DB.instance.get<dynamic>(
boxName: _walletId,
key: "monkeyImageBytesKey",
) as List<int>?;
}
Future<String> getCurrentRepresentative() async {
final serverURI = Uri.parse(getCurrentNode().host);
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE;
}
Future<bool> changeRepresentative(String newRepresentative) async {
try {
final serverURI = Uri.parse(getCurrentNode().host);
final balance = this.balance.spendable.raw.toString();
final String privateKey = await getPrivateKeyFromMnemonic();
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
if (response.accountInfo == null) {
throw response.exception ?? Exception("Failed to get account info");
}
final work = await requestWork(response.accountInfo!.frontier);
return await NanoAPI.changeRepresentative(
server: serverURI,
accountType: NanoAccountType.BANANO,
account: address,
newRepresentative: newRepresentative,
previousBlock: response.accountInfo!.frontier,
balance: balance,
privateKey: privateKey,
work: work!,
);
} catch (_) {
rethrow;
}
}
}

View file

@ -27,6 +27,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart';
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -218,6 +219,15 @@ abstract class CoinServiceAPI {
cachedClient: cachedClient,
tracker: tracker);
case Coin.stellar:
return StellarWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
secureStore: secureStorageInterface,
tracker: tracker,
);
case Coin.wownero:
return WowneroWallet(
walletId: walletId,
@ -275,6 +285,15 @@ abstract class CoinServiceAPI {
cachedClient: cachedClient,
tracker: tracker,
);
case Coin.stellarTestnet:
return StellarWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
secureStore: secureStorageInterface,
tracker: tracker,
);
}
}

View file

@ -1023,6 +1023,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
final response = await EthereumAPI.getEthTransactions(
address: thisAddress,
firstBlock: isRescan ? 0 : firstBlock,
includeTokens: true,
);
if (response.value == null) {
@ -1057,8 +1058,10 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
txFailed = true;
}
isIncoming = false;
} else {
} else if (checksumEthereumAddress(element.to) == thisAddress) {
isIncoming = true;
} else {
continue;
}
//Calculate fees (GasLimit * gasPrice)

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/ordinals_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/mixins/xpubable.dart';
@ -109,7 +110,12 @@ String constructDerivePath({
}
class LitecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface
with
WalletCache,
WalletDB,
ElectrumXParsing,
CoinControlInterface,
OrdinalsInterface
implements XPubAble {
LitecoinWallet({
required String walletId,
@ -130,6 +136,7 @@ class LitecoinWallet extends CoinServiceAPI
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initOrdinalsInterface(walletId: walletId, coin: coin, db: db);
initCoinControlInterface(
walletId: walletId,
walletName: walletName,
@ -1864,14 +1871,6 @@ class LitecoinWallet extends CoinServiceAPI
String? blockReason;
String? label;
final utxoAmount = jsonUTXO["value"] as int;
if (utxoAmount <= 10000) {
shouldBlock = true;
blockReason = "May contain ordinal";
label = "Possible ordinal";
}
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
@ -1886,6 +1885,25 @@ class LitecoinWallet extends CoinServiceAPI
}
}
final utxoAmount = jsonUTXO["value"] as int;
// TODO check the specific output, not just the address in general
// TODO optimize by freezing output in OrdinalsInterface, so one ordinal API calls is made (or at least many less)
if (utxoOwnerAddress != null) {
if (await inscriptionInAddress(utxoOwnerAddress!)) {
shouldBlock = true;
blockReason = "Ordinal";
label = "Ordinal detected at address";
}
} else {
// TODO implement inscriptionInOutput
if (utxoAmount <= 10000) {
shouldBlock = true;
blockReason = "May contain ordinal";
label = "Possible ordinal";
}
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
@ -1910,7 +1928,12 @@ class LitecoinWallet extends CoinServiceAPI
level: LogLevel.Info,
);
await db.updateUTXOs(walletId, outputArray);
bool inscriptionsRefreshNeeded =
await db.updateUTXOs(walletId, outputArray);
if (inscriptionsRefreshNeeded) {
await refreshInscriptions();
}
// finally update balance
await _updateBalance();

View file

@ -21,6 +21,7 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/ordinals_interface.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/services/mixins/xpubable.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -244,6 +245,8 @@ class Manager with ChangeNotifier {
bool get hasCoinControlSupport => _currentWallet is CoinControlInterface;
bool get hasOrdinalsSupport => _currentWallet is OrdinalsInterface;
bool get hasTokenSupport => _currentWallet.coin == Coin.ethereum;
bool get hasWhirlpoolSupport => false;

View file

@ -24,9 +24,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta
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/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -42,8 +42,7 @@ const int MINIMUM_CONFIRMATIONS = 1;
const String DEFAULT_REPRESENTATIVE =
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
class NanoWallet extends CoinServiceAPI
with WalletCache, WalletDB, CoinControlInterface {
class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
NanoWallet({
required String walletId,
required String walletName,
@ -937,4 +936,51 @@ class NanoWallet extends CoinServiceAPI
);
await updateCachedChainHeight(height ?? 0);
}
Future<String> getCurrentRepresentative() async {
final serverURI = Uri.parse(getCurrentNode().host);
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE;
}
Future<bool> changeRepresentative(String newRepresentative) async {
try {
final serverURI = Uri.parse(getCurrentNode().host);
final balance = this.balance.spendable.raw.toString();
final String privateKey = await getPrivateKeyFromMnemonic();
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
if (response.accountInfo == null) {
throw response.exception ?? Exception("Failed to get account info");
}
final work = await requestWork(response.accountInfo!.frontier);
return await NanoAPI.changeRepresentative(
server: serverURI,
accountType: NanoAccountType.NANO,
account: address,
newRepresentative: newRepresentative,
previousBlock: response.accountInfo!.frontier,
balance: balance,
privateKey: privateKey,
work: work!,
);
} catch (_) {
rethrow;
}
}
}

View file

@ -0,0 +1,809 @@
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart' as SWBalance;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'
as SWAddress;
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'
as SWTransaction;
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_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/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart';
import 'package:tuple/tuple.dart';
const int MINIMUM_CONFIRMATIONS = 1;
class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
late StellarSDK stellarSdk;
late Network stellarNetwork;
StellarWallet({
required String walletId,
required String walletName,
required Coin coin,
required TransactionNotificationTracker tracker,
required SecureStorageInterface secureStore,
MainDB? mockableOverride,
}) {
txTracker = tracker;
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
if (coin.name == "stellarTestnet") {
stellarSdk = StellarSDK.TESTNET;
stellarNetwork = Network.TESTNET;
} else {
stellarSdk = StellarSDK.PUBLIC;
stellarNetwork = Network.PUBLIC;
}
}
late final TransactionNotificationTracker txTracker;
late SecureStorageInterface _secureStore;
// final StellarSDK stellarSdk = StellarSDK.PUBLIC;
@override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite;
@override
set isFavorite(bool isFavorite) {
_isFavorite = isFavorite;
updateCachedIsFavorite(isFavorite);
}
@override
bool get shouldAutoSync => _shouldAutoSync;
bool _shouldAutoSync = true;
Timer? timer;
final _prefs = Prefs.instance;
@override
set shouldAutoSync(bool shouldAutoSync) {
if (_shouldAutoSync != shouldAutoSync) {
_shouldAutoSync = shouldAutoSync;
if (!shouldAutoSync) {
timer?.cancel();
timer = null;
stopNetworkAlivePinging();
} else {
startNetworkAlivePinging();
refresh();
}
}
}
Timer? _networkAliveTimer;
void startNetworkAlivePinging() {
// call once on start right away
_periodicPingCheck();
// then periodically check
_networkAliveTimer = Timer.periodic(
Constants.networkAliveTimerDuration,
(_) async {
_periodicPingCheck();
},
);
}
void stopNetworkAlivePinging() {
_networkAliveTimer?.cancel();
_networkAliveTimer = null;
}
void _periodicPingCheck() async {
bool hasNetwork = await testNetworkConnection();
if (_isConnected != hasNetwork) {
NodeConnectionStatus status = hasNetwork
? NodeConnectionStatus.connected
: NodeConnectionStatus.disconnected;
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
status,
walletId,
coin,
),
);
_isConnected = hasNetwork;
if (hasNetwork) {
unawaited(refresh());
}
}
}
@override
String get walletName => _walletName;
late String _walletName;
@override
set walletName(String name) => _walletName = name;
@override
SWBalance.Balance get balance => _balance ??= getCachedBalance();
SWBalance.Balance? _balance;
@override
Coin get coin => _coin;
late Coin _coin;
Future<bool> _accountExists(String accountId) async {
bool exists = false;
try {
AccountResponse receiverAccount =
await stellarSdk.accounts.account(accountId);
if (receiverAccount.accountId != "") {
exists = true;
}
} catch (e, s) {
Logging.instance.log(
"Error getting account ${e.toString()} - ${s.toString()}",
level: LogLevel.Error);
}
return exists;
}
@override
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed');
KeyPair senderKeyPair = KeyPair.fromSecretSeed(secretSeed!);
AccountResponse sender =
await stellarSdk.accounts.account(senderKeyPair.accountId);
final amountToSend = txData['recipientAmt'] as Amount;
//First check if account exists, can be skipped, but if the account does not exist,
// the transaction fee will be charged when the transaction fails.
bool validAccount = await _accountExists(txData['address'] as String);
Transaction transaction;
if (!validAccount) {
//Fund the account, user must ensure account is correct
CreateAccountOperationBuilder createAccBuilder =
CreateAccountOperationBuilder(
txData['address'] as String, amountToSend.decimal.toString());
transaction = TransactionBuilder(sender)
.addOperation(createAccBuilder.build())
.build();
} else {
transaction = TransactionBuilder(sender)
.addOperation(PaymentOperationBuilder(txData['address'] as String,
Asset.NATIVE, amountToSend.decimal.toString())
.build())
.build();
}
transaction.sign(senderKeyPair, stellarNetwork);
try {
SubmitTransactionResponse response =
await stellarSdk.submitTransaction(transaction);
if (!response.success) {
throw ("Unable to send transaction");
}
return response.hash!;
} catch (e, s) {
Logging.instance.log("Error sending TX $e - $s", level: LogLevel.Error);
rethrow;
}
}
Future<SWAddress.Address?> get _currentReceivingAddress => db
.getAddresses(walletId)
.filter()
.typeEqualTo(SWAddress.AddressType.unknown)
.and()
.subTypeEqualTo(SWAddress.AddressSubType.unknown)
.sortByDerivationIndexDesc()
.findFirst();
@override
Future<String> get currentReceivingAddress async =>
(await _currentReceivingAddress)?.value ?? await getAddressSW();
Future<int> getBaseFee() async {
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
final nodeURI = Uri.parse(getCurrentNode().host);
final httpClient = http.Client();
FeeStatsResponse fsp =
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
return int.parse(fsp.lastLedgerBaseFee);
}
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
var baseFee = await getBaseFee();
int fee = 100;
switch (feeRate) {
case 0:
fee = baseFee * 10;
case 1:
case 2:
fee = baseFee * 50;
case 3:
fee = baseFee * 100;
case 4:
fee = baseFee * 200;
default:
fee = baseFee * 50;
}
return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals);
}
@override
Future<void> exit() async {
_hasCalledExit = true;
timer?.cancel();
timer = null;
stopNetworkAlivePinging();
}
NodeModel? _xlmNode;
NodeModel getCurrentNode() {
if (_xlmNode != null) {
return _xlmNode!;
} else if (NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) !=
null) {
return NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin)!;
} else {
return DefaultNodes.getNodeFor(coin);
}
}
@override
Future<FeeObject> get fees async {
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
final nodeURI = Uri.parse(getCurrentNode().host);
final httpClient = http.Client();
FeeStatsResponse fsp =
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
return FeeObject(
numberOfBlocksFast: 0,
numberOfBlocksAverage: 0,
numberOfBlocksSlow: 0,
fast: int.parse(fsp.lastLedgerBaseFee) * 100,
medium: int.parse(fsp.lastLedgerBaseFee) * 50,
slow: int.parse(fsp.lastLedgerBaseFee) * 10);
}
@override
Future<void> fullRescan(
int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async {
await _prefs.init();
await updateTransactions();
await updateChainHeight();
await updateBalance();
}
@override
Future<bool> generateNewAddress() {
// TODO: implement generateNewAddress
throw UnimplementedError();
}
@override
bool get hasCalledExit => _hasCalledExit;
bool _hasCalledExit = false;
@override
Future<void> initializeExisting() async {
await _prefs.init();
}
@override
Future<void> initializeNew() async {
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!");
}
await _prefs.init();
String mnemonic = await Wallet.generate24WordsMnemonic();
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: "");
Wallet wallet = await Wallet.from(mnemonic);
KeyPair keyPair = await wallet.getKeyPair(index: 0);
String address = keyPair.accountId;
String secretSeed =
keyPair.secretSeed; //This will be required for sending a tx
await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed);
final swAddress = SWAddress.Address(
walletId: walletId,
value: address,
publicKey: keyPair.publicKey,
derivationIndex: 0,
derivationPath: null,
type: SWAddress.AddressType.unknown, // TODO: set type
subType: SWAddress.AddressSubType.unknown);
await db.putAddress(swAddress);
await Future.wait(
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
}
Future<String> getAddressSW() async {
var mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic');
Wallet wallet = await Wallet.from(mnemonic!);
KeyPair keyPair = await wallet.getKeyPair(index: 0);
return Future.value(keyPair.accountId);
}
@override
bool get isConnected => _isConnected;
bool _isConnected = false;
@override
bool get isRefreshing => refreshMutex;
bool refreshMutex = false;
@override
// TODO: implement maxFee
Future<int> get maxFee => throw UnimplementedError();
@override
Future<List<String>> get mnemonic =>
mnemonicString.then((value) => value!.split(" "));
@override
Future<String?> get mnemonicPassphrase =>
_secureStore.read(key: '${_walletId}_mnemonicPassphrase');
@override
Future<String?> get mnemonicString =>
_secureStore.read(key: '${_walletId}_mnemonic');
@override
Future<Map<String, dynamic>> prepareSend(
{required String address,
required Amount amount,
Map<String, dynamic>? args}) async {
try {
final feeRate = args?["feeRate"];
var fee = 1000;
if (feeRate is FeeRateType) {
final theFees = await fees;
switch (feeRate) {
case FeeRateType.fast:
fee = theFees.fast;
case FeeRateType.slow:
fee = theFees.slow;
case FeeRateType.average:
default:
fee = theFees.medium;
}
}
Map<String, dynamic> txData = {
"fee": fee,
"address": address,
"recipientAmt": amount,
};
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
return txData;
} catch (e, s) {
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
rethrow;
}
}
@override
Future<void> recoverFromMnemonic(
{required String mnemonic,
String? mnemonicPassphrase,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
required int height}) async {
if ((await mnemonicString) != null ||
(await this.mnemonicPassphrase) != null) {
throw Exception("Attempted to overwrite mnemonic on restore!");
}
var wallet = await Wallet.from(mnemonic);
var keyPair = await wallet.getKeyPair(index: 0);
var address = keyPair.accountId;
var secretSeed = keyPair.secretSeed;
await _secureStore.write(
key: '${_walletId}_mnemonic', value: mnemonic.trim());
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: mnemonicPassphrase ?? "",
);
await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed);
final swAddress = SWAddress.Address(
walletId: walletId,
value: address,
publicKey: keyPair.publicKey,
derivationIndex: 0,
derivationPath: null,
type: SWAddress.AddressType.unknown, // TODO: set type
subType: SWAddress.AddressSubType.unknown);
await db.putAddress(swAddress);
await Future.wait(
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
}
Future<void> updateChainHeight() async {
final height = await stellarSdk.ledgers
.order(RequestBuilderOrder.DESC)
.limit(1)
.execute()
.then((value) => value.records!.first.sequence)
.onError((error, stackTrace) => throw ("Error getting chain height"));
await updateCachedChainHeight(height);
}
Future<void> updateTransactions() async {
try {
List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>>
transactionList = [];
Page<OperationResponse> payments = await stellarSdk.payments
.forAccount(await getAddressSW())
.order(RequestBuilderOrder.DESC)
.execute()
.onError(
(error, stackTrace) => throw ("Could not fetch transactions"));
for (OperationResponse response in payments.records!) {
// PaymentOperationResponse por;
if (response is PaymentOperationResponse) {
PaymentOperationResponse por = response;
Logging.instance.log(
"ALL TRANSACTIONS IS ${por.transactionSuccessful}",
level: LogLevel.Info);
Logging.instance.log("THIS TX HASH IS ${por.transactionHash}",
level: LogLevel.Info);
SWTransaction.TransactionType type;
if (por.sourceAccount == await getAddressSW()) {
type = SWTransaction.TransactionType.outgoing;
} else {
type = SWTransaction.TransactionType.incoming;
}
final amount = Amount(
rawValue: BigInt.parse(float
.parse(por.amount!)
.toStringAsFixed(coin.decimals)
.replaceAll(".", "")),
fractionDigits: coin.decimals,
);
int fee = 0;
int height = 0;
//Query the transaction linked to the payment,
// por.transaction returns a null sometimes
TransactionResponse tx =
await stellarSdk.transactions.transaction(por.transactionHash!);
if (tx.hash.isNotEmpty) {
fee = tx.feeCharged!;
height = tx.ledger;
}
var theTransaction = SWTransaction.Transaction(
walletId: walletId,
txid: por.transactionHash!,
timestamp:
DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000,
type: type,
subType: SWTransaction.TransactionSubType.none,
amount: 0,
amountString: amount.toJsonString(),
fee: fee,
height: height,
isCancelled: false,
isLelantus: false,
slateId: "",
otherData: "",
inputs: [],
outputs: [],
nonce: 0,
numberOfMessages: null,
);
SWAddress.Address? receivingAddress = await _currentReceivingAddress;
SWAddress.Address address =
type == SWTransaction.TransactionType.incoming
? receivingAddress!
: SWAddress.Address(
walletId: walletId,
value: por.sourceAccount!,
publicKey:
KeyPair.fromAccountId(por.sourceAccount!).publicKey,
derivationIndex: 0,
derivationPath: null,
type: SWAddress.AddressType.unknown, // TODO: set type
subType: SWAddress.AddressSubType.unknown);
Tuple2<SWTransaction.Transaction, SWAddress.Address> tuple =
Tuple2(theTransaction, address);
transactionList.add(tuple);
} else if (response is CreateAccountOperationResponse) {
CreateAccountOperationResponse caor = response;
SWTransaction.TransactionType type;
if (caor.sourceAccount == await getAddressSW()) {
type = SWTransaction.TransactionType.outgoing;
} else {
type = SWTransaction.TransactionType.incoming;
}
final amount = Amount(
rawValue: BigInt.parse(float
.parse(caor.startingBalance!)
.toStringAsFixed(coin.decimals)
.replaceAll(".", "")),
fractionDigits: coin.decimals,
);
int fee = 0;
int height = 0;
TransactionResponse tx =
await stellarSdk.transactions.transaction(caor.transactionHash!);
if (tx.hash.isNotEmpty) {
fee = tx.feeCharged!;
height = tx.ledger;
}
var theTransaction = SWTransaction.Transaction(
walletId: walletId,
txid: caor.transactionHash!,
timestamp:
DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000,
type: type,
subType: SWTransaction.TransactionSubType.none,
amount: 0,
amountString: amount.toJsonString(),
fee: fee,
height: height,
isCancelled: false,
isLelantus: false,
slateId: "",
otherData: "",
inputs: [],
outputs: [],
nonce: 0,
numberOfMessages: null,
);
SWAddress.Address? receivingAddress = await _currentReceivingAddress;
SWAddress.Address address =
type == SWTransaction.TransactionType.incoming
? receivingAddress!
: SWAddress.Address(
walletId: walletId,
value: caor.sourceAccount!,
publicKey:
KeyPair.fromAccountId(caor.sourceAccount!).publicKey,
derivationIndex: 0,
derivationPath: null,
type: SWAddress.AddressType.unknown, // TODO: set type
subType: SWAddress.AddressSubType.unknown);
Tuple2<SWTransaction.Transaction, SWAddress.Address> tuple =
Tuple2(theTransaction, address);
transactionList.add(tuple);
}
}
await db.addNewTransactionData(transactionList, walletId);
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from updateTransactions(): $e\n$s",
level: LogLevel.Error);
}
}
Future<void> updateBalance() async {
try {
AccountResponse accountResponse =
await stellarSdk.accounts.account(await getAddressSW());
for (Balance balance in accountResponse.balances) {
switch (balance.assetType) {
case Asset.TYPE_NATIVE:
_balance = SWBalance.Balance(
total: Amount(
rawValue: BigInt.from(float.parse(balance.balance) * 10000000),
fractionDigits: coin.decimals,
),
spendable: Amount(
rawValue: BigInt.from(float.parse(balance.balance) * 10000000),
fractionDigits: coin.decimals,
),
blockedTotal: Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
),
pendingSpendable: Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
),
);
Logging.instance.log(_balance, level: LogLevel.Info);
await updateCachedBalance(_balance!);
}
}
} catch (e, s) {
Logging.instance.log(
"ERROR GETTING BALANCE $e\n$s",
level: LogLevel.Info,
);
}
}
@override
Future<void> refresh() async {
if (refreshMutex) {
Logging.instance.log(
"$walletId $walletName refreshMutex denied",
level: LogLevel.Info,
);
return;
} else {
refreshMutex = true;
}
try {
await _prefs.init();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
await updateChainHeight();
await updateTransactions();
await updateBalance();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
Logging.instance.log(
"Periodic refresh check for $walletId $walletName in object instance: $hashCode",
level: LogLevel.Info);
await refresh();
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"New data found in $walletId $walletName in background!",
walletId,
),
);
});
}
} catch (e, s) {
Logging.instance.log(
"Failed to refresh stellar wallet $walletId: '$walletName': $e\n$s",
level: LogLevel.Warning,
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
}
refreshMutex = false;
}
@override
int get storedChainHeight => getCachedChainHeight();
@override
Future<bool> testNetworkConnection() {
// TODO: implement testNetworkConnection
throw UnimplementedError();
}
@override
Future<List<SWTransaction.Transaction>> get transactions =>
db.getTransactions(walletId).findAll();
@override
Future<void> updateNode(bool shouldRefresh) async {
_xlmNode = NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
if (shouldRefresh) {
unawaited(refresh());
}
}
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final transaction = SWTransaction.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: SWTransaction.TransactionType.outgoing,
subType: SWTransaction.TransactionSubType.none,
// precision may be lost here hence the following amountString
amount: (txData["recipientAmt"] as Amount).raw.toInt(),
amountString: (txData["recipientAmt"] as Amount).toJsonString(),
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
nonce: null,
inputs: [],
outputs: [],
numberOfMessages: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple2(transaction, address),
],
walletId,
);
}
@override
// TODO: implement utxos
Future<List<UTXO>> get utxos => throw UnimplementedError();
@override
bool validateAddress(String address) {
return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
}
@override
String get walletId => _walletId;
late String _walletId;
}

View file

@ -10,7 +10,6 @@
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart';
@ -50,6 +49,7 @@ abstract class EthereumAPI {
static Future<EthereumResponse<List<EthTxDTO>>> getEthTransactions({
required String address,
int firstBlock = 0,
bool includeTokens = false,
}) async {
try {
final response = await get(
@ -67,7 +67,7 @@ abstract class EthereumAPI {
for (final map in list!) {
final txn = EthTxDTO.fromMap(Map<String, dynamic>.from(map as Map));
if (txn.hasToken == 0) {
if (txn.hasToken == 0 || includeTokens) {
txns.add(txn);
}
}
@ -76,9 +76,11 @@ abstract class EthereumAPI {
null,
);
} else {
throw EthApiException(
"getEthTransactions($address) response is empty but status code is "
"${response.statusCode}",
// nice that the api returns an empty body instead of being
// consistent and returning a json object with no transactions
return EthereumResponse(
[],
null,
);
}
} else {
@ -196,9 +198,11 @@ abstract class EthereumAPI {
null,
);
} else {
throw EthApiException(
"getEthTransactionNonces($txns) response is empty but status code is "
"${response.statusCode}",
// nice that the api returns an empty body instead of being
// consistent and returning a json object with no transactions
return EthereumResponse(
[],
null,
);
}
} else {
@ -252,13 +256,13 @@ abstract class EthereumAPI {
);
} else {
throw EthApiException(
"getEthTransaction($txids) response is empty but status code is "
"getEthTokenTransactionsByTxids($txids) response is empty but status code is "
"${response.statusCode}",
);
}
} else {
throw EthApiException(
"getEthTransaction($txids) failed with status code: "
"getEthTokenTransactionsByTxids($txids) failed with status code: "
"${response.statusCode}",
);
}
@ -269,7 +273,7 @@ abstract class EthereumAPI {
);
} catch (e, s) {
Logging.instance.log(
"getEthTransaction($txids): $e\n$s",
"getEthTokenTransactionsByTxids($txids): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(
@ -307,9 +311,11 @@ abstract class EthereumAPI {
null,
);
} else {
throw EthApiException(
"getTokenTransactions($address, $tokenContractAddress) response is empty but status code is "
"${response.statusCode}",
// nice that the api returns an empty body instead of being
// consistent and returning a json object with no transactions
return EthereumResponse(
[],
null,
);
}
} else {
@ -424,10 +430,10 @@ abstract class EthereumAPI {
final map = json["data"].first as Map;
final balance =
Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero;
BigInt.tryParse(map["units"].toString()) ?? BigInt.zero;
return EthereumResponse(
Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int),
Amount(rawValue: balance, fractionDigits: map["decimals"] as int),
null,
);
} else {

View file

@ -135,6 +135,9 @@ class ExchangeDataLoadingService {
Future<void> loadAll() async {
if (!_locked) {
_locked = true;
if (_isar == null) {
await initDB();
}
Logging.instance.log(
"ExchangeDataLoadingService.loadAll starting...",
level: LogLevel.Info,

View file

@ -0,0 +1,79 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
class LitescribeAPI {
static final LitescribeAPI _instance = LitescribeAPI._internal();
factory LitescribeAPI({required String baseUrl}) {
_instance.baseUrl = baseUrl;
return _instance;
}
LitescribeAPI._internal();
late String baseUrl;
Future<LitescribeResponse> _getResponse(String endpoint) async {
final response = await http.get(Uri.parse('$baseUrl$endpoint'));
if (response.statusCode == 200) {
return LitescribeResponse(data: _validateJson(response.body));
} else {
throw Exception(
'LitescribeAPI _getResponse exception: Failed to load data');
}
}
Map<String, dynamic> _validateJson(String responseBody) {
final parsed = jsonDecode(responseBody);
if (parsed is Map<String, dynamic>) {
return parsed;
} else {
throw const FormatException(
'LitescribeAPI _validateJson exception: Invalid JSON format');
}
}
Future<List<InscriptionData>> getInscriptionsByAddress(String address,
{int cursor = 0, int size = 1000}) async {
// size param determines how many inscriptions are returned per response
// default of 1000 is used to cover most addresses (I assume)
// if the total number of inscriptions at the address exceeds the length of the list of inscriptions returned, another call with a higher size is made
final int defaultLimit = 1000;
final response = await _getResponse(
'/address/inscriptions?address=$address&cursor=$cursor&size=$size');
// Check if the number of returned inscriptions equals the limit
final int total = response.data['result']['total'] as int;
int currentSize = 0;
if (total == 0) {
return <InscriptionData>[];
}
final list = response.data['result']!['list'];
currentSize = list.length as int;
if (currentSize == size && currentSize < total) {
// If the number of returned inscriptions equals the limit and there are more inscriptions available,
// increment the cursor and make the next API call to fetch the remaining inscriptions.
final int newCursor = cursor + size;
return getInscriptionsByAddress(address, cursor: newCursor, size: size);
} else {
try {
// Iterate through the list and create InscriptionData objects from each element
final List<InscriptionData> inscriptions = (list as List<dynamic>)
.map((json) =>
InscriptionData.fromJson(json as Map<String, dynamic>))
.toList();
return inscriptions;
} catch (e) {
throw const FormatException(
'LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure');
}
}
}
}

View file

@ -1,61 +0,0 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:stackwallet/db/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? ??
0;
}
Future<void> firoUpdateMintIndex(int mintIndex) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: "mintIndex",
value: mintIndex,
);
}
}

View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
import 'package:stackwallet/services/litescribe_api.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
mixin OrdinalsInterface {
late final String _walletId;
late final Coin _coin;
late final MainDB _db;
void initOrdinalsInterface({
required String walletId,
required Coin coin,
required MainDB db,
}) {
_walletId = walletId;
_coin = coin;
_db = db;
}
final LitescribeAPI litescribeAPI =
LitescribeAPI(baseUrl: 'https://litescribe.io/api');
Future<void> refreshInscriptions() async {
final uniqueAddresses = await _db
.getUTXOs(_walletId)
.filter()
.addressIsNotNull()
.distinctByAddress()
.addressProperty()
.findAll();
final inscriptions =
await _getInscriptionDataFromAddresses(uniqueAddresses.cast<String>());
final ords = inscriptions
.map((e) => Ordinal.fromInscriptionData(e, _walletId))
.toList();
await _db.isar.writeTxn(() async {
await _db.isar.ordinals
.where()
.filter()
.walletIdEqualTo(_walletId)
.deleteAll();
await _db.isar.ordinals.putAll(ords);
});
}
Future<List<InscriptionData>> _getInscriptionDataFromAddresses(
List<String> addresses) async {
List<InscriptionData> allInscriptions = [];
for (String address in addresses) {
try {
var inscriptions =
await litescribeAPI.getInscriptionsByAddress(address);
allInscriptions.addAll(inscriptions);
} catch (e) {
throw Exception("Error fetching inscriptions for address $address: $e");
}
}
return allInscriptions;
}
// check if an inscription is in a given <UTXO> output
Future<bool> inscriptionInOutput(UTXO output) async {
if (output.address != null) {
var inscriptions =
await litescribeAPI.getInscriptionsByAddress("${output.address}");
if (inscriptions.isNotEmpty) {
return true;
} else {
return false;
}
} else {
throw UnimplementedError(
'TODO look up utxo without address. utxo->txid:output->address');
}
}
// check if an inscription is in a given <UTXO> output
Future<bool> inscriptionInAddress(String address) async {
var inscriptions = await litescribeAPI.getInscriptionsByAddress(address);
if (inscriptions.isNotEmpty) {
return true;
} else {
return false;
}
}
}

View file

@ -0,0 +1,40 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/utilities/logger.dart';
final pMonKeyService = Provider((ref) => MonKeyService());
class MonKeyService {
static const baseURL = "https://monkey.banano.cc/api/v1/monkey/";
Future<Uint8List> fetchMonKey({
required String address,
bool png = false,
}) async {
try {
String url = "https://monkey.banano.cc/api/v1/monkey/$address";
if (png) {
url += '?format=png&size=512&background=false';
}
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return response.bodyBytes;
} else {
throw Exception(
"statusCode=${response.statusCode} body=${response.body}",
);
}
} catch (e, s) {
Logging.instance.log(
"Failed fetchMonKey($address): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
}

130
lib/services/nano_api.dart Normal file
View file

@ -0,0 +1,130 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:nanodart/nanodart.dart';
class NanoAPI {
static Future<
({
NAccountInfo? accountInfo,
Exception? exception,
})> getAccountInfo({
required Uri server,
required bool representative,
required String account,
}) async {
NAccountInfo? accountInfo;
Exception? exception;
try {
final response = await http.post(
server,
headers: {
"Content-Type": "application/json",
},
body: jsonEncode({
"action": "account_info",
"representative": "true",
"account": account,
}),
);
final map = jsonDecode(response.body);
if (map is Map && map["error"] != null) {
throw Exception(map["error"].toString());
}
accountInfo = NAccountInfo(
frontier: map["frontier"] as String,
representative: map["representative"] as String,
);
} on Exception catch (e) {
exception = e;
} catch (e) {
exception = Exception(e.toString());
}
return (accountInfo: accountInfo, exception: exception);
}
static Future<bool> changeRepresentative({
required Uri server,
required int accountType,
required String account,
required String newRepresentative,
required String previousBlock,
required String balance,
required String privateKey,
required String work,
}) async {
Map<String, String> block = {
"type": "state",
"account": account,
"previous": previousBlock,
"representative": newRepresentative,
"balance": balance,
"link":
"0000000000000000000000000000000000000000000000000000000000000000",
"work": work,
};
final String hash;
try {
hash = NanoBlocks.computeStateHash(
accountType,
account,
previousBlock,
newRepresentative,
BigInt.parse(balance),
block["link"] as String,
);
} catch (e) {
if (e is RangeError) {
throw Exception("Invalid representative format");
}
rethrow;
}
final signature = NanoSignatures.signBlock(hash, privateKey);
block["signature"] = signature;
final map = await postBlock(server: server, block: block);
if (map is Map && map["error"] != null) {
throw Exception(map["error"].toString());
}
return map["error"] == null;
}
// TODO: GET RID OF DYNAMIC AND USED TYPED DATA
static Future<dynamic> postBlock({
required Uri server,
required Map<String, dynamic> block,
}) async {
final response = await http.post(
server,
headers: {
"Content-Type": "application/json",
},
body: jsonEncode({
"action": "process",
"json_block": "true",
"subtype": "change",
"block": block,
}),
);
return jsonDecode(response.body);
}
}
class NAccountInfo {
final String frontier;
final String representative;
NAccountInfo({required this.frontier, required this.representative});
}

View file

@ -26,6 +26,7 @@ class NotificationApi {
priority: Priority.high,
ticker: 'ticker'),
iOS: IOSNotificationDetails(),
macOS: MacOSNotificationDetails(),
);
}
@ -34,8 +35,13 @@ class NotificationApi {
const iOS = IOSInitializationSettings();
const linux = LinuxInitializationSettings(
defaultActionName: "temporary_stack_wallet");
const settings =
InitializationSettings(android: android, iOS: iOS, linux: linux);
const macOS = MacOSInitializationSettings();
const settings = InitializationSettings(
android: android,
iOS: iOS,
linux: linux,
macOS: macOS,
);
await _notifications.initialize(
settings,
onSelectNotification: (payload) async {
@ -71,8 +77,10 @@ class NotificationApi {
final id = prefs.currentNotificationId;
String confirms = "";
if (txid != null) {
confirms = " (${confirmations!}/${requiredConfirmations!})";
if (txid != null &&
confirmations != null &&
requiredConfirmations != null) {
confirms = " ($confirmations/$requiredConfirmations)";
}
final NotificationModel model = NotificationModel(

View file

@ -100,7 +100,7 @@ class PriceAPI {
Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency"
"=${baseCurrency.toLowerCase()}"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
final coinGeckoResponse = await client.get(

View file

@ -22,3 +22,14 @@ final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
return null;
}
});
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
if (assets is ThemeAssetsV3) {
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
assets.coinCardImages?[coin.mainNetVersion];
} else {
return null;
}
});

View file

@ -28,6 +28,7 @@ class CoinThemeColorDefault {
Color get namecoin => const Color(0xFF91B1E1);
Color get wownero => const Color(0xFFED80C1);
Color get particl => const Color(0xFF8175BD);
Color get stellar => const Color(0xFF6600FF);
Color get nano => const Color(0xFF209CE9);
Color get banano => const Color(0xFFFBDD11);
@ -62,6 +63,9 @@ class CoinThemeColorDefault {
return wownero;
case Coin.particl:
return particl;
case Coin.stellar:
case Coin.stellarTestnet:
return stellar;
case Coin.nano:
return nano;
case Coin.banano:

View file

@ -1707,6 +1707,9 @@ class StackColors extends ThemeExtension<StackColors> {
return _coin.wownero;
case Coin.particl:
return _coin.particl;
case Coin.stellar:
case Coin.stellarTestnet:
return _coin.stellar;
case Coin.nano:
return _coin.nano;
case Coin.banano:

View file

@ -27,7 +27,7 @@ final pThemeService = Provider<ThemeService>((ref) {
});
class ThemeService {
static const _currentDefaultThemeVersion = 3;
static const _currentDefaultThemeVersion = 4;
ThemeService._();
static ThemeService? _instance;
static ThemeService get instance => _instance ??= ThemeService._();

View file

@ -105,6 +105,8 @@ class AddressUtils {
return Address.validateAddress(address, namecoin, namecoin.bech32!);
case Coin.particl:
return Address.validateAddress(address, particl);
case Coin.stellar:
return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
case Coin.nano:
return NanoAccounts.isValid(NanoAccountType.NANO, address);
case Coin.banano:
@ -139,6 +141,8 @@ class AddressUtils {
return Address.validateAddress(address, firoTestNetwork);
case Coin.dogecoinTestNet:
return Address.validateAddress(address, dogecointestnet);
case Coin.stellarTestnet:
return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
}
}

View file

@ -70,6 +70,11 @@ class AmountFormatter {
String string, {
EthContract? ethContract,
}) {
return unit.tryParse(string, locale: locale, coin: coin);
return unit.tryParse(
string,
locale: locale,
coin: coin,
tokenContract: ethContract,
);
}
}

View file

@ -50,6 +50,8 @@ enum AmountUnit {
case Coin.dogecoin:
case Coin.eCash:
case Coin.epicCash:
case Coin.stellar: // TODO: check if this is correct
case Coin.stellarTestnet:
return AmountUnit.values.sublist(0, 4);
case Coin.monero:
@ -168,6 +170,7 @@ extension AmountUnitExt on AmountUnit {
required String locale,
required Coin coin,
EthContract? tokenContract,
bool overrideWithDecimalPlacesFromString = false,
}) {
final precisionLost = value.startsWith("~");
@ -201,7 +204,9 @@ extension AmountUnitExt on AmountUnit {
return null;
}
final decimalPlaces = tokenContract?.decimals ?? coin.decimals;
final decimalPlaces = overrideWithDecimalPlacesFromString
? decimal.scale
: tokenContract?.decimals ?? coin.decimals;
final realShift = math.min(shift, decimalPlaces);
return decimal.shift(0 - realShift).toAmount(fractionDigits: decimalPlaces);

View file

@ -92,6 +92,7 @@ class _SVG {
final coinControl = const _COIN_CONTROL();
String get monkey => "assets/svg/monkey.svg";
String get circleSliders => "assets/svg/configuration.svg";
String get circlePlus => "assets/svg/plus-circle.svg";
String get circlePlusFilled => "assets/svg/circle-plus-filled.svg";
@ -205,6 +206,8 @@ class _SVG {
String get messageQuestion => "assets/svg/message-question-1.svg";
String get list => "assets/svg/list-ul.svg";
String get unclaimedPaynym => "assets/svg/unclaimed.svg";
String get send => "assets/svg/send.svg";
String get ordinal => "assets/svg/ordinal.svg";
String get trocadorRatingA => "assets/svg/trocador_rating_a.svg";
String get trocadorRatingB => "assets/svg/trocador_rating_b.svg";

View file

@ -54,10 +54,14 @@ Uri getDefaultBlockExplorerUrlFor({
return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm");
case Coin.particl:
return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm");
case Coin.stellar:
return Uri.parse("https://stellarchain.io/tx/$txid");
case Coin.nano:
return Uri.parse("https://www.nanolooker.com/block/$txid");
case Coin.banano:
return Uri.parse("https://www.bananolooker.com/block/$txid");
case Coin.stellarTestnet:
return Uri.parse("https://testnet.stellarchain.io/transactions/$txid");
}
}

View file

@ -43,6 +43,7 @@ abstract class Constants {
BigInt.parse("1000000000000000000000000000000"); // 1*10^30
static final BigInt _satsPerCoinBanano =
BigInt.parse("100000000000000000000000000000"); // 1*10^29
static final BigInt _satsPerCoinStellar = BigInt.from(10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision
static final BigInt _satsPerCoin = BigInt.from(100000000);
static const int _decimalPlaces = 8;
static const int _decimalPlacesNano = 30;
@ -51,6 +52,7 @@ abstract class Constants {
static const int _decimalPlacesMonero = 12;
static const int _decimalPlacesEthereum = 18;
static const int _decimalPlacesECash = 2;
static const int _decimalPlacesStellar = 7;
static const int notificationsMax = 0xFFFFFFFF;
static const Duration networkAliveTimerDuration = Duration(seconds: 10);
@ -58,7 +60,7 @@ abstract class Constants {
// Enable Logger.print statements
static const bool disableLogger = false;
static const int currentDataVersion = 10;
static const int currentDataVersion = 11;
static const int rescanV1 = 1;
@ -96,6 +98,10 @@ abstract class Constants {
case Coin.eCash:
return _satsPerCoinECash;
case Coin.stellar:
case Coin.stellarTestnet:
return _satsPerCoinStellar;
}
}
@ -133,6 +139,10 @@ abstract class Constants {
case Coin.eCash:
return _decimalPlacesECash;
case Coin.stellar:
case Coin.stellarTestnet:
return _decimalPlacesStellar;
}
}
@ -155,6 +165,8 @@ abstract class Constants {
case Coin.namecoin:
case Coin.particl:
case Coin.nano:
case Coin.stellar:
case Coin.stellarTestnet:
values.addAll([24, 12]);
break;
case Coin.banano:
@ -214,6 +226,10 @@ abstract class Constants {
case Coin.nano: // TODO: Verify this
case Coin.banano: // TODO: Verify this
return 1;
case Coin.stellar:
case Coin.stellarTestnet:
return 5;
}
}
@ -241,6 +257,8 @@ abstract class Constants {
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
return 24;
case Coin.monero:

View file

@ -180,7 +180,6 @@ class DbVersionMigrator with WalletDB {
// clear possible broken firo cache
await DB.instance.clearSharedTransactionCache(coin: Coin.firo);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 4);
@ -343,12 +342,85 @@ class DbVersionMigrator with WalletDB {
// try to continue migrating
return await migrate(10, secureStore: secureStore);
case 10:
// migrate
await _v10(secureStore);
// update version
await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 11);
// try to continue migrating
return await migrate(11, secureStore: secureStore);
default:
// finally return
return;
}
}
Future<void> _v10(SecureStorageInterface secureStore) async {
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await Hive.openBox<dynamic>(DB.boxNamePrefs);
final walletsService = WalletsService(secureStorageInterface: secureStore);
final prefs = Prefs.instance;
final walletInfoList = await walletsService.walletNames;
await prefs.init();
await MainDB.instance.initMainDB();
for (final walletId in walletInfoList.keys) {
final info = walletInfoList[walletId]!;
assert(info.walletId == walletId);
if (info.coin == Coin.firo &&
MainDB.instance.isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.countSync() ==
0) {
final walletBox = await Hive.openBox<dynamic>(walletId);
final hiveLCoins = DB.instance.get<dynamic>(
boxName: walletId,
key: "_lelantus_coins",
) as List? ??
[];
final jindexes = (DB.instance
.get<dynamic>(boxName: walletId, key: "jindex") as List? ??
[])
.cast<int>();
final List<isar_models.LelantusCoin> coins = [];
for (final e in hiveLCoins) {
final map = e as Map;
final lcoin = map.values.first as LelantusCoin;
final isJMint = jindexes.contains(lcoin.index);
final coin = isar_models.LelantusCoin(
walletId: walletId,
txid: lcoin.txId,
value: lcoin.value.toString(),
mintIndex: lcoin.index,
anonymitySetId: lcoin.anonymitySetId,
isUsed: lcoin.isUsed,
isJMint: isJMint,
otherData: null,
);
coins.add(coin);
}
if (coins.isNotEmpty) {
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.lelantusCoins.putAll(coins);
});
}
}
}
}
Future<void> _v4(SecureStorageInterface secureStore) async {
await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
await Hive.openBox<dynamic>(DB.boxNamePrefs);

View file

@ -34,6 +34,7 @@ abstract class DefaultNodes {
bitcoincashTestnet,
dogecoinTestnet,
firoTestnet,
stellarTestnet,
];
static NodeModel get bitcoin => NodeModel(
@ -181,6 +182,18 @@ abstract class DefaultNodes {
isFailover: true,
isDown: false);
static NodeModel get stellar => NodeModel(
host: "https://horizon.stellar.org",
port: 443,
name: defaultName,
id: _nodeId(Coin.stellar),
useSSL: false,
enabled: true,
coinName: Coin.stellar.name,
isFailover: true,
isDown: false
);
static NodeModel get nano => NodeModel(
host: "https://rainstorm.city/api",
port: 443,
@ -263,6 +276,18 @@ abstract class DefaultNodes {
isDown: false,
);
static NodeModel get stellarTestnet => NodeModel(
host: "https://horizon-testnet.stellar.org/",
port: 50022,
name: defaultName,
id: _nodeId(Coin.stellarTestnet),
useSSL: true,
enabled: true,
coinName: Coin.stellarTestnet.name,
isFailover: true,
isDown: false,
);
static NodeModel getNodeFor(Coin coin) {
switch (coin) {
case Coin.bitcoin:
@ -301,6 +326,9 @@ abstract class DefaultNodes {
case Coin.particl:
return particl;
case Coin.stellar:
return stellar;
case Coin.nano:
return nano;
@ -321,6 +349,9 @@ abstract class DefaultNodes {
case Coin.dogecoinTestNet:
return dogecoinTestnet;
case Coin.stellarTestnet:
return stellarTestnet;
}
}
}

View file

@ -27,6 +27,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'
import 'package:stackwallet/services/coins/nano/nano_wallet.dart' as nano;
import 'package:stackwallet/services/coins/particl/particl_wallet.dart'
as particl;
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart' as xlm;
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow;
import 'package:stackwallet/utilities/constants.dart';
@ -44,6 +45,7 @@ enum Coin {
namecoin,
nano,
particl,
stellar,
wownero,
///
@ -56,6 +58,7 @@ enum Coin {
dogecoinTestNet,
firoTestNet,
litecoinTestNet,
stellarTestnet,
}
final int kTestNetCoinCount = 4; // Util.isDesktop ? 5 : 4;
@ -84,6 +87,8 @@ extension CoinExt on Coin {
return "Monero";
case Coin.particl:
return "Particl";
case Coin.stellar:
return "Stellar";
case Coin.wownero:
return "Wownero";
case Coin.namecoin:
@ -102,6 +107,8 @@ extension CoinExt on Coin {
return "tFiro";
case Coin.dogecoinTestNet:
return "tDogecoin";
case Coin.stellarTestnet:
return "tStellar";
}
}
@ -127,6 +134,8 @@ extension CoinExt on Coin {
return "XMR";
case Coin.particl:
return "PART";
case Coin.stellar:
return "XLM";
case Coin.wownero:
return "WOW";
case Coin.namecoin:
@ -145,6 +154,8 @@ extension CoinExt on Coin {
return "tFIRO";
case Coin.dogecoinTestNet:
return "tDOGE";
case Coin.stellarTestnet:
return "tXLM";
}
}
@ -171,6 +182,8 @@ extension CoinExt on Coin {
return "monero";
case Coin.particl:
return "particl";
case Coin.stellar:
return "stellar";
case Coin.wownero:
return "wownero";
case Coin.namecoin:
@ -189,6 +202,8 @@ extension CoinExt on Coin {
return "firo";
case Coin.dogecoinTestNet:
return "dogecoin";
case Coin.stellarTestnet:
return "stellar";
}
}
@ -215,6 +230,8 @@ extension CoinExt on Coin {
case Coin.wownero:
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
return false;
}
}
@ -242,6 +259,8 @@ extension CoinExt on Coin {
case Coin.firoTestNet:
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
return false;
}
}
@ -262,6 +281,7 @@ extension CoinExt on Coin {
case Coin.nano:
case Coin.banano:
case Coin.eCash:
case Coin.stellar:
return false;
case Coin.dogecoinTestNet:
@ -269,6 +289,7 @@ extension CoinExt on Coin {
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.stellarTestnet:
return true;
}
}
@ -289,6 +310,7 @@ extension CoinExt on Coin {
case Coin.nano:
case Coin.banano:
case Coin.eCash:
case Coin.stellar:
return this;
case Coin.dogecoinTestNet:
@ -305,6 +327,9 @@ extension CoinExt on Coin {
case Coin.firoTestNet:
return Coin.firo;
case Coin.stellarTestnet:
return Coin.stellar;
}
}
@ -345,6 +370,10 @@ extension CoinExt on Coin {
case Coin.particl:
return particl.MINIMUM_CONFIRMATIONS;
case Coin.stellar:
case Coin.stellarTestnet:
return xlm.MINIMUM_CONFIRMATIONS;
case Coin.wownero:
return wow.MINIMUM_CONFIRMATIONS;
@ -404,6 +433,10 @@ Coin coinFromPrettyName(String name) {
case "particl":
return Coin.particl;
case "Stellar":
case "stellar":
return Coin.stellar;
case "Namecoin":
case "namecoin":
return Coin.namecoin;
@ -448,6 +481,11 @@ Coin coinFromPrettyName(String name) {
case "banano":
return Coin.banano;
case "Stellar Testnet":
case "stellarTestnet":
case "tStellar":
return Coin.stellarTestnet;
default:
throw ArgumentError.value(
name,
@ -481,6 +519,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
return Coin.namecoin;
case "part":
return Coin.particl;
case "xlm":
return Coin.stellar;
case "tltc":
return Coin.litecoinTestNet;
case "tbtc":
@ -497,6 +537,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
return Coin.nano;
case "ban":
return Coin.banano;
case "txlm":
return Coin.stellarTestnet;
default:
throw ArgumentError.value(
ticker, "name", "No Coin enum value with that ticker");

View file

@ -49,6 +49,8 @@ extension DerivePathTypeExt on DerivePathType {
case Coin.wownero:
case Coin.nano:
case Coin.banano:
case Coin.stellar:
case Coin.stellarTestnet:
throw UnsupportedError(
"$coin does not use bitcoin style derivation paths");
}

View file

@ -12,15 +12,17 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
Future<T> showLoading<T>({
Future<T?> showLoading<T>({
required Future<T> whileFuture,
required BuildContext context,
required String message,
String? subMessage,
bool isDesktop = false,
bool opaqueBG = false,
void Function(Exception)? onException,
}) async {
unawaited(
showDialog<void>(
@ -43,10 +45,24 @@ Future<T> showLoading<T>({
),
);
final result = await whileFuture;
Exception? ex;
T? result;
try {
result = await whileFuture;
} catch (e, s) {
Logging.instance.log(
"showLoading caught: $e\n$s",
level: LogLevel.Warning,
);
ex = e is Exception ? e : Exception(e.toString());
}
if (context.mounted) {
Navigator.of(context, rootNavigator: isDesktop).pop();
if (ex != null) {
onException?.call(ex);
}
}
return result;

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