mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -191,6 +191,10 @@ class Money implements Comparable<Money> {
|
||||
@override
|
||||
int get hashCode => amount.hashCode ^ currency.hashCode;
|
||||
|
||||
// Added this to reduce the hops we do to convert Money to double
|
||||
// for fiat conversion and display
|
||||
double toDouble() => amount / multiplierOf(currency.decimals);
|
||||
|
||||
@override
|
||||
String toString() => formatFixed(amount, currency.decimals);
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
decimals ??= 0;
|
||||
fractionalDigits ??= decimals;
|
||||
|
||||
var multiplier = getMultiplier(decimals);
|
||||
// Make sure wei is a big number (convert as necessary)
|
||||
final multiplier = multiplierOf(decimals);
|
||||
var negative = value.isNegative;
|
||||
if (negative) value = value * BigInt.from(-1);
|
||||
if (negative) value = -value;
|
||||
|
||||
var fraction =
|
||||
value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0");
|
||||
var fraction = (value % multiplier).toString().padLeft(decimals, "0");
|
||||
|
||||
if (fractionalDigits < 0) fractionalDigits = 0;
|
||||
if (fractionalDigits > decimals) fractionalDigits = decimals;
|
||||
@@ -20,7 +18,7 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
fraction = removeTrailing("0", fraction);
|
||||
}
|
||||
|
||||
final whole = (value ~/ BigInt.parse(multiplier));
|
||||
final whole = value ~/ multiplier;
|
||||
|
||||
final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction";
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ BigInt parseFixed(String value, int decimals) {
|
||||
|
||||
final wholeValue = BigInt.parse(whole);
|
||||
final fractionValue = BigInt.parse(fraction);
|
||||
final multiplierValue = BigInt.parse(multiplier);
|
||||
final multiplierValue = multiplierOf(decimals);
|
||||
|
||||
var wei = (wholeValue * multiplierValue) + fractionValue;
|
||||
|
||||
@@ -69,3 +69,8 @@ BigInt parseFixed(String value, int decimals) {
|
||||
|
||||
// Returns a string "1" followed by decimal "0"s
|
||||
String getMultiplier(int decimals) => "1".padRight(decimals + 1, "0");
|
||||
|
||||
final _multipliers = <int, BigInt>{};
|
||||
|
||||
// this is more direct and faster than having it as string then parsing everytime to get the number
|
||||
BigInt multiplierOf(int decimals) => _multipliers[decimals] ??= BigInt.from(10).pow(decimals);
|
||||
|
||||
@@ -32,14 +32,29 @@ class TransactionFetchResult {
|
||||
});
|
||||
}
|
||||
|
||||
class TransactionSyncResult {
|
||||
final List<SolanaTransactionModel> transactions;
|
||||
final String? newestSignature;
|
||||
|
||||
TransactionSyncResult({
|
||||
required this.transactions,
|
||||
this.newestSignature,
|
||||
});
|
||||
}
|
||||
|
||||
class SolanaWalletClient {
|
||||
// Minimum amount in SOL to consider a transaction valid (to filter spam)
|
||||
static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol);
|
||||
|
||||
static const int _signaturePageSize = 1000;
|
||||
|
||||
late final client = ProxyWrapper().getHttpIOClient();
|
||||
SolanaRPC? _provider;
|
||||
bool _isStopped = false;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
_isStopped = false;
|
||||
String formattedUrl;
|
||||
String protocolUsed = node.isSSL ? "https" : "http";
|
||||
|
||||
@@ -625,10 +640,7 @@ class SolanaWalletClient {
|
||||
incomingAmount = diff.toDouble();
|
||||
incomingMintAddress = mint;
|
||||
final token = await getTokenInfo(mint);
|
||||
printV(token?.symbol);
|
||||
printV(token?.decimals);
|
||||
incomingToken =
|
||||
token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
|
||||
incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
|
||||
incomingTo = walletAddress;
|
||||
// We find the intermediate account
|
||||
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
|
||||
@@ -951,27 +963,67 @@ class SolanaWalletClient {
|
||||
return mints.toList();
|
||||
}
|
||||
|
||||
/// Load the Address's transactions into the account
|
||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||
Future<List<Map<String, dynamic>>> _getAllSignaturesSinceLastFetch(
|
||||
SolAddress address,
|
||||
String? until,
|
||||
Commitment? commitment,
|
||||
) async {
|
||||
final signatures = <Map<String, dynamic>>[];
|
||||
String? before;
|
||||
|
||||
while (true) {
|
||||
final currentPageSignatureResults = await _provider!.request(
|
||||
SolanaRPCGetSignaturesForAddress(
|
||||
account: address,
|
||||
commitment: commitment,
|
||||
until: until,
|
||||
before: before,
|
||||
limit: _signaturePageSize,
|
||||
),
|
||||
);
|
||||
|
||||
if (currentPageSignatureResults.isEmpty) break;
|
||||
|
||||
signatures.addAll(currentPageSignatureResults);
|
||||
|
||||
if (currentPageSignatureResults.length < _signaturePageSize) break;
|
||||
|
||||
if (until == null) break;
|
||||
|
||||
final lastSignatureOnPage = currentPageSignatureResults.last['signature'] as String;
|
||||
|
||||
if (lastSignatureOnPage == before) break;
|
||||
|
||||
before = lastSignatureOnPage;
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
Future<TransactionSyncResult> fetchTransactions(
|
||||
SolAddress address, {
|
||||
SPLToken? splToken,
|
||||
Commitment? commitment,
|
||||
SolAddress? walletAddress,
|
||||
String? untilSignature,
|
||||
required void Function(List<SolanaTransactionModel>) onUpdate,
|
||||
}) async {
|
||||
List<SolanaTransactionModel> transactions = [];
|
||||
final transactions = <SolanaTransactionModel>[];
|
||||
|
||||
try {
|
||||
final signatures = await _provider!.request(
|
||||
SolanaRPCGetSignaturesForAddress(
|
||||
account: address,
|
||||
commitment: commitment,
|
||||
),
|
||||
);
|
||||
final signatures =
|
||||
await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
|
||||
|
||||
if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions);
|
||||
|
||||
// The maximum concurrent batch size.
|
||||
const int batchSize = 10;
|
||||
|
||||
bool hasFailures = false;
|
||||
|
||||
for (int i = 0; i < signatures.length; i += batchSize) {
|
||||
if (_isStopped) return TransactionSyncResult(transactions: transactions);
|
||||
|
||||
final batch = signatures.skip(i).take(batchSize).toList();
|
||||
|
||||
final batchResponses = await Future.wait(batch.map((signature) async {
|
||||
@@ -985,6 +1037,7 @@ class SolanaWalletClient {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
hasFailures = true;
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
@@ -999,16 +1052,16 @@ class SolanaWalletClient {
|
||||
|
||||
final parsedTransactionsLists = await Future.wait(parsedTransactionsFutures);
|
||||
|
||||
// We flatten the list of lists into a single list
|
||||
final batchTransactions = <SolanaTransactionModel>[];
|
||||
for (final parsedList in parsedTransactionsLists) {
|
||||
if (parsedList != null) {
|
||||
transactions.addAll(parsedList);
|
||||
batchTransactions.addAll(parsedList);
|
||||
}
|
||||
}
|
||||
|
||||
// Only update UI if we have new valid transactions
|
||||
if (transactions.isNotEmpty) {
|
||||
onUpdate(List<SolanaTransactionModel>.from(transactions));
|
||||
if (batchTransactions.isNotEmpty) {
|
||||
transactions.addAll(batchTransactions);
|
||||
onUpdate(batchTransactions);
|
||||
}
|
||||
|
||||
if (i + batchSize < signatures.length) {
|
||||
@@ -1016,38 +1069,53 @@ class SolanaWalletClient {
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
return TransactionSyncResult(
|
||||
transactions: transactions,
|
||||
newestSignature: hasFailures ? null : signatures.first['signature'] as String,
|
||||
);
|
||||
} catch (err, s) {
|
||||
printV('Error fetching transactions: $err \n$s');
|
||||
return [];
|
||||
return TransactionSyncResult(transactions: transactions);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SolanaTransactionModel>> getSPLTokenTransfers({
|
||||
final Map<String, ProgramDerivedAddress> associatedTokenAccountCache = {};
|
||||
|
||||
Future<TransactionSyncResult> getSPLTokenTransfers({
|
||||
required String mintAddress,
|
||||
required SPLToken splToken,
|
||||
required SolanaPrivateKey privateKey,
|
||||
String? untilSignature,
|
||||
required void Function(List<SolanaTransactionModel>) onUpdate,
|
||||
}) async {
|
||||
ProgramDerivedAddress? associatedTokenAccount;
|
||||
final ownerWalletAddress = privateKey.publicKey().toAddress();
|
||||
try {
|
||||
associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
|
||||
payerPrivateKey: privateKey,
|
||||
mintAddress: SolAddress(mintAddress),
|
||||
ownerAddress: ownerWalletAddress,
|
||||
shouldCreateATA: false,
|
||||
);
|
||||
} catch (e, s) {
|
||||
printV('$e \n $s');
|
||||
}
|
||||
|
||||
if (associatedTokenAccount == null) return [];
|
||||
var associatedTokenAccount = associatedTokenAccountCache[mintAddress];
|
||||
|
||||
if (associatedTokenAccount == null) {
|
||||
try {
|
||||
associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
|
||||
payerPrivateKey: privateKey,
|
||||
mintAddress: SolAddress(mintAddress),
|
||||
ownerAddress: ownerWalletAddress,
|
||||
shouldCreateATA: false,
|
||||
);
|
||||
} catch (e, s) {
|
||||
printV('$e \n $s');
|
||||
}
|
||||
|
||||
if (associatedTokenAccount == null) {
|
||||
return TransactionSyncResult(transactions: <SolanaTransactionModel>[]);
|
||||
}
|
||||
|
||||
associatedTokenAccountCache[mintAddress] = associatedTokenAccount;
|
||||
}
|
||||
|
||||
return fetchTransactions(
|
||||
associatedTokenAccount.address,
|
||||
splToken: splToken,
|
||||
walletAddress: ownerWalletAddress,
|
||||
untilSignature: untilSignature,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}
|
||||
@@ -1055,16 +1123,9 @@ class SolanaWalletClient {
|
||||
final Map<String, SPLToken?> tokenInfoCache = {};
|
||||
|
||||
Future<SPLToken?> getTokenInfo(String mintAddress) async {
|
||||
if (tokenInfoCache.containsKey(mintAddress)) {
|
||||
printV("Cached");
|
||||
return tokenInfoCache[mintAddress];
|
||||
} else {
|
||||
final token = await fetchSPLTokenInfo(mintAddress);
|
||||
if (token != null) {
|
||||
tokenInfoCache[mintAddress] = token;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress];
|
||||
|
||||
return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress);
|
||||
}
|
||||
|
||||
Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
|
||||
@@ -1139,7 +1200,7 @@ class SolanaWalletClient {
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {}
|
||||
void stop() => _isStopped = true;
|
||||
|
||||
SolanaRPC? get getSolanaProvider => _provider;
|
||||
|
||||
|
||||
@@ -31,18 +31,36 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<Solan
|
||||
await _load();
|
||||
}
|
||||
|
||||
Future<void> _saveQueue = Future.value();
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
|
||||
final data = json.encode({'transactions': transactionMaps});
|
||||
await encryptionFileUtils.write(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
printV('Error while saving solana transaction history: ${e.toString()}');
|
||||
printV(s);
|
||||
}
|
||||
Future<void> save() => saveAndConfirm();
|
||||
|
||||
Future<bool> saveAndConfirm() {
|
||||
final write = _saveQueue.then((_) async {
|
||||
try {
|
||||
await _write();
|
||||
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
printV('Error while saving solana transaction history: ${e.toString()}');
|
||||
printV(s);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
_saveQueue = write;
|
||||
|
||||
return write;
|
||||
}
|
||||
|
||||
Future<void> _write() async {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
|
||||
final data = json.encode({'transactions': transactionMaps});
|
||||
await encryptionFileUtils.write(path: path, password: _password, data: data);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -88,6 +88,8 @@ abstract class SolanaWalletBase
|
||||
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
Future<void>? _currentRefresh;
|
||||
|
||||
late final Box<SPLToken> splTokensBox;
|
||||
|
||||
@override
|
||||
@@ -321,6 +323,33 @@ abstract class SolanaWalletBase
|
||||
]);
|
||||
}
|
||||
|
||||
static const _nativeSource = 'native';
|
||||
|
||||
String _lastSyncedSignatureKey(String source) =>
|
||||
'solana_last_synced_signature_${walletInfo.name}_$source';
|
||||
|
||||
Future<String?> _lastSyncedSignature(String source) async {
|
||||
if (transactionHistory.transactions.isEmpty) return null;
|
||||
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
return prefs.getString(_lastSyncedSignatureKey(source));
|
||||
}
|
||||
|
||||
Future<void> _saveLastSyncedSignature(String source, String? signature) async {
|
||||
if (signature == null) return;
|
||||
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
await prefs.setString(_lastSyncedSignatureKey(source), signature);
|
||||
}
|
||||
|
||||
Future<void> _clearLastSyncedSignature(String source) async {
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
await prefs.remove(_lastSyncedSignatureKey(source));
|
||||
}
|
||||
|
||||
/// Polls for a specific transaction by signature with exponential backoff
|
||||
/// I'm using this in case we make the call to fetch the transaction and it has not finished its confirmations on the solana network and been indexed by the node networks we use.
|
||||
Future<void> pollForTransaction({
|
||||
@@ -365,20 +394,34 @@ abstract class SolanaWalletBase
|
||||
await updateTransactionsHistory();
|
||||
}
|
||||
|
||||
void updateTransactions(List<SolanaTransactionModel> updatedTx) {
|
||||
addTransactionsToTransactionHistory(updatedTx);
|
||||
}
|
||||
void updateTransactions(List<SolanaTransactionModel> updatedTx) => _addTransactions(updatedTx);
|
||||
|
||||
/// Fetches the native SOL transactions linked to the wallet Public Key
|
||||
Future<void> _updateNativeSOLTransactions() async {
|
||||
final transactions =
|
||||
await _client.fetchTransactions(_solanaPublicKey.toAddress(), onUpdate: updateTransactions);
|
||||
final result = await _client.fetchTransactions(
|
||||
_solanaPublicKey.toAddress(),
|
||||
untilSignature: await _lastSyncedSignature(_nativeSource),
|
||||
onUpdate: updateTransactions,
|
||||
);
|
||||
|
||||
await addTransactionsToTransactionHistory(transactions);
|
||||
await _updateStateWhenSyncForTheSourceEnds(_nativeSource, result);
|
||||
}
|
||||
|
||||
Future<void> _updateStateWhenSyncForTheSourceEnds(
|
||||
String source,
|
||||
TransactionSyncResult result,
|
||||
) async {
|
||||
if (result.transactions.isNotEmpty) {
|
||||
final isSaved = await transactionHistory.saveAndConfirm();
|
||||
|
||||
if (!isSaved) return;
|
||||
}
|
||||
|
||||
await _saveLastSyncedSignature(source, result.newestSignature);
|
||||
}
|
||||
|
||||
Future<void> updateSPLTokenTransactions({List<String>? specificMints}) async {
|
||||
final allTokens = balance.keys.whereType<SPLToken>().toList(growable: false);
|
||||
final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false);
|
||||
|
||||
// Filter to specific mints if provided
|
||||
final tokens = specificMints != null
|
||||
@@ -394,30 +437,28 @@ abstract class SolanaWalletBase
|
||||
i,
|
||||
i + batchSize > tokens.length ? tokens.length : i + batchSize,
|
||||
);
|
||||
final results = await Future.wait(
|
||||
|
||||
await Future.wait(
|
||||
batch.map((token) async {
|
||||
try {
|
||||
return await _client.getSPLTokenTransfers(
|
||||
final result = await _client.getSPLTokenTransfers(
|
||||
mintAddress: token.mintAddress,
|
||||
splToken: token,
|
||||
privateKey: _solanaPrivateKey,
|
||||
untilSignature: await _lastSyncedSignature(token.mintAddress),
|
||||
onUpdate: updateTransactions,
|
||||
);
|
||||
} catch (_) {
|
||||
return <SolanaTransactionModel>[];
|
||||
|
||||
await _updateStateWhenSyncForTheSourceEnds(token.mintAddress, result);
|
||||
} catch (e) {
|
||||
printV('Error fetching spl token (${token.symbol}) transfers ${e.toString()}');
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
for (final list in results) {
|
||||
await addTransactionsToTransactionHistory(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addTransactionsToTransactionHistory(
|
||||
List<SolanaTransactionModel> transactions,
|
||||
) async {
|
||||
void _addTransactions(List<SolanaTransactionModel> transactions) {
|
||||
final Map<String, SolanaTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
@@ -436,6 +477,12 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
|
||||
transactionHistory.addMany(result);
|
||||
}
|
||||
|
||||
Future<void> addTransactionsToTransactionHistory(
|
||||
List<SolanaTransactionModel> transactions,
|
||||
) async {
|
||||
_addTransactions(transactions);
|
||||
|
||||
await transactionHistory.save();
|
||||
}
|
||||
@@ -456,6 +503,17 @@ abstract class SolanaWalletBase
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
// we want to handle the case where multiple refresh triggers (our users can swipe down
|
||||
// multiple times), so we track the currrent refresh and join it instead of starting
|
||||
// another one
|
||||
Future<void> _refresh() {
|
||||
return _currentRefresh ??= Future.wait([
|
||||
updateTokenBalance(),
|
||||
updateTransactionsHistory(),
|
||||
_getEstimatedFees(),
|
||||
]).whenComplete(() => _currentRefresh = null);
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
@@ -469,12 +527,7 @@ abstract class SolanaWalletBase
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
updateTokenBalance(),
|
||||
_updateNativeSOLTransactions(),
|
||||
updateSPLTokenTransactions(),
|
||||
_getEstimatedFees(),
|
||||
]);
|
||||
await _refresh();
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
@@ -626,6 +679,14 @@ abstract class SolanaWalletBase
|
||||
|
||||
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
|
||||
|
||||
SPLToken? splTokenBySymbol(String symbol) {
|
||||
for (final token in splTokensBox.values) {
|
||||
if (token.symbol == symbol) return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
|
||||
|
||||
@@ -802,12 +863,27 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
|
||||
Future<void> deleteSPLToken(SPLToken token) async {
|
||||
final sources = <String>{token.mintAddress};
|
||||
|
||||
if (token.symbol == CryptoCurrency.sol.symbol) {
|
||||
sources.add(_nativeSource);
|
||||
}
|
||||
|
||||
if (splTokensBox.isOpen) {
|
||||
sources.addAll(splTokensBox.values
|
||||
.where((t) => t.symbol == token.symbol)
|
||||
.map((t) => t.mintAddress));
|
||||
|
||||
await splTokensBox.delete(token.mintAddress);
|
||||
}
|
||||
|
||||
balance.remove(token);
|
||||
await _removeTokenTransactionsInHistory(token);
|
||||
|
||||
for (final source in sources) {
|
||||
await _clearLastSyncedSignature(source);
|
||||
}
|
||||
|
||||
updateTokenBalance();
|
||||
}
|
||||
|
||||
@@ -831,11 +907,12 @@ abstract class SolanaWalletBase
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
updateTokenBalance();
|
||||
_updateNativeSOLTransactions();
|
||||
updateSPLTokenTransactions();
|
||||
_getEstimatedFees();
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async {
|
||||
try {
|
||||
await _refresh();
|
||||
} catch (e) {
|
||||
printV('Error on periodic solana refresh: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_solana/solana_mnemonics.dart';
|
||||
import 'package:cw_solana/solana_wallet.dart';
|
||||
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SolanaWalletService extends WalletService<
|
||||
SolanaNewWalletCredentials,
|
||||
@@ -96,6 +97,11 @@ class SolanaWalletService extends WalletService<
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
await WalletInfo.delete(walletInfo);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
for (final key in prefs.getKeys().where(
|
||||
(k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) {
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+7
-4
@@ -421,16 +421,19 @@ Future<void> setup({
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
throw Exception(
|
||||
"This should not have happened, because airgapped Wallets don't need View Models");
|
||||
"This should not have happened, because airgapped Wallets don't need View Models",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
getIt.registerLazySingleton(() => LedgerViewModel());
|
||||
getIt.registerLazySingleton(() => LedgerViewModel(getIt<AppStore>()));
|
||||
|
||||
getIt.registerLazySingleton(() => BitboxViewModel());
|
||||
getIt.registerLazySingleton(BitboxViewModel.new);
|
||||
|
||||
getIt.registerLazySingleton(
|
||||
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"));
|
||||
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
|
||||
|
||||
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
|
||||
|
||||
@@ -166,12 +166,15 @@ class CWEVM extends EVM {
|
||||
amount: amount,
|
||||
fee: fee,
|
||||
tokenSymbol: tokenSymbol,
|
||||
exponent: exponent,
|
||||
direction: direction,
|
||||
isPending: isPending,
|
||||
date: date,
|
||||
confirmations: confirmations,
|
||||
to: to,
|
||||
from: from,
|
||||
evmSignatureName: evmSignatureName,
|
||||
contractAddress: contractAddress,
|
||||
chainId: chainId);
|
||||
|
||||
@override
|
||||
|
||||
@@ -433,6 +433,9 @@ class CWMonero extends Monero {
|
||||
@override
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
|
||||
|
||||
@override
|
||||
bool hasGlobalLedgerConnection() => gLedger != null;
|
||||
|
||||
@override
|
||||
String? getLastLedgerCommand() => latestLedgerCommand;
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
final oldTabLength = tabs.length;
|
||||
final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled);
|
||||
final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
|
||||
final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId);
|
||||
tabs = [
|
||||
@@ -58,13 +59,14 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
AssetsSection(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
hasAssetsButton ?
|
||||
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
() {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: widget.dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
})),
|
||||
}) : null),
|
||||
AssetsHistorySectionTab(
|
||||
S.current.history,
|
||||
HistorySection(
|
||||
|
||||
@@ -75,6 +75,23 @@ void startAuthenticationStateChange(
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ledgerVM.suppressMoneroReconnectPrompt = true;
|
||||
|
||||
if (!monero!.hasGlobalLedgerConnection()) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "Unexpected Error",
|
||||
alertContent:
|
||||
"Unexpected Error while trying to connect to the device (has no global LedgerConnection)",
|
||||
buttonText: S.of(context).try_again,
|
||||
buttonAction: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
@@ -85,14 +102,17 @@ void startAuthenticationStateChange(
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
bool tryOpening = true;
|
||||
|
||||
var isConnected = false;
|
||||
var tryOpening = true;
|
||||
while (tryOpening) {
|
||||
try {
|
||||
await loadCurrentWallet();
|
||||
isConnected = true;
|
||||
tryOpening = false;
|
||||
} on Exception catch (e) {
|
||||
final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString());
|
||||
if (ledgerErrorMessage != null) {
|
||||
if (ledgerErrorMessage != null && context.mounted) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
@@ -116,16 +136,17 @@ void startAuthenticationStateChange(
|
||||
}
|
||||
|
||||
getIt.get<BottomSheetService>().showNext();
|
||||
await navigatorKey.currentState!
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
if (isConnected) {
|
||||
ledgerVM.suppressMoneroReconnectPrompt = false;
|
||||
await navigatorKey.currentState!
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
},
|
||||
allowChangeWallet: true,
|
||||
),
|
||||
);
|
||||
|
||||
// await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice));
|
||||
} else {
|
||||
navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final linkViewModel = getIt.get<LinkViewModel>();
|
||||
|
||||
@@ -122,9 +122,13 @@ class CWSolana extends Solana {
|
||||
return CryptoCurrency.sol;
|
||||
}
|
||||
|
||||
return (wallet as SolanaWallet).splTokenCurrencies.firstWhere(
|
||||
(element) => transaction.amount.currency.symbol == element.symbol,
|
||||
);
|
||||
final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
|
||||
|
||||
if (token == null) {
|
||||
throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -401,6 +405,15 @@ class CWSolana extends Solana {
|
||||
}
|
||||
|
||||
await Future.wait(tokenChecks);
|
||||
|
||||
final discoveredMints = result.newTokens
|
||||
.where((item) => item.token.enabled)
|
||||
.map((item) => item.token.mintAddress)
|
||||
.toList();
|
||||
|
||||
if (discoveredMints.isNotEmpty) {
|
||||
await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/base_page.dart";
|
||||
import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/themes/core/material_base_theme.dart";
|
||||
import "package:cake_wallet/utils/responsive_layout_util.dart";
|
||||
import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
|
||||
|
||||
class ConnectDevicePageParams {
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
ConnectDevicePageParams({
|
||||
const ConnectDevicePageParams({
|
||||
required this.walletType,
|
||||
required this.hardwareWalletType,
|
||||
required this.onConnectDevice,
|
||||
this.allowChangeWallet = false,
|
||||
this.isReconnect = true,
|
||||
});
|
||||
}
|
||||
|
||||
class ConnectDevicePage extends BasePage {
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
}
|
||||
|
||||
class ConnectDevicePage extends BasePage {
|
||||
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
|
||||
: walletType = params.walletType,
|
||||
onConnectDevice = params.onConnectDevice,
|
||||
allowChangeWallet = params.allowChangeWallet,
|
||||
isReconnect = params.isReconnect;
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
|
||||
@override
|
||||
String get title => isReconnect
|
||||
@@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage {
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => PopScope(
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
allowChangeWallet,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
));
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
allowChangeWallet: allowChangeWallet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ConnectDevicePageBody extends StatefulWidget {
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme, {
|
||||
this.allowChangeWallet = false,
|
||||
});
|
||||
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
final MaterialThemeBase currentTheme;
|
||||
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.allowChangeWallet,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme,
|
||||
);
|
||||
|
||||
@override
|
||||
ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState();
|
||||
}
|
||||
@@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
|
||||
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
|
||||
|
||||
late Timer? _usbRefreshTimer = null;
|
||||
late Timer? _bleRefreshTimer = null;
|
||||
late Timer? _bleStateTimer = null;
|
||||
late StreamSubscription<HardwareWalletDevice>? _bleRefresh = null;
|
||||
Timer? _usbRefreshTimer;
|
||||
Timer? _bleRefreshTimer;
|
||||
Timer? _bleStateTimer;
|
||||
StreamSubscription<HardwareWalletDevice>? _bleRefresh;
|
||||
|
||||
bool longWait = false;
|
||||
Timer? _longWaitTimer;
|
||||
@@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_bleStateTimer =
|
||||
Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState());
|
||||
_bleStateTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => widget.hardwareWalletVM.updateBleState(),
|
||||
);
|
||||
|
||||
_bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
_bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
_usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
_longWaitTimer = Timer(Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty)
|
||||
_longWaitTimer = Timer(const Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
|
||||
setState(() => longWait = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
_longWaitTimer?.cancel();
|
||||
|
||||
widget.hardwareWalletVM.stopScanning();
|
||||
_isConnectPressed = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refreshUsbDevices() async {
|
||||
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
|
||||
if (usbDevices.length != dev.length) setState(() => usbDevices = dev);
|
||||
|
||||
if (usbDevices.length != dev.length) {
|
||||
setState(() => usbDevices = dev);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshBleDevices() async {
|
||||
try {
|
||||
if (widget.hardwareWalletVM.isBleEnabled) {
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) longWait = false;
|
||||
}))
|
||||
..onError((Object e) {
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen(
|
||||
(device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) {
|
||||
longWait = false;
|
||||
}
|
||||
}),
|
||||
)..onError((e) {
|
||||
printV(e);
|
||||
});
|
||||
_bleRefreshTimer?.cancel();
|
||||
@@ -160,48 +171,66 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connectToDevice(HardwareWalletDevice device) async {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
var _isConnectPressed = false;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isConnected)
|
||||
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
});
|
||||
Future<void> _connectToDevice(HardwareWalletDevice device) async {
|
||||
if (_isConnectPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnectPressed = true;
|
||||
try {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
_isConnectPressed = false;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isConnected) {
|
||||
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
_isConnectPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
|
||||
printV(deviceType);
|
||||
switch (deviceType) {
|
||||
case HardwareWalletDeviceType.ledgerNanoX:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletDeviceType.ledgerNanoGen5:
|
||||
return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg";
|
||||
case HardwareWalletDeviceType.ledgerStax:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_stax.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_stax.svg";
|
||||
case HardwareWalletDeviceType.ledgerFlex:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_flex.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_flex.svg";
|
||||
case HardwareWalletDeviceType.BitBox02:
|
||||
case HardwareWalletDeviceType.BitBox02Nova:
|
||||
return 'assets/new-ui/hardware_wallets/device_bitbox.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletDeviceType.trezorModelOne:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg";
|
||||
case HardwareWalletDeviceType.trezorModelT:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe3:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe5:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe7:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
|
||||
default:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble;
|
||||
if (!Platform.isAndroid) {
|
||||
return S.of(context).connect_your_hardware_wallet_ble;
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet;
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
return S.of(context).connect_your_hardware_wallet;
|
||||
}
|
||||
|
||||
return S.of(context).connect_your_hardware_wallet_usb;
|
||||
}
|
||||
@@ -210,7 +239,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Container(
|
||||
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -218,9 +247,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/hardware_wallet.svg",
|
||||
),
|
||||
const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
@@ -231,7 +258,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
offstage: widget.hardwareWalletVM.isBleEnabled ||
|
||||
!widget.hardwareWalletVM.hasBluetooth,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).ledger_please_enable_bluetooth,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
@@ -242,67 +269,89 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Text(item.name)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (widget.hardwareWalletVM.isConnecting) {
|
||||
return CupertinoActivityIndicator(
|
||||
animating: true,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
radius: 12,
|
||||
);
|
||||
}
|
||||
|
||||
return CakeImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(item.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
itemCount: allDevices.length),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
itemCount: allDevices.length,
|
||||
),
|
||||
),
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
@@ -310,7 +359,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onChangeWallet,
|
||||
)
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -321,7 +370,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => _onHowToConnect(context),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -339,17 +388,25 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet(
|
||||
builder: (_) => InfoStepsBottomSheet(
|
||||
titleText: S.of(context).how_to_connect,
|
||||
steps: [
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg',
|
||||
S.of(context).connect_hw_info_step_1),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg',
|
||||
S.of(context).connect_hw_info_step_2),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg',
|
||||
S.of(context).connect_hw_info_step_3),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg',
|
||||
S.of(context).connect_hw_info_step_4),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step1_power.svg",
|
||||
S.of(context).connect_hw_info_step_1,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step2_connect.svg",
|
||||
S.of(context).connect_hw_info_step_2,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step3_unlock.svg",
|
||||
S.of(context).connect_hw_info_step_3,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step4_select.svg",
|
||||
S.of(context).connect_hw_info_step_4,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+24
-23
@@ -1,25 +1,25 @@
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/themes/core/theme_store.dart';
|
||||
import 'package:cake_wallet/utils/exception_handler.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cake_wallet/store/wallet_list_store.dart';
|
||||
import 'package:cake_wallet/store/authentication_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import "package:cake_wallet/core/amount_parsing_proxy.dart";
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/preferences_key.dart";
|
||||
import "package:cake_wallet/reactions/wallet_connect.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart";
|
||||
import "package:cake_wallet/store/authentication_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/store/wallet_list_store.dart";
|
||||
import "package:cake_wallet/themes/core/theme_store.dart";
|
||||
import "package:cake_wallet/utils/exception_handler.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
part 'app_store.g.dart';
|
||||
part "app_store.g.dart";
|
||||
|
||||
class AppStore = AppStoreBase with _$AppStore;
|
||||
|
||||
@@ -59,8 +59,9 @@ abstract class AppStoreBase with Store {
|
||||
@action
|
||||
Future<void> changeCurrentWallet(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet) async {
|
||||
bool changingToSameWalletType = this.wallet?.type == wallet.type;
|
||||
this.wallet?.close(shouldCleanup: !changingToSameWalletType);
|
||||
final changingToSameWalletType = this.wallet?.type == wallet.type;
|
||||
|
||||
await this.wallet?.close(shouldCleanup: !changingToSameWalletType);
|
||||
this.wallet = wallet;
|
||||
this.wallet!.setExceptionHandler(ExceptionHandler.onError);
|
||||
|
||||
@@ -80,7 +81,7 @@ abstract class AppStoreBase with Store {
|
||||
wcService.create();
|
||||
await wcService.init();
|
||||
} catch (e, s) {
|
||||
printV('WalletConnect setup failed: $e\n$s');
|
||||
printV("WalletConnect setup failed: $e\n$s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
@@ -498,7 +497,6 @@ abstract class BalanceViewModelBase with Store {
|
||||
final record = wallet.balance[curr]!;
|
||||
final available = record.available - (record.secondAvailable ?? Money.zero(curr));
|
||||
final price = fiatConversionStore.prices[curr] ?? 0;
|
||||
printV(record.available);
|
||||
ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
|
||||
.replaceAll(",", "")) ??
|
||||
0;
|
||||
|
||||
@@ -227,9 +227,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
1;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
confirmations = appStore.wallet!.transactionHistory.transactions.values
|
||||
.map((item) => item.isPending)
|
||||
.fold(0, (val, pending) => pending ? val + 1 : val);
|
||||
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
|
||||
.where((item) => item.isPending)
|
||||
.length;
|
||||
confirmations = pendingCount + 1;
|
||||
}
|
||||
return length * confirmations;
|
||||
}, _transactionDisposerCallback, delay: 300);
|
||||
@@ -1331,9 +1332,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
1;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
confirmations = appStore.wallet!.transactionHistory.transactions.values
|
||||
.map((item) => item.isPending)
|
||||
.fold(0, (val, pending) => pending ? val + 1 : val);
|
||||
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
|
||||
.where((item) => item.isPending)
|
||||
.length;
|
||||
confirmations = pendingCount + 1;
|
||||
}
|
||||
return length * confirmations;
|
||||
}, _transactionDisposerCallback, delay: 300);
|
||||
|
||||
@@ -195,7 +195,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
case WalletType.decred:
|
||||
case WalletType.zcash:
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
case WalletType.ethereum:
|
||||
@@ -206,15 +206,15 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
final asset = assetOfTransaction;
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
case WalletType.solana:
|
||||
final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
|
||||
final asset = assetOfTransaction;
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
@@ -222,7 +222,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
@@ -234,7 +234,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
}
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
|
||||
@@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
|
||||
|
||||
@@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
Future<void> stopScanning() async {}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
|
||||
if (!(device is BitboxHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (isConnecting) return false;
|
||||
isConnecting = true;
|
||||
|
||||
try {
|
||||
final appDocDir = await getAppDir();
|
||||
@@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
|
||||
await bitboxManager.channelHashVerify();
|
||||
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isConnecting = false;
|
||||
|
||||
@override
|
||||
bool isConnected(WalletType type) => false;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cw_core/hardware/hardware_wallet_service.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
abstract class HardwareWalletViewModel {
|
||||
HardwareWalletType get hardwareWalletType;
|
||||
bool get isBleEnabled;
|
||||
bool get hasBluetooth;
|
||||
bool get isConnecting;
|
||||
|
||||
bool isConnected(WalletType type);
|
||||
|
||||
@@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel {
|
||||
Future<void> initWallet(WalletBase wallet);
|
||||
|
||||
String? interpretErrorCode(String error) => null;
|
||||
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/hardware/device_connection_type.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cake_wallet/evm/evm.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/monero/monero.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
|
||||
import "package:cake_wallet/wallet_type_utils.dart";
|
||||
import "package:cw_core/hardware/device_connection_type.dart";
|
||||
import "package:cw_core/hardware/hardware_wallet_service.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:permission_handler/permission_handler.dart";
|
||||
|
||||
part 'ledger_view_model.g.dart';
|
||||
part "ledger_view_model.g.dart";
|
||||
|
||||
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
|
||||
|
||||
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
late final sdk.LedgerInterface ledgerPlusBLE;
|
||||
late final sdk.LedgerInterface ledgerPlusUSB;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
|
||||
.isNotEmpty;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
LedgerViewModelBase() {
|
||||
LedgerViewModelBase(AppStore appStore) {
|
||||
if (_doesSupportHardwareWallets) {
|
||||
reaction((_) => isBleEnabled, (_) {
|
||||
if (isBleEnabled) _initBLE();
|
||||
if (isBleEnabled) {
|
||||
_initBLE();
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => appStore.wallet, (wallet) async {
|
||||
if (wallet != null && wallet.type != _connectingWalletType) {
|
||||
await close();
|
||||
}
|
||||
});
|
||||
|
||||
updateBleState();
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
@@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
}
|
||||
}
|
||||
|
||||
late final sdk.LedgerInterface ledgerPlusBLE;
|
||||
late final sdk.LedgerInterface ledgerPlusUSB;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero,
|
||||
HardwareWalletType.ledger,
|
||||
Platform.isIOS,
|
||||
).isNotEmpty;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
|
||||
|
||||
@@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => true;
|
||||
|
||||
@@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
if (isBleEnabled && !_bleIsInitialized) {
|
||||
ledgerPlusBLE = sdk.LedgerInterface.ble(
|
||||
onPermissionRequest: (_) async {
|
||||
if (Platform.isMacOS) return true;
|
||||
if (Platform.isMacOS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
final statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
Permission.bluetoothAdvertise,
|
||||
@@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
|
||||
return statuses.values.where((status) => status.isDenied).isEmpty;
|
||||
},
|
||||
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
|
||||
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
|
||||
);
|
||||
_bleIsInitialized = true;
|
||||
}
|
||||
@@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
|
||||
final newState = bleState == sdk.AvailabilityState.poweredOn;
|
||||
|
||||
if (newState != isBleEnabled) isBleEnabled = newState;
|
||||
if (newState != isBleEnabled) {
|
||||
isBleEnabled = newState;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<HardwareWalletDevice> scanForBleDevices() =>
|
||||
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
|
||||
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
|
||||
|
||||
@override
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
|
||||
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
|
||||
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
|
||||
|
||||
@override
|
||||
Future<void> stopScanning() async {
|
||||
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
|
||||
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
|
||||
if (_bleIsInitialized) {
|
||||
await ledgerPlusBLE.stopScanning();
|
||||
}
|
||||
if (!Platform.isIOS) {
|
||||
await ledgerPlusUSB.stopScanning();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
|
||||
if (!(device is LedgerHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (device is! LedgerHardwareWalletDevice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
_connectingWalletType = type;
|
||||
if (isConnected(type)) {
|
||||
try {
|
||||
@@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
final ledger =
|
||||
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
|
||||
|
||||
if (_connectionChangeSubscription == null) {
|
||||
_connectionChangeSubscription =
|
||||
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
|
||||
}
|
||||
_connectionChangeSubscription ??=
|
||||
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
|
||||
|
||||
try {
|
||||
_connection = await ledger.connect(device.device);
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
|
||||
sdk.LedgerConnection? _connection;
|
||||
bool _isConnecting = false;
|
||||
WalletType? _connectingWalletType;
|
||||
bool suppressMoneroReconnectPrompt = false;
|
||||
|
||||
void _connectionChangeListener(sdk.BleConnectionState event) {
|
||||
printV('Ledger Device State Changed: $event');
|
||||
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
|
||||
printV("Ledger Device State Changed: $event");
|
||||
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
|
||||
_connection = null;
|
||||
if (_connectingWalletType == WalletType.monero) {
|
||||
monero!.resetLedgerConnection();
|
||||
|
||||
if (suppressMoneroReconnectPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(navigatorKey.currentContext!).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
@@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
hardwareWalletType: HardwareWalletType.ledger,
|
||||
allowChangeWallet: true,
|
||||
isReconnect: true,
|
||||
onConnectDevice: (context, ledgerVM) async {
|
||||
onConnectDevice: (context, ledgerVM) {
|
||||
if (context.mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
|
||||
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
|
||||
|
||||
sdk.LedgerConnection get connection => _connection!;
|
||||
|
||||
@override
|
||||
Future<void> initWallet(WalletBase wallet) async {
|
||||
Future<void> initWallet(WalletBase wallet) {
|
||||
switch (wallet.type) {
|
||||
case WalletType.monero:
|
||||
return monero!.setLedgerConnection(wallet, connection);
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!
|
||||
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!
|
||||
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
|
||||
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
return error;
|
||||
}
|
||||
|
||||
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
|
||||
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
|
||||
|
||||
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
|
||||
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
|
||||
if (errorCode.contains("6985")) {
|
||||
return S.current.ledger_error_tx_rejected_by_user;
|
||||
} else if (errorCode.contains("5515")) {
|
||||
return S.current.ledger_error_device_locked;
|
||||
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
|
||||
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
|
||||
return S.current.ledger_error_wrong_app;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
try {
|
||||
printV("Resetting ledger VM");
|
||||
await _connectionChangeSubscription?.cancel();
|
||||
await _connection?.disconnect().catchError((_) {});
|
||||
|
||||
_connectingWalletType = null;
|
||||
_connectionChangeSubscription = null;
|
||||
_connection = null;
|
||||
isConnecting = false;
|
||||
await stopScanning();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
@@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
if (!Platform.isIOS) await trezorUSB.stopScanning();
|
||||
}
|
||||
|
||||
bool _isConnecting = false;
|
||||
sdk.TrezorClient? _client;
|
||||
|
||||
Completer<String?>? _pinCompleter;
|
||||
@@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
void setParingPin(String pin) => _pinCompleter?.complete(pin);
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
|
||||
[bool isRetry = false]) async {
|
||||
if (!(device is TrezorHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (isConnecting) return false;
|
||||
isConnecting = true;
|
||||
paringState = TrezorParingState.initial;
|
||||
|
||||
try {
|
||||
@@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
printV(e);
|
||||
return false;
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1121,6 +1121,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
|
||||
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
|
||||
if (isEVMWallet) {
|
||||
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
|
||||
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
|
||||
);
|
||||
|
||||
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
|
||||
id: pendingTransaction!.evmTxHashFromRawHex!,
|
||||
height: 0,
|
||||
@@ -1132,6 +1136,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
date: DateTime.now(),
|
||||
confirmations: 0,
|
||||
chainId: wallet.chainId ?? 0,
|
||||
contractAddress: selectedToken?.contractAddress,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store {
|
||||
if (walletItem.type == WalletType.haven) {
|
||||
return;
|
||||
}
|
||||
// bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type;
|
||||
// await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType);
|
||||
|
||||
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
|
||||
await _appStore.changeCurrentWallet(wallet);
|
||||
updateList();
|
||||
|
||||
+4
-10
@@ -125,7 +125,7 @@ dependencies:
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/vespr-wallet/ledger-flutter-plus
|
||||
ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9
|
||||
ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28
|
||||
trezor_connect:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor_connect
|
||||
@@ -201,14 +201,8 @@ dependency_overrides:
|
||||
ffi: 2.1.0
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/cake-tech/ledger-flutter-plus
|
||||
ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2
|
||||
ledger_usb_plus:
|
||||
git:
|
||||
# url: https://github.com/konstantinullrich/ledger-usb-plus
|
||||
# ref: dad482c847c0d0eedceabbb40d3478e64b65e630
|
||||
url: https://github.com/cake-tech/ledger-usb-plus
|
||||
ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890
|
||||
url: https://github.com/vespr-wallet/ledger-flutter-plus
|
||||
ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28
|
||||
socks5_proxy:
|
||||
git:
|
||||
url: https://github.com/LacticWhale/socks_dart.git
|
||||
@@ -231,7 +225,7 @@ dependency_overrides:
|
||||
universal_ble:
|
||||
git:
|
||||
url: https://github.com/cake-tech/universal_ble.git
|
||||
ref: f7044558ad111a156987614971e08c583c80175c
|
||||
ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981
|
||||
|
||||
flutter_icons:
|
||||
image_path: "assets/images/app_logo.png"
|
||||
|
||||
@@ -475,6 +475,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir
|
||||
Future<void> setLedgerConnection(Object wallet, ledger.LedgerConnection connection);
|
||||
void resetLedgerConnection();
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection);
|
||||
bool hasGlobalLedgerConnection();
|
||||
String? getLastLedgerCommand();
|
||||
void setHardwareWalletService(Object wallet, HardwareWalletService service);
|
||||
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection);
|
||||
|
||||
Reference in New Issue
Block a user