Merge remote-tracking branch 'cypherstack/staging' into simplex

This commit is contained in:
sneurlax 2023-02-07 10:25:10 -06:00
commit 5e6ff5109d
148 changed files with 9119 additions and 5077 deletions

View file

@ -23,6 +23,7 @@ jobs:
run: |
cargo install cargo-ndk
rustup target add x86_64-unknown-linux-gnu
sudo apt clean
sudo apt update
sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm
sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev

BIN
assets/images/unclaimed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

View file

@ -1,11 +1,18 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_908_22491)">
<g opacity="0.4">
<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#232323"/>
<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#232323"/>
<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#232323"/>
<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#D12B41"/>
<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#D12B41"/>
<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#D12B41"/>
</g>
<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#232323"/>
<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#232323"/>
<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#232323"/>
<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#232323"/>
<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#D12B41"/>
<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#D12B41"/>
<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#D12B41"/>
<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#D12B41"/>
</g>
<defs>
<clipPath id="clip0_908_22491">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6695 1.06575C15.1855 0.607104 15.9756 0.65358 16.4343 1.16956L20.4343 5.66956C20.8552 6.14317 20.8552 6.85686 20.4343 7.33047L16.4343 11.8305C15.9756 12.3464 15.1855 12.3929 14.6695 11.9343C14.1536 11.4756 14.1071 10.6855 14.5657 10.1696L16.7164 7.75001H6C4.48122 7.75001 3.25 8.98123 3.25 10.5C3.25 11.1904 2.69036 11.75 2 11.75C1.30964 11.75 0.75 11.1904 0.75 10.5C0.75 7.60052 3.10051 5.25001 6 5.25001H16.7164L14.5657 2.83047C14.1071 2.31449 14.1536 1.5244 14.6695 1.06575Z" fill="#232323"/>
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M9.33045 23.4342C8.81448 23.8929 8.02439 23.8464 7.56574 23.3304L3.56574 18.8304C3.14475 18.3568 3.14475 17.6431 3.56574 17.1695L7.56574 12.6695C8.02439 12.1536 8.81448 12.1071 9.33046 12.5657C9.84643 13.0244 9.89291 13.8145 9.43426 14.3304L7.28355 16.75L18 16.75C19.5188 16.75 20.75 15.5188 20.75 14C20.75 13.3096 21.3096 12.75 22 12.75C22.6904 12.75 23.25 13.3096 23.25 14C23.25 16.8995 20.8995 19.25 18 19.25L7.28355 19.25L9.43426 21.6695C9.89291 22.1855 9.84643 22.9756 9.33045 23.4342Z" fill="#232323"/>
<path d="M19.5 6.5H6C3.79086 6.5 2 8.29086 2 10.5M19.5 6.5L15.5 2M19.5 6.5L15.5 11" stroke="#D12B41" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.4" d="M4.5 18L18 18C20.2091 18 22 16.2091 22 14M4.5 18L8.5 22.5M4.5 18L8.5 13.5" stroke="#D12B41" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 455 B

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="M10.4173 5.41699V5.91699H10.9173H14.1257C15.1181 5.91699 15.9173 6.71825 15.9173 7.70866V15.5003C15.9173 16.2377 15.3213 16.8337 14.584 16.8337H5.41732C4.67997 16.8337 4.08398 16.2377 4.08398 15.5003V7.70866C4.08398 6.71866 4.88565 5.91699 5.87565 5.91699H9.08398H9.58398V5.41699V3.58366C9.58398 3.35506 9.77205 3.16699 10.0007 3.16699C10.2292 3.16699 10.4173 3.35506 10.4173 3.58366V5.41699ZM8.16732 15.0837H8.58398H8.66732H9.08398H10.9173H11.334H11.4173H11.834H13.6673H14.1673V14.5837V13.667V13.167H13.6673H11.834H11.4173H11.334H10.9173H9.08398H8.66732H8.58398H8.16732H6.33398H5.83398V13.667V14.5837V15.0837H6.33398H8.16732ZM1.33398 10.0003C1.33398 9.89019 1.37745 9.7852 1.45595 9.70663C1.53524 9.62741 1.64114 9.58366 1.75065 9.58366H2.16732V14.0837H1.75065C1.64023 14.0837 1.53452 14.0398 1.45609 13.9614C1.37774 13.8831 1.33398 13.7775 1.33398 13.667V10.0003ZM5.60482 10.0003C5.60482 10.9095 6.34144 11.6462 7.25065 11.6462C8.15987 11.6462 8.89648 10.9095 8.89648 10.0003C8.89648 9.09111 8.15987 8.35449 7.25065 8.35449C6.34144 8.35449 5.60482 9.09111 5.60482 10.0003ZM11.1048 10.0003C11.1048 10.9101 11.8409 11.6462 12.7507 11.6462C13.6599 11.6462 14.3965 10.9095 14.3965 10.0003C14.3965 9.09111 13.6599 8.35449 12.7507 8.35449C11.8416 8.35449 11.1048 9.09037 11.1048 10.0003ZM18.2507 9.58366C18.3599 9.58366 18.4652 9.62719 18.5445 9.70648C18.6238 9.78578 18.6673 9.89112 18.6673 10.0003V13.667C18.6673 13.7774 18.6235 13.8831 18.5451 13.9616C18.4667 14.0399 18.3612 14.0837 18.2507 14.0837H17.834V9.58366H18.2507Z" fill="#8E9192" stroke="#8E9192"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

3
assets/svg/whirlpool.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.16811 18.29C4.45149 18.089 3.04304 17.5064 1.84171 16.5006C1.52855 16.2383 1.08929 15.7904 0.868214 15.5078C0.795124 15.4144 0.815885 15.4205 1.16357 15.5943C2.17257 16.0986 3.56529 16.3941 4.67986 16.3404C5.4666 16.3025 6.00361 16.1827 6.16709 16.0087C6.30087 15.8663 6.24663 15.772 5.85013 15.458C4.68271 14.5333 3.91789 13.4633 3.48109 12.1436C2.95876 10.5655 3.03006 8.85291 3.68121 7.33745C3.85982 6.92178 4.22353 6.25056 4.46108 5.89825C5.0569 5.01461 6.02359 4.09702 7.04772 3.443C7.53459 3.13207 8.67134 2.57608 9.28978 2.34638C10.1175 2.03895 11.0493 1.81907 11.9322 1.72278C12.5152 1.6592 13.8654 1.6973 14.3766 1.79177C15.7252 2.04096 16.8058 2.50254 17.8199 3.26249C18.1679 3.52329 18.8273 4.15363 19.0478 4.43636L19.1673 4.58953L18.7556 4.39158C17.6907 3.87949 16.4125 3.60951 15.2969 3.66106C13.9736 3.72218 13.4271 3.9979 13.8908 4.37051C14.4654 4.83219 14.718 5.05501 14.9678 5.32051C16.047 6.46745 16.6696 7.8829 16.8049 9.49648C16.8912 10.527 16.6998 11.6803 16.2805 12.6564C15.6851 14.0423 14.7972 15.1697 13.573 16.0943C13.0338 16.5014 12.6196 16.756 11.9526 17.09C10.795 17.6696 9.68552 18.033 8.49122 18.2239C8.12694 18.2821 6.49683 18.3285 6.16811 18.29ZM10.567 12.7788C11.6493 12.5494 12.5288 11.6694 12.7584 10.586C12.824 10.2766 12.8238 9.72298 12.7584 9.41267C12.6408 8.85789 12.4049 8.43337 11.9758 8.00424C11.3957 7.42417 10.7838 7.17097 9.95841 7.16954C9.16291 7.16743 8.43295 7.4925 7.88801 8.0894C7.39072 8.63413 7.14979 9.25773 7.14979 10.0002C7.14979 10.8011 7.40895 11.4213 7.98385 11.9962C8.40667 12.419 8.83591 12.6598 9.37601 12.7773C9.67249 12.8418 10.2667 12.8425 10.5673 12.7794L10.567 12.7788Z" fill="#8E9192"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -1 +1 @@
Subproject commit 0309140a95a51388df0effcc39ff0a25b2752b29
Subproject commit 9a150d8cd2c3625424b0059e6b7306f3659fdbe0

@ -1 +1 @@
Subproject commit c1b403ccf6f4fffc9f7c233038c3df40f997c2b3
Subproject commit af88796d5e4988c03422320c3842af5cf6c049ef

View file

@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/main_db/main_db_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
@ -28,7 +30,7 @@ class MainDB {
AddressSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
inspector: false,
inspector: kDebugMode,
name: "wallet_data",
);
return true;
@ -39,39 +41,105 @@ class MainDB {
String walletId) =>
isar.addresses.where().walletIdEqualTo(walletId);
Future<void> putAddress(Address address) => isar.writeTxn(() async {
await isar.addresses.put(address);
Future<int> putAddress(Address address) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.put(address);
});
} catch (e) {
throw MainDBException("failed putAddress: $address", e);
}
}
Future<void> putAddresses(List<Address> addresses) => isar.writeTxn(() async {
await isar.addresses.putAll(addresses);
Future<List<int>> putAddresses(List<Address> addresses) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.putAll(addresses);
});
} catch (e) {
throw MainDBException("failed putAddresses: $addresses", e);
}
}
Future<void> updateAddress(Address oldAddress, Address newAddress) =>
isar.writeTxn(() async {
Future<List<int>> updateOrPutAddresses(List<Address> addresses) async {
try {
List<int> ids = [];
await isar.writeTxn(() async {
for (final address in addresses) {
final storedAddress = await isar.addresses
.getByValueWalletId(address.value, address.walletId);
int id;
if (storedAddress == null) {
id = await isar.addresses.put(address);
} else {
address.id = storedAddress.id;
await storedAddress.transactions.load();
final txns = storedAddress.transactions.toList();
await isar.addresses.delete(storedAddress.id);
id = await isar.addresses.put(address);
address.transactions.addAll(txns);
await address.transactions.save();
}
ids.add(id);
}
});
return ids;
} catch (e) {
throw MainDBException("failed updateOrPutAddresses: $addresses", e);
}
}
Future<Address?> getAddress(String walletId, String address) async {
return isar.addresses.getByValueWalletId(address, walletId);
}
Future<int> updateAddress(Address oldAddress, Address newAddress) async {
try {
return await isar.writeTxn(() async {
newAddress.id = oldAddress.id;
await oldAddress.transactions.load();
final txns = oldAddress.transactions.toList();
await isar.addresses.delete(oldAddress.id);
await isar.addresses.put(newAddress);
final id = await isar.addresses.put(newAddress);
newAddress.transactions.addAll(txns);
await newAddress.transactions.save();
return id;
});
} catch (e) {
throw MainDBException(
"failed updateAddress: from=$oldAddress to=$newAddress", e);
}
}
// transactions
QueryBuilder<Transaction, Transaction, QAfterWhereClause> getTransactions(
String walletId) =>
isar.transactions.where().walletIdEqualTo(walletId);
Future<void> putTransaction(Transaction transaction) =>
isar.writeTxn(() async {
await isar.transactions.put(transaction);
Future<int> putTransaction(Transaction transaction) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.put(transaction);
});
} catch (e) {
throw MainDBException("failed putTransaction: $transaction", e);
}
}
Future<void> putTransactions(List<Transaction> transactions) =>
isar.writeTxn(() async {
await isar.transactions.putAll(transactions);
Future<List<int>> putTransactions(List<Transaction> transactions) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.putAll(transactions);
});
} catch (e) {
throw MainDBException("failed putTransactions: $transactions", e);
}
}
Future<Transaction?> getTransaction(String walletId, String txid) async {
return isar.transactions.getByTxidWalletId(txid, walletId);
}
// utxos
QueryBuilder<UTXO, UTXO, QAfterWhereClause> getUTXOs(String walletId) =>
@ -182,55 +250,60 @@ class MainDB {
}
Future<void> addNewTransactionData(
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId) async {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId,
) async {
try {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
final potentiallyUnconfirmedTx = await getTransactions(walletId)
.filter()
.txidEqualTo(tx.txid)
.findFirst();
if (potentiallyUnconfirmedTx != null) {
// update use id to replace tx
tx.id = potentiallyUnconfirmedTx.id;
await isar.transactions.delete(potentiallyUnconfirmedTx.id);
}
// save transaction
await isar.transactions.put(tx);
// link and save outputs
if (data.item2.isNotEmpty) {
await isar.outputs.putAll(data.item2);
tx.outputs.addAll(data.item2);
await tx.outputs.save();
}
// link and save inputs
if (data.item3.isNotEmpty) {
await isar.inputs.putAll(data.item3);
tx.inputs.addAll(data.item3);
await tx.inputs.save();
}
if (data.item4 != null) {
final address = await getAddresses(walletId)
final potentiallyUnconfirmedTx = await getTransactions(walletId)
.filter()
.valueEqualTo(data.item4!.value)
.txidEqualTo(tx.txid)
.findFirst();
if (potentiallyUnconfirmedTx != null) {
// update use id to replace tx
tx.id = potentiallyUnconfirmedTx.id;
await isar.transactions.delete(potentiallyUnconfirmedTx.id);
}
// save transaction
await isar.transactions.put(tx);
// check if address exists in db and add if it does not
if (address == null) {
await isar.addresses.put(data.item4!);
// link and save outputs
if (data.item2.isNotEmpty) {
await isar.outputs.putAll(data.item2);
tx.outputs.addAll(data.item2);
await tx.outputs.save();
}
// link and save address
tx.address.value = address ?? data.item4!;
await tx.address.save();
// link and save inputs
if (data.item3.isNotEmpty) {
await isar.inputs.putAll(data.item3);
tx.inputs.addAll(data.item3);
await tx.inputs.save();
}
if (data.item4 != null) {
final address = await getAddresses(walletId)
.filter()
.valueEqualTo(data.item4!.value)
.findFirst();
// check if address exists in db and add if it does not
if (address == null) {
await isar.addresses.put(data.item4!);
}
// link and save address
tx.address.value = address ?? data.item4!;
await tx.address.save();
}
}
}
});
});
} catch (e) {
throw MainDBException("failed addNewTransactionData", e);
}
}
}

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart';
@ -132,7 +133,17 @@ class ElectrumX {
final response = await _rpcClient!.request(jsonRequestString);
if (response["error"] != null) {
throw Exception("JSONRPC response error: $response");
if (response["error"]
.toString()
.contains("No such mempool or blockchain transaction")) {
throw NoSuchTransactionException(
"No such mempool or blockchain transaction",
args.first.toString(),
);
}
throw Exception(
"JSONRPC response \ncommand: $command \nargs: $args \nerror: $response");
}
currentFailoverIndex = -1;
@ -544,6 +555,10 @@ class ElectrumX {
verbose,
],
);
if (!verbose) {
return {"rawtx": response["result"] as String};
}
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;

View file

@ -0,0 +1,5 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class AddressException extends SWException {
AddressException(super.message);
}

View file

@ -0,0 +1,7 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class NoSuchTransactionException extends SWException {
final String txid;
NoSuchTransactionException(super.message, this.txid);
}

View file

@ -0,0 +1,12 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class MainDBException extends SWException {
MainDBException(super.message, this.originalError);
final Object originalError;
@override
String toString() {
return "$message: originalError=$originalError";
}
}

View file

@ -0,0 +1,11 @@
// generic stack wallet exception which all other custom exceptions should
// extend from
class SWException with Exception {
SWException(this.message);
final String message;
@override
toString() => message;
}

View file

@ -0,0 +1,5 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class InsufficientBalanceException extends SWException {
InsufficientBalanceException(super.message);
}

View file

@ -0,0 +1,5 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class PaynymSendException extends SWException {
PaynymSendException(super.message);
}

View file

@ -254,6 +254,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
_desktopHasPassword =
await ref.read(storageCryptoHandlerProvider).hasPassword();
}
await MainDB.instance.initMainDB();
}
Future<void> load() async {
@ -267,8 +268,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
await loadShared();
}
await MainDB.instance.initMainDB();
_notificationsService = ref.read(notificationsProvider);
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
_tradesService = ref.read(tradesServiceProvider);
@ -344,12 +343,12 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
case "oceanBreeze":
colorTheme = OceanBreezeColors();
break;
case "light":
colorTheme = LightColors();
break;
case "fruitSorbet":
default:
colorTheme = FruitSorbetColors();
break;
case "light":
default:
colorTheme = LightColors();
}
loadingCompleter = Completer();
WidgetsBinding.instance.addObserver(this);

View file

