Merge branch 'dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	cw_core/lib/format_fixed.dart
#	cw_solana/lib/solana_client.dart
#	lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart
#	lib/di.dart
#	lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart
#	lib/new-ui/widgets/receive_page/receive_top_bar.dart
#	lib/solana/cw_solana.dart
#	lib/src/screens/buy/buy_sell_page.dart
#	lib/src/screens/connect_device/connect_device_page.dart
#	lib/view_model/buy/buy_sell_view_model.dart
#	lib/view_model/hardware_wallet/hardware_wallet_view_model.dart
#	lib/view_model/hardware_wallet/ledger_view_model.dart
#	lib/view_model/send/send_view_model.dart
This commit is contained in:
Robert Malikowski
2026-07-17 13:25:45 +02:00
99 changed files with 5021 additions and 4178 deletions
+107 -107
View File
@@ -6,110 +6,110 @@ defaults:
run:
shell: bash
jobs:
lint:
runs-on: [Linux, amd64, android]
container:
image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly
env:
STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
ANDROID_AVD_HOME: /root/.android/avd
volumes:
- /opt/cw_cache_android/root/.cache:/root/.cache
- /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
- /opt/cw_cache_android/root/.ccache:/root/.ccache
- /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
- /opt/cw_cache_android/root/.gradle/:/root/.gradle
- /opt/cw_cache_android/root/.android/:/root/.android
- /opt/cw_cache_android/root/go/pkg:/root/go/pkg
- /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
- /dev/kvm:/dev/kvm
- /var/run/docker.sock:/var/run/docker.sock
strategy:
matrix:
api-level: [ 29 ]
steps:
- name: Fix github actions messing up $HOME...
run: "echo HOME=/root | sudo tee -a $GITHUB_ENV"
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- name: configure git
run: |
git config --global --add safe.directory '*'
git config --global user.email "ci@cakewallet.com"
git config --global user.name "CakeWallet CI"
- name: Fetch prebuilt Torch
run: |
set -x -e
pushd scripts
wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
rm -rf torch_dart
mkdir torch_dart
pushd torch_dart
tar -xzf ../torch_dart.tar.gz
popd
rm ./torch_dart.tar.gz
popd
- name: Fetch prebuilt Reown
run: |
set -x -e
pushd scripts
# cleaning
rm -rf reown_flutter
rm -f reown_flutter.tar.gz
wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
mkdir reown_flutter
pushd reown_flutter
tar -xzf ../reown_flutter.tar.gz
popd
rm ./reown_flutter.tar.gz
popd
- name: Build Bitbox Flutter
run: |
set -x -e
pushd scripts
./build_bitbox_flutter.sh
popd
- name: Prepare Zcash
run: |
set -x -e
scripts/prepare_zcash.sh
- name: Execute Build and Setup Commands
run: |
pushd scripts/android
source ./app_env.sh cakewallet
export GITHUB_HUH=yeah
./app_config.sh
popd
- name: Install Flutter dependencies
run: |
flutter pub get
- name: Build generated code
run: |
flutter --version
flutter clean
rm -rf .dart_tool
rm pubspec.lock
flutter pub get
./model_generator.sh async
- name: Generate localization
run: |
dart run tool/generate_localization.dart
- name: Lint
run: |
ABORT_ON_CHANGE=yeah scripts/lint.sh
# lint:
# runs-on: [Linux, amd64, android]
# container:
# image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly
# env:
# STORE_PASS: test@cake_wallet
# KEY_PASS: test@cake_wallet
# MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
# BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# ANDROID_AVD_HOME: /root/.android/avd
# volumes:
# - /opt/cw_cache_android/root/.cache:/root/.cache
# - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
# - /opt/cw_cache_android/root/.ccache:/root/.ccache
# - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
# - /opt/cw_cache_android/root/.gradle/:/root/.gradle
# - /opt/cw_cache_android/root/.android/:/root/.android
# - /opt/cw_cache_android/root/go/pkg:/root/go/pkg
# - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
# - /dev/kvm:/dev/kvm
# - /var/run/docker.sock:/var/run/docker.sock
# strategy:
# matrix:
# api-level: [ 29 ]
#
# steps:
# - name: Fix github actions messing up $HOME...
# run: "echo HOME=/root | sudo tee -a $GITHUB_ENV"
# - uses: actions/checkout@v4
# with:
# ref: ${{ inputs.ref }}
#
# - name: configure git
# run: |
# git config --global --add safe.directory '*'
# git config --global user.email "ci@cakewallet.com"
# git config --global user.name "CakeWallet CI"
#
# - name: Fetch prebuilt Torch
# run: |
# set -x -e
# pushd scripts
# wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
# rm -rf torch_dart
# mkdir torch_dart
# pushd torch_dart
# tar -xzf ../torch_dart.tar.gz
# popd
# rm ./torch_dart.tar.gz
# popd
#
# - name: Fetch prebuilt Reown
# run: |
# set -x -e
# pushd scripts
# # cleaning
# rm -rf reown_flutter
# rm -f reown_flutter.tar.gz
#
# wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
# mkdir reown_flutter
# pushd reown_flutter
# tar -xzf ../reown_flutter.tar.gz
# popd
# rm ./reown_flutter.tar.gz
# popd
#
# - name: Build Bitbox Flutter
# run: |
# set -x -e
# pushd scripts
# ./build_bitbox_flutter.sh
# popd
#
# - name: Prepare Zcash
# run: |
# set -x -e
# scripts/prepare_zcash.sh
#
# - name: Execute Build and Setup Commands
# run: |
# pushd scripts/android
# source ./app_env.sh cakewallet
# export GITHUB_HUH=yeah
# ./app_config.sh
# popd
#
# - name: Install Flutter dependencies
# run: |
# flutter pub get
#
# - name: Build generated code
# run: |
# flutter --version
# flutter clean
# rm -rf .dart_tool
# rm pubspec.lock
# flutter pub get
# ./model_generator.sh async
#
# - name: Generate localization
# run: |
# dart run tool/generate_localization.dart
#
# - name: Lint
# run: |
# ABORT_ON_CHANGE=yeah scripts/lint.sh
+40
View File
@@ -0,0 +1,40 @@
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/currency.dart';
class ExchangeRate {
/// The currency being priced (e.g. BTC in a BTC/USD pair).
final Currency base;
/// The price of one whole unit of [base], in the quote currency
/// (e.g. 45000 USD in a BTC/USD pair).
final Money quote;
const ExchangeRate({required this.base, required this.quote});
/// Converts [amount] between the base and quote currencies.
///
/// Results are truncated toward zero.
///
/// Throws an [ArgumentError] if [amount]'s currency is not part of
/// this pair.
Money convert(Money amount) {
if (base == quote.currency && base == amount.currency) return amount;
final scale = BigInt.from(10).pow(base.decimals);
if (amount.currency == base) {
if (quote.isZero) return Money.zero(quote.currency);
return Money(amount.amount * quote.amount ~/ scale, quote.currency);
}
if (amount.currency == quote.currency) {
if (quote.isZero) return Money.zero(base);
return Money(amount.amount * scale ~/ quote.amount, base);
}
throw ArgumentError(
"Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair");
}
}
+4
View File
@@ -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 -6
View File
@@ -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";
+6 -1
View File
@@ -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);
+147
View File
@@ -0,0 +1,147 @@
import 'package:cw_core/amount/exchange_rate.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ExchangeRate', () {
final rate = ExchangeRate(
base: CryptoCurrency.btc,
quote: Money.parse("60000", EUR),
);
test('convert base currency to quote currency: 1 BTC to 60.000 EUR', () {
final result = rate.convert(Money.parse('1', CryptoCurrency.btc));
expect(result, Money.parse("60000.00", EUR));
});
test('convert base currency to quote currency: 0.5 BTC to 30.000 EUR', () {
final result = rate.convert(Money.parse('0.5', CryptoCurrency.btc));
expect(result, Money.parse("30000.00", EUR));
});
test('convert quote currency to base currency: 60.000 EUR to 1 BTC', () {
final result = rate.convert(Money.parse("60000.00", EUR));
expect(result, Money.parse('1', CryptoCurrency.btc));
});
test('convert quote currency to base currency: 30 EUR to 0.0005 BTC', () {
final result = rate.convert(Money.parse("30.00", EUR));
expect(result, Money.parse('0.0005', CryptoCurrency.btc));
});
});
group('Edge Cases', () {
final rate = ExchangeRate(
base: CryptoCurrency.btc,
quote: Money.parse("60000", EUR),
);
test('Converting zero turns into zero', () {
expect(rate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR));
expect(rate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc));
});
test('truncate to smallest unit', () {
final result = rate.convert(Money(BigInt.one, CryptoCurrency.btc)); // 1 sat
expect(result, Money.zero(EUR));
});
test('Truncate base unit of base currency', () {
final result = rate.convert(Money(BigInt.one, EUR));
expect(result, Money.fromInt(16, CryptoCurrency.btc));
});
test('No int overflow with large numbers', () {
final allBtc = Money.parse('21000000', CryptoCurrency.btc);
final result = rate.convert(allBtc);
expect(result, Money(BigInt.from(126000000000000), EUR));
});
test('truncation for negative amounts', () {
final result = rate.convert(Money.fromInt(-1, EUR));
expect(result, Money.fromInt(-16, CryptoCurrency.btc));
});
test('Base currency without decimals places: JPY', () {
final rate = ExchangeRate(
base: JPY,
quote: Money.fromInt(1, EUR),
);
expect(rate.convert(Money.fromInt(500, JPY)), Money.fromInt(500, EUR));
expect(rate.convert(Money.fromInt(500, EUR)), Money.fromInt(500, JPY));
});
test('Base currency with 18 decimals places: ETH', () {
final rate = ExchangeRate(
base: CryptoCurrency.eth,
quote: Money.fromInt(300000, EUR),
);
expect(rate.convert(Money.parse('1', CryptoCurrency.eth)), Money.fromInt(300000, EUR));
expect(rate.convert(Money.fromInt(3000, EUR)), Money.parse('0.01', CryptoCurrency.eth));
});
test('Smalles base unit is less than smalles base unit of quote', () {
final rate = ExchangeRate(
base: CryptoCurrency.eth,
quote: Money.fromInt(300000, EUR),
);
expect(rate.convert(Money(BigInt.one, CryptoCurrency.eth)), Money(BigInt.zero, EUR));
});
test('Cannot convert currency outside of pair', () {
expect(() => rate.convert(Money.parse("60000", USD)), throwsArgumentError);
});
test('handle quote being 0 correctly', () {
final zeroRate = ExchangeRate(
base: CryptoCurrency.btc,
quote: Money(BigInt.zero, EUR),
);
expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR));
expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc));
});
});
}
class FiatCurrency implements Currency {
const FiatCurrency({
required this.symbol,
required this.countryCode,
required this.fullName,
this.decimals = 2,
});
final String countryCode;
@override
final String fullName;
@override
final int decimals;
@override
final String symbol;
@override
String? get iconPath => throw UnimplementedError();
@override
String get name => throw UnimplementedError();
@override
String? get tag => throw UnimplementedError();
@override
Money parseAmount(String value) => Money.parse(value, this);
@override
Money? tryParseAmount(String value) => Money.tryParse(value, this);
}
const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar");
const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0);
@@ -51,8 +51,7 @@ class _Visitor extends SimpleAstVisitor<void> {
if (uriString.startsWith("package:http")) {
final prefixNode = node.prefix;
if (prefixNode != null &&
prefixNode.name == "very_insecure_http_do_not_use") {
if (prefixNode != null && prefixNode.name == "very_insecure_http_do_not_use") {
return;
}
@@ -17,8 +17,7 @@ class ReplaceWithPrintV extends ResolvedCorrectionProducer {
);
@override
CorrectionApplicability get applicability =>
CorrectionApplicability.singleLocation;
CorrectionApplicability get applicability => CorrectionApplicability.singleLocation;
@override
FixKind get fixKind => _fixKind;
@@ -1,11 +1,9 @@
import 'dart:async';
import 'package:flutter/services.dart';
class CwSharedExternal {
static const MethodChannel _channel =
const MethodChannel('cw_shared_external');
static const MethodChannel _channel = const MethodChannel('cw_shared_external');
static Future<String> get platformVersion async {
final String version = (await _channel.invokeMethod('getPlatformVersion')).toString();
+104 -42
View File
@@ -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,8 +640,6 @@ 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);
incomingTo = walletAddress;
@@ -951,27 +964,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 +1038,7 @@ class SolanaWalletClient {
),
);
} catch (e) {
hasFailures = true;
return null;
}
}));
@@ -999,16 +1053,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 +1070,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 +1124,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 +1201,7 @@ class SolanaWalletClient {
}
}
void stop() {}
void stop() => _isStopped = true;
SolanaRPC? get getSolanaProvider => _provider;
+29 -11
View File
@@ -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
+106 -29
View File
@@ -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');
}
});
}
+6
View File
@@ -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
+3 -4
View File
@@ -13,7 +13,6 @@ wownero.WalletListener getWlptr() {
return _wlptr!;
}
wownero.SubaddressAccount? subaddressAccount;
bool isUpdating = false;
@@ -69,6 +68,6 @@ Future<void> addAccount({required String label}) async {
}
Future<void> setLabelForAccount({required int accountIndex, required String label}) async {
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
await store();
}
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
await store();
}
@@ -2,4 +2,4 @@ class ConnectionToNodeException implements Exception {
ConnectionToNodeException({required this.message});
final String message;
}
}
@@ -1,8 +1,8 @@
class CreationTransactionException implements Exception {
CreationTransactionException({required this.message});
final String message;
@override
String toString() => message;
}
}
@@ -1,8 +1,8 @@
class SetupWalletException implements Exception {
SetupWalletException({required this.message});
final String message;
@override
String toString() => message;
}
}
@@ -5,4 +5,4 @@ class WalletCreationException implements Exception {
@override
String toString() => message;
}
}
@@ -5,4 +5,4 @@ class WalletOpeningException implements Exception {
@override
String toString() => message;
}
}
@@ -1,8 +1,8 @@
class WalletRestoreFromKeysException implements Exception {
WalletRestoreFromKeysException({required this.message});
final String message;
@override
String toString() => message;
}
}
@@ -1,5 +1,5 @@
class WalletRestoreFromSeedException implements Exception {
WalletRestoreFromSeedException({required this.message});
final String message;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:ffi/ffi.dart';
class AccountRow extends Struct {
@Int64()
external int id;
external Pointer<Utf8> label;
String getLabel() => label.toDartString();
@@ -1,12 +1,11 @@
class PendingTransactionDescription {
PendingTransactionDescription({
required this.amount,
required this.fee,
required this.hash,
required this.hex,
required this.txKey,
required this.pointerAddress});
PendingTransactionDescription(
{required this.amount,
required this.fee,
required this.hash,
required this.hex,
required this.txKey,
required this.pointerAddress});
final int amount;
final int fee;
@@ -14,4 +13,4 @@ class PendingTransactionDescription {
final String hex;
final String txKey;
final int pointerAddress;
}
}
@@ -4,12 +4,12 @@ import 'package:ffi/ffi.dart';
class SubaddressRow extends Struct {
@Int64()
external int id;
external Pointer<Utf8> address;
external Pointer<Utf8> label;
String getLabel() => label.toDartString();
String getAddress() => address.toDartString();
int getId() => id;
}
}
+15 -13
View File
@@ -38,7 +38,8 @@ class Subaddress {
);
final int addressIndex;
final int accountIndex;
String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
String get label => wownero.Wallet_getSubaddressLabel(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex);
final int txCount;
final int received;
}
@@ -68,7 +69,10 @@ List<Subaddress> getAllSubaddresses() {
final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(",");
final account = wownero.TransactionInfo_subaddrAccount(tx);
newttDetails.add(TinyTransactionDetails(
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
address: List.generate(
subaddrs.length,
(index) => getAddress(
accountIndex: account, addressIndex: int.tryParse(subaddrs[index]) ?? 0)),
amount: wownero.TransactionInfo_amount(tx),
));
}
@@ -79,7 +83,7 @@ List<Subaddress> getAllSubaddresses() {
final list = List.generate(size, (index) {
final ttDetailsLocal = ttDetails.where((element) {
final address = getAddress(
accountIndex: subaddress!.accountIndex,
accountIndex: subaddress!.accountIndex,
addressIndex: index,
);
if (address == element.address) return true;
@@ -97,7 +101,8 @@ List<Subaddress> getAllSubaddresses() {
);
}).reversed.toList();
if (list.isEmpty) {
list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
list.add(Subaddress(
addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
}
return list;
}
@@ -113,7 +118,8 @@ int numSubaddresses(int subaccountIndex) {
void setLabelForSubaddressSync(
{required int accountIndex, required int addressIndex, required String label}) {
wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label);
wownero.Wallet_setSubaddressLabel(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
}
void _addSubaddress(Map<String, dynamic> args) {
@@ -128,8 +134,7 @@ void _setLabelForSubaddress(Map<String, dynamic> args) {
final accountIndex = args['accountIndex'] as int;
final addressIndex = args['addressIndex'] as int;
setLabelForSubaddressSync(
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
setLabelForSubaddressSync(accountIndex: accountIndex, addressIndex: addressIndex, label: label);
}
Future<void> addSubaddress({required int accountIndex, required String label}) async {
@@ -138,11 +143,8 @@ Future<void> addSubaddress({required int accountIndex, required String label}) a
}
Future<void> setLabelForSubaddress(
{required int accountIndex, required int addressIndex, required String label}) async {
_setLabelForSubaddress({
'accountIndex': accountIndex,
'addressIndex': addressIndex,
'label': label
});
{required int accountIndex, required int addressIndex, required String label}) async {
_setLabelForSubaddress(
{'accountIndex': accountIndex, 'addressIndex': addressIndex, 'label': label});
await store();
}
+72 -66
View File
@@ -43,33 +43,36 @@ Future<List<Transaction>> getAllTransactions() async {
await txHistoryMutex.acquire();
txhistory ??= wownero.Wallet_history(wptr!);
int size = countOfTransactions();
final list = List.generate(size, (index) => Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index)));
final list = List.generate(
size,
(index) =>
Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index)));
txHistoryMutex.release();
final accts = wownero.Wallet_numSubaddressAccounts(wptr!);
for (var i = 0; i < accts; i++) {
for (var i = 0; i < accts; i++) {
final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i);
final availBalance = wownero.Wallet_unlockedBalance(wptr!, accountIndex: i);
if (fullBalance > availBalance) {
if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isEmpty) {
dummyTxs.add(
Transaction.dummy(
displayLabel: "",
description: "",
fee: 0,
confirmations: 0,
blockheight: 0,
accountIndex: i,
addressIndex: 0,
addressIndexList: [0],
paymentId: "",
amount: fullBalance - availBalance,
isSpend: false,
hash: "pending",
key: "pending",
txInfo: Pointer.fromAddress(0),
)..timeStamp = DateTime.now()
);
if (list
.where((element) => element.accountIndex == i && element.isConfirmed == false)
.isEmpty) {
dummyTxs.add(Transaction.dummy(
displayLabel: "",
description: "",
fee: 0,
confirmations: 0,
blockheight: 0,
accountIndex: i,
addressIndex: 0,
addressIndexList: [0],
paymentId: "",
amount: fullBalance - availBalance,
isSpend: false,
hash: "pending",
key: "pending",
txInfo: Pointer.fromAddress(0),
)..timeStamp = DateTime.now());
}
}
}
@@ -89,13 +92,13 @@ Future<PendingTransactionDescription> createTransactionSync(
String? amount,
int accountIndex = 0,
List<String> preferredInputs = const []}) async {
final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount);
final address_ = address.toNativeUtf8();
final address_ = address.toNativeUtf8();
final paymentId_ = paymentId.toNativeUtf8();
if (preferredInputs.isEmpty) {
throw WowneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
throw WowneroTransactionCreationException(
"No inputs provided, transaction cannot be constructed");
}
final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8();
@@ -105,7 +108,8 @@ Future<PendingTransactionDescription> createTransactionSync(
final preferredInputsAddr = preferredInputs_.address;
final spaddr = wownero.defaultSeparator.address;
final pendingTx = Pointer<Void>.fromAddress(await Isolate.run(() {
final tx = wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction(
final tx =
wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction(
Pointer.fromAddress(waddr),
Pointer.fromAddress(addraddr).cast(),
Pointer.fromAddress(paymentIdAddr).cast(),
@@ -140,13 +144,13 @@ Future<PendingTransactionDescription> createTransactionSync(
final rTxKey = rHash;
return PendingTransactionDescription(
amount: rAmt,
fee: rFee,
hash: rHash,
hex: '',
txKey: rTxKey,
pointerAddress: pendingTx.address,
);
amount: rAmt,
fee: rFee,
hash: rHash,
hex: '',
txKey: rTxKey,
pointerAddress: pendingTx.address,
);
}
PendingTransactionDescription createTransactionMultDestSync(
@@ -155,7 +159,6 @@ PendingTransactionDescription createTransactionMultDestSync(
required int priorityRaw,
int accountIndex = 0,
List<String> preferredInputs = const []}) {
final txptr = wownero.Wallet_createTransactionMultDest(
wptr!,
dstAddr: outputs.map((e) => e.address).toList(),
@@ -182,8 +185,8 @@ void commitTransactionFromPointerAddress({required int address}) =>
commitTransaction(transactionPointer: wownero.PendingTransaction.fromAddress(address));
void commitTransaction({required wownero.PendingTransaction transactionPointer}) {
final txCommit = wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
final txCommit =
wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
String? error = (() {
final status = wownero.PendingTransaction_status(transactionPointer.cast());
@@ -200,9 +203,8 @@ void commitTransaction({required wownero.PendingTransaction transactionPointer})
}
return wownero.Wallet_errorString(wptr!);
})();
}
if (error != null) {
throw CreationTransactionException(message: error);
}
@@ -270,19 +272,20 @@ Future<PendingTransactionDescription> createTransactionMultDest(
'preferredInputs': preferredInputs
});
class Transaction {
final String displayLabel;
late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex);
late final String address = getAddress(
accountIndex: accountIndex,
addressIndex: addressIndex,
);
late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
getAddress(
accountIndex: accountIndex,
addressIndex: addressIndexList[index],
));
late final List<String> addressList = List.generate(
addressIndexList.length,
(index) => getAddress(
accountIndex: accountIndex,
addressIndex: addressIndexList[index],
));
final String description;
final int fee;
final int confirmations;
@@ -331,33 +334,36 @@ class Transaction {
timeStamp = DateTime.fromMillisecondsSinceEpoch(
wownero.TransactionInfo_timestamp(txInfo) * 1000,
),
isSpend = wownero.TransactionInfo_direction(txInfo) ==
wownero.TransactionInfo_Direction.Out,
isSpend =
wownero.TransactionInfo_direction(txInfo) == wownero.TransactionInfo_Direction.Out,
amount = wownero.TransactionInfo_amount(txInfo),
paymentId = wownero.TransactionInfo_paymentId(txInfo),
accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo),
addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
addressIndex =
int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo)
.split(", ")
.map((e) => int.tryParse(e) ?? 0)
.toList(),
blockheight = wownero.TransactionInfo_blockHeight(txInfo),
confirmations = wownero.TransactionInfo_confirmations(txInfo),
fee = wownero.TransactionInfo_fee(txInfo),
description = wownero.TransactionInfo_description(txInfo),
key = wownero.Wallet_getTxKey(wptr!, txid: wownero.TransactionInfo_hash(txInfo));
Transaction.dummy({
required this.displayLabel,
required this.description,
required this.fee,
required this.confirmations,
required this.blockheight,
required this.accountIndex,
required this.addressIndex,
required this.addressIndexList,
required this.paymentId,
required this.amount,
required this.isSpend,
required this.hash,
required this.key,
required this.txInfo
});
Transaction.dummy(
{required this.displayLabel,
required this.description,
required this.fee,
required this.confirmations,
required this.blockheight,
required this.accountIndex,
required this.addressIndex,
required this.addressIndexList,
required this.paymentId,
required this.amount,
required this.isSpend,
required this.hash,
required this.key,
required this.txInfo});
}
+30 -20
View File
@@ -34,8 +34,7 @@ String getFilename() => wownero.Wallet_filename(wptr!);
String getSeed() {
// monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
final cakepolyseed =
wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
final cakepolyseed = wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
final cakepassphrase = getPassphrase();
final weirdPolyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase);
@@ -65,11 +64,21 @@ String getSeedLegacy(String? language) {
final cakepassphrase = getPassphrase();
var legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
switch (language) {
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
case "Korean": language = "English"; break;
case "Czech": language = "English"; break;
case "Japanese": language = "English"; break;
case "Chinese (Traditional)":
language = "Chinese (simplified)";
break;
case "Chinese (Simplified)":
language = "Chinese (simplified)";
break;
case "Korean":
language = "English";
break;
case "Czech":
language = "English";
break;
case "Japanese":
language = "English";
break;
}
if (wownero.Wallet_status(wptr!) != 0) {
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
@@ -84,6 +93,7 @@ String getSeedLegacy(String? language) {
}
return legacy;
}
Map<int, Map<int, Map<int, String>>> addressCache = {};
String getPassphrase() {
@@ -91,14 +101,14 @@ String getPassphrase() {
}
String getAddress({int accountIndex = 0, int addressIndex = 1}) {
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex) - 1 < addressIndex) {
printV("adding subaddress");
wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
}
addressCache[wptr!.address] ??= {};
addressCache[wptr!.address]![accountIndex] ??= {};
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!,
accountIndex: accountIndex, addressIndex: addressIndex);
addressCache[wptr!.address]![accountIndex]![addressIndex] ??=
wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
}
@@ -106,10 +116,12 @@ int getFullBalance({int accountIndex = 0}) {
if (wptr == null) return 0;
return wownero.Wallet_balance(wptr!, accountIndex: accountIndex);
}
int getUnlockedBalance({int accountIndex = 0}) {
if (wptr == null) return 0;
return wownero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex);
}
int getCurrentHeight() {
if (wptr == null) return 0;
return wownero.Wallet_blockChainHeight(wptr!);
@@ -181,8 +193,7 @@ Future<bool> connectToNode() async {
}
void setRefreshFromBlockHeight({required int height}) =>
wownero.Wallet_setRefreshFromBlockHeight(wptr!,
refresh_from_block_height: height);
wownero.Wallet_setRefreshFromBlockHeight(wptr!, refresh_from_block_height: height);
void setRecoveringFromSeed({required bool isRecovery}) =>
wownero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
@@ -257,8 +268,7 @@ class SyncListener {
_cachedBlockchainHeight = 0;
_lastKnownBlockHeight = 0;
_initialSyncHeight = 0;
_updateSyncInfoTimer ??=
Timer.periodic(Duration(milliseconds: 1200), (_) async {
_updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
if (isNewTransactionExist()) {
onNewTransaction();
}
@@ -297,8 +307,8 @@ class SyncListener {
void stop() => _updateSyncInfoTimer?.cancel();
}
SyncListener setListeners(void Function(int, int, double) onNewBlock,
void Function() onNewTransaction) {
SyncListener setListeners(
void Function(int, int, double) onNewBlock, void Function() onNewTransaction) {
final listener = SyncListener(onNewBlock, onNewTransaction);
// setListenerNative();
return listener;
@@ -360,8 +370,7 @@ String getSubaddressLabel(int accountIndex, int addressIndex) {
accountIndex: accountIndex, addressIndex: addressIndex);
}
Future setTrustedDaemon(bool trusted) async =>
wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted);
Future setTrustedDaemon(bool trusted) async => wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted);
Future<bool> trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!);
@@ -370,7 +379,8 @@ String signMessage(String message, {String address = ""}) {
}
bool verifyMessage(String message, String address, String signature) {
return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
return wownero.Wallet_verifySignedMessage(wptr!,
message: message, address: address, signature: signature);
}
Map<String, List<int>> debugCallLength() => wownero.debugCallLength;
Map<String, List<int>> debugCallLength() => wownero.debugCallLength;
+39 -44
View File
@@ -34,15 +34,18 @@ void checkIfMoneroCIsFine() {
final dartCsExp = wownero.wallet2_api_c_exp_sha256;
if (cppCsCpp != dartCsCpp) {
throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'");
throw MoneroCException(
"monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'");
}
if (cppCsH != dartCsH) {
throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'");
throw MoneroCException(
"monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'");
}
if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) {
throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
throw MoneroCException(
"monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
}
}
@@ -133,10 +136,10 @@ void restoreWalletFromSeedSync(
}
wptr = newWptr;
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
openedWalletsByPath[path] = wptr!;
store();
@@ -153,29 +156,27 @@ void restoreWalletFromKeysSync(
int restoreHeight = 0}) {
txhistory = null;
var newWptr = (spendKey != "")
? wownero.WalletManager_createDeterministicWalletFromSpendKey(
wmPtr,
path: path,
password: password,
language: language,
spendKeyString: spendKey,
newWallet: true, // TODO(mrcyjanek): safe to remove
restoreHeight: restoreHeight)
: wownero.WalletManager_createWalletFromKeys(
wmPtr,
path: path,
password: password,
restoreHeight: restoreHeight,
addressString: address,
viewKeyString: viewKey,
spendKeyString: spendKey,
nettype: 0,
);
? wownero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr,
path: path,
password: password,
language: language,
spendKeyString: spendKey,
newWallet: true, // TODO(mrcyjanek): safe to remove
restoreHeight: restoreHeight)
: wownero.WalletManager_createWalletFromKeys(
wmPtr,
path: path,
password: password,
restoreHeight: restoreHeight,
addressString: address,
viewKeyString: viewKey,
spendKeyString: spendKey,
nettype: 0,
);
final status = wownero.Wallet_status(newWptr);
if (status != 0) {
throw WalletRestoreFromKeysException(
message: wownero.Wallet_errorString(newWptr));
throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr));
}
// CW-712 - Try to restore deterministic wallet first, if the view key doesn't
// match the view key provided
@@ -184,7 +185,7 @@ void restoreWalletFromKeysSync(
if (viewKey != viewKeyRestored && viewKey != "") {
wownero.WalletManager_closeWallet(wmPtr, newWptr, false);
File(path).deleteSync();
File(path+".keys").deleteSync();
File(path + ".keys").deleteSync();
newWptr = wownero.WalletManager_createWalletFromKeys(
wmPtr,
path: path,
@@ -197,8 +198,7 @@ void restoreWalletFromKeysSync(
);
final status = wownero.Wallet_status(newWptr);
if (status != 0) {
throw WalletRestoreFromKeysException(
message: wownero.Wallet_errorString(newWptr));
throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr));
}
}
}
@@ -207,8 +207,6 @@ void restoreWalletFromKeysSync(
openedWalletsByPath[path] = wptr!;
}
// English only, because normalization.
void restoreWalletFromPolyseedWithOffset(
{required String path,
@@ -217,7 +215,6 @@ void restoreWalletFromPolyseedWithOffset(
required String seedOffset,
required String language,
int nettype = 0}) {
txhistory = null;
final newWptr = wownero.WalletManager_createWalletFromPolyseed(
wmPtr,
@@ -332,8 +329,7 @@ String _lastOpenedWallet = "";
Map<String, wownero.wallet> openedWalletsByPath = {};
void loadWallet(
{required String path, required String password, int nettype = 0}) {
void loadWallet({required String path, required String password, int nettype = 0}) {
if (openedWalletsByPath[path] != null) {
txhistory = null;
wptr = openedWalletsByPath[path]!;
@@ -347,8 +343,7 @@ void loadWallet(
});
}
txhistory = null;
final newWptr = wownero.WalletManager_openWallet(wmPtr,
path: path, password: password);
final newWptr = wownero.WalletManager_openWallet(wmPtr, path: path, password: password);
_lastOpenedWallet = path;
final status = wownero.Wallet_status(newWptr);
@@ -379,7 +374,11 @@ void _restoreFromSeed(Map<String, dynamic> args) {
final restoreHeight = args['restoreHeight'] as int;
restoreWalletFromSeedSync(
path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight);
path: path,
password: password,
passphrase: passphrase,
seed: seed,
restoreHeight: restoreHeight);
}
void _restoreFromKeys(Map<String, dynamic> args) {
@@ -418,19 +417,15 @@ void _restoreFromSpendKey(Map<String, dynamic> args) {
spendKey: spendKey);
}
Future<void> _openWallet(Map<String, String> args) async => loadWallet(
path: args['path'] as String, password: args['password'] as String);
Future<void> _openWallet(Map<String, String> args) async =>
loadWallet(path: args['path'] as String, password: args['password'] as String);
Future<bool> _isWalletExist(String path) async => isWalletExistSync(path: path);
void openWallet(
{required String path,
required String password,
int nettype = 0}) async =>
void openWallet({required String path, required String password, int nettype = 0}) async =>
loadWallet(path: path, password: password, nettype: nettype);
Future<void> openWalletAsync(Map<String, String> args) async =>
_openWallet(args);
Future<void> openWalletAsync(Map<String, String> args) async => _openWallet(args);
Future<void> createWallet(
{required String path,
+1 -1
View File
@@ -3,4 +3,4 @@ class WowneroOutput {
final String address;
final String amount;
}
}
-1
View File
@@ -1,4 +1,3 @@
import 'cw_wownero_platform_interface.dart';
class CwWownero {
@@ -5,4 +5,4 @@ class WowneroTransactionCreationException implements Exception {
@override
String toString() => message;
}
}
@@ -4,5 +4,6 @@ class WowneroTransactionNoInputsException implements Exception {
int inputsSize;
@override
String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control';
String toString() =>
'Not enough inputs ($inputsSize) selected. Please select more under Coin Control';
}
@@ -1627,4 +1627,4 @@ class ChineseSimplifiedMnemonics {
"浙",
"貌"
];
}
}
+1 -1
View File
@@ -1627,4 +1627,4 @@ class DutchMnemonics {
"zwijmel",
"zworen"
];
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1627,4 +1627,4 @@ class GermanMnemonics {
"Zweck",
"Zyklop"
];
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1627,4 +1627,4 @@ class JapaneseMnemonics {
"びじゅつかん",
"ひしょ"
];
}
}
+1 -1
View File
@@ -1627,4 +1627,4 @@ class PortugueseMnemonics {
"zenite",
"zumbi"
];
}
}
+1 -1
View File
@@ -1627,4 +1627,4 @@ class RussianMnemonics {
"ячейка",
"ящик"
];
}
}
+1 -1
View File
@@ -1627,4 +1627,4 @@ class SpanishMnemonics {
"ritmo",
"rito"
];
}
}
+3 -6
View File
@@ -31,12 +31,9 @@ String mnemonicDecode(String seed) {
wlist.removeLast();
for (var i = 0; i < wlist.length; i += 3) {
final w1 =
tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength));
final w2 = tructWords(englistWordSet)
.indexOf(wlist[i + 1].substring(0, prefixLength));
final w3 = tructWords(englistWordSet)
.indexOf(wlist[i + 2].substring(0, prefixLength));
final w1 = tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength));
final w2 = tructWords(englistWordSet).indexOf(wlist[i + 1].substring(0, prefixLength));
final w3 = tructWords(englistWordSet).indexOf(wlist[i + 2].substring(0, prefixLength));
if (w1 == -1 || w2 == -1 || w3 == -1) {
printV("invalid word in mnemonic");
@@ -48,7 +48,7 @@ class PendingWowneroTransaction with PendingTransaction {
rethrow;
}
}
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
+20 -27
View File
@@ -56,14 +56,13 @@ abstract class WowneroSubaddressListBase with Store {
final address = s.address;
final label = s.label;
final id = s.addressIndex;
final hasDefaultAddressName =
label.toLowerCase() == 'Primary account'.toLowerCase() ||
label.toLowerCase() == 'Untitled account'.toLowerCase();
final hasDefaultAddressName = label.toLowerCase() == 'Primary account'.toLowerCase() ||
label.toLowerCase() == 'Untitled account'.toLowerCase();
final isPrimaryAddress = id == 0 && hasDefaultAddressName;
return Subaddress(
id: id,
address: address,
balance: (s.received/1e12).toStringAsFixed(6),
balance: (s.received / 1e12).toStringAsFixed(6),
txCount: s.txCount,
label: isPrimaryAddress
? 'Primary address'
@@ -139,33 +138,27 @@ abstract class WowneroSubaddressListBase with Store {
}
}
return allAddresses
.map((s) {
final id = s.addressIndex;
final address = s.address;
final label = s.label;
return Subaddress(
id: id,
address: address,
balance: (s.received/1e12).toStringAsFixed(6),
txCount: s.txCount,
label: id == 0 &&
label.toLowerCase() == 'Primary account'.toLowerCase()
? 'Primary address'
: label);
})
.toList();
return allAddresses.map((s) {
final id = s.addressIndex;
final address = s.address;
final label = s.label;
return Subaddress(
id: id,
address: address,
balance: (s.received / 1e12).toStringAsFixed(6),
txCount: s.txCount,
label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase()
? 'Primary address'
: label);
}).toList();
}
Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
return subaddress_list
.getAllSubaddresses()
.where((s) {
final address = s.address;
return !_usedAddresses.contains(address);
})
.isNotEmpty;
return subaddress_list.getAllSubaddresses().where((s) {
final address = s.address;
return !_usedAddresses.contains(address);
}).isNotEmpty;
}
}
@@ -5,11 +5,10 @@ import 'package:cw_wownero/wownero_transaction_info.dart';
part 'wownero_transaction_history.g.dart';
class WowneroTransactionHistory = WowneroTransactionHistoryBase
with _$WowneroTransactionHistory;
class WowneroTransactionHistory = WowneroTransactionHistoryBase with _$WowneroTransactionHistory;
abstract class WowneroTransactionHistoryBase
extends TransactionHistoryBase<WowneroTransactionInfo> with Store {
abstract class WowneroTransactionHistoryBase extends TransactionHistoryBase<WowneroTransactionInfo>
with Store {
WowneroTransactionHistoryBase() {
transactions = ObservableMap<String, WowneroTransactionInfo>();
}
@@ -18,11 +17,9 @@ abstract class WowneroTransactionHistoryBase
Future<void> save() async {}
@override
void addOne(WowneroTransactionInfo transaction) =>
transactions[transaction.id] = transaction;
void addOne(WowneroTransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, WowneroTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
}
+3 -4
View File
@@ -8,10 +8,9 @@ import 'package:cw_core/format_amount.dart';
import 'package:cw_wownero/api/transaction_history.dart';
class WowneroTransactionInfo extends TransactionInfo {
WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date,
this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee,
this.confirmations) :
id = "${txHash}_${amount}_${accountIndex}_${addressIndex}";
WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date, this.isPending,
this.amount, this.accountIndex, this.addressIndex, this.fee, this.confirmations)
: id = "${txHash}_${amount}_${accountIndex}_${addressIndex}";
WowneroTransactionInfo.fromMap(Map<String, Object?> map)
: id = "${map['hash']}_${map['amount']}_${map['accountIndex']}_${map['addressIndex']}",
+7 -5
View File
@@ -54,7 +54,10 @@ abstract class WowneroWalletBase
extends WalletBase<WowneroBalance, WowneroTransactionHistory, WowneroTransactionInfo>
with Store {
WowneroWalletBase(
{required WalletInfo walletInfo, required DerivationInfo derivationInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo, required String password})
{required WalletInfo walletInfo,
required DerivationInfo derivationInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password})
: balance = ObservableMap<CryptoCurrency, WowneroBalance>.of({
CryptoCurrency.wow: WowneroBalance(
fullBalance:
@@ -144,8 +147,8 @@ abstract class WowneroWalletBase
passphrase: wownero_wallet.getPassphrase());
int? get restoreHeight =>
transactionHistory.transactions.values.firstOrNull?.height ?? wownero.Wallet_getRefreshFromBlockHeight(wptr!);
transactionHistory.transactions.values.firstOrNull?.height ??
wownero.Wallet_getRefreshFromBlockHeight(wptr!);
wownero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
@@ -635,8 +638,7 @@ abstract class WowneroWalletBase
wownero_wallet.getSubaddressLabel(accountIndex, addressIndex);
Future<List<WowneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
(await transaction_history
.getAllTransactions())
(await transaction_history.getAllTransactions())
.map(
(row) => WowneroTransactionInfo(
row.hash,
+14 -10
View File
@@ -34,25 +34,26 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
@override
String get latestAddress {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1;
var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address)) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
}
return address;
}
@override
String get addressForExchange {
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1;
var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) {
addressIndex++;
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
}
return address;
}
@observable
Account? account;
@@ -69,7 +70,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> init() async {
accountList.update();
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
account = accountList.accounts.isEmpty
? Account(id: 0, label: "Primary address")
: accountList.accounts.first;
updateSubaddressList(accountIndex: account?.id ?? 0);
await updateAddressesInBox();
}
@@ -143,7 +146,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
accountIndex: accountIndex,
defaultLabel: defaultLabel,
usedAddresses: usedAddresses.toList());
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
subaddress = (subaddressList.subaddresses.isEmpty)
? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0)
: subaddressList.subaddresses.last;
address = subaddress!.address;
}
@@ -152,6 +157,5 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false;
@override
PaymentURI getPaymentUri(String amount) =>
WowneroURI(address: address, amount: amount);
PaymentURI getPaymentUri(String amount) => WowneroURI(address: address, amount: amount);
}
+54 -22
View File
@@ -23,7 +23,11 @@ import 'package:monero/wownero.dart' as wownero;
class WowneroNewWalletCredentials extends WalletCredentials {
WowneroNewWalletCredentials(
{required String name, required this.language, required this.isPolyseed, this.passphrase, String? password})
{required String name,
required this.language,
required this.isPolyseed,
this.passphrase,
String? password})
: super(name: name, password: password);
final String language;
@@ -33,7 +37,11 @@ class WowneroNewWalletCredentials extends WalletCredentials {
class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials {
WowneroRestoreWalletFromSeedCredentials(
{required String name, required this.mnemonic, required this.passphrase, int height = 0, String? password})
{required String name,
required this.mnemonic,
required this.passphrase,
int height = 0,
String? password})
: super(name: name, password: password, height: height);
final String mnemonic;
@@ -97,9 +105,15 @@ class WowneroWalletService extends WalletService<
}
await wownero_wallet_manager.createWallet(
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??'');
path: path,
password: credentials.password!,
language: credentials.language,
passphrase: credentials.passphrase ?? '');
final wallet = WowneroWallet(
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: credentials.password!);
await wallet.init();
return wallet;
@@ -138,7 +152,11 @@ class WowneroWalletService extends WalletService<
throw Exception('Wallet not found');
}
wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
wallet = WowneroWallet(
walletInfo: walletInfo,
derivationInfo: await walletInfo.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: password);
throw WalletDeprecationException(seed: wallet.seed, curr: wallet.currency);
final isValid = wallet.walletAddresses.validate();
@@ -224,7 +242,11 @@ class WowneroWalletService extends WalletService<
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
final currentWallet = WowneroWallet(walletInfo: currentWalletInfo, derivationInfo: await currentWalletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
final currentWallet = WowneroWallet(
walletInfo: currentWalletInfo,
derivationInfo: await currentWalletInfo.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: password);
await currentWallet.renameWalletFiles(newName);
@@ -249,7 +271,10 @@ class WowneroWalletService extends WalletService<
viewKey: credentials.viewKey,
spendKey: credentials.spendKey);
final wallet = WowneroWallet(
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: credentials.password!);
await wallet.init();
return wallet;
@@ -283,7 +308,10 @@ class WowneroWalletService extends WalletService<
seed: credentials.mnemonic,
restoreHeight: credentials.height!);
final wallet = WowneroWallet(
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
walletInfo: credentials.walletInfo!,
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: credentials.password!);
await wallet.init();
return wallet;
@@ -303,7 +331,8 @@ class WowneroWalletService extends WalletService<
final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase);
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
passphrase: credentials.passphrase);
} catch (e) {
// TODO: Implement Exception for wallet list service.
printV('WowneroWalletsManager Error: $e');
@@ -313,20 +342,19 @@ class WowneroWalletService extends WalletService<
Future<WowneroWallet> _restoreFromPolyseed(
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight, String? passphrase}) async {
if (polyseed.isEncrypted == false &&
(passphrase??'') != "") {
{PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO,
int? overrideHeight,
String? passphrase}) async {
if (polyseed.isEncrypted == false && (passphrase ?? '') != "") {
// Fallback to the different passphrase offset method, when a passphrase
// was provided but the polyseed is not encrypted.
wownero_wallet_manager.restoreWalletFromPolyseedWithOffset(
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seedOffset: passphrase??'',
language: "English");
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seedOffset: passphrase ?? '',
language: "English");
final wallet = WowneroWallet(
walletInfo: walletInfo,
derivationInfo: await walletInfo.getDerivationInfo(),
@@ -357,9 +385,13 @@ class WowneroWalletService extends WalletService<
spendKey: spendKey);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??'');
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase ?? '');
final wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
final wallet = WowneroWallet(
walletInfo: walletInfo,
derivationInfo: await walletInfo.getDerivationInfo(),
unspentCoinsInfo: unspentCoinsInfoSource,
password: password);
await wallet.init();
return wallet;
+9 -7
View File
@@ -8,18 +8,20 @@ import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:hive/hive.dart';
import 'package:mocktail/mocktail.dart';
class MockAppStore extends Mock implements AppStore{}
class MockAuthService extends Mock implements AuthService{}
class MockSettingsStore extends Mock implements SettingsStore {}
class MockAuthenticationStore extends Mock implements AuthenticationStore{}
class MockWalletListStore extends Mock implements WalletListStore{}
class MockAppStore extends Mock implements AppStore {}
class MockAuthService extends Mock implements AuthService {}
class MockSettingsStore extends Mock implements SettingsStore {}
class MockLinkViewModel extends Mock implements LinkViewModel {}
class MockAuthenticationStore extends Mock implements AuthenticationStore {}
class MockWalletListStore extends Mock implements WalletListStore {}
class MockLinkViewModel extends Mock implements LinkViewModel {}
class MockHiveInterface extends Mock implements HiveInterface {}
class MockHiveBox extends Mock implements Box<dynamic> {}
class MockSecureStorage extends Mock implements SecureStorage{}
class MockSecureStorage extends Mock implements SecureStorage {}
@@ -34,7 +34,7 @@ class DisclaimerPageRobot {
Future<void> tapAcceptButton() async {
await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
await commonTestCases.defaultSleepTime();
}
}
@@ -66,7 +66,7 @@ class RestoreFromSeedOrKeysPageRobot {
Future<void> enterSeedPhraseForWalletRestore(String text) async {
ValidatableAnnotatedEditableTextState seedTextState =
await tester.state(find.byType(ValidatableAnnotatedEditableText));
await tester.state(find.byType(ValidatableAnnotatedEditableText));
seedTextState.widget.controller.text = text;
await tester.pumpAndSettle();
@@ -21,7 +21,7 @@ class SecurityAndBackupPageRobot {
Future<void> navigateToShowKeysPage() async {
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key');
await tester.pumpAndSettle(Duration(milliseconds: 1000));
}
@@ -26,7 +26,7 @@ void main() {
(tester) async {
// Store the original FlutterError.onError handler
final originalOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
@@ -25,7 +25,7 @@ void main() {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
exchangePageRobot = ExchangePageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester);
@@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
@@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage {
),
bottomCardChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: card.denominationItems.isNotEmpty
? DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominationItems,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
: EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
child: Column(children: [
if (card.prepaidRange.isNotEmpty)
PrepaidRangeAmountWidget(
fiatCurrency: card.fiatCurrency.title,
prepaidRanges: card.prepaidRange,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
)
else if (card.denominationItems.isNotEmpty)
DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominationItems,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
)
else
EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
),
]))),
Expanded(
flex: 2,
child: Padding(
+108 -21
View File
@@ -1,8 +1,21 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
enum CakePayCardType {
prepaid('Prepaid Cards'),
gift('gift'),
onDemand('on_demand'),
custom('custom');
const CakePayCardType(this.apiValue);
final String apiValue;
}
class CakePayCard {
final int id;
final String name;
final CakePayCardType? type;
final String? description;
final String? termsAndConditions;
final String? howToUse;
@@ -15,26 +28,77 @@ class CakePayCard {
final String? minValue;
final String? maxValue;
final List<Denomination> denominationItems;
final List<PrepaidRange> prepaidRange;
CakePayCard({
required this.id,
required this.name,
this.description,
this.termsAndConditions,
this.howToUse,
this.expiryAndValidity,
this.cardImageUrl,
this.country,
required this.fiatCurrency,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
CakePayCard(
{required this.id,
required this.name,
this.type,
this.description,
this.termsAndConditions,
this.howToUse,
this.expiryAndValidity,
this.cardImageUrl,
this.country,
required this.fiatCurrency,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
List<Denomination>? denominationItems,
List<PrepaidRange>? prepaidRange})
: denominationItems = denominationItems ?? const [],
prepaidRange = prepaidRange ?? const [];
CakePayCard copyWith({
int? id,
String? name,
CakePayCardType? type,
String? description,
String? termsAndConditions,
String? howToUse,
String? expiryAndValidity,
String? cardImageUrl,
String? country,
FiatCurrency? fiatCurrency,
String? minValueUsd,
String? maxValueUsd,
String? minValue,
String? maxValue,
List<Denomination>? denominationItems,
}) : denominationItems = denominationItems ?? const [];
List<PrepaidRange>? prepaidRange,
}) {
return CakePayCard(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
description: description ?? this.description,
termsAndConditions: termsAndConditions ?? this.termsAndConditions,
howToUse: howToUse ?? this.howToUse,
expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity,
cardImageUrl: cardImageUrl ?? this.cardImageUrl,
country: country ?? this.country,
fiatCurrency: fiatCurrency ?? this.fiatCurrency,
minValueUsd: minValueUsd ?? this.minValueUsd,
maxValueUsd: maxValueUsd ?? this.maxValueUsd,
minValue: minValue ?? this.minValue,
maxValue: maxValue ?? this.maxValue,
denominationItems: denominationItems ?? this.denominationItems,
prepaidRange: prepaidRange ?? this.prepaidRange,
);
}
static CakePayCardType? cakePayCardTypeFromApi(String? value) {
if (value == null) return null;
final normalized = value.trim().toLowerCase();
return CakePayCardType.values.firstWhereOrNull(
(e) => e.apiValue.toLowerCase() == normalized,
);
}
factory CakePayCard.fromJson(Map<String, dynamic> json) {
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
final typeString = json['type'] as String? ?? '';
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
@@ -63,9 +127,12 @@ class CakePayCard {
}
}
final cakePayCardType = cakePayCardTypeFromApi(typeString);
return CakePayCard(
id: json['id'] as int? ?? 0,
name: name,
type: cakePayCardType,
description: description,
termsAndConditions: termsAndConditions,
howToUse: howToUse,
@@ -103,12 +170,6 @@ class Denomination {
this.usdValue,
});
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
factory Denomination.fromJson(Map<String, dynamic> json) {
return Denomination(
value: _toDouble(json['value']) ?? 0,
@@ -117,3 +178,29 @@ class Denomination {
);
}
}
class PrepaidRange {
final double minValue;
final double maxValue;
final String rangeId;
PrepaidRange({
required this.minValue,
required this.maxValue,
required this.rangeId,
});
factory PrepaidRange.fromCard(CakePayCard card) {
return PrepaidRange(
minValue: _toDouble(card.minValue) ?? 0,
maxValue: _toDouble(card.maxValue) ?? 0,
rangeId: card.id.toString(),
);
}
}
double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
+20 -3
View File
@@ -31,9 +31,26 @@ class CakePayVendor {
if (cardsJson != null && cardsJson.isNotEmpty) {
try {
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
return Country.normalizeName(card['country'] as String) == country;
}) as Map<String, dynamic>);
final cards = cardsJson
.map((cardJson) => CakePayCard.fromJson(cardJson as Map<String, dynamic>))
.toList();
final cardsForSelectedCountry =
cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList();
if (cardsForSelectedCountry.isNotEmpty) {
final isPrepaid =
cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid);
if (isPrepaid) {
final firstCard = cardsForSelectedCountry.first;
final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList()
..sort((a, b) => a.minValue.compareTo(b.minValue));
cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges);
} else {
cardForVendor = cardsForSelectedCountry.first;
}
}
} catch (e) {
printV('Error parsing card for vendor: $e');
}
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
class CakePayApi {
static const testBaseUri = FeatureFlag.hasDevOptions;
static const testBaseUri = false; //FeatureFlag.hasDevOptions;
static const baseTestCakePayUri = 'api-stg.cakepay.com';
static const baseProdCakePayUri = 'api-prod.cakepay.com';
@@ -0,0 +1,134 @@
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:flutter/material.dart';
import 'enter_amount_widget.dart';
class PrepaidRangeAmountWidget extends StatefulWidget {
const PrepaidRangeAmountWidget({
required this.fiatCurrency,
required this.prepaidRanges,
required this.amountFieldFocus,
required this.amountController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
});
final String fiatCurrency;
final List<PrepaidRange> prepaidRanges;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
@override
State<PrepaidRangeAmountWidget> createState() => _PrepaidRangeAmountWidgetState();
}
class _PrepaidRangeAmountWidgetState extends State<PrepaidRangeAmountWidget> {
late PrepaidRange _selectedRange;
@override
void initState() {
super.initState();
_selectedRange = widget.prepaidRanges.first;
_applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select range:',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<PrepaidRange>(
value: _selectedRange,
isExpanded: true,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
filled: true,
fillColor: Theme.of(context).cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withAlpha(40),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
items: widget.prepaidRanges
.map(
(range) => DropdownMenuItem<PrepaidRange>(
value: range,
child: Text(_rangeLabel(range)),
),
)
.toList(),
onChanged: (range) {
if (range == null) return;
setState(() => _selectedRange = range);
widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range);
_applySelectedRange(forceAmountUpdate: true);
},
),
const SizedBox(height: 12),
EnterAmountWidget(
minValue: _formatAmount(_selectedRange.minValue),
maxValue: _formatAmount(_selectedRange.maxValue),
fiatCurrency: widget.fiatCurrency,
amountFieldFocus: widget.amountFieldFocus,
amountController: widget.amountController,
onAmountChanged: (value) {
widget.onAmountChanged(value);
widget.cakePayBuyCardViewModel.selectedDenomination = (
value.replaceAll(',', '.'),
int.tryParse(_selectedRange.rangeId),
);
},
),
],
);
}
void _applySelectedRange({required bool forceAmountUpdate}) {
final currentAmount = double.tryParse(widget.amountController.text);
final isCurrentAmountInRange = currentAmount != null &&
currentAmount >= _selectedRange.minValue &&
currentAmount <= _selectedRange.maxValue;
final nextAmount = forceAmountUpdate || !isCurrentAmountInRange
? _formatAmount(_selectedRange.minValue)
: widget.amountController.text;
widget.amountController.text = nextAmount;
widget.onAmountChanged(nextAmount);
widget.cakePayBuyCardViewModel.selectedDenomination = (
nextAmount.replaceAll(',', '.'),
int.tryParse(_selectedRange.rangeId),
);
}
String _rangeLabel(PrepaidRange range) {
return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}';
}
String _formatAmount(double value) {
return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2);
}
}
@@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
height: screenHeight * 0.50,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
borderRadius: constBorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
Flexible(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25.0),
bottomRight: Radius.circular(25.0),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
color: Theme.of(context).colorScheme.surfaceContainerLow,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
height: screenHeight * 0.38,
width: double.infinity,
child: topCardChild)),
bottomCardChild,
color: Theme.of(context).colorScheme.surfaceContainerLow,
),
constraints: BoxConstraints(
maxHeight: screenHeight * 0.38,
),
width: double.infinity,
child: topCardChild,
),
),
),
Flexible(
child: bottomCardChild,
),
],
),
),
+7 -4
View File
@@ -423,16 +423,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>()));
+3
View File
@@ -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
+3
View File
@@ -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(
@@ -12,7 +12,6 @@ class ModalTopBar extends StatelessWidget {
this.onTrailingPressed = nothing,
this.leadingIcon,
this.trailingIcon,
this.bottomText,
this.padding,
this.leadingWidget,
this.trailingWidget}) {
@@ -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>();
+16 -3
View File
@@ -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,67 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
}
}
var _isConnectPressed = false;
Future<void> _connectToDevice(HardwareWalletDevice device) async {
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
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);
});
{
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 +240,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 +248,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 +259,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 +270,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 +360,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: _onChangeWallet,
)
),
],
],
),
@@ -321,7 +371,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary,
onPressed: () => _onHowToConnect(context),
)
),
],
),
),
@@ -339,17 +389,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
View File
@@ -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");
}
}
}
@@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
: 0,
quantity = 1,
card = vendor.card!,
min = _toDouble(vendor.card!.minValue) ?? 0,
max = _toDouble(vendor.card!.maxValue) ?? 0 {
min = vendor.card!.prepaidRange.isNotEmpty
? vendor.card!.prepaidRange.first.minValue
: _toDouble(vendor.card!.minValue) ?? 0,
max = vendor.card!.prepaidRange.isNotEmpty
? vendor.card!.prepaidRange.first.maxValue
: _toDouble(vendor.card!.maxValue) ?? 0 {
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
}
@@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
final CakePayVendor vendor;
final SendViewModel sendViewModel;
final CakePayService _cakePayService;
final double max;
final double min;
final CakePayCard card;
final WalletType walletType;
final Box<Order> orders;
@@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
bool confirmsNoVpn = false;
bool confirmsVoidedRefund = false;
bool confirmsTermsAgreed = false;
double max;
double min;
(String, int?) selectedDenomination = ('', null);
String simulatedResponse = '';
@@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store {
@action
void onQuantityChanged(int? input) => quantity = input ?? 1;
void onPrepaidRangeChanged(PrepaidRange range) {
min = range.minValue;
max = range.maxValue;
selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId));
if (amount < min || amount > max) {
amount = min;
}
}
@action
void onAmountChanged(String input) {
if (input.isEmpty) return;
amount = double.parse(input.replaceAll(',', '.'));
if (card.prepaidRange.isNotEmpty) {
selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2);
}
}
@action
@@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store {
sendViewModel.state =
FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.');
}
final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty;
int selectedCardId = card.id;
String selectedPrice = amount.toString();
if (isPrepaidRangeSelected) {
selectedCardId = selectedDenomination.$2 ?? card.id;
selectedPrice =
selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString();
} else if (isDenominationSelected) {
selectedCardId = selectedDenomination.$2 ?? card.id;
selectedPrice = selectedDenomination.$1;
}
try {
order = await _cakePayService.createOrder(
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
cardId: selectedCardId,
price: selectedPrice,
quantity: quantity,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
@@ -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;
}
}
+4
View File
@@ -1121,6 +1121,9 @@ 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 +1135,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
View File
@@ -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"
+6 -3
View File
@@ -48,7 +48,8 @@ void main() {
group("getCanonicalCryptoAmount", () {
test("Amount should not change for Bitcoin", () {
final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc);
final amount =
amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc);
expect(amount, "0.000001");
});
@@ -58,7 +59,8 @@ void main() {
});
test("Amount should not change for Bitcoin", () {
final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln);
final amount =
amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln);
expect(amount, "0.000001");
});
@@ -118,7 +120,8 @@ void main() {
});
test("Amount should be formated to Satoshi for Bitcoin Lightning", () {
final amount = amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln);
final amount =
amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln);
expect(amount, "100");
});
@@ -48,7 +48,8 @@ void main() {
});
test('detects Lightning invoice', () {
const invoice = 'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n';
const invoice =
'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n';
final result = UniversalAddressDetector.detectAddress(invoice);
expect(result.isValid, true);
@@ -39,8 +39,7 @@ void main() {
});
test('extracts Ethereum address correctly', () {
final raw =
'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks';
final raw = 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks';
final result = AddressResolverUtils.extractAddressByType(
raw: raw,
type: CryptoCurrency.eth,
@@ -60,25 +59,21 @@ void main() {
});
test('extracts Bitcoin Cash address correctly', () {
final raw =
'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj';
final raw = 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj';
final result = AddressResolverUtils.extractAddressByType(
raw: raw,
type: CryptoCurrency.bch,
);
expect(
result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj');
expect(result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj');
});
test('extracts Nano address correctly', () {
final raw =
'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd';
final raw = 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd';
final result = AddressResolverUtils.extractAddressByType(
raw: raw,
type: CryptoCurrency.nano,
);
expect(result,
'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd');
expect(result, 'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd');
});
test('returns null for unsupported currency', () {
@@ -101,9 +96,7 @@ void main() {
final raw =
'monero_wallet:467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz?spend_key=029c559cd7669f14e91fd835144916009f8697ab5ac5c7f7c06e1ff869c17b0b&view_key=afaf646edbff3d3bcee8efd3383ffe5d20c947040f74e1110b70ca0fbb0ef90d';
final result = AddressResolverUtils.extractAddressByType(
raw: raw,
type: CryptoCurrency.xmr,
requireSurroundingWhitespaces: false);
raw: raw, type: CryptoCurrency.xmr, requireSurroundingWhitespaces: false);
expect(result,
'467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz');
});
@@ -119,16 +112,16 @@ sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucw
#LTC MWEB
ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc
''';
final resultXmr = AddressResolverUtils.extractAddressByType(
raw: raw, type: CryptoCurrency.xmr);
final resultXmr =
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.xmr);
expect(resultXmr,
'89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc');
final resultBtc = AddressResolverUtils.extractAddressByType(
raw: raw, type: CryptoCurrency.btc);
final resultBtc =
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.btc);
expect(resultBtc,
'sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45');
final resultLtc = AddressResolverUtils.extractAddressByType(
raw: raw, type: CryptoCurrency.ltc);
final resultLtc =
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.ltc);
expect(resultLtc,
'ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc');
});
+1 -2
View File
@@ -23,8 +23,7 @@ void main() {
});
test("extract address and amount from Cake Style Uri", () {
final uri =
Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00");
final uri = Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00");
final paymentRequest = PaymentRequest.fromUri(uri);
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
+2 -2
View File
@@ -26,11 +26,11 @@ void main(List<String> args) async {
}
print('Alphabetizing all files...');
for (var lang in langs) {
final fileName = getArbFileName(lang);
alphabetizeArbFile(fileName);
}
print('Done!');
}
}
+1
View File
@@ -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);
+6 -8
View File
@@ -7,7 +7,7 @@ import 'package:archive/archive_io.dart';
final _dio = Dio();
final List<String> triplets = [
"x86_64-linux-gnu", // linux desktop - majority of users onlinux
"x86_64-linux-gnu", // linux desktop - majority of users onlinux
// "i686-linux-gnu", // not supported by cake
// "i686-meego-linux-gnu", // sailfishos (emulator)- not supported by cake
// "aarch64-linux-gnu", // not (yet) supported by cake - (mostly) mobile linux
@@ -18,7 +18,7 @@ final List<String> triplets = [
"armv7a-linux-androideabi",
// "i686-w64-mingw32", // 32bit windows - not supported by monero_c
"x86_64-w64-mingw32",
// "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake
// "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake
// "aarch64-apple-darwin11", // apple silicon macbooks (contrib) - not used by cake
// "x86_64-host-apple-darwin", // not available on CI (yet)
"aarch64-apple-darwin", // apple silicon macbooks
@@ -55,10 +55,8 @@ Future<void> main() async {
}
if (Platform.isMacOS) {
print("Generating ios framework");
final result = Process.runSync("bash", [
"-c",
"cd scripts/ios && ./gen_framework.sh && cd ../.."
]);
print((result.stdout+result.stderr).toString().trim());
final result =
Process.runSync("bash", ["-c", "cd scripts/ios && ./gen_framework.sh && cd ../.."]);
print((result.stdout + result.stderr).toString().trim());
}
}
}
+19 -21
View File
@@ -13,21 +13,21 @@ const srcDir = 'srcDir';
const defaultLocale = 'en';
Future<void> main(List<String> args) async {
final extraInfo = args.isNotEmpty ?
args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
final parts = arg.split('=');
var key = normalizeKeyName(parts[0]);
if (key.contains('--')) {
key = key.substring(2);
}
acc[key] = parts.length > 1
? parts[1].isNotEmpty
? parts[1]
: inputPath
: inputPath;
return acc;
})
: <String, dynamic> {srcDir : inputPath};
final extraInfo = args.isNotEmpty
? args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
final parts = arg.split('=');
var key = normalizeKeyName(parts[0]);
if (key.contains('--')) {
key = key.substring(2);
}
acc[key] = parts.length > 1
? parts[1].isNotEmpty
? parts[1]
: inputPath
: inputPath;
return acc;
})
: <String, dynamic>{srcDir: inputPath};
final outputDir = Directory(outputPath);
@@ -59,10 +59,10 @@ Future<void> main(List<String> args) async {
return;
}
final parts =
fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_');
fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_');
final locale = parts.length > 1
? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}'
: parts.first.toLowerCase();
? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}'
: parts.first.toLowerCase();
localePath[locale] = element.path;
});
@@ -78,8 +78,7 @@ Future<void> main(List<String> args) async {
output += part1;
output += textDirectionDeclaration;
var inputContent =
File(localePath[defaultLocale].toString()).readAsStringSync();
var inputContent = File(localePath[defaultLocale].toString()).readAsStringSync();
var config = json.decode(inputContent) as Map<String, dynamic>;
output += localizedStrings(config: config, hasOverride: false);
@@ -161,4 +160,3 @@ String localizedStrings({required Map<String, dynamic> config, required bool has
return output;
}
+1 -1
View File
@@ -9,4 +9,4 @@ Future<void> main(List<String> args) async => generateSecrets(args);
Future<void> generateSecrets(List<String> args) async {
await generateSecretsConfig(args);
await importSecretsConfig();
}
}
+2 -2
View File
@@ -9,12 +9,12 @@ Future<void> main(List<String> args) async {
final pubspecDescription = File(pubspecDescriptionPath);
if (!pubspecBase.existsSync() || !pubspecDescription.existsSync()) {
throw("$pubspecBasePath or $pubspecDescriptionPath doesn't exists");
throw ("$pubspecBasePath or $pubspecDescriptionPath doesn't exists");
}
final pubspecBaseContent = await pubspecBase.readAsString();
final pubspecDescriptionContent = await pubspecDescription.readAsString();
final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent;
final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent;
final outputPubspec = File(outputPubspecPath);
if (outputPubspec.existsSync()) {
+2 -2
View File
@@ -61,9 +61,9 @@ Future<void> generateSecretsConfig(List<String> args) async {
return;
}
}
await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets);
await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets);
await writeConfig(solanaConfigFile, SecretKey.solanaSecrets);
await writeConfig(nanoConfigFile, SecretKey.nanoSecrets);
@@ -157,4 +157,4 @@ String getLang(Locale l) => l == null
: l.countryCode != null && l.countryCode!.isEmpty
? l.languageCode
: l.toString();
""";
""";
+1
View File
@@ -0,0 +1 @@
+4 -5
View File
@@ -8,9 +8,9 @@ import 'utils/translation/translation_utils.dart';
const changelogsFilePath = "./assets/new-ui/changelog/text";
void main() async {
final List<dynamic> defaultLangChangelog = jsonDecode(
await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString())
as List<dynamic>;
final List<dynamic> defaultLangChangelog =
jsonDecode(await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString())
as List<dynamic>;
int currentLang = 0;
for (final lang in langs) {
@@ -50,7 +50,6 @@ void main() async {
"\r$currentLang/${langs.length} langs done, $currentItem/${defaultLangChangelog.length} items done");
}
currentLang++;
await File("$changelogsFilePath/changelog_$lang.json")
.writeAsString(jsonEncode(out));
await File("$changelogsFilePath/changelog_$lang.json").writeAsString(jsonEncode(out));
}
}
+13 -5
View File
@@ -217,11 +217,20 @@ IMPORTANT: Respond with a JSON object in the exact format, you must think about
void main(List<String> args) async {
final parser = ArgParser()
..addOption('source', abbr: 's', help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)', mandatory: true)
..addOption('destination', abbr: 'd', help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)', mandatory: true)
..addOption('source',
abbr: 's',
help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)',
mandatory: true)
..addOption('destination',
abbr: 'd',
help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)',
mandatory: true)
..addOption('key', abbr: 'k', help: 'Translate only this specific key (optional)')
..addOption('ollama-url', help: 'Ollama server URL (default: http://localhost:11434)', defaultsTo: 'http://localhost:11434')
..addOption('model', help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b')
..addOption('ollama-url',
help: 'Ollama server URL (default: http://localhost:11434)',
defaultsTo: 'http://localhost:11434')
..addOption('model',
help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b')
..addFlag('help', abbr: 'h', help: 'Show this help message', negatable: false);
try {
@@ -268,7 +277,6 @@ void main(List<String> args) async {
destinationArbPath: destinationPath,
specificKey: specificKey,
);
} catch (e) {
print('Error: $e');
print('Run with --help for more information.');
+1 -2
View File
@@ -8,8 +8,7 @@ import 'utils/translation/translation_utils.dart';
void main(List<String> args) async {
if (args.length != 1) {
throw Exception(
'Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`');
throw Exception('Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`');
}
final targetLang = args.first;
+1 -2
View File
@@ -28,8 +28,7 @@ void main(List<String> args) async {
missingKeys.forEach((key) {
print('Missing in "$lang": "$key"');
if (doFix)
missingDefaults[key] = arbObj[key] as String;
if (doFix) missingDefaults[key] = arbObj[key] as String;
});
if (missingDefaults.isNotEmpty) {
+2 -4
View File
@@ -12,8 +12,7 @@ Future<void> main(List<String> args) async {
}
Future<void> updateSecretsConfig(List<String> args) async {
final extraInfo =
args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
final extraInfo = args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
final parts = arg.split('=');
final key = normalizeKeyName(parts[0]);
acc[key] = parts.length > 1 ? parts[1] : 1;
@@ -32,8 +31,7 @@ Future<void> updateSecretsConfig(List<String> args) async {
return false;
});
final fileConfig =
json.decode(configFile.readAsStringSync()) as Map<String, dynamic>;
final fileConfig = json.decode(configFile.readAsStringSync()) as Map<String, dynamic>;
fileConfig.forEach((key, dynamic value) {
if (secrets[key] == null) {
secrets[key] = value;