diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index f8cc8f9ca..a5a1f6bbc 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- - name: Feature or Enhancement Request ✨
- url: https://github.com/cake-tech/cake_wallet/discussions/new?category=feature-requests
- about: Suggest an idea for Cake Wallet
- name: Not sure where to start?
url: https://docs.cakewallet.com
about: Start by reading checking out the guides!
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 000000000..e496201aa
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,115 @@
+name: Lint
+
+on: [pull_request]
+
+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
\ No newline at end of file
diff --git a/.github/workflows/no_http_imports.yaml b/.github/workflows/no_http_imports.yaml
index dad6821ac..0bfca1d06 100644
--- a/.github/workflows/no_http_imports.yaml
+++ b/.github/workflows/no_http_imports.yaml
@@ -11,11 +11,10 @@ jobs:
- name: Check for http package usage
if: github.event_name == 'pull_request'
run: |
- GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) || true)"
+ GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) | (grep -v '^cw_custom_lints/' || test $? = 1) || true)"
[[ "x$GIT_GREP_OUT" == "x" ]] && exit 0
echo "$GIT_GREP_OUT"
echo "There are .dart files which use http imports"
echo "Using http package breaks proxy integration"
echo "Please use ProxyWrapper.getHttpClient() from package:cw_core/utils/proxy_wrapper.dart"
exit 1
-
\ No newline at end of file
diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml
index 507793bd8..69885c9b5 100644
--- a/.github/workflows/no_print_in_dart.yaml
+++ b/.github/workflows/no_print_in_dart.yaml
@@ -11,7 +11,7 @@ jobs:
- name: Check for print() statements in dart code (use printV() instead)
if: github.event_name == 'pull_request'
run: |
- GIT_GREP_OUT="$(git grep ' print(' | (grep .dart: || test $? = 1) | (grep -v print_verbose.dart || test $? = 1) | (grep -v print_verbose_dummy.dart || test $? = 1) || true)"
+ GIT_GREP_OUT="$(git grep ' print(' | (grep .dart: || test $? = 1) | (grep -v print_verbose.dart || test $? = 1) | (grep -v print_verbose_dummy.dart || test $? = 1) | (grep -v '^tool/' || test $? = 1) | (grep -v '^cw_custom_lints/' || test $? = 1) || true)"
[[ "x$GIT_GREP_OUT" == "x" ]] && exit 0
echo "$GIT_GREP_OUT"
echo "There are .dart files which use print() statements"
diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml
index 8cb442447..879ea247f 100644
--- a/.github/workflows/pr_test_build_linux.yml
+++ b/.github/workflows/pr_test_build_linux.yml
@@ -179,9 +179,6 @@ jobs:
with:
path: ${{ github.workspace }}/build/linux/x64/release/cakewallet_linux.zip
name: cakewallet_linux
- - name: Verify lint
- run: |
- exec env ABORT_ON_CHANGE=true ./scripts/lint.sh
- name: Prepare virtual desktop
if: ${{ contains(env.message, 'run tests') }}
diff --git a/Dockerfile b/Dockerfile
index 7a0c8c022..78e119877 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -52,7 +52,7 @@ RUN set -o xtrace \
# for x86 emulators
libatk-bridge2.0-0 libgdk-pixbuf-xlib-2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \
# Linux desktop dependencies
- clang cmake libgtk-3-dev ninja-build pkg-config libsecret-1-0 libsecret-1-dev gir1.2-secret-1 \
+ clang lld cmake libgtk-3-dev ninja-build pkg-config libsecret-1-0 libsecret-1-dev gir1.2-secret-1 \
# monero_c dependencies
autoconf automake build-essential ccache gperf libtool llvm \
# extra stuff for KVM
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 98dfe6ae4..70e87faf3 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,3 @@
-include: package:lints/recommended.yaml
analyzer:
@@ -9,6 +8,7 @@ analyzer:
cw_core/lib/**.g.dart,
cw_haven/lib/**.g.dart,
cw_monero/lib/**.g.dart,
+ cw_mweb/lib/**.g.dart,
lib/generated/*.dart,
cw_monero/ios/External/**,
cw_shared_external/**,
@@ -18,65 +18,124 @@ analyzer:
strict-casts: true
strict-raw-types: true
+plugins:
+ cw_custom_lints:
+ path: cw_custom_lints
+
linter:
rules:
- cancel_subscriptions
- always_declare_return_types
- prefer_final_fields
+ - always_put_control_body_on_new_line
+ - always_put_required_named_parameters_first
+ - always_use_package_imports
+ - annotate_overrides
+ - annotate_redeclares
+ - avoid_empty_else
+ - avoid_function_literals_in_foreach_calls
+ - avoid_init_to_null
+ - avoid_multiple_declarations_per_line
+ - avoid_positional_boolean_parameters
+ - avoid_relative_lib_imports
+ - avoid_renaming_method_parameters
+ - avoid_return_types_on_setters
+ - avoid_returning_null_for_void
+ - avoid_slow_async_io
+ - avoid_types_as_parameter_names
+ - avoid_types_on_closure_parameters
+ - avoid_unnecessary_containers
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - await_only_futures
+ - camel_case_extensions
+ - camel_case_types
+ - cast_nullable_to_non_nullable
+ - collection_methods_unrelated_type
+ - constant_identifier_names
+ - curly_braces_in_flow_control_structures
+ - directives_ordering
+ - empty_constructor_bodies
+ - empty_statements
+ - eol_at_end_of_file
+ - flutter_style_todos
+ - hash_and_equals
+ - implicit_reopen
+ - library_private_types_in_public_api
+ - non_constant_identifier_names
+ - only_throw_errors
+ - package_names
+ - prefer_asserts_with_message
+ - prefer_conditional_assignment
+ - prefer_const_constructors
+ - prefer_const_constructors_in_immutables
+ - prefer_const_declarations
+ - prefer_constructors_over_static_methods
+ - prefer_contains
+ - prefer_double_quotes
+ - prefer_expression_function_bodies
+ - prefer_final_in_for_each
+ - prefer_function_declarations_over_variables
+ - prefer_if_null_operators
+ - prefer_initializing_formals
+ - prefer_inlined_adds
+ - prefer_int_literals
+ - prefer_is_empty
+ - prefer_is_not_empty
+ - prefer_is_not_operator
+ - prefer_iterable_wheretype
+ - prefer_null_aware_operators
+ - prefer_spread_collections
+ - prefer_typing_uninitialized_variables
+ - recursive_getters
+ - require_trailing_commas
+ - sized_box_for_whitespace
+ - sort_child_properties_last
+ - sort_constructors_first
+ - sort_unnamed_constructors_first
+ - test_types_in_equals
+ - unawaited_futures
+ - unnecessary_async
+ - unnecessary_await_in_return
+ - unnecessary_const
+ - unnecessary_constructor_name
+ - unnecessary_lambdas
+ - unnecessary_late
+ - unnecessary_new
+ - unnecessary_null_aware_assignments
+ - unnecessary_null_checks
+ - unnecessary_null_in_if_null_operators
+ - unnecessary_nullable_for_final_variable_declarations
+ - unnecessary_overrides
+ - unnecessary_parenthesis
+ - unnecessary_string_escapes
+ - unnecessary_string_interpolations
+ - unnecessary_this
+ - unnecessary_to_list_in_spreads
+ - unnecessary_underscores
+ - unreachable_from_main
+ - unrelated_type_equality_checks
+ - use_build_context_synchronously
+ - use_colored_box
+ - use_full_hex_values_for_flutter_colors
+ - use_function_type_syntax_for_parameters
+ - use_is_even_rather_than_modulo
+ - use_named_constants
+ - use_null_aware_elements
+ - use_raw_strings
+ - use_rethrow_when_possible
+ - use_string_buffers
+ - use_super_parameters
+ - use_truncating_division
+ - valid_regexps
+ - void_checks
+ - prefer_final_locals
+ - prefer_interpolation_to_compose_strings
+ - null_closures
+ - no_wildcard_variable_uses
+
+
formatter:
page_width: 100
-
-
-# analyzer:
-# strong-mode:
-# implicit-casts: false
-# implicit-dynamic: false
-# exclude: [build/**, lib/generated/*.dart, lib/**.g.dart, cw_monero/ios/External/**, cw_shared_external/**, shared_external/**]
-
-# linter:
-# rules:
-# - always_declare_return_types
-# - annotate_overrides
-# - avoid_empty_else
-# - avoid_init_to_null
-# - avoid_return_types_on_setters
-# - await_only_futures
-# - camel_case_types
-# - cancel_subscriptions
-# - close_sinks
-# - comment_references
-# - constant_identifier_names
-# - control_flow_in_finally
-# - empty_catches
-# - empty_constructor_bodies
-# - empty_statements
-# - hash_and_equals
-# - invariant_booleans
-# - iterable_contains_unrelated_type
-# - library_names
-# - library_prefixes
-# - list_remove_unrelated_type
-# - literal_only_boolean_expressions
-# - non_constant_identifier_names
-# - one_member_abstracts
-# - only_throw_errors
-# - overridden_fields
-# - package_api_docs
-# - package_names
-# - package_prefixed_library_names
-# - parameter_assignments
-# - prefer_final_fields
-# - prefer_final_locals
-# - prefer_is_not_empty
-# - slash_for_doc_comments
-# - sort_constructors_first
-# - sort_unnamed_constructors_first
-# - test_types_in_equals
-# - throw_in_finally
-# - type_init_formals
-# - unawaited_futures
-# - unnecessary_getters_setters
-# - unrelated_type_equality_checks
-# - valid_regexps
diff --git a/android/gradle.properties b/android/gradle.properties
index 7be3f195e..80241dfa7 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx6144M
android.useAndroidX=true
android.enableJetifier=true
-android.ndk.suppressMinSdkVersionError=21
\ No newline at end of file
+
+android.ndk.suppressMinSdkVersionError=21
diff --git a/assets/images/radar.png b/assets/images/radar.png
new file mode 100644
index 000000000..386511418
Binary files /dev/null and b/assets/images/radar.png differ
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index a2b2f798f..a0d88ce9a 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -345,12 +345,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return balance;
}
+ try {
final lBalance = await lightningWallet!.getBalance();
this.balance[CryptoCurrency.btcln] = ElectrumBalance(
confirmed: lBalance,
unconfirmed: Money.zero(CryptoCurrency.btcln),
frozen: Money.zero(CryptoCurrency.btcln));
+ } catch (e) {
+ printV("Error fetching lightning balance: $e");
+ }
return ElectrumBalance(
confirmed: balance.confirmed,
@@ -551,8 +555,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return tx;
}
- List getUtxoWithPrivateKeys() => unspentCoins
- .where((e) => (e.isSending && !e.isFrozen))
+ List getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
+ .where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart
index 6163a8a82..8a034d8cf 100644
--- a/cw_bitcoin/lib/bitcoin_wallet_service.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart
@@ -8,7 +8,6 @@ import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/zpub.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_core/pathForWallet.dart';
@@ -130,31 +129,6 @@ class BitcoinWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await BitcoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- payjoinBox: payjoinSessionSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async {
diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart
index 9e18d0e60..a58829e00 100644
--- a/cw_bitcoin/lib/electrum_transaction_info.dart
+++ b/cw_bitcoin/lib/electrum_transaction_info.dart
@@ -18,7 +18,7 @@ class ElectrumTransactionBundle {
{required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction;
- final List ins;
+ final List ins;
final int? time;
final int confirmations;
}
@@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
List inputAddresses = [];
List outputAddresses = [];
+ var hasMissingInputTx = false;
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
- final inputTransaction = bundle.ins[i];
+ final inputTransaction = i < bundle.ins.length ? bundle.ins[i] : null;
+ if (inputTransaction == null || input.txIndex >= inputTransaction.outputs.length) {
+ hasMissingInputTx = true;
+ continue;
+ }
final outTransaction = inputTransaction.outputs[input.txIndex];
inputAmount += outTransaction.amount.toInt();
if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) {
@@ -184,9 +189,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
final b = tx.outputs.first.scriptPubKey.toBytes();
return b.length == 34 && b[0] == 88 && b[1] == 32;
};
- final isHogEx = isHogExTx(bundle.originalTransaction) && isHogExTx(bundle.ins.first);
+ final firstInput = bundle.ins.isNotEmpty ? bundle.ins.first : null;
+ final isHogEx = firstInput != null &&
+ isHogExTx(bundle.originalTransaction) &&
+ isHogExTx(firstInput);
- final fee = inputAmount - totalOutAmount;
+ final fee = hasMissingInputTx ? null : inputAmount - totalOutAmount;
+ final walletCurrency = walletTypeToCryptoCurrency(type);
+ final feeMoney = fee != null ? Money.fromInt(fee, walletCurrency) : null;
return ElectrumTransactionInfo(type,
id: bundle.originalTransaction.txId(),
height: height,
@@ -194,11 +204,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
isReplaced: false,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
- fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
+ fee: feeMoney,
direction: direction,
- amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
+ amount: Money.fromInt(amount, walletCurrency),
date: date,
isHogEx: isHogEx,
+ additionalInfo: {'hasMissingInputTx': hasMissingInputTx},
confirmations: bundle.confirmations);
}
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 095d1a45e..ccd41d010 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
@@ -38,7 +37,6 @@ import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart';
-import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/sync_status.dart';
@@ -1539,6 +1537,7 @@ abstract class ElectrumWalletBase
fee: estimatedTx.fee.amount,
network: network,
memo: estimatedTx.memo,
+ inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
@@ -1736,37 +1735,6 @@ abstract class ElectrumWalletBase
await transactionHistory.save();
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
- }
-
- // Delete old name's dir and files
- final dir = Directory(currentDirPath);
- for (var attempt = 0; attempt < 3; attempt++) {
- try {
- await dir.delete(recursive: true);
- break;
- } on FileSystemException {
- if (attempt == 2) rethrow;
- await Future.delayed(const Duration(milliseconds: 200));
- }
- }
- }
-
@override
Future changePassword(String password) async {
_password = password;
@@ -2118,6 +2086,9 @@ abstract class ElectrumWalletBase
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) {
+ throw Exception("Missing input transaction for fee calculation");
+ }
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
allInputsAmount += outTransaction.amount.toInt();
@@ -2146,6 +2117,9 @@ abstract class ElectrumWalletBase
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) {
+ throw Exception("Missing input transaction for replace-by-fee");
+ }
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
@@ -2329,6 +2303,7 @@ abstract class ElectrumWalletBase
fee: BigInt.from(newFee),
network: network,
memo: memo,
+ inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
@@ -2434,20 +2409,24 @@ abstract class ElectrumWalletBase
}
final original = BtcTransaction.fromRaw(transactionHex);
- final ins = [];
+ final ins = [];
for (final vin in original.inputs) {
- final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
+ try {
+ final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
- final String inputTransactionHex;
+ final String inputTransactionHex;
- if (verboseTransaction.isEmpty) {
- inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
- } else {
- inputTransactionHex = verboseTransaction['hex'] as String;
+ if (verboseTransaction.isEmpty) {
+ inputTransactionHex = await electrumClient.getTransactionHex(hash: vin.txId);
+ } else {
+ inputTransactionHex = verboseTransaction['hex'] as String;
+ }
+
+ ins.add(inputTransactionHex.isEmpty ? null : BtcTransaction.fromRaw(inputTransactionHex));
+ } catch (_) {
+ ins.add(null);
}
-
- ins.add(BtcTransaction.fromRaw(inputTransactionHex));
}
return ElectrumTransactionBundle(
@@ -3204,22 +3183,11 @@ abstract class ElectrumWalletBase
}
}
- final ins = [];
final inputTxids = inputTxidsByHash[txid] ?? const [];
- bool allInputsPresent = true;
- for (final inputTxid in inputTxids) {
- final inTx = parsedInputTxById[inputTxid];
- if (inTx == null) {
- allInputsPresent = false;
- break;
- }
- ins.add(inTx);
- }
-
- if (!allInputsPresent || ins.length != original.inputs.length) {
- continue;
- }
+ final ins = [
+ for (final inputTxid in inputTxids) parsedInputTxById[inputTxid],
+ ];
bundles[txid] = ElectrumTransactionBundle(
original,
@@ -3796,6 +3764,7 @@ abstract class ElectrumWalletBase
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) continue;
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart
index d78c88d13..42dda7cb2 100644
--- a/cw_bitcoin/lib/lightning/lightning_wallet.dart
+++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart
@@ -123,7 +123,9 @@ class LightningWallet {
Future close() async {
_eventSubscription?.cancel();
- await _sdk?.disconnect();
+ try {
+ await _sdk?.disconnect();
+ } catch (_) {}
_logSubscription?.cancel();
}
@@ -439,6 +441,11 @@ class LightningWallet {
direction = TransactionDirection.incoming;
}
+ String? preimage;
+ if (payment.details != null && payment.details is PaymentDetails_Lightning) {
+ preimage = (payment.details as PaymentDetails_Lightning).htlcDetails.preimage;
+ }
+
return ElectrumTransactionInfo(
WalletType.bitcoin,
id: payment.id,
@@ -448,7 +455,10 @@ class LightningWallet {
fee: Money(payment.fees, currency),
date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000),
confirmations: payment.status == PaymentStatus.pending ? 0 : 10,
- additionalInfo: {"isLightning": true},
+ additionalInfo: {
+ "isLightning": true,
+ if (preimage != null) "preimage": preimage,
+ },
);
}
diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart
index 51cc8a4d8..2f9ae0b3c 100644
--- a/cw_bitcoin/lib/litecoin_wallet.dart
+++ b/cw_bitcoin/lib/litecoin_wallet.dart
@@ -497,22 +497,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mwebUtxosBox = await CakeHive.openBox(boxName);
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- // rename the hive box:
- final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
- final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ static Future copyMwebBox({
+ required String fromName,
+ required String toName,
+ }) async {
+ final oldBoxName = "${fromName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ final newBoxName = "${toName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ if (oldBoxName == newBoxName) return;
final oldBox = await CakeHive.openBox(oldBoxName);
- mwebUtxosBox = await CakeHive.openBox(newBoxName);
+ final newBox = await CakeHive.openBox(newBoxName);
for (final key in oldBox.keys) {
- final value = oldBox.get(key);
- await oldBox.delete(key);
- await mwebUtxosBox.put(key, value!);
+ await newBox.put(key, oldBox.get(key)!);
}
- oldBox.deleteFromDisk();
+ }
- await super.renameWalletFiles(newWalletName);
+ static Future deleteMwebBox(String name) async {
+ final boxName = "${name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
+ final box = await CakeHive.openBox(boxName);
+ await box.deleteFromDisk();
}
@action
diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart
index 739602e24..0156ee672 100644
--- a/cw_bitcoin/lib/litecoin_wallet_service.dart
+++ b/cw_bitcoin/lib/litecoin_wallet_service.dart
@@ -12,8 +12,6 @@ import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
-import 'package:cw_core/wallet_base.dart';
-import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:path_provider/path_provider.dart';
@@ -139,26 +137,11 @@ class LitecoinWalletService extends WalletService<
@override
Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await LitecoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
+ if (currentName == newName) return;
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
+ await LitecoinWalletBase.copyMwebBox(fromName: currentName, toName: newName);
+ await super.rename(currentName, password, newName);
+ await LitecoinWalletBase.deleteMwebBox(currentName);
}
@override
diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart
index 437fed3ff..4fa2e2a63 100644
--- a/cw_bitcoin/lib/payjoin/manager.dart
+++ b/cw_bitcoin/lib/payjoin/manager.dart
@@ -297,10 +297,10 @@ class PayjoinManager {
break;
case PayjoinReceiverRequestTypes.getCandidateInputs:
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
if (utxos.isEmpty) {
await _wallet.updateAllUnspents();
- utxos = _wallet.getUtxoWithPrivateKeys();
+ utxos = _wallet.getUtxoWithPrivateKeys(confirmedOnly: true);
}
mainToIsolateSendPort?.send({
'requestId': message['requestId'],
diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml
index b0752cf79..98e1ec46c 100644
--- a/cw_bitcoin/pubspec.yaml
+++ b/cw_bitcoin/pubspec.yaml
@@ -6,7 +6,7 @@ author: Cake Wallet
homepage: https://cakewallet.com
environment:
- sdk: ">=2.17.5 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
flutter: ">=1.20.0"
dependencies:
diff --git a/cw_bitcoin_cash/analysis_options.yaml b/cw_bitcoin_cash/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_bitcoin_cash/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
index 1861b21f4..c7316ec4c 100644
--- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
+++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart
@@ -1,13 +1,11 @@
import 'dart:io';
import 'package:bip39/bip39.dart';
-import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
@@ -99,29 +97,6 @@ class BitcoinCashWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await BitcoinCashWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect));
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
throw UnimplementedError(
diff --git a/cw_core/lib/amount/money_double.dart b/cw_core/lib/amount/money_double.dart
new file mode 100644
index 000000000..b3331c44b
--- /dev/null
+++ b/cw_core/lib/amount/money_double.dart
@@ -0,0 +1,13 @@
+import 'dart:math';
+import 'package:cw_core/amount/money.dart';
+import 'package:cw_core/currency.dart';
+
+extension ToMoney on double {
+ /// Turn a double representation of a currency amount to a proper Money representation
+ /// truncating currencies with more than 20 decimals because double can not handle more
+ Money? tryToMoney(Currency currency) =>
+ Money.tryParse(toStringAsFixed(min(currency.decimals, 20)), currency);
+
+ Money toMoney(Currency currency) =>
+ Money.parse(toStringAsFixed(min(currency.decimals, 20)), currency);
+}
diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart
index fede4df09..51a6a7a58 100644
--- a/cw_core/lib/parse_fixed.dart
+++ b/cw_core/lib/parse_fixed.dart
@@ -29,6 +29,13 @@ BigInt? tryParseFixed(String value, int decimals) {
BigInt parseFixed(String value, int decimals) {
final multiplier = getMultiplier(decimals);
+ /// handle weird cases where users enter spaces and currency after the amount
+ /// This should be handled from UI field to prevent non numerical values
+ /// but will be in the refactoring
+ if (value.contains(" ")) {
+ value = value.split(" ").first;
+ }
+
final negative = value.startsWith("-");
if (negative) value = value.substring(1);
diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart
index cb5a4fe78..9fe5f717a 100644
--- a/cw_core/lib/pathForWallet.dart
+++ b/cw_core/lib/pathForWallet.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
Future pathForWalletTypeDir({required WalletType type}) async {
final root = await getAppDir();
@@ -36,3 +37,43 @@ Future outdatedAndroidPathForWalletDir({required String name}) async {
return pathDir;
}
+
+Future copyWalletFilesTo({
+ required String fromName,
+ required String toName,
+ required WalletType type,
+}) async {
+ if (fromName == toName) return;
+
+ final typeRoot = await pathForWalletTypeDir(type: type);
+ final sourceDir = Directory(p.join(typeRoot, fromName));
+ if (!sourceDir.existsSync()) {
+ throw "Source wallet not found: $fromName $type";
+ }
+
+ if (Directory(p.join(typeRoot, toName)).existsSync()) {
+ throw Exception('Cannot rename wallet: "$toName" already exists');
+ }
+
+ final destinationDir = Directory(p.join(typeRoot, toName));
+ await _copyDirectory(sourceDir, destinationDir);
+
+ for (final suffix in const ['', '.keys', '.keys.backup']) {
+ final file = File(p.join(destinationDir.path, '$fromName$suffix'));
+ if (file.existsSync()) {
+ await file.rename(p.join(destinationDir.path, '$toName$suffix'));
+ }
+ }
+}
+
+Future _copyDirectory(Directory source, Directory destination) async {
+ await destination.create(recursive: true);
+ await for (final entity in source.list(followLinks: false)) {
+ final name = p.basename(entity.path);
+ if (entity is File) {
+ await entity.copy(p.join(destination.path, name));
+ } else if (entity is Directory) {
+ await _copyDirectory(entity, Directory(p.join(destination.path, name)));
+ }
+ }
+}
diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart
index daf56af2f..9e4191072 100644
--- a/cw_core/lib/payment_uris.dart
+++ b/cw_core/lib/payment_uris.dart
@@ -7,6 +7,13 @@ abstract class PaymentURI {
final String address;
}
+class ExternalAddressURI extends PaymentURI {
+ ExternalAddressURI({required super.amount, required super.address});
+
+ @override
+ String toString() => address;
+}
+
class MoneroURI extends PaymentURI {
MoneroURI({required super.amount, required super.address});
diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart
index ccbb8aa34..765afaa58 100644
--- a/cw_core/lib/wallet_addresses.dart
+++ b/cw_core/lib/wallet_addresses.dart
@@ -71,11 +71,12 @@ abstract class WalletAddresses {
Future saveAddressesInBox() async {
try {
walletInfo.address = address;
- walletInfo.setAddresses(addressesMap);
- walletInfo.setAddressInfos(addressInfos);
- walletInfo.setUsedAddresses(usedAddresses.toList());
- walletInfo.setHiddenAddresses(hiddenAddresses.toList());
- walletInfo.setManualAddresses(manualAddresses.toList());
+ // TODO: check if it will affect the performance of each wallet
+ await walletInfo.setAddresses(addressesMap);
+ await walletInfo.setAddressInfos(addressInfos);
+ await walletInfo.setUsedAddresses(usedAddresses.toList());
+ await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
+ await walletInfo.setManualAddresses(manualAddresses.toList());
await walletInfo.save();
} catch (e) {
diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart
index 917d86ef4..89d6356da 100644
--- a/cw_core/lib/wallet_base.dart
+++ b/cw_core/lib/wallet_base.dart
@@ -12,6 +12,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:cw_core/pathForWallet.dart';
abstract class WalletBase {
@@ -115,7 +116,8 @@ abstract class WalletBase null;
- Future renameWalletFiles(String newWalletName);
+ Future renameWalletFiles(String newWalletName) =>
+ copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
Future signMessage(String message, {String? address = null});
diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart
index 5877ad2da..a43bfa154 100644
--- a/cw_core/lib/wallet_info.dart
+++ b/cw_core/lib/wallet_info.dart
@@ -428,11 +428,9 @@ class WalletInfo {
Future setAddresses(Map addresses) async {
await WalletInfoAddressMap.deleteByWalletInfoId(internalId);
- final keys = addresses.keys.toList();
- final values = addresses.values.toList();
- // ToDo: check why the addresses list gets changed half way through
- for (int i = 0; i < keys.length; i++) {
- await WalletInfoAddressMap.insert(internalId, keys[i], values[i]);
+ final entries = addresses.entries.toList();
+ for (final entry in entries) {
+ await WalletInfoAddressMap.insert(internalId, entry.key, entry.value);
}
}
diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart
index 016f3cc68..8d19a9f8c 100644
--- a/cw_core/lib/wallet_service.dart
+++ b/cw_core/lib/wallet_service.dart
@@ -3,9 +3,12 @@ import 'dart:io';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/file.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
+import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
abstract class WalletService {
@@ -25,7 +28,30 @@ abstract class WalletService remove(String wallet);
- Future rename(String currentName, String password, String newName);
+ Future rename(String currentName, String password, String newName) async {
+ if (currentName == newName) return;
+
+ final currentWalletInfo = await WalletInfo.get(currentName, getType());
+ if (currentWalletInfo == null) {
+ throw Exception('Wallet not found');
+ }
+
+ await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType());
+ await saveBackup(newName);
+
+ currentWalletInfo.id = WalletBase.idFor(newName, getType());
+ currentWalletInfo.name = newName;
+ await currentWalletInfo.save();
+
+ final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
+ if (oldDir.existsSync()) {
+ try {
+ await oldDir.delete(recursive: true);
+ } catch (e) {
+ printV('rename: failed to delete old wallet dir "$currentName": $e');
+ }
+ }
+ }
Future restoreWalletFilesFromBackup(String name) async {
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
diff --git a/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart b/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart
new file mode 100644
index 000000000..69d0ea7f7
--- /dev/null
+++ b/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart
@@ -0,0 +1,62 @@
+import "package:analyzer/analysis_rule/analysis_rule.dart";
+import "package:analyzer/analysis_rule/rule_context.dart";
+import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
+import "package:analyzer/dart/ast/ast.dart";
+import "package:analyzer/dart/ast/visitor.dart";
+import "package:analyzer/error/error.dart";
+
+class HttpForceProxyRule extends AnalysisRule {
+ HttpForceProxyRule()
+ : super(
+ name: "no_http_imports",
+ description:
+ "Using the http package breaks proxy integration. Please use ProxyWrapper or alias it as \"very_insecure_http_do_not_use\".",
+ );
+
+ static const LintCode code = LintCode(
+ "no_http_imports",
+ "Using the http package breaks proxy integration. Please use ProxyWrapper or alias it as \"very_insecure_http_do_not_use\".",
+ severity: DiagnosticSeverity.WARNING,
+ );
+
+ @override
+ LintCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(
+ RuleVisitorRegistry registry,
+ RuleContext context,
+ ) {
+ final filePath = context.definingUnit.file.path;
+
+ if (filePath.endsWith("proxy_wrapper.dart")) {
+ return;
+ }
+
+ registry.addImportDirective(this, _Visitor(this));
+ }
+}
+
+class _Visitor extends SimpleAstVisitor {
+ _Visitor(this.rule);
+
+ final AnalysisRule rule;
+
+ @override
+ void visitImportDirective(ImportDirective node) {
+ final uriString = node.uri.stringValue;
+ if (uriString == null) {
+ return;
+ }
+
+ if (uriString.startsWith("package:http")) {
+ final prefixNode = node.prefix;
+ if (prefixNode != null &&
+ prefixNode.name == "very_insecure_http_do_not_use") {
+ return;
+ }
+
+ rule.reportAtNode(node);
+ }
+ }
+}
diff --git a/cw_custom_lints/lib/main.dart b/cw_custom_lints/lib/main.dart
new file mode 100644
index 000000000..39e07ba30
--- /dev/null
+++ b/cw_custom_lints/lib/main.dart
@@ -0,0 +1,22 @@
+import "package:analysis_server_plugin/plugin.dart";
+import "package:analysis_server_plugin/registry.dart";
+import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart";
+import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart";
+import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart";
+import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart";
+
+final plugin = CwCustomLintsPlugin();
+
+class CwCustomLintsPlugin extends Plugin {
+ @override
+ String get name => "cw_custom_lints";
+
+ @override
+ void register(PluginRegistry registry) {
+ registry.registerWarningRule(PrintVerboseRule());
+ registry.registerWarningRule(RestrictedImportsRule());
+ registry.registerWarningRule(HttpForceProxyRule());
+
+ registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new);
+ }
+}
diff --git a/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart b/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart
new file mode 100644
index 000000000..b7a2daf52
--- /dev/null
+++ b/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart
@@ -0,0 +1,50 @@
+import "package:analysis_server_plugin/edit/dart/correction_producer.dart";
+import "package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart";
+import "package:analyzer/dart/ast/ast.dart";
+import "package:analyzer_plugin/utilities/change_builder/change_builder_core.dart";
+import "package:analyzer_plugin/utilities/fixes/fixes.dart";
+import "package:analyzer_plugin/utilities/range_factory.dart";
+
+class ReplaceWithPrintV extends ResolvedCorrectionProducer {
+ ReplaceWithPrintV({required super.context});
+
+ static const _importString = "package:cw_core/utils/print_verbose.dart";
+
+ static const _fixKind = FixKind(
+ "dart.fix.replaceWithPrintV",
+ DartFixKindPriority.standard,
+ "Replace with printV()",
+ );
+
+ @override
+ CorrectionApplicability get applicability =>
+ CorrectionApplicability.singleLocation;
+
+ @override
+ FixKind get fixKind => _fixKind;
+
+ @override
+ Future compute(ChangeBuilder builder) async {
+ final invocation = node.thisOrAncestorOfType();
+ if (invocation == null) {
+ return;
+ }
+
+ final root = invocation.root;
+ final hasImport = root is CompilationUnit &&
+ root.directives.whereType().any(
+ (directive) => directive.uri.stringValue == _importString,
+ );
+
+ await builder.addDartFileEdit(file, (builder) {
+ builder.addSimpleReplacement(
+ range.node(invocation.methodName),
+ "printV",
+ );
+
+ if (!hasImport) {
+ builder.importLibrary(Uri.parse(_importString));
+ }
+ });
+ }
+}
diff --git a/cw_custom_lints/lib/print_verbose/print_verbose_rule.dart b/cw_custom_lints/lib/print_verbose/print_verbose_rule.dart
new file mode 100644
index 000000000..0fee39bc6
--- /dev/null
+++ b/cw_custom_lints/lib/print_verbose/print_verbose_rule.dart
@@ -0,0 +1,52 @@
+import "package:analyzer/analysis_rule/analysis_rule.dart";
+import "package:analyzer/analysis_rule/rule_context.dart";
+import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
+import "package:analyzer/dart/ast/ast.dart";
+import "package:analyzer/dart/ast/visitor.dart";
+import "package:analyzer/error/error.dart";
+
+class PrintVerboseRule extends AnalysisRule {
+ PrintVerboseRule()
+ : super(
+ name: "use_print_v",
+ description: "Use printV() from cw_core instead of print().",
+ );
+
+ static const LintCode code = LintCode(
+ "use_print_v",
+ "Use printV() from cw_core instead",
+ correctionMessage: "Replace print with printV",
+ severity: DiagnosticSeverity.WARNING,
+ );
+
+ @override
+ LintCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(
+ RuleVisitorRegistry registry,
+ RuleContext context,
+ ) {
+ final filePath = context.definingUnit.file.path;
+
+ if (filePath.contains("/tool/") || filePath.contains("print_verbose.dart")) {
+ // tool/ is allowed to use print as it never makes its way into the app
+ return;
+ }
+
+ registry.addMethodInvocation(this, _Visitor(this));
+ }
+}
+
+class _Visitor extends SimpleAstVisitor {
+ _Visitor(this.rule);
+
+ final AnalysisRule rule;
+
+ @override
+ void visitMethodInvocation(MethodInvocation node) {
+ if (node.methodName.name == "print" && node.target == null) {
+ rule.reportAtNode(node);
+ }
+ }
+}
diff --git a/cw_custom_lints/lib/restricted_imports/restricted_imports_rule.dart b/cw_custom_lints/lib/restricted_imports/restricted_imports_rule.dart
new file mode 100644
index 000000000..80680af87
--- /dev/null
+++ b/cw_custom_lints/lib/restricted_imports/restricted_imports_rule.dart
@@ -0,0 +1,79 @@
+import "package:analyzer/analysis_rule/analysis_rule.dart";
+import "package:analyzer/analysis_rule/rule_context.dart";
+import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
+import "package:analyzer/dart/ast/ast.dart";
+import "package:analyzer/dart/ast/visitor.dart";
+import "package:analyzer/error/error.dart";
+
+class RestrictedImportsRule extends AnalysisRule {
+ RestrictedImportsRule()
+ : super(
+ name: "no_restricted_imports_in_lib",
+ description:
+ "Do not import from coin-specific packages. If necessary, wire your logic through configure.dart",
+ );
+
+ static const LintCode code = LintCode(
+ "no_restricted_imports_in_lib",
+ "Do not import from coin-specific packages. If necessary, wire your logic through configure.dart",
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ static const _restrictedPackages = {
+ "cw_bitcoin",
+ "cw_bitcoin_cash",
+ "cw_electrum",
+ "cw_litecoin",
+ "cw_evm",
+ "cw_mweb",
+ "cw_nano",
+ "cw_solana",
+ "cw_tron",
+ "cw_zano",
+ };
+
+ @override
+ LintCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(
+ RuleVisitorRegistry registry,
+ RuleContext context,
+ ) {
+ final filePath = context.definingUnit.file.path;
+ final fileName = filePath.split("/").last;
+
+ // ignore cw_something folders
+ if (filePath.contains("cw_")) {
+ return;
+ }
+
+ // allow ex. cw_bitcoin in bitcoin.dart
+ if (_restrictedPackages.contains("cw_" + fileName.replaceAll(".dart", ""))) {
+ return;
+ }
+
+ registry.addImportDirective(this, _Visitor(this));
+ }
+}
+
+class _Visitor extends SimpleAstVisitor {
+ _Visitor(this.rule);
+
+ final AnalysisRule rule;
+
+ @override
+ void visitImportDirective(ImportDirective node) {
+ final uriString = node.uri.stringValue;
+ if (uriString == null || !uriString.startsWith("package:")) {
+ return;
+ }
+
+ final pathWithoutScheme = uriString.replaceFirst("package:", "");
+ final packageName = pathWithoutScheme.split("/").first;
+
+ if (RestrictedImportsRule._restrictedPackages.contains(packageName)) {
+ rule.reportAtNode(node);
+ }
+ }
+}
diff --git a/cw_custom_lints/pubspec.yaml b/cw_custom_lints/pubspec.yaml
new file mode 100644
index 000000000..399fa59c6
--- /dev/null
+++ b/cw_custom_lints/pubspec.yaml
@@ -0,0 +1,15 @@
+name: cw_custom_lints
+version: 0.0.1
+environment:
+ sdk: '>=3.9.0 <4.0.0'
+
+
+dependencies:
+ analysis_server_plugin: 0.3.15
+ analyzer: 13.0.0
+ analyzer_plugin: 0.14.9
+
+dev_dependencies:
+ analyzer_testing: 0.2.6
+ test_reflective_loader: 0.4.0
+ test: 1.31.2
diff --git a/cw_decred/analysis_options.yaml b/cw_decred/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_decred/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_dogecoin/analysis_options.yaml b/cw_dogecoin/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_dogecoin/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_service.dart b/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
index 988abd2e7..1a78e14ff 100644
--- a/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
+++ b/cw_dogecoin/lib/src/dogecoin_wallet_service.dart
@@ -1,12 +1,10 @@
import 'dart:io';
import 'package:bip39/bip39.dart';
-import 'package:collection/collection.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart';
-import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
@@ -100,29 +98,6 @@ class DogeCoinWalletService extends WalletService<
}
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await DogeCoinWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- unspentCoinsInfo: unspentCoinsInfoSource,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect));
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future restoreFromHardwareWallet(DogeCoinNewWalletCredentials credentials) {
throw UnimplementedError(
diff --git a/cw_evm/analysis_options.yaml b/cw_evm/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_evm/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_evm/lib/clients/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart
index 2751e94bb..21973e30c 100644
--- a/cw_evm/lib/clients/evm_chain_client.dart
+++ b/cw_evm/lib/clients/evm_chain_client.dart
@@ -618,74 +618,117 @@ class EVMChainClient {
return [];
}
- final uri = Uri.https(
- 'deep-index.moralis.io',
- '/api/v2.2/$address/erc20',
- {
- "chain": chainName,
- },
- );
-
- final response = await client.get(
- uri,
- headers: {
- "Accept": "application/json",
- "X-API-Key": secrets.moralisApiKey,
- },
- );
-
- if (response.statusCode < 200 || response.statusCode >= 300) {
- printV('Moralis API returned invalid status code: ${response.statusCode}');
- return [];
- }
-
- final decodedResponse = jsonDecode(response.body) as List;
-
+ const maxPages = 3;
+ String? cursor;
+ int pageCount = 0;
final List tokens = [];
- for (final item in decodedResponse) {
- final tokenData = item as Map;
+ do {
+ final params = {
+ "chain": chainName,
+ if (cursor != null && cursor.isNotEmpty) "cursor": cursor,
+ };
- final balanceStr = tokenData['balance'] as String? ?? '0';
- final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
- if (balanceWei == BigInt.zero) continue;
-
- final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
- final name = (tokenData['name'] as String? ?? '').toString();
- final symbol = (tokenData['symbol'] as String? ?? '').toString();
- final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
-
- final decimalsRaw = tokenData['decimals'];
- final decimals =
- decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
-
- final logo = tokenData['logo'] as String?;
- final thumbnail = tokenData['thumbnail'] as String?;
- final iconUrl = logo ?? thumbnail;
-
- final possibleSpamRaw = tokenData['possible_spam'];
- final possibleSpam = possibleSpamRaw is bool
- ? possibleSpamRaw
- : (possibleSpamRaw.toString().toLowerCase() == 'true');
-
- final verifiedContractRaw = tokenData['verified_contract'];
- final verifiedContract = verifiedContractRaw is bool
- ? verifiedContractRaw
- : (verifiedContractRaw.toString().toLowerCase() == 'true');
-
- tokens.add(
- MoralisWalletTokenBalance(
- contractAddress: contractAddress,
- name: name,
- symbol: symbolFiltered,
- decimals: decimals,
- iconUrl: iconUrl,
- balanceWei: balanceWei,
- possibleSpam: possibleSpam,
- verifiedContract: verifiedContract,
- ),
+ final uri = Uri.https(
+ 'deep-index.moralis.io',
+ '/api/v2.2/wallets/$address/tokens',
+ params,
);
- }
+
+ final response = await client.get(
+ uri,
+ headers: {
+ "Accept": "application/json",
+ "X-API-Key": secrets.moralisApiKey,
+ },
+ );
+
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ printV('Moralis API returned invalid status code: ${response.statusCode}');
+ return tokens;
+ }
+
+ final decoded = jsonDecode(response.body);
+ if (decoded is! Map) return tokens;
+
+ final result = decoded['result'];
+ if (result is! List) return tokens;
+
+ for (final item in result) {
+ if (item is! Map) continue;
+ final tokenData = item;
+
+ final nativeRaw = tokenData['native_token'];
+ final nativeToken = nativeRaw is bool
+ ? nativeRaw
+ : (nativeRaw?.toString().toLowerCase() == 'true');
+ if (nativeToken) continue;
+
+ final balanceStr = tokenData['balance'] as String? ?? '0';
+ final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
+ if (balanceWei == BigInt.zero) continue;
+
+ final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
+ final name = (tokenData['name'] as String? ?? '').toString();
+ final symbol = (tokenData['symbol'] as String? ?? '').toString();
+ final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
+
+ final decimalsRaw = tokenData['decimals'];
+ final decimals =
+ decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
+
+ final logo = tokenData['logo'] as String?;
+ final thumbnail = tokenData['thumbnail'] as String?;
+ final iconUrl = logo ?? thumbnail;
+
+ final possibleSpamRaw = tokenData['possible_spam'];
+ final possibleSpam = possibleSpamRaw is bool
+ ? possibleSpamRaw
+ : (possibleSpamRaw?.toString().toLowerCase() == 'true');
+
+ final verifiedContractRaw = tokenData['verified_contract'];
+ final verifiedContract = verifiedContractRaw is bool
+ ? verifiedContractRaw
+ : (verifiedContractRaw?.toString().toLowerCase() == 'true');
+
+ final usdPriceRaw = tokenData['usd_price'];
+ final double? usdPrice = usdPriceRaw is num
+ ? usdPriceRaw.toDouble()
+ : (usdPriceRaw is String ? double.tryParse(usdPriceRaw) : null);
+
+ final usdValueRaw = tokenData['usd_value'];
+ final double? usdValue = usdValueRaw is num
+ ? usdValueRaw.toDouble()
+ : (usdValueRaw is String ? double.tryParse(usdValueRaw) : null);
+
+ final securityRaw = tokenData['security_score'];
+ final int? securityScore = securityRaw is int
+ ? securityRaw
+ : (securityRaw is num
+ ? securityRaw.toInt()
+ : (securityRaw is String ? int.tryParse(securityRaw) : null));
+
+ tokens.add(
+ MoralisWalletTokenBalance(
+ contractAddress: contractAddress,
+ name: name,
+ symbol: symbolFiltered,
+ decimals: decimals,
+ iconUrl: iconUrl,
+ balanceWei: balanceWei,
+ possibleSpam: possibleSpam,
+ verifiedContract: verifiedContract,
+ usdPrice: usdPrice,
+ usdValue: usdValue,
+ securityScore: securityScore,
+ ),
+ );
+ }
+
+ final nextCursor = decoded['cursor'];
+ cursor = nextCursor is String && nextCursor.isNotEmpty ? nextCursor : null;
+ pageCount++;
+ } while (cursor != null && pageCount < maxPages);
return tokens;
} catch (e, stackTrace) {
@@ -737,6 +780,9 @@ class MoralisWalletTokenBalance {
final BigInt balanceWei;
final bool possibleSpam;
final bool verifiedContract;
+ final double? usdPrice;
+ final double? usdValue;
+ final int? securityScore;
MoralisWalletTokenBalance({
required this.contractAddress,
@@ -747,5 +793,8 @@ class MoralisWalletTokenBalance {
required this.balanceWei,
required this.possibleSpam,
required this.verifiedContract,
+ this.usdPrice,
+ this.usdValue,
+ this.securityScore,
});
}
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index 4b002e396..d404013fb 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'dart:typed_data';
import 'package:bip32/bip32.dart' as bip32;
@@ -545,67 +544,112 @@ abstract class EVMChainWalletBase
await save();
}
- bool isTokenPropertiesSuspicious(Erc20Token token) {
- bool isTokenWhitelisted = getDefaultTokenContractAddresses
- .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
+ static const _urlLikeSuspiciousMarkers = [
+ 't.me',
+ '.me',
+ 'telegram',
+ 'http',
+ 'https',
+ '.com',
+ '.org',
+ '.top',
+ '.live',
+ '.xyz',
+ 'www',
+ '🎁',
+ 'airdrop',
+ 'distribution',
+ ];
- final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
+ static final _suspiciousWordPattern =
+ RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
+
+ static const _knownNonEvmNativeSymbols = {
+ 'ICP',
+ 'SOL',
+ 'TRX',
+ 'ATOM',
+ 'DOT',
+ 'ADA',
+ 'XRP',
+ 'XLM',
+ 'XMR',
+ 'ALGO',
+ 'NEAR',
+ 'TON',
+ 'HBAR',
+ 'APT',
+ 'SUI',
+ 'KAS',
+ };
+
+ static bool _hasSuspiciousData(String normalized) {
+ final lower = normalized.toLowerCase();
+ if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
+ return _suspiciousWordPattern.hasMatch(lower);
+ }
+
+ bool isTokenPropertiesSuspicious(
+ Erc20Token token, {
+ Set? cachedWhitelistLower,
+ Set? cachedDefaultSymbolsUpper,
+ }) {
+ final whitelistLower = cachedWhitelistLower ??
+ getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
+ final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
+ EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
+
+ final isTokenWhitelisted = whitelistLower.contains(token.contractAddress.toLowerCase());
- // Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
- final suspiciousStrings = [
- 't.me',
- '.me',
- 'telegram',
- 'http',
- 'https',
- '.com',
- '.org',
- '.top',
- '.live',
- 'airdrop',
- 'reward',
- 'distribution',
- 'www',
- '.xyz',
- '🎁',
- 'bot',
- 'claim',
- 'reward',
- ];
-
- final hasSuspiciousData = suspiciousStrings.any(
- (element) =>
- normalizedName.toLowerCase().contains(element) ||
- normalizedSymbol.toLowerCase().contains(element) ||
- normalizedTitle.toLowerCase().contains(element),
- );
+ final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
+ _hasSuspiciousData(normalizedSymbol) ||
+ _hasSuspiciousData(normalizedTitle);
final nativeSymbol = currency.title.toUpperCase();
final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
final hasSuspiciousDefaultTokenSymbol =
- defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+ defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
- return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
+ final hasSuspiciousNonEvmNativeSymbol =
+ _knownNonEvmNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ return hasSuspiciousData ||
+ hasSuspiciousNativeSymbol ||
+ hasSuspiciousDefaultTokenSymbol ||
+ hasSuspiciousNonEvmNativeSymbol;
}
- Future _checkForExistingScamTokens() async {
- for (var token in erc20Currencies) {
- bool isPotentialScam = false;
+ String get _scamCheckDoneKey => 'evm_scam_check_v2_done_${walletInfo.name}';
- if (isTokenPropertiesSuspicious(token)) {
- isPotentialScam = true;
+ Future _checkForExistingScamTokens() async {
+ final prefs = await sharedPrefs.future;
+ if (prefs.getBool(_scamCheckDoneKey) == true) return;
+
+ final whitelistLower =
+ getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
+ final defaultSymbolsUpper =
+ EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
+
+ for (var token in erc20Currencies) {
+ final suspicious = isTokenPropertiesSuspicious(
+ token,
+ cachedWhitelistLower: whitelistLower,
+ cachedDefaultSymbolsUpper: defaultSymbolsUpper,
+ );
+
+ if (suspicious && !token.isPotentialScam) {
token.isPotentialScam = true;
token.iconPath = null;
await token.save();
+ continue;
}
- // For fixing wrongly classified tokens
- if (!isPotentialScam && token.isPotentialScam) {
+ if (!suspicious && token.isPotentialScam) {
token.isPotentialScam = false;
if (token.iconPath == null || token.iconPath!.isEmpty) {
@@ -621,6 +665,8 @@ abstract class EVMChainWalletBase
await token.save();
}
}
+
+ await prefs.setBool(_scamCheckDoneKey, true);
}
Future discoverTokensFromMoralis() async {
@@ -672,6 +718,9 @@ abstract class EVMChainWalletBase
DiscoveredToken(
token: newToken,
balanceWei: token.balanceWei,
+ verifiedContract: token.verifiedContract,
+ moralisUsdPrice: token.usdPrice,
+ moralisUsdValue: token.usdValue,
),
);
}
@@ -1653,30 +1702,6 @@ abstract class EVMChainWalletBase
);
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final transactionHistoryFileNameForWallet = getTransactionHistoryFileName();
-
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
@@ -1757,10 +1782,16 @@ class GasParamsHandler {
class DiscoveredToken {
final Erc20Token token;
final BigInt balanceWei;
+ final bool verifiedContract;
+ final double? moralisUsdPrice;
+ final double? moralisUsdValue;
const DiscoveredToken({
required this.token,
required this.balanceWei,
+ required this.verifiedContract,
+ this.moralisUsdPrice,
+ this.moralisUsdValue,
});
}
diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart
index d63686225..425bdceb8 100644
--- a/cw_evm/lib/evm_chain_wallet_service.dart
+++ b/cw_evm/lib/evm_chain_wallet_service.dart
@@ -3,10 +3,12 @@ import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.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_service.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:path/path.dart' as p;
import 'package:cw_evm/clients/evm_chain_client.dart';
import 'package:cw_evm/evm_chain_client_factory.dart';
import 'package:cw_evm/evm_chain_registry.dart';
@@ -166,29 +168,30 @@ class EVMChainWalletService extends WalletService<
@override
Future rename(String currentName, String password, String newName) async {
+ if (currentName == newName) return;
+
final currentWalletInfo = await _findWalletByName(currentName);
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
- final currentWallet = await _openWalletInstance(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
+ final type = currentWalletInfo.type;
- await currentWallet.renameWalletFiles(newName);
+ await copyWalletFilesTo(fromName: currentName, toName: newName, type: type);
+ await saveBackup(newName, walletInfo: currentWalletInfo);
- // Update walletInfo with new name before saving backup
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, currentWalletInfo.type);
- newWalletInfo.name = newName;
+ currentWalletInfo.id = WalletBase.idFor(newName, type);
+ currentWalletInfo.name = newName;
+ await currentWalletInfo.save();
- // Pass walletInfo to saveBackup to avoid lookup (since WalletInfo not saved yet)
- await saveBackup(newName, walletInfo: newWalletInfo);
-
- await newWalletInfo.save();
+ final oldDir = Directory(p.join(await pathForWalletTypeDir(type: type), currentName));
+ if (oldDir.existsSync()) {
+ try {
+ await oldDir.delete(recursive: true);
+ } catch (e) {
+ printV('rename: failed to delete old wallet dir "$currentName": $e');
+ }
+ }
}
@override
diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml
index 0186841c5..5cc563e6a 100644
--- a/cw_evm/pubspec.yaml
+++ b/cw_evm/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
http: ^1.1.0
hive: ^2.2.3
collection: ^1.17.1
+ path: ^1.8.0
shared_preferences: ^2.0.15
mobx: ^2.0.7+4
cw_core:
diff --git a/cw_monero/analysis_options.yaml b/cw_monero/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_monero/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_mweb/analysis_options.yaml b/cw_mweb/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_mweb/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml
index a3ef2ea67..449b1b8f5 100644
--- a/cw_nano/pubspec.yaml
+++ b/cw_nano/pubspec.yaml
@@ -6,7 +6,7 @@ author: Cake Wallet
homepage: https://cakewallet.com
environment:
- sdk: '>=2.18.2 <3.0.0'
+ sdk: '>=3.0.0 <4.0.0'
flutter: ">=1.17.0"
dependencies:
diff --git a/cw_solana/analysis_options.yaml b/cw_solana/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_solana/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
index ad72c74a9..3bea89e76 100644
--- a/cw_solana/lib/solana_client.dart
+++ b/cw_solana/lib/solana_client.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/amount/money.dart';
+import 'package:cw_core/amount/money_double.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/node.dart';
@@ -654,7 +655,7 @@ class SolanaWalletClient {
from: outgoingFrom,
to: outgoingTo,
id: outgoingId,
- amount: Money.parse(outgoingAmount.toStringAsFixed(outgoingToken.decimals), outgoingToken),
+ amount: outgoingAmount.toMoney(outgoingToken),
programId: outgoingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
@@ -672,7 +673,7 @@ class SolanaWalletClient {
from: incomingFrom,
to: incomingTo,
id: incomingId,
- amount: Money.parse(incomingAmount.toStringAsFixed(incomingToken.decimals), incomingToken),
+ amount: incomingAmount.toMoney(incomingToken),
programId: incomingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
@@ -869,8 +870,7 @@ class SolanaWalletClient {
from: sender,
to: receiver,
id: signature,
- amount: Money.parse(amount.toStringAsFixed((splToken ?? CryptoCurrency.sol).decimals),
- splToken ?? CryptoCurrency.sol),
+ amount: amount.toMoney(splToken ?? CryptoCurrency.sol),
programId: SPLTokenProgramConst.tokenProgramId.address,
blockTimeInInt: blockTime?.toInt() ?? 0,
fee: Money.fromInt(fee, CryptoCurrency.sol),
diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart
index 48a1b0e71..b5b9309fb 100644
--- a/cw_solana/lib/solana_wallet.dart
+++ b/cw_solana/lib/solana_wallet.dart
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/cake_hive.dart';
@@ -12,6 +11,7 @@ import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
+import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
@@ -135,6 +135,8 @@ abstract class SolanaWalletBase
splTokensBox = await CakeHive.openBox(boxName);
+ await _checkForExistingScamTokens();
+
// Create the privatekey using either the mnemonic or the privateKey
_solanaPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
@@ -153,6 +155,36 @@ abstract class SolanaWalletBase
await save();
}
+ String get _scamCheckDoneKey => 'solana_scam_check_v2_done_${walletInfo.name}';
+
+ Future _checkForExistingScamTokens() async {
+ if (!splTokensBox.isOpen) return;
+
+ final prefs = await _sharedPrefs.future;
+ if (prefs.getBool(_scamCheckDoneKey) == true) return;
+
+ final defaultMints =
+ DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
+ final defaultSymbolsUpper = DefaultSPLTokens()
+ .initialSPLTokens
+ .map((t) => t.symbol.toUpperCase())
+ .toSet();
+
+ for (final token in splTokensBox.values) {
+ final suspicious = isTokenPropertiesSuspicious(
+ token,
+ cachedDefaultMints: defaultMints,
+ cachedDefaultSymbolsUpper: defaultSymbolsUpper,
+ );
+ if (suspicious && !token.isPotentialScam) {
+ token.isPotentialScam = true;
+ await token.save();
+ }
+ }
+
+ await prefs.setBool(_scamCheckDoneKey, true);
+ }
+
Future getPrivateKey({
String? mnemonic,
String? privateKey,
@@ -671,7 +703,98 @@ abstract class SolanaWalletBase
}
}
+ static const _urlLikeSuspiciousMarkers = [
+ 't.me',
+ '.me',
+ 'telegram',
+ 'http',
+ 'https',
+ '.com',
+ '.org',
+ '.top',
+ '.live',
+ '.xyz',
+ 'www',
+ '🎁',
+ 'airdrop',
+ 'distribution',
+ ];
+
+ static final _suspiciousWordPattern =
+ RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
+
+ static const _knownNonSolanaNativeSymbols = {
+ 'BTC',
+ 'ETH',
+ 'BNB',
+ 'AVAX',
+ 'MATIC',
+ 'POL',
+ 'ICP',
+ 'TRX',
+ 'ATOM',
+ 'DOT',
+ 'ADA',
+ 'XRP',
+ 'XLM',
+ 'XMR',
+ 'ALGO',
+ 'NEAR',
+ 'TON',
+ 'HBAR',
+ 'APT',
+ 'SUI',
+ 'KAS',
+ };
+
+ static bool _hasSuspiciousData(String normalized) {
+ final lower = normalized.toLowerCase();
+ if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
+ return _suspiciousWordPattern.hasMatch(lower);
+ }
+
+ bool isTokenPropertiesSuspicious(
+ SPLToken token, {
+ Set? cachedDefaultMints,
+ Set? cachedDefaultSymbolsUpper,
+ }) {
+ final defaultMints = cachedDefaultMints ??
+ DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
+ final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
+ DefaultSPLTokens()
+ .initialSPLTokens
+ .map((t) => t.symbol.toUpperCase())
+ .toSet();
+
+ final isTokenWhitelisted = defaultMints.contains(token.mintAddress);
+
+ final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
+ final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
+ final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
+
+ final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
+ _hasSuspiciousData(normalizedSymbol) ||
+ _hasSuspiciousData(normalizedTitle);
+
+ const nativeSymbol = 'SOL';
+ final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
+
+ final hasSuspiciousDefaultTokenSymbol =
+ defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ final hasSuspiciousNonSolanaNativeSymbol =
+ _knownNonSolanaNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
+
+ return hasSuspiciousData ||
+ hasSuspiciousNativeSymbol ||
+ hasSuspiciousDefaultTokenSymbol ||
+ hasSuspiciousNonSolanaNativeSymbol;
+ }
+
Future addSPLToken(SPLToken token) async {
+ final isSuspicious = isTokenPropertiesSuspicious(token);
+ token.isPotentialScam = token.isPotentialScam || isSuspicious;
+
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
@@ -709,28 +832,6 @@ abstract class SolanaWalletBase
}
}
- @override
- Future renameWalletFiles(String newWalletName) async {
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart
index a16f3ee6f..d1fce3b20 100644
--- a/cw_solana/lib/solana_wallet_service.dart
+++ b/cw_solana/lib/solana_wallet_service.dart
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
-import 'package:collection/collection.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
@@ -14,7 +13,6 @@ 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:hive/hive.dart';
class SolanaWalletService extends WalletService {
@@ -138,29 +136,6 @@ class SolanaWalletService extends WalletService rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await SolanaWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
diff --git a/cw_tron/analysis_options.yaml b/cw_tron/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_tron/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart
index 1a4d98b48..24fa83f15 100644
--- a/cw_tron/lib/tron_wallet.dart
+++ b/cw_tron/lib/tron_wallet.dart
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
-import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/blockchain_utils.dart';
@@ -607,30 +606,6 @@ abstract class TronWalletBase
Future getTronToken(String contractAddress) async =>
await _client.getTronToken(contractAddress, _tronAddress);
- @override
- Future renameWalletFiles(String newWalletName) async {
- const transactionHistoryFileNameForWallet = 'tron_transactions.json';
-
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
- final currentWalletFile = File(currentWalletPath);
-
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
- final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
-
- // Copies current wallet files into new wallet name's dir and files
- if (currentWalletFile.existsSync()) {
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- await currentWalletFile.copy(newWalletPath);
- }
- if (currentTransactionsFile.existsSync()) {
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
- await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
- }
-
- // Delete old name's dir and files
- await Directory(currentDirPath).delete(recursive: true);
- }
-
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart
index cf83b9158..9b81a9a57 100644
--- a/cw_tron/lib/tron_wallet_service.dart
+++ b/cw_tron/lib/tron_wallet_service.dart
@@ -14,7 +14,6 @@ import 'package:cw_tron/tron_client.dart';
import 'package:cw_tron/tron_exception.dart';
import 'package:cw_tron/tron_wallet.dart';
import 'package:cw_tron/tron_wallet_creation_credentials.dart';
-import 'package:hive/hive.dart';
class TronWalletService extends WalletService<
TronNewWalletCredentials,
@@ -134,29 +133,6 @@ class TronWalletService extends WalletService<
return wallet;
}
- @override
- Future rename(String currentName, String password, String newName) async {
- final currentWalletInfo = await WalletInfo.get(currentName, getType());
- if (currentWalletInfo == null) {
- throw Exception('Wallet not found');
- }
- final currentWallet = await TronWalletBase.open(
- password: password,
- name: currentName,
- walletInfo: currentWalletInfo,
- encryptionFileUtils: encryptionFileUtilsFor(isDirect),
- );
-
- await currentWallet.renameWalletFiles(newName);
- await saveBackup(newName);
-
- final newWalletInfo = currentWalletInfo;
- newWalletInfo.id = WalletBase.idFor(newName, getType());
- newWalletInfo.name = newName;
-
- await newWalletInfo.save();
- }
-
@override
Future isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
diff --git a/cw_wownero/analysis_options.yaml b/cw_wownero/analysis_options.yaml
deleted file mode 100644
index a5744c1cf..000000000
--- a/cw_wownero/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml
index 7aa32af60..7e8ede8dd 100644
--- a/cw_zano/pubspec.yaml
+++ b/cw_zano/pubspec.yaml
@@ -5,7 +5,7 @@ publish_to: none
homepage: https://cakewallet.com
environment:
- sdk: ">=2.19.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
flutter: ">=1.20.0"
dependencies:
diff --git a/cw_zcash/analysis_options.yaml b/cw_zcash/analysis_options.yaml
deleted file mode 100644
index f43bd483d..000000000
--- a/cw_zcash/analysis_options.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-analyzer:
- errors:
- implementation_imports: ignore
- must_be_immutable: ignore
- overridden_fields: ignore
- exclude:
- - "**.g.dart"
- - "external/**"
-
-linter:
- rules:
- unawaited_futures: true
- avoid_print: false
- parameter_assignments: true
- prefer_const_declarations: true
- prefer_final_fields: true
- prefer_final_in_for_each: true
- prefer_final_locals: true
- prefer_final_parameters: true
- avoid_void_async: true
- require_trailing_commas: true
- sort_child_properties_last: true
- sort_constructors_first: true
- sort_unnamed_constructors_first: true
- unnecessary_async: true
- unnecessary_await_in_return: true
- unnecessary_string_interpolations: true
- always_declare_return_types: true
-
-formatter:
- page_width: 100
diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart
index a5d118322..724c9197f 100644
--- a/lib/bitcoin/cw_bitcoin.dart
+++ b/lib/bitcoin/cw_bitcoin.dart
@@ -177,7 +177,7 @@ class CWBitcoin extends Bitcoin {
List getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addressesByReceiveType
- .map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress(
+ .map((addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: addr.address,
diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart
index dfd09e130..9b9d5dd91 100644
--- a/lib/buy/moonpay/moonpay_provider.dart
+++ b/lib/buy/moonpay/moonpay_provider.dart
@@ -79,7 +79,7 @@ class MoonPayProvider extends BuyProvider {
@override
bool get isAggregator => false;
- static String get _apiKey => secrets.moonPayApiKey;
+ String get _apiKey => isTestEnvironment ? secrets.moonPaySandboxApiKey : secrets.moonPayApiKey;
String get currencyCode => walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title.toLowerCase();
@@ -96,8 +96,9 @@ class MoonPayProvider extends BuyProvider {
}
}
- Future getMoonpaySignature(String query) async {
- final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
+ Future getMoonpaySignedQuery(String query) async {
+ final uri =
+ Uri.https(_cIdBaseUrl, "/api/moonpay", isTestEnvironment ? {"useSandbox": "true"} : null);
final response = await ProxyWrapper().post(
clearnetUri: uri,
@@ -107,7 +108,8 @@ class MoonPayProvider extends BuyProvider {
if (response.statusCode == 200) {
- return (jsonDecode(response.body) as Map)['signature'] as String;
+ printV((jsonDecode(response.body) as Map));
+ return (jsonDecode(response.body) as Map)['query'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
@@ -297,18 +299,13 @@ class MoonPayProvider extends BuyProvider {
required Map params,
String? amount,
}) async {
- if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
+ if (_apiKey.isNotEmpty) params["apiKey"] = _apiKey;
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
- final originalUri = Uri.https(baseUrl, '', params);
+ final originalUri = Uri.https(baseUrl, "", params);
- if (isTestEnvironment) return originalUri;
-
- final signature = await getMoonpaySignature('?${originalUri.query}');
- final query = Map.from(originalUri.queryParameters);
- query['signature'] = signature;
- final signedUri = originalUri.replace(queryParameters: query);
- return signedUri;
+ final query = await getMoonpaySignedQuery("?${originalUri.query}");
+ return Uri.parse(query);
}
Future findOrderById(String id) async {
diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
index 5442dcff1..8380897b9 100644
--- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
@@ -11,8 +11,8 @@ import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.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';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/execution_state.dart';
-import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/routes.dart';
@@ -584,8 +584,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = _sendViewModel.outputs
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
- addresses: [o.address],
- name: 'Cake Pay',
+ parsedAddressByCurrencyMap: {
+ cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
+ o.address,
+ },
+ handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
@@ -653,8 +656,11 @@ class CakePayBuyCardPage extends BasePage {
final displayingOutputs = outputsCopy
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
- addresses: [o.address],
- name: 'Cake Pay',
+ parsedAddressByCurrencyMap: {
+ cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency:
+ o.address,
+ },
+ handle: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
diff --git a/lib/core/address_resolver/address_lookup_provider.dart b/lib/core/address_resolver/address_lookup_provider.dart
new file mode 100644
index 000000000..9feff5610
--- /dev/null
+++ b/lib/core/address_resolver/address_lookup_provider.dart
@@ -0,0 +1,22 @@
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/wallet_base.dart';
+
+abstract class AddressLookupProvider {
+
+ AddressSource get source;
+
+ List get supportedCurrencies;
+
+ bool isEnabled(SettingsStore settingsStore);
+
+ bool canHandle(String query);
+
+ Future> resolve({
+ required String query,
+ required List currencies,
+ required WalletBase wallet,
+ });
+}
diff --git a/lib/core/address_resolver/address_resolver_service.dart b/lib/core/address_resolver/address_resolver_service.dart
new file mode 100644
index 000000000..8592c53c6
--- /dev/null
+++ b/lib/core/address_resolver/address_resolver_service.dart
@@ -0,0 +1,143 @@
+import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/ens/ens_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/fio/fio_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/nostr/nostr_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/openalias/openalias_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/core/address_resolver/thorchain/thorchain_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/twitter/twitter_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/yat/yat_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zano/zano_alias_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zcash/zcash_address_provider.dart';
+import 'package:cake_wallet/core/address_resolver/zcash/zcash_me_address_provider.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_base.dart';
+
+class AddressResolverService {
+ AddressResolverService({required this.settingsStore});
+
+ final SettingsStore settingsStore;
+
+// Sources available for address resolution and lookup settings.
+ static const supportedSources = [
+ AddressSource.lnurlPay,
+ AddressSource.twitter,
+ AddressSource.mastodon,
+ AddressSource.yatRecord,
+ AddressSource.unstoppableDomains,
+ AddressSource.openAlias,
+ AddressSource.ens,
+ AddressSource.zcashName,
+ AddressSource.zcashAddress,
+ AddressSource.wellKnown,
+ AddressSource.zanoAlias,
+ AddressSource.bip353,
+ AddressSource.fio,
+ AddressSource.thorChain,
+ AddressSource.nostr,
+ ];
+
+ List _buildProviders() {
+ final allProviders = [
+ LNUrlPayAddressProvider(),
+ TwitterAddressProvider(),
+ MastodonAddressProvider(),
+ YatAddressProvider(),
+ UnstoppableAddressProvider(),
+ OpenaliasAddressProvider(),
+ EnsAddressProvider(),
+ ZcashNameAddressProvider(),
+ ZcashMeAddressProvider(),
+ WellKnownAddressProvider(),
+ ZanoAliasAddressProvider(),
+ Bip353AddressProvider(),
+ FioAddressProvider(),
+ ThorchainAddressProvider(),
+ NostrAddressProvider(),
+ ];
+
+ return allProviders.where((provider) => supportedSources.contains(provider.source)).toList();
+ }
+
+ Future> resolve({
+ required String query,
+ required WalletBase wallet,
+ required CryptoCurrency currency,
+ }) async {
+ try {
+ final providers = _buildProviders();
+ // return first entry if currency is specified and provider supports it
+ if (currency != null) {
+ for (final provider in providers) {
+ final cur = currency.title;
+ final src = provider.source.label;
+
+ printV('[address resolver service] checking $src - $cur - $query');
+ if (!provider.isEnabled(settingsStore)) {
+ printV('[address resolver service] ...skipping $src (disabled)');
+ continue;
+ }
+ if (!provider.canHandle(query)) {
+ printV('[address resolver service] ...skipping $src (cannot handle)');
+ continue;
+ }
+ if (!provider.supportedCurrencies.contains(currency)) {
+ printV('[address resolver service] ...skipping $src (unsupported currency)');
+ continue;
+ }
+
+ try {
+ printV('[address resolver service] ...resolving $src - $cur - $query');
+ final results = await provider.resolve(
+ query: query,
+ currencies: [currency],
+ wallet: wallet,
+ );
+
+ if (results.isNotEmpty) {
+ final parsedAddress = results.first.parsedAddressByCurrencyMap[currency];
+ final parsedAddressStr = parsedAddress == null || parsedAddress.isEmpty ? 'N/A' : parsedAddress;
+ printV('[address resolver service] resolved $src - $cur - $parsedAddressStr');
+ return [results.first];
+ }
+ } catch (e) {
+ printV('[address resolver service] Error resolving $src: $e');
+ }
+ }
+
+ return [];
+ }
+
+ return []; // TODO: Implement multi-currency resolution if no specific currency is provided.
+
+ final tasks = >>[];
+
+ for (final provider in providers) {
+ if (!provider.isEnabled(settingsStore)) continue;
+ if (!provider.canHandle(query)) continue;
+
+ tasks.add(
+ provider.resolve(
+ query: query,
+ currencies: provider.supportedCurrencies,
+ wallet: wallet,
+ ),
+ );
+ }
+
+ final results = await Future.wait(tasks);
+ return results.expand((items) => items).toList();
+ } catch (e) {
+ printV('Error resolving address: $e');
+ return [];
+ }
+ }
+}
diff --git a/lib/core/address_resolver/address_resolver_utils.dart b/lib/core/address_resolver/address_resolver_utils.dart
new file mode 100644
index 000000000..f9f59240c
--- /dev/null
+++ b/lib/core/address_resolver/address_resolver_utils.dart
@@ -0,0 +1,104 @@
+import 'package:cake_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
+import 'package:cake_wallet/core/address_validator.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+
+class AddressResolverUtils {
+ static Future
', caseSensitive: false), '\n')
+ .replaceAll(RegExp(r'<[^>]*>'), ' ')
+ .trim();
+ }
+}
diff --git a/lib/core/address_resolver/address_sources.dart b/lib/core/address_resolver/address_sources.dart
new file mode 100644
index 000000000..729ef478a
--- /dev/null
+++ b/lib/core/address_resolver/address_sources.dart
@@ -0,0 +1,87 @@
+import 'package:cake_wallet/core/address_validator.dart';
+import 'package:cw_core/crypto_currency.dart';
+
+enum AddressSource {
+ twitter(label: 'X', iconPath: 'assets/new-ui/address_sources/x.svg', alias: '@username'),
+ unstoppableDomains(
+ label: 'Unstoppable Domains',
+ iconPath: 'assets/new-ui/address_sources/unstoppable.svg',
+ alias: 'domain.tld'),
+ openAlias(
+ label: 'OpenAlias',
+ iconPath: 'assets/new-ui/address_sources/openalias.svg',
+ alias: 'name.domain.tld'),
+ yatRecord(label: 'Yat', iconPath: 'assets/new-ui/address_sources/yat.svg', alias: '🎂🎂🎂'),
+ fio(label: 'FIO', iconPath: 'assets/new-ui/address_sources/fio.svg', alias: 'user@domain'),
+ ens(
+ label: 'Ethereum Name Service',
+ iconPath: 'assets/new-ui/address_sources/ens.svg',
+ alias: 'domain.eth'),
+ mastodon(
+ label: 'Mastodon',
+ iconPath: 'assets/new-ui/address_sources/mastodon.svg',
+ alias: '@username@domain.tld'),
+ nostr(
+ label: 'Nostr',
+ iconPath: 'assets/new-ui/address_sources/nostr.svg',
+ alias: 'user@domain.tld'),
+ thorChain(
+ label: 'ThorChain',
+ iconPath: 'assets/new-ui/address_sources/icon-thorchain.svg',
+ alias: 'name'),
+ wellKnown(
+ label: '.wellknown',
+ iconPath: 'assets/new-ui/address_sources/wellknown.svg',
+ alias: 'domain.tld'),
+ zanoAlias(
+ label: 'Zano Alias', iconPath: 'assets/new-ui/address_sources/zano.svg', alias: '@alias'),
+ bip353(
+ label: 'BIP353',
+ iconPath: 'assets/new-ui/address_sources/bip353.svg',
+ alias: 'user@domain.com'),
+ zcashAddress(
+ label: 'Zcash.me',
+ iconPath: 'assets/new-ui/address_sources/icon-zcash-me.svg',
+ alias: 'zcash.me/username'),
+ zcashName(
+ label: 'Zcash Names',
+ iconPath: 'assets/new-ui/address_sources/icon-zcashnames.svg',
+ alias: 'name.zec'),
+ lnurlPay(
+ label: 'LNURL Pay',
+ iconPath: 'assets/new-ui/address_sources/icon-lnurl-pay.svg',
+ alias: 'user@domain.com'),
+ contact(label: 'Contact', iconPath: ''),
+ notParsed(label: 'Unknown', iconPath: ''),
+ ;
+
+ const AddressSource({
+ required this.label,
+ required this.iconPath,
+ this.alias = '',
+ });
+
+ final String label;
+ final String iconPath;
+ final String alias;
+}
+
+extension AddressSourceIndex on AddressSource {
+ int get raw => index;
+
+ static AddressSource fromRaw(int raw) =>
+ AddressSource.values[raw.clamp(0, AddressSource.values.length - 1)];
+}
+
+extension AddressSourceNameParser on AddressSource {
+ static AddressSource fromLabel(String? text) {
+ if (text == null || text.trim().isEmpty) {
+ return AddressSource.notParsed;
+ }
+ final needle = text.trim().toLowerCase();
+ return AddressSource.values.firstWhere(
+ (src) => src.label.toLowerCase() == needle,
+ orElse: () => AddressSource.notParsed,
+ );
+ }
+}
diff --git a/lib/core/address_resolver/bip_353/bip_353_address_provider.dart b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart
new file mode 100644
index 000000000..3a46f87a4
--- /dev/null
+++ b/lib/core/address_resolver/bip_353/bip_353_address_provider.dart
@@ -0,0 +1,68 @@
+import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
+import 'package:cake_wallet/core/address_resolver/address_sources.dart';
+import 'package:cake_wallet/core/address_resolver/bip_353/bip_353_record.dart';
+import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_base.dart';
+
+class Bip353AddressProvider extends AddressLookupProvider {
+ @override
+ AddressSource get source => AddressSource.bip353;
+
+ @override
+ List get supportedCurrencies => [CryptoCurrency.btc];
+
+ @override
+ bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsBip353;
+
+ @override
+ bool canHandle(String query) {
+ final value = query.trim();
+ return value.contains('@') && value.contains('.') && !value.startsWith('@');
+ }
+
+ @override
+ Future> resolve({
+ required String query,
+ required List currencies,
+ required WalletBase wallet,
+ }) async {
+ try {
+ final result = {};
+ String? dnsProof;
+
+ for (final cur in currencies) {
+ final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(query, cur.title);
+ if (bip353AddressMap == null || bip353AddressMap.isEmpty) continue;
+
+ final spAddress = bip353AddressMap['sp'];
+ final address = bip353AddressMap['address'];
+ final chosenAddress = spAddress?.isNotEmpty == true ? spAddress : address;
+
+ if (chosenAddress != null && chosenAddress.isNotEmpty) {
+ result[cur] = chosenAddress;
+ }
+ }
+
+ if (result.isEmpty) return [];
+
+ try {
+ dnsProof = await Bip353Record.fetchDnsProof(query);
+ } catch (e) {
+ printV('Bip353Record.fetchDnsProof error: $e');
+ }
+
+ return [ParsedAddress(
+ parsedAddressByCurrencyMap: result,
+ addressSource: AddressSource.bip353,
+ handle: query,
+ bip353DnsProof: dnsProof ?? '',
+ )];
+ } catch (e) {
+ printV('[address resolver] Error resolving BIP353 address: $e');
+ return [];
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/entities/bip_353_record.dart b/lib/core/address_resolver/bip_353/bip_353_record.dart
similarity index 59%
rename from lib/entities/bip_353_record.dart
rename to lib/core/address_resolver/bip_353/bip_353_record.dart
index 9a2a50e0a..257d76737 100644
--- a/lib/entities/bip_353_record.dart
+++ b/lib/core/address_resolver/bip_353/bip_353_record.dart
@@ -2,13 +2,9 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
-import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:dnssec_proof/dnssec_proof.dart';
-import 'package:flutter/material.dart';
class Bip353Record {
Bip353Record({
@@ -104,69 +100,4 @@ class Bip353Record {
}
}
- static Future pickBip353AddressChoice(
- BuildContext context,
- String bip353Name,
- Map addressMap,
- ) async {
- if (addressMap.length == 1) {
- return addressMap.values.first;
- }
-
- final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
-
- return chosenAddress;
- }
-
- static Future _showAddressChoiceDialog(
- BuildContext context,
- String bip353Name,
- Map addressMap,
- ) async {
- final entriesList = addressMap.entries.toList();
- final List