@ -1,14 +1,10 @@
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/address/address_exception.dart';
import 'package:stackwallet/models/isar/models/address/crypto_currency_address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
part 'address.g.dart';
class AddressException extends SWException {
AddressException(super.message);
}
@Collection(accessor: "addresses")
class Address extends CryptoCurrencyAddress {
Address({
@ -73,6 +69,7 @@ class Address extends CryptoCurrencyAddress {
"}";
}
// do not modify
enum AddressType {
p2pkh,
p2sh,
@ -83,6 +80,7 @@ enum AddressType {
nonWallet,
}
// do not modify
enum AddressSubType {
receiving,
change,

View file

@ -584,7 +584,7 @@ class _AddAddressBookEntryViewState
"Address ${i + 1}",
style: STextStyles.smallMed12(context),
),
BlueTextButton(
CustomTextButton(
onTap: () {
_removeForm(forms[i].id);
},
@ -601,7 +601,7 @@ class _AddAddressBookEntryViewState
const SizedBox(
height: 16,
),
BlueTextButton(
CustomTextButton(
onTap: () {
_addForm();
scrollController.animateTo(

View file

@ -311,7 +311,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
"Addresses",
style: STextStyles.itemSubtitle(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -122,12 +122,12 @@ class _BuyFormState extends ConsumerState<BuyForm> {
setState(() {
_amountOutOfRangeErrorString = "Invalid amount";
});
} else if (value > maxFiat) {
} else if (value > maxFiat && buyWithFiat) {
setState(() {
_amountOutOfRangeErrorString =
"Maximum amount: ${maxFiat.toStringAsFixed(2)}";
});
} else if (value < minFiat) {
} else if (value < minFiat && buyWithFiat) {
setState(() {
_amountOutOfRangeErrorString =
"Minimum amount: ${minFiat.toStringAsFixed(2)}";
@ -280,6 +280,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
minFiat = fiat.minAmount != minFiat ? fiat.minAmount : minFiat;
maxFiat = fiat.maxAmount != maxFiat ? fiat.maxAmount : maxFiat;
});
validateAmount();
},
);
}
@ -530,25 +531,25 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
} else {
// Error; probably amount out of bounds
String errorMessage = "${quoteResponse.exception?.errorMessage}";
if (errorMessage.contains('must be between')) {
errorMessage = errorMessage.substring(
errorMessage.indexOf('getQuote exception: ') + 20,
errorMessage.indexOf(", value: null"));
_BuyFormState.boundedCryptoTicker = errorMessage.substring(
errorMessage.indexOf('The ') + 4,
errorMessage.indexOf(' amount must be between'));
_BuyFormState.minCrypto = Decimal.parse(errorMessage.substring(
errorMessage.indexOf('must be between ') + 16,
errorMessage.indexOf(' and ')));
_BuyFormState.maxCrypto = Decimal.parse(errorMessage.substring(
errorMessage.indexOf("$minCrypto and ") + "$minCrypto and ".length,
errorMessage.length));
if (Decimal.parse(_buyAmountController.text) >
_BuyFormState.maxCrypto) {
_buyAmountController.text = _BuyFormState.maxCrypto.toString();
}
}
// String errorMessage = "${quoteResponse.exception?.errorMessage}";
// if (errorMessage.contains('must be between')) {
// errorMessage = errorMessage.substring(
// errorMessage.indexOf('getQuote exception: ') + 20,
// errorMessage.indexOf(", value: null"));
// _BuyFormState.boundedCryptoTicker = errorMessage.substring(
// errorMessage.indexOf('The ') + 4,
// errorMessage.indexOf(' amount must be between'));
// _BuyFormState.minCrypto = Decimal.parse(errorMessage.substring(
// errorMessage.indexOf('must be between ') + 16,
// errorMessage.indexOf(' and ')));
// _BuyFormState.maxCrypto = Decimal.parse(errorMessage.substring(
// errorMessage.indexOf("$minCrypto and ") + "$minCrypto and ".length,
// errorMessage.length));
// if (Decimal.parse(_buyAmountController.text) >
// _BuyFormState.maxCrypto) {
// _buyAmountController.text = _BuyFormState.maxCrypto.toString();
// }
// }
await showDialog<dynamic>(
context: context,
barrierDismissible: true,
@ -570,7 +571,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
height: 24,
),
Text(
errorMessage,
quoteResponse.exception!.errorMessage,
style: STextStyles.smallMed14(context),
),
const SizedBox(
@ -632,10 +633,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
level: LogLevel.Warning,
);
return BuyResponse(
exception: BuyException(
response.toString(),
BuyExceptionType.generic,
),
exception: response.exception ??
BuyException(
response.toString(),
BuyExceptionType.generic,
),
);
}
}
@ -951,12 +953,13 @@ class _BuyFormState extends ConsumerState<BuyForm> {
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
BlueTextButton(
CustomTextButton(
text: buyWithFiat ? "Use crypto amount" : "Use fiat amount",
onTap: () {
setState(() {
buyWithFiat = !buyWithFiat;
});
validateAmount();
},
)
],
@ -1126,8 +1129,8 @@ class _BuyFormState extends ConsumerState<BuyForm> {
),
),
if (isStackCoin(selectedCrypto?.ticker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin = coinFromTickerCaseInsensitive(
@ -1150,7 +1153,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
_receiveAddressController.text =
await manager.currentReceivingAddress;
setState(() {});
setState(() {
_addressToggleFlag =
_receiveAddressController.text.isNotEmpty;
});
validateAmount();
}
});
} catch (e, s) {

View file

@ -18,16 +18,28 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class BuyWarningPopup extends StatelessWidget {
BuyWarningPopup({
class BuyWarningPopup extends StatefulWidget {
const BuyWarningPopup({
Key? key,
required this.quote,
this.order,
}) : super(key: key);
final SimplexQuote quote;
final SimplexOrder? order;
@override
State<BuyWarningPopup> createState() => _BuyWarningPopupState();
}
class _BuyWarningPopupState extends State<BuyWarningPopup> {
late final bool isDesktop;
SimplexOrder? order;
String get title => "Buy ${widget.quote.crypto.ticker}";
String get message =>
"This purchase is provided and fulfilled by Simplex by nuvei "
"(a third party). You will be taken to their website. Please follow "
"their instructions.";
Future<BuyResponse<SimplexOrder>> newOrder(SimplexQuote quote) async {
final orderResponse = await SimplexAPI.instance.newOrder(quote);
@ -38,174 +50,241 @@ class BuyWarningPopup extends StatelessWidget {
return SimplexAPI.instance.redirect(order);
}
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
Future<void> _buyInvoice() async {
await showDialog<void>(
context: context,
// useRootNavigator: isDesktop,
builder: (context) {
return isDesktop
? DesktopDialog(
maxHeight: 700,
maxWidth: 580,
child: Column(
Future<void> _buyInvoice() async {
await showDialog<void>(
context: context,
// useRootNavigator: isDesktop,
builder: (context) {
return isDesktop
? DesktopDialog(
maxHeight: 700,
maxWidth: 580,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Order details",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Order details",
style: STextStyles.desktopH3(context),
Expanded(
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
borderColor: Theme.of(context)
.extension<StackColors>()!
.background,
child: BuyOrderDetailsView(
order: order as SimplexOrder,
),
),
),
const DesktopDialogCloseButton(),
],
),
),
),
],
),
)
: BuyOrderDetailsView(
order: order as SimplexOrder,
);
},
);
}
Future<void> onContinue() async {
BuyResponse<SimplexOrder> orderResponse = await newOrder(widget.quote);
if (orderResponse.exception == null) {
await redirect(orderResponse.value as SimplexOrder)
.then((_response) async {
order = orderResponse.value as SimplexOrder;
Navigator.of(context, rootNavigator: isDesktop).pop();
Navigator.of(context, rootNavigator: isDesktop).pop();
await _buyInvoice();
});
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (context) {
if (isDesktop) {
return DesktopDialog(
maxWidth: 450,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Simplex API error",
style: STextStyles.desktopH3(context),
),
const SizedBox(
height: 24,
),
Text(
"${orderResponse.exception?.errorMessage}",
style: STextStyles.smallMed14(context),
),
const SizedBox(
height: 56,
),
Row(
children: [
const Spacer(),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: Row(
children: [
Expanded(
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
borderColor: Theme.of(context)
.extension<StackColors>()!
.background,
child: BuyOrderDetailsView(
order: order as SimplexOrder,
),
),
),
],
),
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Ok",
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).pop(); // weee
},
),
),
],
),
)
: BuyOrderDetailsView(
order: order as SimplexOrder,
);
});
}
return StackDialog(
title: "Buy ${quote.crypto.ticker}",
message: "This purchase is provided and fulfilled by Simplex by nuvei "
"(a third party). You will be taken to their website. Please follow "
"their instructions.",
leftButton: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(context, rootNavigator: isDesktop).pop,
),
rightButton: PrimaryButton(
label: "Continue",
onPressed: () async {
BuyResponse<SimplexOrder> orderResponse = await newOrder(quote);
if (orderResponse.exception == null) {
await redirect(orderResponse.value as SimplexOrder)
.then((_response) async {
this.order = orderResponse.value as SimplexOrder;
Navigator.of(context, rootNavigator: isDesktop).pop();
Navigator.of(context, rootNavigator: isDesktop).pop();
await _buyInvoice();
});
)
],
),
),
);
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: true,
builder: (context) {
if (isDesktop) {
return DesktopDialog(
maxWidth: 450,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Simplex API error",
style: STextStyles.desktopH3(context),
),
const SizedBox(
height: 24,
),
Text(
"${orderResponse.exception?.errorMessage}",
style: STextStyles.smallMed14(context),
),
const SizedBox(
height: 56,
),
Row(
children: [
const Spacer(),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Ok",
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).pop(); // weee
},
),
),
],
)
],
),
),
);
} else {
return StackDialog(
title: "Simplex API error",
message: "${orderResponse.exception?.errorMessage}",
// "${quoteResponse.exception?.errorMessage.substring(8, (quoteResponse.exception?.errorMessage?.length ?? 109) - (8 + 6))}",
rightButton: TextButton(
style: Theme.of(context)
return StackDialog(
title: "Simplex API error",
message: "${orderResponse.exception?.errorMessage}",
// "${quoteResponse.exception?.errorMessage.substring(8, (quoteResponse.exception?.errorMessage?.length ?? 109) - (8 + 6))}",
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).pop(); // weee
},
),
);
}
},
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
Navigator.of(context).pop(); // weee
},
),
);
}
},
),
icon: SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
);
}
}
@override
void initState() {
order = widget.order;
isDesktop = Util.isDesktop;
super.initState();
}
@override
Widget build(BuildContext context) {
if (isDesktop) {
return DesktopDialog(
maxWidth: 580,
maxHeight: 350,
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: STextStyles.desktopH3(context),
),
SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
),
),
],
),
const Spacer(),
Text(
message,
style: STextStyles.desktopTextSmall(context),
),
const Spacer(
flex: 2,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: ButtonHeight.l,
onPressed:
Navigator.of(context, rootNavigator: isDesktop).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Continue",
onPressed: onContinue,
),
),
],
)
],
),
),
),
);
);
} else {
return StackDialog(
title: title,
message: message,
leftButton: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(context, rootNavigator: isDesktop).pop,
),
rightButton: PrimaryButton(
label: "Continue",
onPressed: onContinue,
),
icon: SizedBox(
width: 64,
height: 32,
child: SvgPicture.asset(
Assets.buy.simplexLogo(context),
),
),
);
}
}
}

View file

@ -189,8 +189,8 @@ class _Step2ViewState extends ConsumerState<Step2View> {
style: STextStyles.smallMed12(context),
),
if (isStackCoin(model.receiveTicker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin =
@ -448,8 +448,8 @@ class _Step2ViewState extends ConsumerState<Step2View> {
style: STextStyles.smallMed12(context),
),
if (isStackCoin(model.sendTicker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin =

View file

@ -516,7 +516,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
const SizedBox(
height: 10,
),
BlueTextButton(
CustomTextButton(
text: "View transaction",
onTap: () {
final Coin coin =

View file

@ -50,7 +50,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
Future<bool> _onWillPop() async {
// go to home view when tapping back on the main exchange view
if (ref.read(homeViewPageIndexStateProvider.state).state == 1) {
if (ref.read(homeViewPageIndexStateProvider.state).state != 0) {
ref.read(homeViewPageIndexStateProvider.state).state = 0;
return false;
}

View file

@ -5,16 +5,17 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -25,6 +26,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:tuple/tuple.dart';
class PaynymDetailsPopup extends ConsumerStatefulWidget {
const PaynymDetailsPopup({
@ -43,6 +45,19 @@ class PaynymDetailsPopup extends ConsumerStatefulWidget {
class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
bool _showInsufficientFundsInfo = false;
Future<void> _onSend() async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
await Navigator.of(context).pushNamed(
SendView.routeName,
arguments: Tuple3(
manager.walletId,
manager.coin,
widget.accountLite,
),
);
}
Future<void> _onConnectPressed() async {
bool canPop = false;
unawaited(
@ -57,30 +72,24 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
// sanity check to prevent second notifcation tx
if (wallet.hasConnectedConfirmed(widget.accountLite.code)) {
canPop = true;
Navigator.of(context).pop();
// TODO show info popup
return;
} else if (wallet.hasConnected(widget.accountLite.code)) {
final wallet = manager.wallet as PaynymWalletInterface;
if (await wallet.hasConnected(widget.accountLite.code)) {
canPop = true;
Navigator.of(context).pop();
// TODO show info popup
return;
}
final rates = await wallet.fees;
final rates = await manager.fees;
Map<String, dynamic> preparedTx;
try {
preparedTx = await wallet.buildNotificationTx(
preparedTx = await wallet.prepareNotificationTx(
selectedTxFeeRate: rates.medium,
targetPaymentCodeString: widget.accountLite.code,
);
@ -117,7 +126,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => ConfirmTransactionView(
walletId: wallet.walletId,
walletId: manager.walletId,
routeOnSuccessName: PaynymHomeView.routeName,
isPaynymNotificationTransaction: true,
transactionInfo: {
@ -133,7 +142,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
);
},
amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int),
coin: wallet.coin,
coin: manager.coin,
),
);
}
@ -141,6 +150,11 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
@override
Widget build(BuildContext context) {
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
final wallet = manager.wallet as PaynymWalletInterface;
return DesktopDialog(
maxWidth: MediaQuery.of(context).size.width - 32,
maxHeight: double.infinity,
@ -162,31 +176,97 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.w600_12(context),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.accountLite.nymName,
style: STextStyles.w600_14(context),
),
FutureBuilder(
future:
wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.data == true) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const SizedBox(
height: 2,
),
Text(
"Connected",
style: STextStyles.w500_12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
)
],
);
} else {
return Container();
}
},
),
],
),
],
),
PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.l,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 10,
height: 10,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
onPressed: _onConnectPressed,
FutureBuilder(
future: wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
if (snapshot.data!) {
return PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 8,
width: 100,
onPressed: _onSend,
);
} else {
return PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 13,
height: 13,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 8,
width: 128,
onPressed: _onConnectPressed,
);
}
} else {
return const SizedBox(
height: 32,
child: LoadingIndicator(),
);
}
},
),
],
),
@ -211,6 +291,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
fontSize: 12,
),
),
),
@ -241,7 +322,9 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
Text(
"PayNym address",
style: STextStyles.infoSmall(context),
style: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -252,6 +335,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
@ -266,7 +350,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 86,
size: 100,
data: widget.accountLite.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
@ -295,16 +379,16 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: () async {
await Clipboard.setData(
ClipboardData(

View file

@ -56,7 +56,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
PayNymBot(
paymentCodeString: paynymAccount.codes.first.code,
size: isDesktop ? 56 : 32,
size: isDesktop ? 56 : 36,
),
const SizedBox(
width: 12,
@ -65,7 +65,7 @@ class PaynymQrPopup extends StatelessWidget {
paynymAccount.nymName,
style: isDesktop
? STextStyles.w500_24(context)
: STextStyles.w600_12(context),
: STextStyles.w600_14(context),
),
],
),
@ -87,7 +87,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 107),
constraints: const BoxConstraints(minHeight: 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -100,7 +100,9 @@ class PaynymQrPopup extends StatelessWidget {
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.infoSmall(context),
: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -113,14 +115,15 @@ class PaynymQrPopup extends StatelessWidget {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
height: 6,
),
BlueTextButton(
CustomTextButton(
text: "Copy",
textSize: isDesktop ? 18 : 10,
textSize: isDesktop ? 18 : 14,
onTap: () async {
await Clipboard.setData(
ClipboardData(
@ -146,7 +149,7 @@ class PaynymQrPopup extends StatelessWidget {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 107,
size: 130,
data: paynymAccount.codes.first.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,

View file

@ -9,9 +9,9 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
@ -117,7 +117,7 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
),
Image(
image: AssetImage(
Assets.png.stack,
Assets.png.unclaimedPaynym,
),
width: MediaQuery.of(context).size.width / 2,
),
@ -159,16 +159,18 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
).then((value) => shouldCancel = value == true),
);
// get wallet to access paynym calls
final wallet = ref
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
.getManager(widget.walletId);
// get wallet to access paynym calls
final wallet = manager.wallet as PaynymWalletInterface;
if (shouldCancel) return;
// get payment code
final pCode = await wallet.getPaymentCode();
final pCode = await wallet.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin));
if (shouldCancel) return;

View file

@ -4,6 +4,7 @@ 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:share_plus/share_plus.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/dialogs/paynym_qr_popup.dart';
@ -200,7 +201,7 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
.accentColorDark,
),
onPressed: () {
Navigator.of(context).pushNamed(
@ -222,7 +223,7 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
.accentColorDark,
),
onPressed: () {
// todo info ?
@ -302,7 +303,9 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
.code,
12,
5),
style: STextStyles.label(context),
style: STextStyles.label(context).copyWith(
fontSize: 14,
),
),
const SizedBox(
height: 11,
@ -312,14 +315,14 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: CopyIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
.buttonTextSecondary,
),
onPressed: () async {
await Clipboard.setData(
@ -349,17 +352,34 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Share",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: ShareIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
.buttonTextSecondary,
),
onPressed: () {
// copy to clipboard
onPressed: () async {
Rect? sharePositionOrigin;
if (await Util.isIPad) {
final box =
context.findRenderObject() as RenderBox?;
if (box != null) {
sharePositionOrigin =
box.localToGlobal(Offset.zero) & box.size;
}
}
await Share.share(
ref
.read(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
sharePositionOrigin: sharePositionOrigin);
},
),
),
@ -369,14 +389,14 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Address",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: QrCodeIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
.buttonTextSecondary,
),
onPressed: () {
showDialog<void>(
@ -536,7 +556,7 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
child: child,
),
child: SizedBox(
height: isDesktop ? 56 : 40,
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
onColor: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -5,14 +5,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -57,30 +57,24 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
// sanity check to prevent second notification tx
if (wallet.hasConnectedConfirmed(widget.accountLite.code)) {
canPop = true;
Navigator.of(context, rootNavigator: true).pop();
// TODO show info popup
return;
} else if (wallet.hasConnected(widget.accountLite.code)) {
final wallet = manager.wallet as PaynymWalletInterface;
if (await wallet.hasConnected(widget.accountLite.code)) {
canPop = true;
Navigator.of(context, rootNavigator: true).pop();
// TODO show info popup
return;
}
final rates = await wallet.fees;
final rates = await manager.fees;
Map<String, dynamic> preparedTx;
try {
preparedTx = await wallet.buildNotificationTx(
preparedTx = await wallet.prepareNotificationTx(
selectedTxFeeRate: rates.medium,
targetPaymentCodeString: widget.accountLite.code,
);
@ -118,7 +112,7 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
maxHeight: double.infinity,
maxWidth: 580,
child: ConfirmTransactionView(
walletId: wallet.walletId,
walletId: manager.walletId,
isPaynymNotificationTransaction: true,
transactionInfo: {
"hex": preparedTx["hex"],
@ -147,7 +141,7 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
);
},
amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int),
coin: wallet.coin,
coin: manager.coin,
),
);
}
@ -159,10 +153,11 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
@override
Widget build(BuildContext context) {
final wallet = ref
.watch(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
final wallet = manager.wallet as PaynymWalletInterface;
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
@ -176,14 +171,47 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.desktopTextSmall(context),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.accountLite.nymName,
style: STextStyles.desktopTextSmall(context),
),
FutureBuilder(
future: wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.data == true) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 2,
),
Text(
"Connected",
style: STextStyles.desktopTextSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
)
],
);
} else {
return Container();
}
},
),
],
),
],
),
@ -192,40 +220,53 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
),
Row(
children: [
if (!wallet.hasConnected(widget.accountLite.code))
Expanded(
child: PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onConnectPressed,
),
),
if (wallet.hasConnected(widget.accountLite.code))
Expanded(
child: PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onSend,
),
Expanded(
child: FutureBuilder(
future: wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
if (snapshot.data!) {
return PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onSend,
);
} else {
return PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.s,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 6,
onPressed: _onConnectPressed,
);
}
} else {
return const SizedBox(
height: 100,
child: LoadingIndicator(),
);
}
},
),
),
const SizedBox(
width: 20,
),
@ -316,7 +357,7 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
const SizedBox(
height: 8,
),
BlueTextButton(
CustomTextButton(
text: "Copy",
onTap: () async {
await Clipboard.setData(

View file

@ -37,7 +37,7 @@ class _PaynymCardState extends State<PaynymCard> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.paymentCodeString,
),
const SizedBox(
@ -56,7 +56,7 @@ class _PaynymCardState extends State<PaynymCard> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -65,7 +65,7 @@ class _PaynymCardState extends State<PaynymCard> {
Format.shorten(widget.paymentCodeString, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -77,7 +77,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.accountLite.code,
),
const SizedBox(
@ -96,7 +96,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -105,7 +105,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
Format.shorten(widget.accountLite.code, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -3,11 +3,17 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card_button.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class PaynymFollowersList extends ConsumerStatefulWidget {
@ -54,61 +60,97 @@ class _PaynymFollowersListState extends ConsumerState<PaynymFollowersList> {
ref.watch(myPaynymAccountStateProvider.state).state?.followers;
final count = followers?.length ?? 0;
return ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym followers will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return ConditionalParent(
condition: !isDesktop,
builder: (child) => RefreshIndicator(
child: child,
onRefresh: () async {
try {
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId);
return Container(
key: Key("paynymCardKey_${followers![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers[index],
),
);
}
},
// get wallet to access paynym calls
final wallet = manager.wallet as PaynymWalletInterface;
// get payment code
final pCode = await wallet.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin),
);
// get account from api
final account =
await ref.read(paynymAPIProvider).nym(pCode.toString());
// update my account
if (account.value != null) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
}
} catch (e) {
Logging.instance.log(
"Failed pull down refresh of paynym home page: $e",
level: LogLevel.Warning,
);
}
},
),
child: ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym followers will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return Container(
key: Key("paynymCardKey_${followers![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: followers[index],
),
);
}
},
),
);
}
}

View file

@ -3,11 +3,17 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/paynym/subwidgets/paynym_card_button.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class PaynymFollowingList extends ConsumerStatefulWidget {
@ -54,61 +60,97 @@ class _PaynymFollowingListState extends ConsumerState<PaynymFollowingList> {
ref.watch(myPaynymAccountStateProvider.state).state?.following;
final count = following?.length ?? 0;
return ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym contacts will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return ConditionalParent(
condition: !isDesktop,
builder: (child) => RefreshIndicator(
child: child,
onRefresh: () async {
try {
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId);
return Container(
key: Key("paynymCardKey_${following![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following[index],
),
);
}
},
// get wallet to access paynym calls
final wallet = manager.wallet as PaynymWalletInterface;
// get payment code
final pCode = await wallet.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin),
);
// get account from api
final account =
await ref.read(paynymAPIProvider).nym(pCode.toString());
// update my account
if (account.value != null) {
ref.read(myPaynymAccountStateProvider.state).state =
account.value!;
}
} catch (e) {
Logging.instance.log(
"Failed pull down refresh of paynym home page: $e",
level: LogLevel.Warning,
);
}
},
),
child: ListView.separated(
itemCount: max(count, 1),
separatorBuilder: (BuildContext context, int index) => Container(
height: 1.5,
color: Colors.transparent,
),
itemBuilder: (BuildContext context, int index) {
if (count == 0) {
return RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your PayNym contacts will appear here",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.label(context),
),
],
),
);
} else if (count == 1) {
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following![0],
),
);
} else {
BorderRadius? borderRadius;
if (index == 0) {
borderRadius = _borderRadiusFirst;
} else if (index == count - 1) {
borderRadius = _borderRadiusLast;
}
return Container(
key: Key("paynymCardKey_${following![index].nymId}"),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
child: PaynymCardButton(
walletId: widget.walletId,
accountLite: following[index],
),
);
}
},
),
);
}
}

View file

@ -0,0 +1,132 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_qr_popup.dart';
import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/copy_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class AddressCard extends StatelessWidget {
const AddressCard({
Key? key,
required this.address,
required this.walletId,
required this.coin,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
final Address address;
final String walletId;
final Coin coin;
final ClipboardInterface clipboard;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"TODO: label",
style: STextStyles.itemSubtitle(context),
),
CustomTextButton(
text: "Edit label",
textSize: 14,
onTap: () {
Navigator.of(context).pushNamed(
EditAddressLabelView.routeName,
arguments: Tuple2(
address,
walletId,
),
);
},
),
],
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: SelectableText(
address.value,
style: STextStyles.itemSubtitle12(context),
),
)
],
),
const SizedBox(
height: 10,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Copy address",
icon: CopyIcon(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () async {
await clipboard.setData(
ClipboardData(
text: address.value,
),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
},
),
),
const SizedBox(
width: 12,
),
Expanded(
child: SecondaryButton(
label: "Show QR Code",
icon: QrCodeIcon(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () {
showDialog<void>(
context: context,
builder: (context) => AddressQrPopup(
address: address,
coin: coin,
clipboard: clipboard,
),
);
},
),
),
],
)
],
),
);
}
}

View file

@ -0,0 +1,193 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.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 AddressQrPopup extends StatefulWidget {
const AddressQrPopup({
Key? key,
required this.address,
required this.coin,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
final Address address;
final Coin coin;
final ClipboardInterface clipboard;
@override
State<AddressQrPopup> createState() => _AddressQrPopupState();
}
class _AddressQrPopupState extends State<AddressQrPopup> {
final _qrKey = GlobalKey();
final isDesktop = Util.isDesktop;
Future<void> _capturePng(bool shouldSaveInsteadOfShare) async {
try {
RenderRepaintBoundary boundary =
_qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage();
ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
if (shouldSaveInsteadOfShare) {
if (isDesktop) {
final dir = Directory("${Platform.environment['HOME']}");
if (!dir.existsSync()) {
throw Exception(
"Home dir not found while trying to open filepicker on QR image save");
}
final path = await FilePicker.platform.saveFile(
fileName: "qrcode.png",
initialDirectory: dir.path,
);
if (path != null) {
final file = File(path);
if (file.existsSync()) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "$path already exists!",
context: context,
),
);
} else {
await file.writeAsBytes(pngBytes);
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "$path saved!",
context: context,
),
);
}
}
} else {
// await DocumentFileSavePlus.saveFile(
// pngBytes,
// "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png",
// "image/png");
}
} else {
final tempDir = await getTemporaryDirectory();
final file = await File("${tempDir.path}/qrcode.png").create();
await file.writeAsBytes(pngBytes);
await Share.shareFiles(["${tempDir.path}/qrcode.png"],
text: "Receive URI QR Code");
}
} catch (e) {
//todo: comeback to this
debugPrint(e.toString());
}
}
@override
Widget build(BuildContext context) {
return StackDialogBase(
child: Column(
children: [
Text(
"todo: custom label",
style: STextStyles.pageTitleH2(context),
),
const SizedBox(
height: 8,
),
Text(
widget.address.value,
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
height: 16,
),
Center(
child: RepaintBoundary(
key: _qrKey,
child: QrImage(
data: AddressUtils.buildUriString(
widget.coin,
widget.address.value,
{},
),
size: 220,
backgroundColor:
Theme.of(context).extension<StackColors>()!.popupBG,
foregroundColor:
Theme.of(context).extension<StackColors>()!.accentColorDark,
),
),
),
const SizedBox(
height: 16,
),
Row(
children: [
Expanded(
child: SecondaryButton(
width: 170,
buttonHeight: isDesktop ? ButtonHeight.l : null,
onPressed: () async {
await _capturePng(false);
},
label: "Share",
icon: SvgPicture.asset(
Assets.svg.share,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
width: 170,
onPressed: () async {
await _capturePng(true);
},
label: "Save",
icon: SvgPicture.asset(
Assets.svg.arrowDown,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
),
),
],
)
],
),
);
}
}

View file

@ -0,0 +1,241 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/isar/models/address/address.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/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_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_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 EditAddressLabelView extends ConsumerStatefulWidget {
const EditAddressLabelView({
Key? key,
required this.address,
required this.walletId,
}) : super(key: key);
static const String routeName = "/editAddressLabel";
final Address address;
final String walletId;
@override
ConsumerState<EditAddressLabelView> createState() =>
_EditAddressLabelViewState();
}
class _EditAddressLabelViewState extends ConsumerState<EditAddressLabelView> {
late final TextEditingController _labelFieldController;
final labelFieldFocusNode = FocusNode();
late final bool isDesktop;
@override
void initState() {
isDesktop = Util.isDesktop;
_labelFieldController = TextEditingController();
_labelFieldController.text = "todo: address.label";
super.initState();
}
@override
void dispose() {
_labelFieldController.dispose();
labelFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: child,
),
child: Scaffold(
backgroundColor: isDesktop
? Colors.transparent
: Theme.of(context).extension<StackColors>()!.background,
appBar: isDesktop
? null
: 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(
"Edit label",
style: STextStyles.navBarTitle(context),
),
),
body: ConditionalParent(
condition: !isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.all(12),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
);
},
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Edit label",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _labelFieldController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: labelFieldFocusNode,
decoration: standardInputDecoration(
"Address label",
labelFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _labelFieldController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_labelFieldController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
// if (!isDesktop)
const Spacer(),
if (isDesktop)
Padding(
padding: const EdgeInsets.all(32),
child: PrimaryButton(
label: "Save",
onPressed: () async {
// todo: update address
// await ref
// .read(notesServiceChangeNotifierProvider(
// widget.walletId))
// .editOrAddNote(
// txid: widget.txid,
// note: _labelFieldController.text,
// );
// if (mounted) {
// Navigator.of(context).pop();
// }
},
),
),
if (!isDesktop)
TextButton(
onPressed: () async {
// todo: update address
// await ref
// .read(
// notesServiceChangeNotifierProvider(widget.walletId))
// .editOrAddNote(
// txid: widget.txid,
// note: _labelFieldController.text,
// );
// if (mounted) {
// Navigator.of(context).pop();
// }
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Save",
style: STextStyles.button(context),
),
)
],
),
),
),
);
}
}

View file

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_card.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.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/loading_indicator.dart';
class ReceivingAddressesView extends ConsumerWidget {
const ReceivingAddressesView({
Key? key,
required this.walletId,
required this.isDesktop,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
static const String routeName = "/receivingAddressesView";
final String walletId;
final bool isDesktop;
final ClipboardInterface clipboard;
@override
Widget build(BuildContext context, WidgetRef ref) {
final coin = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).coin));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Receiving addresses",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
child: FutureBuilder(
future: MainDB.instance
.getAddresses(walletId)
.filter()
.subTypeEqualTo(AddressSubType.receiving)
.and()
.not()
.typeEqualTo(AddressType.nonWallet)
.sortByDerivationIndexDesc()
.findAll(),
builder: (context, AsyncSnapshot<List<Address>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
// listview
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (_, __) => Container(
height: 10,
),
itemBuilder: (_, index) => AddressCard(
walletId: walletId,
address: snapshot.data![index],
coin: coin,
),
);
} else {
return const Center(
child: LoadingIndicator(
height: 200,
width: 200,
),
);
}
},
),
);
}
}

View file

@ -6,11 +6,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.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/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -19,6 +21,7 @@ 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/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class ReceiveView extends ConsumerStatefulWidget {
const ReceiveView({
@ -128,6 +131,94 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
"Receive ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("walletNetworkSettingsAddNewNodeViewButton"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.verticalEllipsis,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
showDialog<dynamic>(
barrierColor: Colors.transparent,
barrierDismissible: true,
context: context,
builder: (_) {
return Stack(
children: [
Positioned(
top: 9,
right: 10,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
// boxShadow: [CFColors.standardBoxShadow],
boxShadow: const [],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
ReceivingAddressesView.routeName,
arguments: Tuple2(walletId, false),
);
},
child: RoundedWhiteContainer(
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow,
],
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
child: Text(
"Address list",
style: STextStyles.field(context),
),
),
),
),
),
],
),
),
),
],
);
},
);
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(12),
@ -233,7 +324,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
const SizedBox(
height: 20,
),
BlueTextButton(
CustomTextButton(
text: "Create new QR code",
onTap: () async {
unawaited(Navigator.of(context).push(

View file

@ -4,6 +4,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart';
@ -12,10 +13,9 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -92,11 +92,10 @@ class _ConfirmTransactionViewState
try {
String txid;
if (widget.isPaynymNotificationTransaction) {
txid = await (manager.wallet as DogecoinWallet)
.confirmNotificationTx(preparedTx: transactionInfo);
txid = await (manager.wallet as PaynymWalletInterface)
.broadcastNotificationTx(preparedTx: transactionInfo);
} else if (widget.isPaynymTransaction) {
//
throw UnimplementedError("paynym send not implemented yet");
txid = await manager.confirmSend(txData: transactionInfo);
} else {
final coin = manager.coin;
if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
@ -334,14 +333,20 @@ class _ConfirmTransactionViewState
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Recipient",
widget.isPaynymTransaction
? "PayNym recipient"
: "Recipient",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
"${transactionInfo["address"] ?? "ERROR"}",
widget.isPaynymTransaction
? (transactionInfo["paynymAccountLite"]
as PaynymAccountLite)
.nymName
: "${transactionInfo["address"] ?? "ERROR"}",
style: STextStyles.itemSubtitle12(context),
),
],

File diff suppressed because it is too large Load diff

View file

@ -484,7 +484,7 @@ class AboutView extends ConsumerWidget {
const SizedBox(
height: 4,
),
BlueTextButton(
CustomTextButton(
text: "https://stackwallet.com",
onTap: () {
launchUrl(

View file

@ -282,7 +282,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BlueTextButton(
CustomTextButton(
text: "Save Debug Info to clipboard",
onTap: () async {
try {
@ -350,7 +350,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
},
),
const Spacer(),
BlueTextButton(
CustomTextButton(
text: "Save logs to file",
onTap: () async {
final systemfile = SWBFileSystem();

View file

@ -92,7 +92,7 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
),
textAlign: TextAlign.left,
),
BlueTextButton(
CustomTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -327,7 +327,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BlueTextButton(
CustomTextButton(
text: "Back up now",
onTap: () {
ref.read(autoSWBServiceProvider).doBackup();
@ -448,7 +448,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
height: 20,
),
Center(
child: BlueTextButton(
child: CustomTextButton(
text: "Edit Auto Backup",
onTap: () async {
Navigator.of(context)

View file

@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class RescanningDialog extends StatefulWidget {
@ -21,9 +24,12 @@ class _RescanningDialogState extends State<RescanningDialog>
late AnimationController? _spinController;
late Animation<double> _spinAnimation;
late final bool isDesktop;
// late final VoidCallback onCancel;
@override
void initState() {
isDesktop = Util.isDesktop;
// onCancel = widget.onCancel;
_spinController = AnimationController(
@ -53,33 +59,42 @@ class _RescanningDialogState extends State<RescanningDialog>
onWillPop: () async {
return false;
},
child: StackDialog(
title: "Rescanning blockchain",
message: "This may take a while. Please do not exit this screen.",
icon: RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate3,
width: 24,
height: 24,
color: Theme.of(context).extension<StackColors>()!.accentColorDark,
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxHeight: 200,
maxWidth: 500,
child: child,
),
child: StackDialog(
title: "Rescanning blockchain",
message: "This may take a while. Please do not exit this screen.",
icon: RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate3,
width: 24,
height: 24,
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
),
),
// rightButton: TextButton(
// style: Theme.of(context).textButtonTheme.style?.copyWith(
// backgroundColor: MaterialStateProperty.all<Color>(
// CFColors.buttonGray,
// ),
// ),
// child: Text(
// "Cancel",
// style: STextStyles.itemSubtitle12(context),
// ),
// onPressed: () {
// Navigator.of(context).pop();
// onCancel.call();
// },
// ),
),
// rightButton: TextButton(
// style: Theme.of(context).textButtonTheme.style?.copyWith(
// backgroundColor: MaterialStateProperty.all<Color>(
// CFColors.buttonGray,
// ),
// ),
// child: Text(
// "Cancel",
// style: STextStyles.itemSubtitle12(context),
// ),
// onPressed: () {
// Navigator.of(context).pop();
// onCancel.call();
// },
// ),
),
);
}

View file

@ -76,6 +76,8 @@ class _WalletNetworkSettingsViewState
StreamSubscription<dynamic>? _blocksRemainingSubscription;
// late StreamSubscription _nodeStatusSubscription;
late final bool isDesktop;
late double _percent;
late int _blocksRemaining;
bool _advancedIsExpanded = false;
@ -114,7 +116,7 @@ class _WalletNetworkSettingsViewState
if (mounted) {
// pop rescanning dialog
Navigator.pop(context);
Navigator.of(context, rootNavigator: isDesktop).pop();
// show success
await showDialog<dynamic>(
@ -132,7 +134,7 @@ class _WalletNetworkSettingsViewState
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context, rootNavigator: isDesktop).pop();
},
),
),
@ -143,7 +145,7 @@ class _WalletNetworkSettingsViewState
if (mounted) {
// pop rescanning dialog
Navigator.pop(context);
Navigator.of(context, rootNavigator: isDesktop).pop();
// show error
await showDialog<dynamic>(
@ -162,7 +164,7 @@ class _WalletNetworkSettingsViewState
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context, rootNavigator: isDesktop).pop();
},
),
),
@ -183,6 +185,7 @@ class _WalletNetworkSettingsViewState
@override
void initState() {
isDesktop = Util.isDesktop;
_currentSyncStatus = widget.initialSyncStatus;
// _currentNodeStatus = widget.initialNodeStatus;
if (_currentSyncStatus == WalletSyncStatus.synced) {
@ -270,7 +273,6 @@ class _WalletNetworkSettingsViewState
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final bool isDesktop = Util.isDesktop;
final progressLength = isDesktop
? 430.0
@ -747,7 +749,7 @@ class _WalletNetworkSettingsViewState
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(
@ -878,7 +880,7 @@ class _WalletNetworkSettingsViewState
top: 16,
bottom: 6,
),
child: BlueTextButton(
child: CustomTextButton(
text: "Rescan",
onTap: () async {
await Navigator.of(context).push(

View file

@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
@ -21,6 +20,8 @@ import 'package:stackwallet/widgets/trade_card.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
import 'package:tuple/tuple.dart';
import '../../../utilities/enums/coin_enum.dart';
class TransactionsList extends ConsumerStatefulWidget {
const TransactionsList({
Key? key,
@ -68,11 +69,18 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
BuildContext context,
Transaction tx,
BorderRadius? radius,
Coin coin,
) {
final matchingTrades = ref
.read(tradesServiceProvider)
.trades
.where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
final isConfirmed = tx.isConfirmed(
ref.watch(
widget.managerProvider.select((value) => value.currentHeight)),
coin.requiredConfirmations);
if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
@ -85,7 +93,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
children: [
TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
key: isConfirmed
? Key(tx.txid + tx.type.name + tx.address.value.toString())
: UniqueKey(), //
transaction: tx,
walletId: widget.walletId,
),
@ -180,7 +190,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
),
child: TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
key: isConfirmed
? Key(tx.txid + tx.type.name + tx.address.value.toString())
: UniqueKey(),
transaction: tx,
walletId: widget.walletId,
),
@ -188,13 +200,6 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
}
}
void updateHeightProvider(Manager manager) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(currentHeightProvider(manager.coin).state).state =
manager.currentHeight;
});
}
@override
void initState() {
managerProvider = widget.managerProvider;
@ -203,14 +208,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
@override
Widget build(BuildContext context) {
// final managerProvider = ref
// .watch(walletsChangeNotifierProvider)
// .getManagerProvider(widget.walletId);
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
updateHeightProvider(manager);
return FutureBuilder(
future: manager.transactions,
builder: (fbContext, AsyncSnapshot<List<Transaction>> snapshot) {
@ -264,7 +264,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
radius = _borderRadiusFirst;
}
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
return itemBuilder(context, tx, radius, manager.coin);
},
separatorBuilder: (context, index) {
return Container(
@ -291,7 +291,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
radius = _borderRadiusFirst;
}
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
return itemBuilder(context, tx, radius, manager.coin);
},
),
);

View file

@ -8,16 +8,16 @@ import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
class WalletNavigationBar extends StatefulWidget {
class WalletNavigationBar extends ConsumerStatefulWidget {
const WalletNavigationBar({
Key? key,
required this.onReceivePressed,
@ -40,10 +40,11 @@ class WalletNavigationBar extends StatefulWidget {
final String walletId;
@override
State<WalletNavigationBar> createState() => _WalletNavigationBarState();
ConsumerState<WalletNavigationBar> createState() =>
_WalletNavigationBarState();
}
class _WalletNavigationBarState extends State<WalletNavigationBar> {
class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
double scale = 0;
final duration = const Duration(milliseconds: 200);
@ -61,41 +62,41 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
opacity: scale,
duration: duration,
child: GestureDetector(
onTap: () {},
child: Container(
padding: const EdgeInsets.all(16),
width: 146,
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
boxShadow: [
Theme.of(context)
.extension<StackColors>()!
.standardBoxShadow
],
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Whirlpool",
style: STextStyles.w600_12(context),
),
],
),
),
),
),
const SizedBox(
height: 8,
),
// AnimatedOpacity(
// opacity: scale,
// duration: duration,
// child: GestureDetector(
// onTap: () {},
// child: Container(
// padding: const EdgeInsets.all(16),
// width: 146,
// decoration: BoxDecoration(
// color:
// Theme.of(context).extension<StackColors>()!.popupBG,
// boxShadow: [
// Theme.of(context)
// .extension<StackColors>()!
// .standardBoxShadow
// ],
// borderRadius: BorderRadius.circular(
// widget.height / 2.0,
// ),
// ),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// "Whirlpool",
// style: STextStyles.w600_12(context),
// ),
// ],
// ),
// ),
// ),
// ),
// const SizedBox(
// height: 8,
// ),
AnimatedOpacity(
opacity: scale,
duration: duration,
@ -114,13 +115,15 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
),
);
// todo make generic and not doge specific
final wallet = (ref
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet);
.getManager(widget.walletId);
final code = await wallet.getPaymentCode();
final paynymInterface =
manager.wallet as PaynymWalletInterface;
final code = await paynymInterface.getPaymentCode(
DerivePathTypeExt.primaryFor(manager.coin));
final account = await ref
.read(paynymAPIProvider)
@ -172,7 +175,18 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
children: [
Text(
"Paynym",
style: STextStyles.w600_12(context),
style: STextStyles.buttonSmall(context),
),
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.robotHead,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
],
),
@ -357,61 +371,6 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
),
),
),
if (widget.coin.hasPaynymSupport)
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: () {
if (scale == 0) {
setState(() {
scale = 1;
});
} else if (scale == 1) {
setState(() {
scale = 0;
});
}
},
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const SizedBox(
height: 2,
),
SvgPicture.asset(
Assets.svg.bars,
width: 20,
height: 20,
),
const SizedBox(
height: 6,
),
Text(
"More",
style: STextStyles.buttonSmall(context),
),
const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
if (widget.coin.hasBuySupport)
RawMaterialButton(
constraints: const BoxConstraints(
@ -451,10 +410,65 @@ class _WalletNavigationBarState extends State<WalletNavigationBar> {
),
),
),
if (widget.coin.hasBuySupport)
const SizedBox(
width: 12,
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: () {
if (scale == 0) {
setState(() {
scale = 1;
});
} else if (scale == 1) {
setState(() {
scale = 0;
});
}
},
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const SizedBox(
height: 2,
),
SvgPicture.asset(
Assets.svg.bars,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
const SizedBox(
height: 6,
),
Text(
"More",
style: STextStyles.buttonSmall(context),
),
const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
],
),
),

View file

@ -10,7 +10,6 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
@ -131,7 +130,9 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
// check if address book name contains
contains |= contacts
.where((e) =>
e.addresses.where((a) => a.address == tx.address).isNotEmpty &&
e.addresses
.where((a) => a.address == tx.address.value?.value)
.isNotEmpty &&
e.name.toLowerCase().contains(keyword))
.isNotEmpty;
@ -853,7 +854,8 @@ class _DesktopTransactionCardRowState
prefix = "";
}
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -11,7 +11,6 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
@ -298,7 +297,8 @@ class _TransactionDetailsViewState
@override
Widget build(BuildContext context) {
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
return ConditionalParent(
condition: !isDesktop,
@ -1092,7 +1092,7 @@ class _TransactionDetailsViewState
height: 8,
),
if (coin != Coin.epicCash)
BlueTextButton(
CustomTextButton(
text: "Open in block explorer",
onTap: () async {
final uri =

View file

@ -649,7 +649,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
.textDark3,
),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -24,7 +24,7 @@ class AllWallets extends StatelessWidget {
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () {
Navigator.of(context).pushNamed(AddWalletView.routeName);

View file

@ -66,7 +66,7 @@ class DesktopAddressCard extends StatelessWidget {
),
Row(
children: [
BlueTextButton(
CustomTextButton(
text: "Copy",
onTap: () {
clipboard.setData(
@ -87,7 +87,7 @@ class DesktopAddressCard extends StatelessWidget {
if (contactId != "default")
Consumer(
builder: (context, ref, child) {
return BlueTextButton(
return CustomTextButton(
text: "Edit",
onTap: () async {
ref.refresh(

View file

@ -182,7 +182,7 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> {
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () async {
ref.refresh(

View file

@ -300,8 +300,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
if (isStackCoin(ref.watch(desktopExchangeModelProvider
.select((value) => value!.receiveTicker))))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: selectRecipientAddressFromStack,
),
],
@ -432,8 +432,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
if (isStackCoin(ref.watch(desktopExchangeModelProvider
.select((value) => value!.sendTicker))))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: selectRefundAddressFromStack,
),
],

View file

@ -221,7 +221,7 @@ class _DesktopChooseFromStackState
const SizedBox(
width: 80,
),
BlueTextButton(
CustomTextButton(
text: "Select wallet",
onTap: () async {
final address =

View file

@ -72,7 +72,7 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> {
"Recent trades",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context)

View file

@ -39,7 +39,7 @@ class DesktopFavoriteWallets extends ConsumerWidget {
.textFieldActiveSearchIconRight,
),
),
BlueTextButton(
CustomTextButton(
text: "Edit",
onTap: () {
Navigator.of(context).pushNamed(ManageFavoritesView.routeName);

View file

@ -38,7 +38,7 @@ class _MyWalletsState extends ConsumerState<MyWallets> {
),
),
const Spacer(),
BlueTextButton(
CustomTextButton(
text: "Add new wallet",
onTap: () {
Navigator.of(

View file

@ -46,10 +46,15 @@ class _WalletTableState extends ConsumerState<WalletSummaryTable> {
VoidCallback? expandOverride;
if (providers.length == 1) {
expandOverride = () {
Navigator.of(context).pushNamed(
expandOverride = () async {
final manager = ref.read(providers.first);
if (manager.coin == Coin.monero ||
manager.coin == Coin.wownero) {
await manager.initializeExisting();
}
await Navigator.of(context).pushNamed(
DesktopWalletView.routeName,
arguments: ref.read(providers.first).walletId,
arguments: manager.walletId,
);
};
}

View file

@ -19,14 +19,14 @@ import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -209,13 +209,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
),
);
// todo make generic and not doge specific
final wallet = (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet);
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
final code = await wallet.getPaymentCode();
final wallet = manager.wallet as PaynymWalletInterface;
final code =
await wallet.getPaymentCode(DerivePathTypeExt.primaryFor(manager.coin));
final account = await ref.read(paynymAPIProvider).nym(code.toString());
@ -445,7 +445,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
);
},
),
if (coin.hasPaynymSupport)
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
SecondaryButton(
label: "PayNym",
width: 160,

View file

@ -133,7 +133,7 @@ class _ContactListItemState extends ConsumerState<ContactListItem> {
],
),
),
BlueTextButton(
CustomTextButton(
text: "Select wallet",
onTap: () {
Navigator.of(context).pop(e);

View file

@ -987,7 +987,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
textAlign: TextAlign.left,
),
BlueTextButton(
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: sendAllTapped,
),

View file

@ -37,7 +37,7 @@ class _RecentDesktopTransactionsState
.textFieldActiveSearchIconLeft,
),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -261,7 +261,7 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
const SizedBox(
height: 60,
),
BlueTextButton(
CustomTextButton(
text: "Forgot password?",
textSize: 20,
onTap: () {

View file

@ -450,7 +450,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
STextStyles.itemSubtitle(
context),
),
BlueTextButton(
CustomTextButton(
text: "Back up now",
onTap: () {
ref

View file

@ -678,7 +678,7 @@ class DesktopAboutView extends ConsumerWidget {
const SizedBox(
height: 2,
),
BlueTextButton(
CustomTextButton(
text:
"https://stackwallet.com",
onTap: () {

View file

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

View file

@ -6,6 +6,8 @@ import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/address/address.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_wallet_view/add_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
@ -43,6 +45,8 @@ import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart';
import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
@ -472,6 +476,21 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case EditAddressLabelView.routeName:
if (args is Tuple2<Address, String>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => EditAddressLabelView(
address: args.item1,
walletId: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case EditTradeNoteView.routeName:
if (args is Tuple2<String, String>) {
return getRoute(
@ -819,6 +838,21 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ReceivingAddressesView.routeName:
if (args is Tuple2<String, bool>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ReceivingAddressesView(
walletId: args.item1,
isDesktop: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case SendView.routeName:
if (args is Tuple2<String, Coin>) {
return getRoute(
@ -843,6 +877,18 @@ class RouteGenerator {
name: settings.name,
),
);
} else if (args is Tuple3<String, Coin, PaynymAccountLite>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => SendView(
walletId: args.item1,
coin: args.item2,
accountLite: args.item3,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");

View file

@ -1,4 +1,8 @@
enum BuyExceptionType { generic, serializeResponseError }
enum BuyExceptionType {
generic,
serializeResponseError,
cryptoAmountOutOfRange,
}
class BuyException implements Exception {
String errorMessage;

View file

@ -210,7 +210,20 @@ class SimplexAPI {
BuyResponse<SimplexQuote> _parseQuote(dynamic jsonArray) {
try {
String cryptoAmount = "${jsonArray['digital_money']['amount']}";
// final Map<String, dynamic> lol =
// Map<String, dynamic>.from(jsonArray as Map);
double? cryptoAmount = jsonArray['digital_money']?['amount'] as double?;
if (cryptoAmount == null) {
String error = jsonArray['error'] as String;
return BuyResponse(
exception: BuyException(
error,
BuyExceptionType.cryptoAmountOutOfRange,
),
);
}
SimplexQuote quote = jsonArray['quote'] as SimplexQuote;
final SimplexQuote _quote = SimplexQuote(
@ -277,9 +290,9 @@ class SimplexAPI {
}
final jsonArray = jsonDecode(res.body); // TODO check if valid json
if (jsonArray.containsKey('error') as bool) {
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
throw Exception(jsonArray['message']);
}
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
throw Exception(jsonArray['message']);
}
}
SimplexOrder _order = SimplexOrder(

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:bech32/bech32.dart';
import 'package:bip32/bip32.dart' as bip32;
@ -13,16 +14,18 @@ import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.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/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -33,10 +36,12 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/paynym_is_api.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
@ -50,8 +55,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
enum DerivePathType { bip44, bip49, bip84 }
bip32.BIP32 getBip32Node(
int chain,
int index,
@ -103,7 +106,7 @@ bip32.BIP32 getBip32NodeFromRoot(
case DerivePathType.bip84:
return root.derivePath("m/84'/$coinType'/0'/$chain/$index");
default:
throw Exception("DerivePathType must not be null.");
throw Exception("DerivePathType $derivePathType not supported");
}
}
@ -138,7 +141,8 @@ bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) {
return getBip32Root(args.item1, args.item2);
}
class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
class BitcoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing, PaynymWalletInterface {
static const integrationTestFlag =
bool.fromEnvironment("IS_INTEGRATION_TEST");
@ -178,8 +182,16 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<List<isar_models.UTXO>> get utxos => db.getUTXOs(walletId).findAll();
@override
Future<List<isar_models.Transaction>> get transactions =>
db.getTransactions(walletId).sortByTimestampDesc().findAll();
Future<List<isar_models.Transaction>> get transactions => db
.getTransactions(walletId)
.filter()
.not()
.group((q) => q
.subTypeEqualTo(isar_models.TransactionSubType.bip47Notification)
.and()
.typeEqualTo(isar_models.TransactionType.incoming))
.sortByTimestampDesc()
.findAll();
@override
Future<String> get currentReceivingAddress async =>
@ -193,7 +205,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip84);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddress async =>
(await _currentChangeAddress).value;
@ -206,7 +218,20 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip84);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddressP2PKH async =>
(await _currentChangeAddressP2PKH).value;
Future<isar_models.Address> get _currentChangeAddressP2PKH async =>
(await db
.getAddresses(walletId)
.filter()
.typeEqualTo(isar_models.AddressType.p2pkh)
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip44);
@override
Future<void> exit() async {
@ -241,6 +266,14 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -403,7 +436,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType $type not supported");
}
final address = isar_models.Address(
@ -491,6 +524,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
}) async {
longMutex = true;
@ -664,14 +698,49 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2wpkhChangeAddressArray.add(address);
}
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
if (isRescan) {
await db.updateOrPutAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
} else {
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
}
// get own payment code
final myCode = await getPaymentCode(DerivePathType.bip44);
// refresh transactions to pick up any received notification transactions
await _refreshTransactions();
final Set<String> codesToCheck = {};
final nym = await PaynymIsApi().nym(myCode.toString());
if (nym.value != null) {
for (final follower in nym.value!.followers) {
codesToCheck.add(follower.code);
}
for (final following in nym.value!.following) {
codesToCheck.add(following.code);
}
}
// restore paynym transactions
await restoreAllHistory(
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
paymentCodeStrings: codesToCheck,
);
await _updateUTXOs();
@ -737,6 +806,13 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
}
return needsRefresh;
} on NoSuchTransactionException catch (e) {
// TODO: move direct transactions elsewhere
await db.isar.writeTxn(() async {
await db.isar.transactions.deleteByTxidWalletId(e.txid, walletId);
});
await txTracker.deleteTransaction(e.txid);
return true;
} catch (e, s) {
Logging.instance.log(
"Exception caught in refreshIfThereIsNewData: $e\n$s",
@ -890,6 +966,17 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
final myCode = await getPaymentCode(DerivePathType.bip44);
final Set<String> codesToCheck = {};
final nym = await PaynymIsApi().nym(myCode.toString());
if (nym.value != null) {
for (final follower in nym.value!.followers) {
codesToCheck.add(follower.code);
}
for (final following in nym.value!.following) {
codesToCheck.add(following.code);
}
}
final currentHeight = await chainHeight;
const storedHeight = 1; //await storedChainHeight;
@ -906,6 +993,9 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
await _checkCurrentReceivingAddressesForTransactions();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.4, walletId));
await checkAllCurrentReceivingPaynymAddressesForTransactions();
final fetchFuture = _refreshTransactions();
final utxosRefreshFuture = _updateUTXOs();
GlobalEventBus.instance
@ -924,6 +1014,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.fire(RefreshPercentChangedEvent(0.80, walletId));
await fetchFuture;
await checkForNotificationTransactionsTo(codesToCheck);
await getAllTxsToWatch();
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.90, walletId));
@ -1176,46 +1267,31 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _fetchTransactionData();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
@override
@ -1264,6 +1340,29 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
initPaynymWalletInterface(
walletId: walletId,
walletName: walletName,
network: _network,
coin: coin,
db: db,
electrumXClient: electrumXClient,
secureStorage: secureStore,
getMnemonic: () => mnemonic,
getChainHeight: () => chainHeight,
getCurrentChangeAddress: () => currentChangeAddressP2PKH,
estimateTxFee: estimateTxFee,
prepareSend: prepareSend,
getTxCount: getTxCount,
fetchBuildTxData: fetchBuildTxData,
refresh: refresh,
checkChangeAddressForTransactions:
_checkP2PKHChangeAddressForTransactions,
addDerivation: addDerivation,
addDerivations: addDerivations,
dustLimitP2PKH: DUST_LIMIT_P2PKH,
minConfirms: MINIMUM_CONFIRMATIONS,
);
}
@override
@ -1324,62 +1423,11 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.getAddresses(walletId)
.filter()
.not()
.typeEqualTo(isar_models.AddressType.unknown)
.and()
.not()
.typeEqualTo(isar_models.AddressType.nonWallet)
.and()
.group((q) => q
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.or()
.subTypeEqualTo(isar_models.AddressSubType.change))
.not()
.subTypeEqualTo(isar_models.AddressSubType.nonWallet)
.findAll();
// final List<String> allAddresses = [];
// final receivingAddresses = DB.instance.get<dynamic>(
// boxName: walletId, key: 'receivingAddressesP2WPKH') as List<dynamic>;
// final changeAddresses = DB.instance.get<dynamic>(
// boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>;
// final receivingAddressesP2PKH = DB.instance.get<dynamic>(
// boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>;
// final changeAddressesP2PKH =
// DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH')
// as List<dynamic>;
// final receivingAddressesP2SH = DB.instance.get<dynamic>(
// boxName: walletId, key: 'receivingAddressesP2SH') as List<dynamic>;
// final changeAddressesP2SH =
// DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH')
// as List<dynamic>;
//
// for (var i = 0; i < receivingAddresses.length; i++) {
// if (!allAddresses.contains(receivingAddresses[i])) {
// allAddresses.add(receivingAddresses[i] as String);
// }
// }
// for (var i = 0; i < changeAddresses.length; i++) {
// if (!allAddresses.contains(changeAddresses[i])) {
// allAddresses.add(changeAddresses[i] as String);
// }
// }
// for (var i = 0; i < receivingAddressesP2PKH.length; i++) {
// if (!allAddresses.contains(receivingAddressesP2PKH[i])) {
// allAddresses.add(receivingAddressesP2PKH[i] as String);
// }
// }
// for (var i = 0; i < changeAddressesP2PKH.length; i++) {
// if (!allAddresses.contains(changeAddressesP2PKH[i])) {
// allAddresses.add(changeAddressesP2PKH[i] as String);
// }
// }
// for (var i = 0; i < receivingAddressesP2SH.length; i++) {
// if (!allAddresses.contains(receivingAddressesP2SH[i])) {
// allAddresses.add(receivingAddressesP2SH[i] as String);
// }
// }
// for (var i = 0; i < changeAddressesP2SH.length; i++) {
// if (!allAddresses.contains(changeAddressesP2SH[i])) {
// allAddresses.add(changeAddressesP2SH[i] as String);
// }
// }
return allAddresses;
}
@ -1509,6 +1557,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
address = P2WPKH(network: _network, data: data).data.address!;
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType $derivePathType not supported");
}
// add generated address & info to derivations
@ -1555,6 +1605,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
type = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType unsupported");
}
address = await db
.getAddresses(walletId)
@ -1582,6 +1634,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
key = "${walletId}_${chainId}DerivationsP2WPKH";
break;
default:
throw Exception("DerivePathType unsupported");
}
return key;
}
@ -1754,15 +1808,34 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
coin: coin,
);
// todo check here if we should mark as blocked
// fetch stored tx to see if paynym notification tx and block utxo
final storedTx = await db.getTransaction(
walletId,
fetchedUtxoList[i][j]["tx_hash"] as String,
);
bool shouldBlock = false;
String? blockReason;
if (storedTx?.subType ==
isar_models.TransactionSubType.bip47Notification &&
storedTx?.type == isar_models.TransactionType.incoming) {
// probably safe to assume this is an incoming tx as it is a utxo
// belonging to this wallet. The extra check may be redundant but
// just in case...
shouldBlock = true;
blockReason = "Incoming paynym notification transaction.";
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isBlocked: shouldBlock,
blockedReason: blockReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
@ -1790,7 +1863,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1868,7 +1941,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1887,7 +1960,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1907,7 +1980,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip84);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1926,12 +1999,56 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Future<void> _checkP2PKHChangeAddressForTransactions() async {
try {
final currentChange = await _currentChangeAddressP2PKH;
final int txCount = await getTxCount(address: currentChange.value);
Logging.instance.log(
'Number of txs for current change address $currentChange: $txCount',
level: LogLevel.Info);
if (txCount >= 1 || currentChange.derivationIndex < 0) {
// First increment the change index
final newChangeIndex = currentChange.derivationIndex + 1;
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip44);
final existing = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(newChangeAddress.value)
.findFirst();
if (existing == null) {
// Add that new change address
await db.putAddress(newChangeAddress);
} else {
// we need to update the address
await db.updateAddress(existing, newChangeAddress);
}
// keep checking until address with no tx history is set as current
await _checkP2PKHChangeAddressForTransactions();
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -2166,6 +2283,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
Logging.instance.log("availableOutputs.length: ${availableOutputs.length}",
level: LogLevel.Info);
Logging.instance
.log("spendableOutputs: $spendableOutputs", level: LogLevel.Info);
Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue",
@ -2258,24 +2377,38 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
return transactionObject;
}
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip84),
],
satoshiAmounts: [
satoshiAmountToSend,
satoshisBeingUsed - satoshiAmountToSend - 1
], // dust limit is the minimum amount a change output should be
))["vSize"] as int;
final int vSizeForOneOutput;
try {
vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
} catch (e) {
Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error);
rethrow;
}
final int vSizeForTwoOutPuts;
try {
vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
max(0, satoshisBeingUsed - satoshiAmountToSend - 1),
],
))["vSize"] as int;
} catch (e) {
Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error);
rethrow;
}
// Assume 1 output, only for recipient and no change
final feeForOneOutput = estimateTxFee(
@ -2308,8 +2441,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await _checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip84);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2515,6 +2648,8 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
addressesP2WPKH.add(address);
break;
default:
throw Exception("DerivePathType unsupported");
}
}
}
@ -2794,6 +2929,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -3035,7 +3171,7 @@ class BitcoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);

View file

@ -34,6 +34,7 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
@ -50,8 +51,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
enum DerivePathType { bip44, bip49 }
bip32.BIP32 getBip32Node(int chain, int index, String mnemonic,
NetworkType network, DerivePathType derivePathType) {
final root = getBip32Root(mnemonic, network);
@ -78,7 +77,8 @@ bip32.BIP32 getBip32NodeFromRoot(
String coinType;
switch (root.network.wif) {
case 0x80: // bch mainnet wif
coinType = "145"; // bch mainnet
coinType =
derivePathType == DerivePathType.bch44 ? "145" : "0"; // bch mainnet
break;
case 0xef: // bch testnet wif
coinType = "1"; // bch testnet
@ -88,6 +88,7 @@ bip32.BIP32 getBip32NodeFromRoot(
}
switch (derivePathType) {
case DerivePathType.bip44:
case DerivePathType.bch44:
return root.derivePath("m/44'/$coinType'/0'/$chain/$index");
case DerivePathType.bip49:
return root.derivePath("m/49'/$coinType'/0'/$chain/$index");
@ -170,7 +171,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip44);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddress async =>
(await _currentChangeAddress).value;
@ -183,7 +184,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip44);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
@override
Future<void> exit() async {
@ -218,6 +219,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -322,6 +331,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic.trim(),
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
coin: coin,
);
} catch (e, s) {
Logging.instance.log(
@ -380,6 +390,11 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2pkh;
addressString = bitbox.Address.toCashAddress(addressString);
break;
case DerivePathType.bch44:
addressString = P2PKH(data: data, network: _network).data.address!;
addrType = isar_models.AddressType.unknown;
addressString = bitbox.Address.toCashAddress(addressString);
break;
case DerivePathType.bip49:
addressString = P2SH(
data: PaymentData(
@ -390,7 +405,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2sh;
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType $type not supported");
}
final address = isar_models.Address(
@ -480,24 +495,32 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
Coin? coin,
}) async {
longMutex = true;
Map<String, Map<String, String>> p2pkhReceiveDerivations = {};
Map<String, Map<String, String>> bip44P2pkhReceiveDerivations = {};
Map<String, Map<String, String>> bch44P2pkhReceiveDerivations = {};
Map<String, Map<String, String>> p2shReceiveDerivations = {};
Map<String, Map<String, String>> p2pkhChangeDerivations = {};
Map<String, Map<String, String>> bip44P2pkhChangeDerivations = {};
Map<String, Map<String, String>> bch44P2pkhChangeDerivations = {};
Map<String, Map<String, String>> p2shChangeDerivations = {};
final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network));
List<isar_models.Address> p2pkhReceiveAddressArray = [];
List<isar_models.Address> bip44P2pkhReceiveAddressArray = [];
List<isar_models.Address> bch44P2pkhReceiveAddressArray = [];
List<isar_models.Address> p2shReceiveAddressArray = [];
int p2pkhReceiveIndex = -1;
int bch44P2pkhReceiveIndex = -1;
int bip44P2pkhReceiveIndex = -1;
int p2shReceiveIndex = -1;
List<isar_models.Address> p2pkhChangeAddressArray = [];
List<isar_models.Address> bip44P2pkhChangeAddressArray = [];
List<isar_models.Address> bch44P2pkhChangeAddressArray = [];
List<isar_models.Address> p2shChangeAddressArray = [];
int p2pkhChangeIndex = -1;
int bch44P2pkhChangeIndex = -1;
int bip44P2pkhChangeIndex = -1;
int p2shChangeIndex = -1;
// The gap limit will be capped at [maxUnusedAddressGap]
@ -508,10 +531,11 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
const txCountBatchSize = 12;
try {
bool testnet = ((coin ?? null) == Coin.bitcoincashTestnet) ? true : false;
// receiving addresses
Logging.instance
.log("checking receiving addresses...", level: LogLevel.Info);
final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck,
final resultReceiveBip44 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0);
final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck,
@ -520,23 +544,23 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log("checking change addresses...", level: LogLevel.Info);
// change addresses
final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck,
final bip44ResultChange = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1);
final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1);
await Future.wait([
resultReceive44,
resultReceiveBip44,
resultReceive49,
resultChange44,
bip44ResultChange,
resultChange49,
]);
p2pkhReceiveAddressArray =
(await resultReceive44)['addressArray'] as List<isar_models.Address>;
p2pkhReceiveIndex = (await resultReceive44)['index'] as int;
p2pkhReceiveDerivations = (await resultReceive44)['derivations']
bip44P2pkhReceiveAddressArray = (await resultReceiveBip44)['addressArray']
as List<isar_models.Address>;
bip44P2pkhReceiveIndex = (await resultReceiveBip44)['index'] as int;
bip44P2pkhReceiveDerivations = (await resultReceiveBip44)['derivations']
as Map<String, Map<String, String>>;
p2shReceiveAddressArray =
@ -545,10 +569,10 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2shReceiveDerivations = (await resultReceive49)['derivations']
as Map<String, Map<String, String>>;
p2pkhChangeAddressArray =
(await resultChange44)['addressArray'] as List<isar_models.Address>;
p2pkhChangeIndex = (await resultChange44)['index'] as int;
p2pkhChangeDerivations = (await resultChange44)['derivations']
bip44P2pkhChangeAddressArray = (await bip44ResultChange)['addressArray']
as List<isar_models.Address>;
bip44P2pkhChangeIndex = (await bip44ResultChange)['index'] as int;
bip44P2pkhChangeDerivations = (await bip44ResultChange)['derivations']
as Map<String, Map<String, String>>;
p2shChangeAddressArray =
@ -558,11 +582,11 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
as Map<String, Map<String, String>>;
// save the derivations (if any)
if (p2pkhReceiveDerivations.isNotEmpty) {
if (bip44P2pkhReceiveDerivations.isNotEmpty) {
await addDerivations(
chain: 0,
derivePathType: DerivePathType.bip44,
derivationsToAdd: p2pkhReceiveDerivations);
derivationsToAdd: bip44P2pkhReceiveDerivations);
}
if (p2shReceiveDerivations.isNotEmpty) {
await addDerivations(
@ -570,11 +594,11 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
derivePathType: DerivePathType.bip49,
derivationsToAdd: p2shReceiveDerivations);
}
if (p2pkhChangeDerivations.isNotEmpty) {
if (bip44P2pkhChangeDerivations.isNotEmpty) {
await addDerivations(
chain: 1,
derivePathType: DerivePathType.bip44,
derivationsToAdd: p2pkhChangeDerivations);
derivationsToAdd: bip44P2pkhChangeDerivations);
}
if (p2shChangeDerivations.isNotEmpty) {
await addDerivations(
@ -585,10 +609,10 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// If restoring a wallet that never received any funds, then set receivingArray manually
// If we didn't do this, it'd store an empty array
if (p2pkhReceiveIndex == -1) {
if (bip44P2pkhReceiveIndex == -1) {
final address =
await _generateAddressForChain(0, 0, DerivePathType.bip44);
p2pkhReceiveAddressArray.add(address);
bip44P2pkhReceiveAddressArray.add(address);
}
if (p2shReceiveIndex == -1) {
final address =
@ -598,10 +622,10 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
if (p2pkhChangeIndex == -1) {
if (bip44P2pkhChangeIndex == -1) {
final address =
await _generateAddressForChain(1, 0, DerivePathType.bip44);
p2pkhChangeAddressArray.add(address);
bip44P2pkhChangeAddressArray.add(address);
}
if (p2shChangeIndex == -1) {
final address =
@ -609,12 +633,82 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2shChangeAddressArray.add(address);
}
await db.putAddresses([
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
final addressesToStore = [
...bip44P2pkhReceiveAddressArray,
...bip44P2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
];
if (!testnet) {
final resultReceiveBch44 = _checkGaps(
maxNumberOfIndexesToCheck,
maxUnusedAddressGap,
txCountBatchSize,
root,
DerivePathType.bch44,
0);
final Future<Map<String, dynamic>> bch44ResultChange = _checkGaps(
maxNumberOfIndexesToCheck,
maxUnusedAddressGap,
txCountBatchSize,
root,
DerivePathType.bch44,
1);
await Future.wait([
resultReceiveBch44,
bch44ResultChange,
]);
bch44P2pkhReceiveAddressArray =
(await resultReceiveBch44)['addressArray']
as List<isar_models.Address>;
bch44P2pkhReceiveIndex = (await resultReceiveBch44)['index'] as int;
bch44P2pkhReceiveDerivations = (await resultReceiveBch44)['derivations']
as Map<String, Map<String, String>>;
bch44P2pkhChangeAddressArray = (await bch44ResultChange)['addressArray']
as List<isar_models.Address>;
bch44P2pkhChangeIndex = (await bch44ResultChange)['index'] as int;
bch44P2pkhChangeDerivations = (await bch44ResultChange)['derivations']
as Map<String, Map<String, String>>;
if (bch44P2pkhReceiveDerivations.isNotEmpty) {
await addDerivations(
chain: 0,
derivePathType: DerivePathType.bch44,
derivationsToAdd: bch44P2pkhReceiveDerivations);
}
if (bch44P2pkhChangeDerivations.isNotEmpty) {
await addDerivations(
chain: 1,
derivePathType: DerivePathType.bch44,
derivationsToAdd: bch44P2pkhChangeDerivations);
}
if (bch44P2pkhReceiveIndex == -1) {
final address =
await _generateAddressForChain(0, 0, DerivePathType.bch44);
bch44P2pkhReceiveAddressArray.add(address);
}
if (bch44P2pkhChangeIndex == -1) {
final address =
await _generateAddressForChain(1, 0, DerivePathType.bch44);
bch44P2pkhChangeAddressArray.add(address);
}
addressesToStore.addAll([
...bch44P2pkhReceiveAddressArray,
...bch44P2pkhChangeAddressArray,
]);
}
if (isRescan) {
await db.updateOrPutAddresses(addressesToStore);
} else {
await db.putAddresses(addressesToStore);
}
await _updateUTXOs();
@ -1106,46 +1200,31 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _fetchTransactionData();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
bool validateCashAddr(String cashAddr) {
@ -1403,6 +1482,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
),
);
final data = PaymentData(pubkey: node.publicKey);
String address;
isar_models.AddressType addrType;
@ -1412,6 +1492,11 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2pkh;
address = bitbox.Address.toCashAddress(address);
break;
case DerivePathType.bch44:
address = P2PKH(data: data, network: _network).data.address!;
addrType = isar_models.AddressType.unknown;
address = bitbox.Address.toCashAddress(address);
break;
case DerivePathType.bip49:
address = P2SH(
data: PaymentData(
@ -1421,9 +1506,10 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.address!;
addrType = isar_models.AddressType.p2sh;
break;
// default:
// // should never hit this due to all enum cases handled
// return null;
case DerivePathType.bip84:
throw UnsupportedError("bip84 not supported by BCH");
default:
throw Exception("DerivePathType $derivePathType not supported");
}
// add generated address & info to derivations
@ -1463,9 +1549,16 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip44:
type = isar_models.AddressType.p2pkh;
break;
case DerivePathType.bch44:
type = isar_models.AddressType.unknown;
break;
case DerivePathType.bip49:
type = isar_models.AddressType.p2sh;
break;
case DerivePathType.bip84:
throw UnsupportedError("bip84 not supported by BCH");
default:
throw Exception("DerivePathType $derivePathType not supported");
}
final address = await db
@ -1488,9 +1581,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip44:
key = "${walletId}_${chainId}DerivationsP2PKH";
break;
case DerivePathType.bch44:
key = "${walletId}_${chainId}DerivationsBch44P2PKH";
break;
case DerivePathType.bip49:
key = "${walletId}_${chainId}DerivationsP2SH";
break;
case DerivePathType.bip84:
throw UnsupportedError("bip84 not supported by BCH");
}
return key;
}
@ -1663,7 +1761,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1752,7 +1850,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip44);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1771,12 +1869,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1796,7 +1894,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip44);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1815,12 +1913,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1951,7 +2049,8 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.where((e) => e.subType == isar_models.AddressSubType.receiving)
.map((e) {
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
addressType(address: e.value) == DerivePathType.bip44) {
(addressType(address: e.value) == DerivePathType.bip44 ||
addressType(address: e.value) == DerivePathType.bch44)) {
return bitbox.Address.toCashAddress(e.value);
} else {
return e.value;
@ -1962,7 +2061,8 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
.where((e) => e.subType == isar_models.AddressSubType.change)
.map((e) {
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
addressType(address: e.value) == DerivePathType.bip44) {
(addressType(address: e.value) == DerivePathType.bip44 ||
addressType(address: e.value) == DerivePathType.bch44)) {
return bitbox.Address.toCashAddress(e.value);
} else {
return e.value;
@ -2346,7 +2446,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip44),
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
@ -2399,8 +2499,8 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await _checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip44);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2591,13 +2691,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final n = output["n"];
if (n != null && n == utxosToUse[i].vout) {
String address = output["scriptPubKey"]["addresses"][0] as String;
if (bitbox.Address.detectFormat(address) ==
if (bitbox.Address.detectFormat(address) !=
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw Exception(
"Unsupported address found during fetchBuildTxData(): $address");
try {
address = bitbox.Address.toCashAddress(address);
} catch (_) {
rethrow;
}
}
if (!addressTxid.containsKey(address)) {
@ -2606,11 +2705,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
(addressTxid[address] as List).add(txid);
switch (addressType(address: address)) {
case DerivePathType.bip44:
case DerivePathType.bch44:
addressesP2PKH.add(address);
break;
case DerivePathType.bip49:
addressesP2SH.add(address);
break;
case DerivePathType.bip84:
throw UnsupportedError("bip84 not supported by BCH");
}
}
}
@ -2619,19 +2721,28 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// p2pkh / bip44
final p2pkhLength = addressesP2PKH.length;
if (p2pkhLength > 0) {
final receiveDerivations = await _fetchDerivations(
final receiveDerivationsBip44 = await _fetchDerivations(
chain: 0,
derivePathType: DerivePathType.bip44,
);
final changeDerivations = await _fetchDerivations(
final changeDerivationsBip44 = await _fetchDerivations(
chain: 1,
derivePathType: DerivePathType.bip44,
);
final receiveDerivationsBch44 = await _fetchDerivations(
chain: 0,
derivePathType: DerivePathType.bch44,
);
final changeDerivationsBch44 = await _fetchDerivations(
chain: 1,
derivePathType: DerivePathType.bch44,
);
for (int i = 0; i < p2pkhLength; i++) {
String address = addressesP2PKH[i];
// receives
final receiveDerivation = receiveDerivations[address];
final receiveDerivation = receiveDerivationsBip44[address] ??
receiveDerivationsBch44[address];
// if a match exists it will not be null
if (receiveDerivation != null) {
final data = P2PKH(
@ -2652,7 +2763,8 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} else {
// if its not a receive, check change
final changeDerivation = changeDerivations[address];
final changeDerivation = changeDerivationsBip44[address] ??
changeDerivationsBch44[address];
// if a match exists it will not be null
if (changeDerivation != null) {
final data = P2PKH(
@ -2863,6 +2975,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -2997,7 +3110,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip44);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);

File diff suppressed because it is too large Load diff

View file

@ -14,16 +14,17 @@ import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.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/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -34,6 +35,7 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
@ -50,8 +52,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"bb0a78264637406b6360aad926284d544d7049f45189db5664f3c4d07350559e";
enum DerivePathType { bip44 }
bip32.BIP32 getBip32Node(int chain, int index, String mnemonic,
NetworkType network, DerivePathType derivePathType) {
final root = getBip32Root(mnemonic, network);
@ -90,7 +90,8 @@ bip32.BIP32 getBip32NodeFromRoot(
case DerivePathType.bip44:
return root.derivePath("m/44'/$coinType'/0'/$chain/$index");
default:
throw Exception("DerivePathType must not be null.");
throw Exception(
"DerivePathType null or unsupported (${DerivePathType.bip44})");
}
}
@ -125,7 +126,8 @@ bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) {
return getBip32Root(args.item1, args.item2);
}
class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
class DogecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
static const integrationTestFlag =
bool.fromEnvironment("IS_INTEGRATION_TEST");
final _prefs = Prefs.instance;
@ -150,8 +152,16 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<List<isar_models.UTXO>> get utxos => db.getUTXOs(walletId).findAll();
@override
Future<List<isar_models.Transaction>> get transactions =>
db.getTransactions(walletId).sortByTimestampDesc().findAll();
Future<List<isar_models.Transaction>> get transactions => db
.getTransactions(walletId)
.filter()
.not()
.group((q) => q
.subTypeEqualTo(isar_models.TransactionSubType.bip47Notification)
.and()
.typeEqualTo(isar_models.TransactionType.incoming))
.sortByTimestampDesc()
.findAll();
@override
Coin get coin => _coin;
@ -168,7 +178,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip44);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
// @override
Future<String> get currentChangeAddress async =>
@ -182,7 +192,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip44);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
@override
Future<void> exit() async {
@ -217,6 +227,14 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -371,7 +389,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
);
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType $type not supported");
}
receivingNodes.addAll({
"${_id}_$j": {
@ -447,6 +465,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
}) async {
longMutex = true;
@ -523,11 +542,30 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
await _generateAddressForChain(1, 0, DerivePathType.bip44);
p2pkhChangeAddressArray.add(address);
}
if (isRescan) {
await db.updateOrPutAddresses([
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
} else {
await db.putAddresses([
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
}
await db.putAddresses([
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
// paynym stuff
// // generate to ensure notification address is in db before refreshing transactions
// await getMyNotificationAddress(DerivePathType.bip44);
//
// // refresh transactions to pick up any received notification transactions
// await _refreshTransactions();
//
// // restore paynym transactions
// await restoreAllHistory(
// maxUnusedAddressGap: maxUnusedAddressGap,
// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
// );
await _updateUTXOs();
@ -596,6 +634,13 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
}
return needsRefresh;
} on NoSuchTransactionException catch (e) {
// TODO: move direct transactions elsewhere
await db.isar.writeTxn(() async {
await db.isar.transactions.deleteByTxidWalletId(e.txid, walletId);
});
await txTracker.deleteTransaction(e.txid);
return true;
} catch (e, s) {
Logging.instance.log(
"Exception caught in refreshIfThereIsNewData: $e\n$s",
@ -761,6 +806,9 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
await _checkCurrentReceivingAddressesForTransactions();
// paynym stuff
// await checkAllCurrentReceivingPaynymAddressesForTransactions();
final fetchFuture = _refreshTransactions();
final utxosRefreshFuture = _updateUTXOs();
GlobalEventBus.instance
@ -1011,46 +1059,31 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _fetchTransactionData();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
@override
@ -1099,6 +1132,27 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
// paynym stuff
// initPaynymWalletInterface(
// walletId: walletId,
// walletName: walletName,
// network: network,
// coin: coin,
// db: db,
// electrumXClient: electrumXClient,
// getMnemonic: () => mnemonic,
// getChainHeight: () => chainHeight,
// getCurrentChangeAddress: () => currentChangeAddress,
// estimateTxFee: estimateTxFee,
// prepareSend: prepareSend,
// getTxCount: getTxCount,
// fetchBuildTxData: fetchBuildTxData,
// refresh: refresh,
// checkChangeAddressForTransactions: checkChangeAddressForTransactions,
// addDerivation: addDerivation,
// addDerivations: addDerivations,
// );
}
@override
@ -1161,10 +1215,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.not()
.typeEqualTo(isar_models.AddressType.nonWallet)
.and()
.group((q) => q
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.or()
.subTypeEqualTo(isar_models.AddressSubType.change))
.not()
.subTypeEqualTo(isar_models.AddressSubType.nonWallet)
.findAll();
return allAddresses;
}
@ -1274,9 +1326,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip44:
address = P2PKH(data: data, network: network).data.address!;
break;
// default:
// // should never hit this due to all enum cases handled
// return null;
default:
throw Exception("Unsupported DerivePathType");
}
// add generated address & info to derivations
@ -1321,6 +1372,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.sortByDerivationIndexDesc()
.findFirst();
break;
default:
throw Exception("Unsupported DerivePathType");
}
return address!.value;
}
@ -1333,6 +1386,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip44:
key = "${walletId}_${chainId}DerivationsP2PKH";
break;
default:
throw Exception("Unsupported DerivePathType");
}
return key;
}
@ -1503,15 +1558,34 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
coin: coin,
);
// todo check here if we should mark as blocked
// fetch stored tx to see if paynym notification tx and block utxo
final storedTx = await db.getTransaction(
walletId,
fetchedUtxoList[i][j]["tx_hash"] as String,
);
bool shouldBlock = false;
String? blockReason;
if (storedTx?.subType ==
isar_models.TransactionSubType.bip47Notification &&
storedTx?.type == isar_models.TransactionType.incoming) {
// probably safe to assume this is an incoming tx as it is a utxo
// belonging to this wallet. The extra check may be redundant but
// just in case...
shouldBlock = true;
blockReason = "Incoming paynym notification transaction.";
}
final utxo = isar_models.UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: fetchedUtxoList[i][j]["tx_pos"] as int,
value: fetchedUtxoList[i][j]["value"] as int,
name: "",
isBlocked: false,
blockedReason: null,
isBlocked: shouldBlock,
blockedReason: blockReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: fetchedUtxoList[i][j]["height"] as int?,
@ -1539,7 +1613,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1656,7 +1730,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip44);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1675,12 +1749,12 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions($DerivePathType.bip44): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions($DerivePathType.bip44): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1700,7 +1774,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip44);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1719,7 +1793,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkChangeAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
"Exception rethrown from _checkChangeAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -2088,7 +2162,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip44),
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
@ -2141,8 +2215,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip44);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2340,6 +2414,8 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip44:
addressesP2PKH.add(address);
break;
default:
throw Exception("Unsupported DerivePathType");
}
}
}
@ -2490,6 +2566,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -2757,7 +2834,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip44);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);
@ -2775,7 +2852,7 @@ class DogecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Dogecoin Network
final dogecoin = NetworkType(
messagePrefix: '\x18Dogecoin Signed Message:\n',
bech32: 'bc',
// bech32: 'bc',
bip32: Bip32Type(public: 0x02facafd, private: 0x02fac398),
pubKeyHash: 0x1e,
scriptHash: 0x16,
@ -2783,7 +2860,7 @@ final dogecoin = NetworkType(
final dogecointestnet = NetworkType(
messagePrefix: '\x18Dogecoin Signed Message:\n',
bech32: 'tb',
// bech32: 'tb',
bip32: Bip32Type(public: 0x043587cf, private: 0x04358394),
pubKeyHash: 0x71,
scriptHash: 0xc4,

View file

@ -839,6 +839,10 @@ class EpicCashWallet extends CoinServiceAPI
isar_models.Address? address = await db
.getAddresses(walletId)
.filter()
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.and()
.typeEqualTo(isar_models.AddressType.mimbleWimble)
.and()
.derivationIndexEqualTo(index)
.findFirst();
@ -877,8 +881,14 @@ class EpicCashWallet extends CoinServiceAPI
(await _currentReceivingAddress)?.value ??
(await _getReceivingAddressForIndex(0)).value;
Future<isar_models.Address?> get _currentReceivingAddress =>
db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst();
Future<isar_models.Address?> get _currentReceivingAddress => db
.getAddresses(walletId)
.filter()
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.and()
.typeEqualTo(isar_models.AddressType.mimbleWimble)
.sortByDerivationIndexDesc()
.findFirst();
@override
Future<void> exit() async {
@ -1416,6 +1426,14 @@ class EpicCashWallet extends CoinServiceAPI
});
await updateCachedChainHeight(latestHeight!);
if (latestHeight! > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return latestHeight!;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",

View file

@ -818,44 +818,31 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final currentPrice = await firoPrice;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _fetchTransactionData();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
/// Holds the max fee that can be sent
@ -3665,7 +3652,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -4897,6 +4884,14 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",

View file

@ -17,13 +17,13 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.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/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -33,6 +33,7 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
@ -50,8 +51,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
enum DerivePathType { bip44, bip49, bip84 }
bip32.BIP32 getBip32Node(
int chain,
int index,
@ -103,7 +102,7 @@ bip32.BIP32 getBip32NodeFromRoot(
case DerivePathType.bip84:
return root.derivePath("m/84'/$coinType'/0'/$chain/$index");
default:
throw Exception("DerivePathType must not be null.");
throw Exception("DerivePathType unsupported");
}
}
@ -138,7 +137,8 @@ bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) {
return getBip32Root(args.item1, args.item2);
}
class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
class LitecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
static const integrationTestFlag =
bool.fromEnvironment("IS_INTEGRATION_TEST");
@ -193,7 +193,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip84);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddress async =>
(await _currentChangeAddress).value;
@ -206,7 +206,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip84);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
@override
Future<void> exit() async {
@ -241,6 +241,14 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -421,7 +429,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType unsupported");
}
final address = isar_models.Address(
@ -509,6 +517,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
}) async {
longMutex = true;
@ -682,14 +691,25 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2wpkhChangeAddressArray.add(address);
}
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
if (isRescan) {
await db.updateOrPutAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
} else {
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
}
await _updateUTXOs();
@ -1194,46 +1214,31 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _refreshTransactions();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
@override
@ -1532,6 +1537,8 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.address!;
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType unsupported");
}
// add generated address & info to derivations
@ -1578,6 +1585,8 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
type = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType unsupported");
}
address = await db
.getAddresses(walletId)
@ -1605,6 +1614,8 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
key = "${walletId}_${chainId}DerivationsP2WPKH";
break;
default:
throw Exception("DerivePathType unsupported");
}
return key;
}
@ -1776,7 +1787,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1891,7 +1902,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1910,7 +1921,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1930,7 +1941,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip84);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1949,12 +1960,12 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -2349,7 +2360,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip84),
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
@ -2388,8 +2399,8 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await _checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip84);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2595,6 +2606,8 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
addressesP2WPKH.add(address);
break;
default:
throw Exception("DerivePathType unsupported");
}
}
}
@ -2879,6 +2892,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -3345,7 +3359,7 @@ class LitecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);

View file

@ -9,6 +9,7 @@ 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/global_event_bus.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -109,8 +110,15 @@ class Manager with ChangeNotifier {
try {
final txid = await _currentWallet.confirmSend(txData: txData);
txData["txid"] = txid;
await _currentWallet.updateSentCachedTxData(txData);
try {
txData["txid"] = txid;
await _currentWallet.updateSentCachedTxData(txData);
} catch (e, s) {
// do not rethrow as that would get handled as a send failure further up
// also this is not critical code and transaction should show up on \
// refresh regardless
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
}
notifyListeners();
return txid;
@ -214,4 +222,6 @@ class Manager with ChangeNotifier {
}
int get currentHeight => _currentWallet.storedChainHeight;
bool get hasPaynymSupport => _currentWallet is PaynymWalletInterface;
}

View file

@ -17,13 +17,13 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.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/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -33,6 +33,7 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
@ -50,8 +51,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008";
enum DerivePathType { bip44, bip49, bip84 }
bip32.BIP32 getBip32Node(
int chain,
int index,
@ -135,7 +134,8 @@ bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) {
return getBip32Root(args.item1, args.item2);
}
class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
class NamecoinWallet extends CoinServiceAPI
with WalletCache, WalletDB, ElectrumXParsing {
static const integrationTestFlag =
bool.fromEnvironment("IS_INTEGRATION_TEST");
@ -188,7 +188,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip84);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddress async =>
(await _currentChangeAddress).value;
@ -201,7 +201,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip84);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
@override
Future<void> exit() async {
@ -236,6 +236,14 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -411,7 +419,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType $type not supported");
}
final address = isar_models.Address(
@ -499,6 +507,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
}) async {
longMutex = true;
@ -672,14 +681,25 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2wpkhChangeAddressArray.add(address);
}
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
if (isRescan) {
await db.updateOrPutAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
} else {
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
...p2shReceiveAddressArray,
...p2shChangeAddressArray,
]);
}
await _updateUTXOs();
@ -1183,46 +1203,31 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _refreshTransactions();
// _transactionData = Future(() => data);
// }
//
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
@override
@ -1418,17 +1423,21 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) {
final features = await electrumXClient.getServerFeatures();
Logging.instance.log("features: $features", level: LogLevel.Info);
switch (coin) {
case Coin.namecoin:
if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
throw Exception("genesis hash does not match main net!");
}
break;
default:
throw Exception(
"Attempted to generate a NamecoinWallet using a non namecoin coin type: ${coin.name}");
try {
final features = await electrumXClient.getServerFeatures();
Logging.instance.log("features: $features", level: LogLevel.Info);
switch (coin) {
case Coin.namecoin:
if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
throw Exception("genesis hash does not match main net!");
}
break;
default:
throw Exception(
"Attempted to generate a NamecoinWallet using a non namecoin coin type: ${coin.name}");
}
} catch (e, s) {
Logging.instance.log("$e/n$s", level: LogLevel.Info);
}
}
@ -1509,6 +1518,8 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
.address!;
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType must not be null.");
}
// add generated address & info to derivations
@ -1555,6 +1566,9 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
type = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception(
"DerivePathType null or unsupported (${DerivePathType.bip44})");
}
address = await db
.getAddresses(walletId)
@ -1582,6 +1596,8 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
key = "${walletId}_${chainId}DerivationsP2WPKH";
break;
default:
throw Exception("DerivePathType $derivePathType not supported");
}
return key;
}
@ -1755,7 +1771,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1873,7 +1889,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1892,7 +1908,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1912,7 +1928,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip84);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1932,12 +1948,12 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -2336,7 +2352,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip84),
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
@ -2375,8 +2391,8 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await _checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip84);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2869,6 +2885,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -3336,7 +3353,7 @@ class NamecoinWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);

View file

@ -32,6 +32,7 @@ import 'package:stackwallet/utilities/assets.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/derive_path_type_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/format.dart';
@ -48,8 +49,6 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"0000594ada5310b367443ee0afd4fa3d0bbd5850ea4e33cdc7d6a904a7ec7c90";
enum DerivePathType { bip44, bip84 }
bip32.BIP32 getBip32Node(
int chain,
int index,
@ -96,7 +95,7 @@ bip32.BIP32 getBip32NodeFromRoot(
case DerivePathType.bip84:
return root.derivePath("m/84'/$coinType'/0'/$chain/$index");
default:
throw Exception("DerivePathType must not be null.");
throw Exception("DerivePathType $derivePathType not supported");
}
}
@ -184,7 +183,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(0, 0, DerivePathType.bip84);
await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddress async =>
(await _currentChangeAddress).value;
@ -197,7 +196,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip84);
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
@override
Future<void> exit() async {
@ -232,6 +231,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -394,7 +401,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("No Path type $type exists");
throw Exception("DerivePathType $type not supported");
}
final address = isar_models.Address(
@ -482,6 +489,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
required String mnemonic,
int maxUnusedAddressGap = 20,
int maxNumberOfIndexesToCheck = 1000,
bool isRescan = false,
}) async {
longMutex = true;
@ -607,12 +615,21 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
p2wpkhChangeAddressArray.add(address);
}
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
if (isRescan) {
await db.updateOrPutAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
} else {
await db.putAddresses([
...p2wpkhReceiveAddressArray,
...p2wpkhChangeAddressArray,
...p2pkhReceiveAddressArray,
...p2pkhChangeAddressArray,
]);
}
await _updateUTXOs();
@ -1114,46 +1131,31 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// transactions locally in a good way
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
// final priceData =
// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
// final String worthNow = Format.localizedStringAsFixed(
// value:
// ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
// Decimal.fromInt(Constants.satsPerCoin(coin)))
// .toDecimal(scaleOnInfinitePrecision: 2),
// decimalPlaces: 2,
// locale: locale!);
//
// final tx = models.Transaction(
// txid: txData["txid"] as String,
// confirmedStatus: false,
// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
// txType: "Sent",
// amount: txData["recipientAmt"] as int,
// worthNow: worthNow,
// worthAtBlockTimestamp: worthNow,
// fees: txData["fee"] as int,
// inputSize: 0,
// outputSize: 0,
// inputs: [],
// outputs: [],
// address: txData["address"] as String,
// height: -1,
// confirmations: 0,
// );
//
// if (cachedTxData == null) {
// final data = await _refreshTransactions();
// _transactionData = Future(() => data);
// } else {
// final transactions = cachedTxData!.getAllTransactions();
// transactions[tx.txid] = tx;
// cachedTxData = models.TransactionData.fromMap(transactions);
// _transactionData = Future(() => cachedTxData!);
// }
final transaction = isar_models.Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: isar_models.TransactionType.outgoing,
subType: isar_models.TransactionSubType.none,
amount: txData["recipientAmt"] as int,
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple4(transaction, [], [], address),
],
walletId,
);
}
@override
@ -1336,17 +1338,21 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) {
final features = await electrumXClient.getServerFeatures();
Logging.instance.log("features: $features", level: LogLevel.Info);
switch (coin) {
case Coin.particl:
if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
throw Exception("genesis hash does not match main net!");
}
break;
default:
throw Exception(
"Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}");
try {
final features = await electrumXClient.getServerFeatures();
Logging.instance.log("features: $features", level: LogLevel.Info);
switch (coin) {
case Coin.particl:
if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
throw Exception("genesis hash does not match main net!");
}
break;
default:
throw Exception(
"Attempted to generate a ParticlWallet using a non particl coin type: ${coin.name}");
}
} catch (e, s) {
Logging.instance.log("$e/n$s", level: LogLevel.Info);
}
}
@ -1407,6 +1413,8 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
address = P2WPKH(network: _network, data: data).data.address!;
addrType = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType $derivePathType not supported");
}
// add generated address & info to derivations
@ -1450,6 +1458,8 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
type = isar_models.AddressType.p2wpkh;
break;
default:
throw Exception("DerivePathType $derivePathType not supported");
}
address = await db
.getAddresses(walletId)
@ -1475,6 +1485,8 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
key = "${walletId}_${chainId}DerivationsP2WPKH";
break;
default:
throw Exception("DerivePathType $derivePathType not supported");
}
return key;
}
@ -1646,7 +1658,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1761,7 +1773,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1780,7 +1792,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -1800,7 +1812,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip84);
1, newChangeIndex, DerivePathTypeExt.primaryFor(coin));
final existing = await db
.getAddresses(walletId)
@ -1819,12 +1831,12 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $se\n$s",
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s",
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
@ -2189,7 +2201,6 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance.log(s.toString(), level: LogLevel.Warning);
}
// Logging.instance.log("output is transparent", level: LogLevel.Info);
} else if (output.containsKey('ct_fee') as bool) {
// or type: data
// TODO handle CT tx
@ -2501,7 +2512,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathType.bip84),
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
@ -2540,8 +2551,8 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
feeForTwoOutputs) {
// generate new change address if current change address has been used
await _checkChangeAddressForTransactions();
final String newChangeAddress =
await _getCurrentAddressForChain(1, DerivePathType.bip84);
final String newChangeAddress = await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin));
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
@ -2743,6 +2754,9 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
case DerivePathType.bip84:
addressesP2WPKH.add(address);
break;
default:
throw Exception(
"DerivePathType ${addressType(address: address)} not supported");
}
}
}
@ -2967,6 +2981,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
mnemonic: mnemonic!,
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
isRescan: true,
);
longMutex = false;
@ -3328,7 +3343,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain(
0, newReceivingIndex, DerivePathType.bip84);
0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin));
// Add that new receiving address
await db.putAddress(newReceivingAddress);

View file

@ -0,0 +1,224 @@
import 'package:bip47/src/util.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:tuple/tuple.dart';
mixin ElectrumXParsing {
Future<Tuple4<Transaction, List<Output>, List<Input>, Address>>
parseTransaction(
Map<String, dynamic> txData,
dynamic electrumxClient,
List<Address> myAddresses,
Coin coin,
int minConfirms,
String walletId,
) async {
Set<String> receivingAddresses = myAddresses
.where((e) =>
e.subType == AddressSubType.receiving ||
e.subType == AddressSubType.paynymReceive ||
e.subType == AddressSubType.paynymNotification)
.map((e) => e.value)
.toSet();
Set<String> changeAddresses = myAddresses
.where((e) => e.subType == AddressSubType.change)
.map((e) => e.value)
.toSet();
Set<String> inputAddresses = {};
Set<String> outputAddresses = {};
int totalInputValue = 0;
int totalOutputValue = 0;
int amountSentFromWallet = 0;
int amountReceivedInWallet = 0;
int changeAmount = 0;
// parse inputs
for (final input in txData["vin"] as List) {
final prevTxid = input["txid"] as String;
final prevOut = input["vout"] as int;
// fetch input tx to get address
final inputTx = await electrumxClient.getTransaction(
txHash: prevTxid,
coin: coin,
);
for (final output in inputTx["vout"] as List) {
// check matching output
if (prevOut == output["n"]) {
// get value
final value = Format.decimalAmountToSatoshis(
Decimal.parse(output["value"].toString()),
coin,
);
// add value to total
totalInputValue += value;
// get input(prevOut) address
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
inputAddresses.add(address);
// if input was from my wallet, add value to amount sent
if (receivingAddresses.contains(address) ||
changeAddresses.contains(address)) {
amountSentFromWallet += value;
}
}
}
}
}
// parse outputs
for (final output in txData["vout"] as List) {
// get value
final value = Format.decimalAmountToSatoshis(
Decimal.parse(output["value"].toString()),
coin,
);
// add value to total
totalOutputValue += value;
// get output address
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
outputAddresses.add(address);
// if output was to my wallet, add value to amount received
if (receivingAddresses.contains(address)) {
amountReceivedInWallet += value;
} else if (changeAddresses.contains(address)) {
changeAmount += value;
}
}
}
final mySentFromAddresses = [
...receivingAddresses.intersection(inputAddresses),
...changeAddresses.intersection(inputAddresses)
];
final myReceivedOnAddresses =
receivingAddresses.intersection(outputAddresses);
final myChangeReceivedOnAddresses =
changeAddresses.intersection(outputAddresses);
final fee = totalInputValue - totalOutputValue;
// this is the address initially used to fetch the txid
Address transactionAddress = txData["address"] as Address;
TransactionType type;
int amount;
if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) {
// tx is sent to self
type = TransactionType.sentToSelf;
// should be 0
amount =
amountSentFromWallet - amountReceivedInWallet - fee - changeAmount;
} else if (mySentFromAddresses.isNotEmpty) {
// outgoing tx
type = TransactionType.outgoing;
amount = amountSentFromWallet - changeAmount - fee;
final possible =
outputAddresses.difference(myChangeReceivedOnAddresses).first;
if (transactionAddress.value != possible) {
transactionAddress = Address(
walletId: walletId,
value: possible,
derivationIndex: -1,
subType: AddressSubType.nonWallet,
type: AddressType.nonWallet,
publicKey: [],
);
}
} else {
// incoming tx
type = TransactionType.incoming;
amount = amountReceivedInWallet;
}
List<Output> outs = [];
List<Input> ins = [];
for (final json in txData["vin"] as List) {
bool isCoinBase = json['coinbase'] != null;
final input = Input(
walletId: walletId,
txid: json['txid'] as String,
vout: json['vout'] as int? ?? -1,
scriptSig: json['scriptSig']?['hex'] as String?,
scriptSigAsm: json['scriptSig']?['asm'] as String?,
isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?,
sequence: json['sequence'] as int?,
innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?,
);
ins.add(input);
}
for (final json in txData["vout"] as List) {
final output = Output(
walletId: walletId,
scriptPubKey: json['scriptPubKey']?['hex'] as String?,
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?,
scriptPubKeyType: json['scriptPubKey']?['type'] as String?,
scriptPubKeyAddress:
json["scriptPubKey"]?["addresses"]?[0] as String? ??
json['scriptPubKey']?['type'] as String? ??
"",
value: Format.decimalAmountToSatoshis(
Decimal.parse(json["value"].toString()),
coin,
),
);
outs.add(output);
}
TransactionSubType txSubType = TransactionSubType.none;
if (this is PaynymWalletInterface && outs.length > 1 && ins.isNotEmpty) {
for (int i = 0; i < outs.length; i++) {
List<String>? scriptChunks = outs[i].scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
txSubType = TransactionSubType.bip47Notification;
}
}
}
}
final tx = Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: txData["blocktime"] as int? ??
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
type: type,
subType: txSubType,
amount: amount,
fee: fee,
height: txData["height"] as int?,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: null,
);
return Tuple4(tx, outs, ins, transactionAddress);
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/notification_model.dart';
@ -169,12 +170,14 @@ class NotificationsService extends ChangeNotifier {
}
// replaces the current notification with the updated one
add(updatedNotification, true);
await add(updatedNotification, true);
}
} else {
// TODO: check non electrumx coins
}
}
} on NoSuchTransactionException catch (e, s) {
await _deleteWatchedTxNotification(notification);
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
}

View file

@ -54,4 +54,25 @@ class TransactionNotificationTracker {
key: "notifiedConfirmedTransactions",
value: notifiedConfirmedTransactions);
}
Future<void> deleteTransaction(String txid) async {
final notifiedPendingTransactions = DB.instance.get<dynamic>(
boxName: walletId, key: "notifiedPendingTransactions") as Map? ??
{};
final notifiedConfirmedTransactions = DB.instance.get<dynamic>(
boxName: walletId, key: "notifiedConfirmedTransactions") as Map? ??
{};
notifiedPendingTransactions.remove(txid);
notifiedConfirmedTransactions.remove(txid);
await DB.instance.put<dynamic>(
boxName: walletId,
key: "notifiedConfirmedTransactions",
value: notifiedConfirmedTransactions);
await DB.instance.put<dynamic>(
boxName: walletId,
key: "notifiedPendingTransactions",
value: notifiedPendingTransactions);
}
}

View file

@ -194,6 +194,8 @@ class _SVG {
String get exitDesktop => "assets/svg/exit-desktop.svg";
String get keys => "assets/svg/keys.svg";
String get arrowDown => "assets/svg/arrow-down.svg";
String get robotHead => "assets/svg/robot-head.svg";
String get whirlPool => "assets/svg/whirlpool.svg";
String get ellipse1 => "assets/svg/Ellipse-43.svg";
String get ellipse2 => "assets/svg/Ellipse-42.svg";
@ -262,6 +264,7 @@ class _PNG {
const _PNG();
String get stack => "assets/images/stack.png";
String get unclaimedPaynym => "assets/images/unclaimed.png";
String get splash => "assets/images/splash.png";
String get monero => "assets/images/monero.png";

View file

@ -0,0 +1,32 @@
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoindart/bitcoindart.dart';
import 'package:flutter/foundation.dart';
import 'package:tuple/tuple.dart';
abstract class Bip32Utils {
static bip32.BIP32 getBip32RootSync(String mnemonic, NetworkType network) {
final seed = bip39.mnemonicToSeed(mnemonic);
final networkType = bip32.NetworkType(
wif: network.wif,
bip32: bip32.Bip32Type(
public: network.bip32.public,
private: network.bip32.private,
),
);
final root = bip32.BIP32.fromSeed(seed, networkType);
return root;
}
static Future<bip32.BIP32> getBip32Root(
String mnemonic, NetworkType network) async {
final root = await compute(_getBip32RootWrapper, Tuple2(mnemonic, network));
return root;
}
/// wrapper for compute()
static bip32.BIP32 _getBip32RootWrapper(Tuple2<String, NetworkType> args) {
return getBip32RootSync(args.item1, args.item2);
}
}

View file

@ -0,0 +1,37 @@
import 'dart:typed_data';
import 'package:bip47/src/util.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
abstract class Bip47Utils {
/// looks at tx outputs and returns a blinded payment code if found
static Uint8List? getBlindedPaymentCodeBytesFrom(Transaction transaction) {
for (int i = 0; i < transaction.outputs.length; i++) {
final bytes = getBlindedPaymentCodeBytesFromOutput(
transaction.outputs.elementAt(i));
if (bytes != null) {
return bytes;
}
}
return null;
}
static Uint8List? getBlindedPaymentCodeBytesFromOutput(Output output) {
Uint8List? blindedCodeBytes;
List<String>? scriptChunks = output.scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
blindedCodeBytes = bytes;
}
}
return blindedCodeBytes;
}
}

View file

@ -4,9 +4,12 @@ import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/utilities/logger.dart';
const String _kKeyBlobKey = "swbKeyBlobKeyStringID";
const String _kKeyBlobVersionKey = "swbKeyBlobVersionKeyStringID";
const int kLatestBlobVersion = 2;
String _getMessageFromException(Object exception) {
if (exception is IncorrectPassphrase) {
if (exception is IncorrectPassphraseOrVersion) {
return exception.errMsg();
}
if (exception is BadDecryption) {
@ -18,6 +21,9 @@ String _getMessageFromException(Object exception) {
if (exception is EncodingError) {
return exception.errMsg();
}
if (exception is VersionError) {
return exception.errMsg();
}
return exception.toString();
}
@ -41,7 +47,10 @@ class DPS {
}
try {
_handler = await StorageCryptoHandler.fromNewPassphrase(passphrase);
_handler = await StorageCryptoHandler.fromNewPassphrase(
passphrase,
kLatestBlobVersion,
);
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
await DB.instance.put<String>(
@ -49,6 +58,7 @@ class DPS {
key: _kKeyBlobKey,
value: await _handler!.getKeyBlob(),
);
await _updateStoredKeyBlobVersion(kLatestBlobVersion);
await box.close();
} catch (e, s) {
Logging.instance.log(
@ -78,7 +88,24 @@ class DPS {
}
try {
_handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob);
final blobVersion = await _getStoredKeyBlobVersion();
_handler = await StorageCryptoHandler.fromExisting(
passphrase,
keyBlob,
blobVersion,
);
if (blobVersion < kLatestBlobVersion) {
// update blob
await _handler!.resetPassphrase(passphrase, kLatestBlobVersion);
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
await DB.instance.put<String>(
boxName: DB.boxNameDesktopData,
key: _kKeyBlobKey,
value: await _handler!.getKeyBlob(),
);
await _updateStoredKeyBlobVersion(kLatestBlobVersion);
await box.close();
}
} catch (e, s) {
Logging.instance.log(
"${_getMessageFromException(e)}\n$s",
@ -102,7 +129,8 @@ class DPS {
}
try {
await StorageCryptoHandler.fromExisting(passphrase, keyBlob);
final blobVersion = await _getStoredKeyBlobVersion();
await StorageCryptoHandler.fromExisting(passphrase, keyBlob, blobVersion);
// existing passphrase matches key blob
return true;
} catch (e, s) {
@ -135,8 +163,10 @@ class DPS {
return false;
}
final blobVersion = await _getStoredKeyBlobVersion();
try {
await _handler!.resetPassphrase(passphraseNew);
await _handler!.resetPassphrase(passphraseNew, blobVersion);
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
await DB.instance.put<String>(
@ -144,6 +174,7 @@ class DPS {
key: _kKeyBlobKey,
value: await _handler!.getKeyBlob(),
);
await _updateStoredKeyBlobVersion(blobVersion);
await box.close();
// successfully updated passphrase
@ -164,4 +195,22 @@ class DPS {
);
return keyBlob != null;
}
Future<int> _getStoredKeyBlobVersion() async {
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
final keyBlobVersionString = DB.instance.get<String>(
boxName: DB.boxNameDesktopData,
key: _kKeyBlobVersionKey,
);
await box.close();
return int.tryParse(keyBlobVersionString ?? "1") ?? 1;
}
Future<void> _updateStoredKeyBlobVersion(int version) async {
await DB.instance.put<String>(
boxName: DB.boxNameDesktopData,
key: _kKeyBlobVersionKey,
value: version.toString(),
);
}
}

View file

@ -171,30 +171,6 @@ extension CoinExt on Coin {
}
}
bool get hasPaynymSupport {
switch (this) {
case Coin.bitcoin:
case Coin.litecoin:
case Coin.bitcoincash:
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.epicCash:
case Coin.monero:
case Coin.wownero:
return false;
case Coin.dogecoin:
case Coin.dogecoinTestNet:
// return true;
return false;
}
}
bool get hasBuySupport {
switch (this) {
case Coin.bitcoin:

View file

@ -0,0 +1,36 @@
import 'package:stackwallet/utilities/enums/coin_enum.dart';
enum DerivePathType {
bip44,
bch44,
bip49,
bip84,
}
extension DerivePathTypeExt on DerivePathType {
static DerivePathType primaryFor(Coin coin) {
switch (coin) {
case Coin.bitcoincash:
case Coin.bitcoincashTestnet:
case Coin.dogecoin:
case Coin.dogecoinTestNet:
case Coin.firo:
case Coin.firoTestNet:
return DerivePathType.bip44;
case Coin.bitcoin:
case Coin.bitcoinTestNet:
case Coin.litecoin:
case Coin.litecoinTestNet:
case Coin.namecoin:
case Coin.particl:
return DerivePathType.bip84;
case Coin.epicCash:
case Coin.monero:
case Coin.wownero:
throw UnsupportedError(
"$coin does not use bitcoin style derivation paths");
}
}
}

View file

@ -1,12 +1,12 @@
abstract class FeaturedPaynyms {
// TODO: replace with actual values
static const String samouraiWalletDevFund =
"PM8TJYkuSdYXJnwDBq8ChfinfXv3srxhQrx3eoEwbSw51wMjdo9JJ2DsycwT3gt3zHQ7cV1grvabMmmf1Btj6fY7tgkgSz9B8MZuR3kjYfgMLMURJCXN";
// TODO: replace with actual value
// static const String samouraiWalletDevFund =
// "PM8TJYkuSdYXJnwDBq8ChfinfXv3srxhQrx3eoEwbSw51wMjdo9JJ2DsycwT3gt3zHQ7cV1grvabMmmf1Btj6fY7tgkgSz9B8MZuR3kjYfgMLMURJCXN";
static const String stackWallet =
"PM8TJYkuSdYXJnwDBq8ChfinfXv3srxhQrx3eoEwbSw51wMjdo9JJ2DsycwT3gt3zHQ7cV1grvabMmmf1Btj6fY7tgkgSz9B8MZuR3kjYfgMLMURJCXN";
"PM8TJdQcNk27JpxGRtNR7Hnh8VkJk4Nf17BthLx89fM3iX3UL2YshyaiTAvKgTCVvpgsAgY1DbojkAaUd3Rcn48NEn4uUBuqkaSddgKL8TPAAEQXNuE6";
static Map<String, String> get featured => {
"Stack Wallet": stackWallet,
"Samourai Wallet Dev Fund": samouraiWalletDevFund,
// "Samourai Wallet Dev Fund": samouraiWalletDevFund,
};
}

View file

@ -46,6 +46,8 @@ abstract class SecureStorageInterface {
MacOsOptions? mOptions,
WindowsOptions? wOptions,
});
Future<List<String>> get keys;
}
class DesktopSecureStore {
@ -110,6 +112,10 @@ class DesktopSecureStore {
await isar.encryptedStringValues.deleteByKey(key);
});
}
Future<List<String>> get keys async {
return await isar.encryptedStringValues.where().keyProperty().findAll();
}
}
/// all *Options params ignored on desktop
@ -229,6 +235,15 @@ class SecureStorageWrapper implements SecureStorageInterface {
);
}
}
@override
Future<List<String>> get keys async {
if (_isDesktop) {
return (_store as DesktopSecureStore).keys;
} else {
return (await (_store as FlutterSecureStorage).readAll()).keys.toList();
}
}
}
// Mock class for testing purposes
@ -305,4 +320,7 @@ class FakeSecureStorage implements SecureStorageInterface {
@override
dynamic get store => throw UnimplementedError();
@override
Future<List<String>> get keys => Future(() => _store.keys.toList());
}

View file

@ -13,31 +13,31 @@ class STextStyles {
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
}
}
@ -932,6 +932,76 @@ class STextStyles {
}
}
static TextStyle w600_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
}
}
static TextStyle w500_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
}
}
static TextStyle w500_12(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
@ -967,6 +1037,41 @@ class STextStyles {
}
}
static TextStyle w500_10(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
}
}
static TextStyle syncPercent(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
@ -1030,7 +1135,7 @@ class STextStyles {
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
color: _theme(context).bottomNavIconIcon,
fontWeight: FontWeight.w500,
fontSize: 12,
);

View file

@ -327,7 +327,7 @@ class FruitSorbetColors extends StackColorTheme {
@override
Color get rateTypeToggleDesktopColorOn => const Color(0xFFFFD8CE);
@override
Color get rateTypeToggleDesktopColorOff => buttonBackSecondary;
Color get rateTypeToggleDesktopColorOff => popupBG;
@override
BoxShadow get standardBoxShadow => BoxShadow(

View file

@ -94,7 +94,7 @@ class OceanBreezeColors extends StackColorTheme {
@override
Color get buttonTextPrimary => const Color(0xFFFFFFFF);
@override
Color get buttonTextSecondary => const Color(0xFF232323);
Color get buttonTextSecondary => accentColorDark;
@override
Color get buttonTextPrimaryDisabled => const Color(0xFFFFFFFF);
@override

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
abstract class Util {
@ -22,6 +23,14 @@ abstract class Util {
return Platform.isLinux || Platform.isMacOS || Platform.isWindows;
}
static Future<bool> get isIPad async {
final deviceInfo = (await DeviceInfoPlugin().deviceInfo);
if (deviceInfo is IosDeviceInfo) {
return (deviceInfo).name?.toLowerCase().contains("ipad") == true;
}
return false;
}
static MaterialColor createMaterialColor(Color color) {
List<double> strengths = <double>[.05];
final swatch = <int, Color>{};

View file

@ -1,17 +1,17 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class BlueTextButton extends ConsumerStatefulWidget {
const BlueTextButton({
class _CustomTextButton extends StatefulWidget {
const _CustomTextButton({
Key? key,
required this.text,
required this.enabledColor,
required this.disabledColor,
this.onTap,
this.enabled = true,
this.textSize,
@ -21,12 +21,14 @@ class BlueTextButton extends ConsumerStatefulWidget {
final VoidCallback? onTap;
final bool enabled;
final double? textSize;
final Color enabledColor;
final Color disabledColor;
@override
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
State<_CustomTextButton> createState() => _CustomTextButtonState();
}
class _BlueTextButtonState extends ConsumerState<BlueTextButton>
class _CustomTextButtonState extends State<_CustomTextButton>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation<dynamic>? animation;
@ -37,18 +39,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
@override
void initState() {
if (widget.enabled) {
color = ref.read(colorThemeProvider.state).state.buttonTextBorderless;
color = widget.enabledColor;
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
animation = ColorTween(
begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless,
end: ref
.read(colorThemeProvider.state)
.state
.buttonTextBorderless
.withOpacity(0.4),
begin: widget.enabledColor,
end: widget.enabledColor.withOpacity(0.4),
).animate(controller!);
animation!.addListener(() {
@ -57,7 +55,7 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
});
});
} else {
color = ref.read(colorThemeProvider.state).state.textSubtitle1;
color = widget.disabledColor;
}
super.initState();
@ -116,3 +114,32 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
);
}
}
class CustomTextButton extends StatelessWidget {
const CustomTextButton({
Key? key,
required this.text,
this.onTap,
this.enabled = true,
this.textSize,
}) : super(key: key);
final String text;
final VoidCallback? onTap;
final bool enabled;
final double? textSize;
@override
Widget build(BuildContext context) {
return _CustomTextButton(
key: UniqueKey(),
text: text,
enabledColor:
Theme.of(context).extension<StackColors>()!.buttonTextBorderless,
disabledColor: Theme.of(context).extension<StackColors>()!.textSubtitle1,
enabled: enabled,
textSize: textSize,
onTap: onTap,
);
}
}

View file

@ -9,9 +9,9 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/paynym_api_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart';
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
@ -58,16 +58,18 @@ class _PaynymFollowToggleButtonState
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
// get wallet to access paynym calls
final wallet = manager.wallet as PaynymWalletInterface;
final followedAccount = await ref
.read(paynymAPIProvider)
.nym(widget.paymentCodeStringToFollow, true);
final myPCode = await wallet.getPaymentCode();
final myPCode =
await wallet.getPaymentCode(DerivePathTypeExt.primaryFor(manager.coin));
PaynymResponse<String> token =
await ref.read(paynymAPIProvider).token(myPCode.toString());
@ -158,16 +160,17 @@ class _PaynymFollowToggleButtonState
),
);
final wallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as DogecoinWallet;
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
final wallet = manager.wallet as PaynymWalletInterface;
final followedAccount = await ref
.read(paynymAPIProvider)
.nym(widget.paymentCodeStringToFollow, true);
final myPCode = await wallet.getPaymentCode();
final myPCode =
await wallet.getPaymentCode(DerivePathTypeExt.primaryFor(manager.coin));
PaynymResponse<String> token =
await ref.read(paynymAPIProvider).token(myPCode.toString());
@ -269,8 +272,8 @@ class _PaynymFollowToggleButtonState
switch (widget.style) {
case PaynymFollowToggleButtonStyle.primary:
return PrimaryButton(
width: isDesktop ? 120 : 84,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l,
width: isDesktop ? 120 : 100,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.xl,
label: isFollowing ? "Unfollow" : "Follow",
onPressed: _onPressed,
);
@ -278,15 +281,15 @@ class _PaynymFollowToggleButtonState
case PaynymFollowToggleButtonStyle.detailsPopup:
return SecondaryButton(
label: isFollowing ? "Unfollow" : "Follow",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
isFollowing ? Assets.svg.userMinus : Assets.svg.userPlus,
width: 10,
height: 10,
width: 16,
height: 16,
color:
Theme.of(context).extension<StackColors>()!.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: _onPressed,
);

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