This commit is contained in:
Robert Malikowski
2026-07-14 21:06:21 +02:00
208 changed files with 5655 additions and 3209 deletions
-3
View File
@@ -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!
+115
View File
@@ -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
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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"
@@ -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') }}
+1 -1
View File
@@ -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
+113 -54
View File
@@ -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
+2 -1
View File
@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx6144M
android.useAndroidX=true
android.enableJetifier=true
android.ndk.suppressMinSdkVersionError=21
android.ndk.suppressMinSdkVersionError=21
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+6 -2
View File
@@ -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<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
.where((e) => (e.isSending && !e.isFrozen))
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
.where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
.toList();
@@ -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<void> 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<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async {
+17 -6
View File
@@ -18,7 +18,7 @@ class ElectrumTransactionBundle {
{required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction;
final List<BtcTransaction> ins;
final List<BtcTransaction?> ins;
final int? time;
final int confirmations;
}
@@ -127,9 +127,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
List<String> inputAddresses = [];
List<String> 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);
}
+25 -56
View File
@@ -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<void> 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<void>.delayed(const Duration(milliseconds: 200));
}
}
}
@override
Future<void> 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 = <BtcTransaction>[];
final ins = <BtcTransaction?>[];
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 = <BtcTransaction>[];
final inputTxids = inputTxidsByHash[txid] ?? const <String>[];
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 = <BtcTransaction?>[
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);
+12 -2
View File
@@ -123,7 +123,9 @@ class LightningWallet {
Future<void> 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,
},
);
}
+14 -11
View File
@@ -497,22 +497,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(boxName);
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
// rename the hive box:
final oldBoxName = "${walletInfo.name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
final newBoxName = "${newWalletName.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
static Future<void> 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<MwebUtxo>(oldBoxName);
mwebUtxosBox = await CakeHive.openBox<MwebUtxo>(newBoxName);
final newBox = await CakeHive.openBox<MwebUtxo>(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<void> deleteMwebBox(String name) async {
final boxName = "${name.replaceAll(" ", "_")}_${MwebUtxo.boxName}";
final box = await CakeHive.openBox<MwebUtxo>(boxName);
await box.deleteFromDisk();
}
@action
+4 -21
View File
@@ -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<void> 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
+2 -2
View File
@@ -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'],
+1 -1
View File
@@ -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:
-4
View File
@@ -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
@@ -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<void> 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<BitcoinCashWallet> restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
throw UnimplementedError(
+13
View File
@@ -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);
}
+7
View File
@@ -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);
+41
View File
@@ -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<String> pathForWalletTypeDir({required WalletType type}) async {
final root = await getAppDir();
@@ -36,3 +37,43 @@ Future<String> outdatedAndroidPathForWalletDir({required String name}) async {
return pathDir;
}
Future<void> 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<void> _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)));
}
}
}
+7
View File
@@ -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});
+6 -5
View File
@@ -71,11 +71,12 @@ abstract class WalletAddresses {
Future<void> 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) {
+3 -1
View File
@@ -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<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
TransactionType extends TransactionInfo> {
@@ -115,7 +116,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
Future<void> renameWalletFiles(String newWalletName);
Future<void> renameWalletFiles(String newWalletName) =>
copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
Future<String> signMessage(String message, {String? address = null});
+3 -5
View File
@@ -428,11 +428,9 @@ class WalletInfo {
Future<void> setAddresses(Map<String, String> 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);
}
}
+27 -1
View File
@@ -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<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials, RFH extends WalletCredentials> {
@@ -25,7 +28,30 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
Future<void> remove(String wallet);
Future<void> rename(String currentName, String password, String newName);
Future<void> 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<void> restoreWalletFilesFromBackup(String name) async {
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
@@ -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<void> {
_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);
}
}
}
+22
View File
@@ -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);
}
}
@@ -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<void> compute(ChangeBuilder builder) async {
final invocation = node.thisOrAncestorOfType<MethodInvocation>();
if (invocation == null) {
return;
}
final root = invocation.root;
final hasImport = root is CompilationUnit &&
root.directives.whereType<ImportDirective>().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));
}
});
}
}
@@ -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<void> {
_Visitor(this.rule);
final AnalysisRule rule;
@override
void visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name == "print" && node.target == null) {
rule.reportAtNode(node);
}
}
}
@@ -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<void> {
_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);
}
}
}
+15
View File
@@ -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
-4
View File
@@ -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
-4
View File
@@ -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
@@ -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<void> 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<DogeCoinWallet> restoreFromHardwareWallet(DogeCoinNewWalletCredentials credentials) {
throw UnimplementedError(
-4
View File
@@ -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
+113 -64
View File
@@ -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<MoralisWalletTokenBalance> tokens = [];
for (final item in decodedResponse) {
final tokenData = item as Map<String, dynamic>;
do {
final params = <String, String>{
"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<String, dynamic>) return tokens;
final result = decoded['result'];
if (result is! List) return tokens;
for (final item in result) {
if (item is! Map<String, dynamic>) 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,
});
}
+97 -66
View File
@@ -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<String>? cachedWhitelistLower,
Set<String>? 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<void> _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<void> _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<MoralisDiscoveryResult> 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<void> 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,
});
}
+18 -15
View File
@@ -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<void> 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
+1
View File
@@ -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:
-4
View File
@@ -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
-4
View File
@@ -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
+1 -1
View File
@@ -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:
-4
View File
@@ -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
+4 -4
View File
@@ -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),
+124 -23
View File
@@ -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<SPLToken>(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<void> _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<SolanaPrivateKey> 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<String>? cachedDefaultMints,
Set<String>? 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<void> 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<void> 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();
-25
View File
@@ -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<SolanaNewWalletCredentials,
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey, SolanaNewWalletCredentials> {
@@ -138,29 +136,6 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
return wallet;
}
@override
Future<void> 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<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
-4
View File
@@ -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
-25
View File
@@ -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<TronToken?> getTronToken(String contractAddress) async =>
await _client.getTronToken(contractAddress, _tronAddress);
@override
Future<void> 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();
-24
View File
@@ -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<void> 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<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
-4
View File
@@ -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
+1 -1
View File
@@ -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:
-31
View File
@@ -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
+1 -1
View File
@@ -177,7 +177,7 @@ class CWBitcoin extends Bitcoin {
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addressesByReceiveType
.map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress(
.map<ElectrumSubAddress>((addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: addr.address,
+10 -13
View File
@@ -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<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
Future<String> 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<String, dynamic>)['signature'] as String;
printV((jsonDecode(response.body) as Map<String, dynamic>));
return (jsonDecode(response.body) as Map<String, dynamic>)['query'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
@@ -297,18 +299,13 @@ class MoonPayProvider extends BuyProvider {
required Map<String, String> 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<String, dynamic>.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<Order> findOrderById(String id) async {
@@ -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 ?? '',
),
@@ -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<CryptoCurrency> get supportedCurrencies;
bool isEnabled(SettingsStore settingsStore);
bool canHandle(String query);
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
});
}
@@ -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<AddressLookupProvider> _buildProviders() {
final allProviders = <AddressLookupProvider>[
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<List<ParsedAddress>> 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 = <Future<List<ParsedAddress>>>[];
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 [];
}
}
}
@@ -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<Map<CryptoCurrency, String>> resolveUnstoppableDomainFromText({
required String raw,
required List<CryptoCurrency> currencies,
}) async {
final domain = extractUnstoppableDomain(raw);
if (domain.isEmpty) return {};
final result = <CryptoCurrency, String>{};
for (final currency in currencies) {
final address = await UnstoppableAddressProvider.fetchUnstoppableDomainAddress(domain, currency.title);
if (address.isNotEmpty) {
result[currency] = address;
}
}
return result;
}
static String extractUnstoppableDomain(String raw) {
// sort by length to avoid matching shorter tld instead of longer
final domains = List<String>.from(UnstoppableAddressProvider.unstoppableDomains)
..sort((a, b) => b.length.compareTo(a.length));
for (final tld in domains) {
final pattern = RegExp(r'([A-Za-z0-9-]+)\.' + RegExp.escape(tld), caseSensitive: false);
final match = pattern.firstMatch(raw);
if (match != null) return match.group(0)!;
}
return '';
}
static Map<CryptoCurrency, String> extractAddressesFromText({
required String raw,
required List<CryptoCurrency> currencies,
bool requireSurroundingWhitespaces = true,
}) {
final result = <CryptoCurrency, String>{};
var text = raw;
for (final currency in currencies) {
final address = extractAddressByType(
raw: text,
type: currency,
requireSurroundingWhitespaces: requireSurroundingWhitespaces,
);
if (address != null && address.isNotEmpty) {
result[currency] = address;
text = text.replaceFirst(address, '');
}
}
return result;
}
static String? extractAddressByType({
required String raw,
required CryptoCurrency type,
bool requireSurroundingWhitespaces = true,
}) {
var addressPattern = AddressValidator.getAddressFromStringPattern(type);
if (addressPattern == null) {
printV('Unknown pattern for $type');
return null;
}
if (requireSurroundingWhitespaces) addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX";
final text = stripHtmlTags(raw);
final match = RegExp(addressPattern, multiLine: true, caseSensitive: false).firstMatch(text);
if (match == null) return null;
return match.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'),
(Match match) {
String group = match.group(0)!;
if (group.startsWith('bitcoincash:') ||
group.startsWith('nano_') ||
group.startsWith('ban_')) {
return group;
}
return '';
});
}
static bool isEmailFormat(String address) {
final RegExp emailRegex = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
caseSensitive: false,
);
return emailRegex.hasMatch(address);
}
static String stripHtmlTags(String value) {
return value
.replaceAll(RegExp(r'[\u2028\u2029]'), '\n')
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'<[^>]*>'), ' ')
.trim();
}
}
@@ -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,
);
}
}
@@ -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<CryptoCurrency> 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<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final result = <CryptoCurrency, String>{};
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 [];
}
}
}
@@ -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<String?> pickBip353AddressChoice(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
if (addressMap.length == 1) {
return addressMap.values.first;
}
final chosenAddress = await _showAddressChoiceDialog(context, bip353Name, addressMap);
return chosenAddress;
}
static Future<String?> _showAddressChoiceDialog(
BuildContext context,
String bip353Name,
Map<String, String> addressMap,
) async {
final entriesList = addressMap.entries.toList();
final List<Map<String, String>> displayItems = entriesList.map((entry) {
final originalKey = entry.key;
final originalValue = entry.value;
final extendedKeyName = keyDisplayMap[originalKey] ?? originalKey;
final truncatedValue = _truncate(originalValue, front: 6, back: 6);
return {
'displayKey': extendedKeyName,
'displayValue': truncatedValue,
'originalKey': originalKey,
'originalValue': originalValue,
};
}).toList();
String? selectedOriginalValue;
if (context.mounted) {
await showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithPickerOption(
alertTitle: S.of(context).multiple_addresses_detected + '\n$bip353Name',
alertTitleTextSize: 14,
alertSubtitle: S.of(context).please_choose_one + ':',
options: displayItems,
onOptionSelected: (Map<String, String> chosenItem) {
selectedOriginalValue = chosenItem['originalValue'];
},
alertBarrierDismissible: true,
);
},
);
}
return selectedOriginalValue;
}
static String _truncate(String value, {int front = 6, int back = 6}) {
if (value.length <= front + back) return value;
final start = value.substring(0, front);
final end = value.substring(value.length - back);
return '$start...$end';
}
}
}
@@ -0,0 +1,49 @@
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/ens/ens_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/wallet_base.dart';
class EnsAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.ens;
@override
List<CryptoCurrency> get supportedCurrencies =>
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsENS;
@override
bool canHandle(String query) => query.endsWith('.eth'); // ENS handle example: name.eth
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
final Map<CryptoCurrency, String> result = {};
for (final cur in currencies) {
final address = await EnsRecord.fetchEnsAddress(query, cur, wallet: wallet);
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
result[cur] = address;
}
}
if (result.isNotEmpty) {
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.ens,
handle: query,
)
];
}
return [];
}
}
@@ -0,0 +1,55 @@
import 'package:cake_wallet/evm/evm.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:ens_dart/ens_dart.dart';
import 'package:web3dart/web3dart.dart';
class EnsRecord {
static Future<String> fetchEnsAddress(
String name,
CryptoCurrency cur, {
WalletBase? wallet,
}) async {
Web3Client? _client;
if (wallet?.type == WalletType.ethereum || wallet?.type == WalletType.polygon) {
_client = evm!.getWeb3Client(wallet!);
}
_client ??= Web3Client(
'https://ethereum-rpc.publicnode.com',
ProxyWrapper().getHttpIOClient(),
);
final ens = Ens(client: _client);
final coinType = getEnsCoinType(cur);
if (coinType == null) {
printV('Unsupported currency for ENS: $cur');
return '';
}
try {
if (coinType == CoinType.ETH || coinType == CoinType.MATIC) {
return (await ens.withName(name).getAddress()).hex;
} else {
return await ens.withName(name).getCoinAddress(coinType);
}
} catch (e) {
printV(e);
return '';
}
}
static CoinType? getEnsCoinType(CryptoCurrency cur) => switch (cur) {
CryptoCurrency.xmr => CoinType.XMR,
CryptoCurrency.btc => CoinType.BTC,
CryptoCurrency.ltc => CoinType.LTC,
CryptoCurrency.eth => CoinType.ETH,
CryptoCurrency.matic => CoinType.MATIC,
_ => null,
};
}
@@ -0,0 +1,125 @@
import 'dart:convert';
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/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.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/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
class FioAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.fio;
@override
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
@override
bool canHandle(String q) =>
!q.startsWith('@') &&
q.contains('@') &&
!q.contains('.'); // FIO handle example: username@domain
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsFio;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
final Map<CryptoCurrency, String> result = {};
try {
final bool isFioRegistered = await FioAddressProvider.checkAvail(query);
if (!isFioRegistered) return [];
for (final cur in currencies) {
final address = await FioAddressProvider.getPubAddress(query, cur.title);
if (address != null && address.isNotEmpty) {
result[cur] = address;
}
}
if (result.isNotEmpty) {
return [
ParsedAddress(
parsedAddressByCurrencyMap: result, addressSource: AddressSource.fio, handle: query)
];
}
return [];
} catch (e) {
printV('FioAddressProvider.resolve error: $e');
return [];
}
}
static const _apiAuthority = 'fio.blockpane.com';
static const _availCheck = '/v1/chain/avail_check';
static const _getAddress = '/v1/chain/get_pub_address';
static Future<bool> checkAvail(String fioAddress) async {
bool isFioRegistered = false;
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{"fio_name": fioAddress};
final uri = Uri.https(_apiAuthority, _availCheck);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 200) {
return isFioRegistered;
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
isFioRegistered = responseJSON['is_registered'] as int == 1;
return isFioRegistered;
}
static Future<String?> getPubAddress(String fioAddress, String token) async {
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{
"fio_address": fioAddress,
"chain_code": token.toUpperCase(),
"token_code": token.toUpperCase(),
};
final uri = Uri.https(_apiAuthority, _getAddress);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode == 400) {
final error = responseJSON['error'] as String;
final message = responseJSON['message'] as String;
printV('${error}\n$message');
return null;
}
if (response.statusCode != 200) {
final String message = responseJSON['message'] as String? ?? 'Unknown error';
printV('Error fetching public address for token $token: $message');
return null;
}
final String pubAddress = responseJSON['public_address'] as String? ?? '';
if (pubAddress.isNotEmpty) {
return pubAddress;
}
return null;
}
}
@@ -0,0 +1,66 @@
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/lnurl_pay/lnurlpay_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';
import 'package:cw_core/wallet_type.dart';
class LNUrlPayAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.lnurlPay;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.btcln];
@override
bool canHandle(String q) => q.contains('.') && q.contains('@'); // LNURL-pay handle example: user@domain.com
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsLNUrl;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
if (wallet.type != WalletType.bitcoin) return [];
final formattedName = query.trim();
if (formattedName.isEmpty) return [];
final result = <CryptoCurrency, String>{};
for (final currency in currencies) {
if (!supportedCurrencies.contains(currency)) continue;
final record = await LNUrlPayRecord.fetchAddressAndName(
formattedName: formattedName,
currency: currency,
);
if (record != null && record.address.isNotEmpty) {
result[currency] = record.address;
}
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.lnurlPay,
handle: formattedName,
profileName: formattedName,
),
];
} catch (e) {
printV('[address resolver] Error resolving LNURL-pay address: $e');
return [];
}
}
}
@@ -0,0 +1,93 @@
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_api.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.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 MastodonAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.mastodon;
@override
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsMastodon;
@override
bool canHandle(String query) =>
query.startsWith('@') &&
query.contains('@', 1) &&
query.contains('.', 1); // Mastodon handle example: @username@hostname.xxx
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final subText = query.substring(1); // Remove '@' from the beginning of the handle
final hostNameIndex = subText.indexOf('@');
if (hostNameIndex <= 0 || hostNameIndex == subText.length - 1) return [];
final userName = subText.substring(0, hostNameIndex);
final hostName = subText.substring(hostNameIndex + 1);
final result = <CryptoCurrency, String>{};
final mastodonUser = await MastodonAPI.lookupUserByUserName(
userName: userName,
apiHost: hostName,
);
if (mastodonUser == null) return [];
final bio = AddressResolverUtils.stripHtmlTags(mastodonUser.note);
result.addAll(
AddressResolverUtils.extractAddressesFromText(
raw: bio,
currencies: currencies,
),
);
final pinnedPosts = await MastodonAPI.getPinnedPosts(
userId: mastodonUser.id,
apiHost: hostName,
);
if (pinnedPosts.isNotEmpty) {
final pinnedPostsText = pinnedPosts
.map((item) => AddressResolverUtils.stripHtmlTags(item.content))
.where((text) => text.isNotEmpty)
.join('\n');
result.addAll(
AddressResolverUtils.extractAddressesFromText(
raw: pinnedPostsText,
currencies: currencies,
),
);
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.mastodon,
handle: query,
profileImageUrl: mastodonUser.profileImageUrl,
profileName: mastodonUser.username,
),
];
} catch (e) {
printV('[address resolver] Error resolving Mastodon address: $e');
return [];
}
}
}
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/core/address_resolver/mastodon/mastodon_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/mastodon/mastodon_user.dart';
class MastodonAPI {
static const httpsScheme = 'https';
@@ -0,0 +1,66 @@
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/core/address_resolver/nostr/nostr_api.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.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 NostrAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.nostr;
@override
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
@override
bool canHandle(String q) =>
AddressResolverUtils.isEmailFormat(q); // Nostr handle example: username@domain
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsNostr;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final profile = await NostrProfileHandler.queryProfile(query);
if (profile == null) return [];
final data = await NostrProfileHandler.processRelays(profile, query);
if (data == null) return [];
final result = <CryptoCurrency, String>{};
String queryTxt = data.about;
for (final cur in currencies) {
final addr = AddressResolverUtils.extractAddressByType(raw: queryTxt, type: cur);
if (addr != null && addr.isNotEmpty) {
result[cur] = addr;
queryTxt = queryTxt.replaceFirst(addr, '');
}
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.nostr,
handle: query,
profileImageUrl: data.picture,
profileName: data.name,
)
];
} catch (e) {
printV('Error looking up Nostr profile: $e');
return [];
}
}
}
@@ -0,0 +1,127 @@
import 'dart:async';
import 'dart:convert';
import 'package:cake_wallet/core/address_resolver/nostr/nostr_user.dart';
import 'package:nostr_tools/nostr_tools.dart';
import 'dart:async' show Completer, TimeoutException, runZonedGuarded;
class NostrProfileHandler {
static final relayToDomainMap = {
'relay.snort.social': 'snort.social',
};
static final Nip05 _nip05 = Nip05();
static Future<ProfilePointer?> queryProfile(String nip05Address) async {
final profile = await _nip05.queryProfile(nip05Address);
if (profile?.pubkey != null && profile?.relays?.isNotEmpty == true) {
return profile;
}
return null;
}
static Future<UserMetadata?> processRelays(
ProfilePointer profile,
String nip05Address,
) async {
final userDomain = _extractDomain(nip05Address);
const int metaKind = 0;
// Domain-matched relays first
for (final String relayUrl in profile.relays ?? []) {
final relayDomain =
relayToDomainMap[_getDomainFromRelayUrl(relayUrl)] ?? _getDomainFromRelayUrl(relayUrl);
if (relayDomain == userDomain) {
final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
if (data != null) return data;
}
}
// Then try every remaining relay
for (final String relayUrl in profile.relays ?? []) {
final data = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaKind]);
if (data != null) return data;
}
// Nothing found
return null;
}
static const Duration _relayTimeout = Duration(seconds: 3);
static Future<UserMetadata?> _fetchInfoFromRelay(
String relayUrl, String userPubKey, List<int> kinds) async {
// sanitize so obvious junk (like '#') doesn't reach connect()
final clean = _sanitizeRelay(relayUrl);
if (clean.isEmpty) return null;
final result = Completer<UserMetadata?>();
runZonedGuarded(() async {
try {
final relay = RelayApi(relayUrl: clean);
final stream = await relay.connect().timeout(
_relayTimeout,
onTimeout: () {
relay.close();
throw TimeoutException('Relay connect timeout');
},
);
relay.sub([
Filter(kinds: kinds, authors: [userPubKey])
]);
final sub = stream.listen((msg) {
if (msg.type == 'EVENT' && !result.isCompleted) {
try {
final event = msg.message as Event;
final jsonMap = json.decode(event.content) as Map<String, dynamic>;
result.complete(UserMetadata.fromJson(jsonMap));
} catch (_) {
if (!result.isCompleted) result.complete(null);
}
}
}, onError: (_) {
if (!result.isCompleted) result.complete(null);
}, onDone: () {
if (!result.isCompleted) result.complete(null);
});
final value = await result.future.timeout(_relayTimeout, onTimeout: () => null);
await sub.cancel();
relay.close();
if (!result.isCompleted) result.complete(value);
} catch (_) {
if (!result.isCompleted) result.complete(null);
}
}, (error, stack) {
// swallow ALL async errors from the websocket layer (including "was not upgraded to websocket")
if (!result.isCompleted) result.complete(null);
});
return result.future;
}
static String _sanitizeRelay(String url) {
url = url.replaceFirst(RegExp(r'^https?://'), 'wss://');
final uri = Uri.parse(url);
return Uri(
scheme: uri.scheme.isEmpty ? 'wss' : uri.scheme,
host: uri.host,
port: uri.hasPort ? uri.port : 443,
).toString();
}
static String _extractDomain(String nip05) => nip05.split('@').last;
static String _getDomainFromRelayUrl(String url) {
try {
return Uri.parse(url).host;
} catch (_) {
return '';
}
}
}
@@ -0,0 +1,64 @@
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/openalias/openalias_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 OpenaliasAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.openAlias;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc];
@override
bool canHandle(String q) {
final formattedName = OpenaliasRecord.formatDomainName(q);
return formattedName.contains(".");
}
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsOpenAlias;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final formatted = OpenaliasRecord.formatDomainName(query);
final txtRecords = await OpenaliasRecord.lookupOpenAliasRecord(formatted);
if (txtRecords == null) return [];
final result = <CryptoCurrency, String>{};
for (final cur in currencies) {
final rec = OpenaliasRecord.fetchAddressAndName(
formattedName: formatted,
ticker: cur.title.toLowerCase(),
txtRecord: txtRecords,
);
if (rec.address.isNotEmpty) result[cur] = rec.address;
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.openAlias,
handle: query,
)
];
} catch (e) {
printV('[address resolver] Error resolving OpenAlias address: $e');
return [];
}
}
}
@@ -0,0 +1,69 @@
import 'package:basic_utils/basic_utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
class OpenaliasRecord {
OpenaliasRecord({
required this.address,
required this.name,
required this.description,
});
final String name;
final String address;
final String description;
static String formatDomainName(String name) {
String formattedName = name;
if (name.contains("@")) {
formattedName = name.replaceAll("@", ".");
}
return formattedName;
}
static Future<List<RRecord>?> lookupOpenAliasRecord(String name) async {
try {
final txtRecord = await DnsUtils.lookupRecord(name, RRecordType.TXT, dnssec: true);
return txtRecord;
} catch (e) {
printV("${e.toString()}");
return null;
}
}
static OpenaliasRecord fetchAddressAndName({
required String formattedName,
required String ticker,
required List<RRecord> txtRecord,
}) {
var address = '';
var name = formattedName;
var note = '';
final addrRe = RegExp(r'recipient_address=([^;\s]+)', caseSensitive: false);
final nameRe = RegExp(r'recipient_name=([^;]+)', caseSensitive: false);
final noteRe = RegExp(r'tx_description=([^;]+)', caseSensitive: false);
final tag = 'oa1:$ticker';
for (final rr in txtRecord) {
final txt = rr.data.replaceAll('"', '');
if (!txt.toLowerCase().contains(tag)) continue;
final addrM = addrRe.firstMatch(txt);
if (addrM != null) address = addrM.group(1)!;
final nameM = nameRe.firstMatch(txt);
if (nameM != null) name = nameM.group(1)!.trim();
final noteM = noteRe.firstMatch(txt);
if (noteM != null) note = noteM.group(1)!.trim();
break;
}
return OpenaliasRecord(address: address, name: name, description: note);
}
}
@@ -0,0 +1,22 @@
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cw_core/crypto_currency.dart';
class ParsedAddress {
const ParsedAddress({
required this.parsedAddressByCurrencyMap,
this.addressSource = AddressSource.notParsed,
this.handle = '',
this.profileImageUrl = '',
this.profileName = '',
this.description = '',
this.bip353DnsProof,
});
final Map<CryptoCurrency, String> parsedAddressByCurrencyMap;
final AddressSource addressSource;
final String handle;
final String profileImageUrl;
final String profileName;
final String description;
final String? bip353DnsProof;
}
@@ -0,0 +1,65 @@
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.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 ThorchainAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.thorChain;
@override
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
@override
bool canHandle(String q) => q.isNotEmpty; // can be any string
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsThorChain;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final isNormalAddress = currencies.any((cur) =>
AddressResolverUtils.extractAddressByType(raw: query, type: cur)?.isNotEmpty ?? false);
if (query.length > 30 || isNormalAddress) return [];
final map = await ThorChainExchangeProvider.lookupAddressByName(query);
if (map == null || map.isEmpty) return [];
final result = <CryptoCurrency, String>{};
for (final cur in currencies) {
final key = cur.title.toUpperCase();
final addr = map[key];
final runeAddr = cur.title.toUpperCase() == 'RUNE' ? map['THOR'] : null;
final resolvedAddress = addr ?? runeAddr;
if (resolvedAddress != null && resolvedAddress.isNotEmpty) {
if (!result.containsValue(resolvedAddress)) result[cur] = resolvedAddress;
}
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.thorChain,
handle: query,
)
];
} catch (e) {
printV('[address resolver] Error resolving address: $e');
return [];
}
}
}
@@ -0,0 +1,97 @@
import 'package:cake_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:cake_wallet/core/address_resolver/address_sources.dart';
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
import 'package:cake_wallet/core/address_resolver/twitter/twitter_api.dart';
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:cake_wallet/core/address_validator.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 TwitterAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.twitter;
@override
List<CryptoCurrency> get supportedCurrencies => AddressValidator.reliableValidateCurrencies;
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsTwitter;
@override
bool canHandle(String query) =>
query.startsWith('@') && !query.substring(1).contains('@'); // x handle example: @username
/// Resolves a Twitter handle to a list of ParsedAddress objects by extracting potential cryptocurrency addresses
/// from the user's, profile.
/// fallback to resolving Unstoppable Domains if no addresses are found in each of the Twitter user's bio, location, and pinned tweet.
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
final formattedName = query.replaceFirst("@", "");
try {
final result = <CryptoCurrency, String>{};
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
final bio = (text: twitterUser.description, whitespaces: true);
final location = (text: twitterUser.location, whitespaces: false);
final pinnedTweet = (text: twitterUser.pinnedTweet?.text ?? '', whitespaces: true);
final twitterTexts = [
bio,
location,
pinnedTweet,
].where((item) => item.text.isNotEmpty);
for (final item in twitterTexts) {
result.addAll(
AddressResolverUtils.extractAddressesFromText(
raw: item.text,
currencies: currencies,
requireSurroundingWhitespaces: item.whitespaces,
),
);
if (result.isEmpty) {
final domainResult = await AddressResolverUtils.resolveUnstoppableDomainFromText(
raw: item.text,
currencies: currencies,
);
if (domainResult.isNotEmpty) {
return [_buildTwitterParsedAddress(query, twitterUser, domainResult)];
}
}
}
if (result.isEmpty) return [];
return [_buildTwitterParsedAddress(query, twitterUser, result)];
} catch (e) {
printV('[address resolver service] Twitter error: $e');
return [];
}
}
ParsedAddress _buildTwitterParsedAddress(
String query,
TwitterUser twitterUser,
Map<CryptoCurrency, String> result,
) {
return ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.twitter,
handle: query,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name,
);
}
}
@@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/twitter/twitter_user.dart';
import 'package:cake_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class TwitterApi {
@@ -0,0 +1,235 @@
import 'dart:convert';
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/openalias/openalias_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/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
class UnstoppableAddressProvider extends AddressLookupProvider {
static const unstoppableDomains = [
"888",
"academy",
"agency",
"altimist",
"anime",
"austin",
"bald",
"bay",
"benji",
"bet",
"binanceus",
"bitcoin",
"bitget",
"bitscrunch",
"blockchain",
"boomer",
"boston",
"ca",
"caw",
"cc",
"chat",
"chomp",
"clay",
"club",
"co",
"com",
"company",
"crypto",
"dao",
"design",
"dfz",
"digital",
"doga",
"donut",
"dream",
"email",
"emir",
"eth",
"ethermail",
"family",
"farms",
"finance",
"fun",
"fyi",
"games",
"global",
"go",
"group",
"guru",
"hi",
"hockey",
"host",
"info",
"io",
"klever",
"kresus",
"kryptic",
"lfg",
"life",
"live",
"llc",
"ltc",
"ltd",
"manga",
"me",
"media",
"metropolis",
"miami",
"miku",
"money",
"moon",
"mumu",
"net",
"network",
"news",
"nft",
"npc",
"onchain",
"online",
"org",
"podcast",
"pog",
"polygon",
"base",
"arbitrum",
"press",
"privacy",
"pro",
"propykeys",
"pudgy",
"pw",
"quantum",
"rad",
"raiin",
"retardio",
"rip",
"rocks",
"secret",
"services",
"site",
"smobler",
"social",
"solutions",
"space",
"stepn",
"store",
"studio",
"systems",
"tball",
"tea",
"team",
"tech",
"technology",
"today",
"tribe",
"u",
"ubu",
"uno",
"unstoppable",
"vip",
"wallet",
"website",
"wif",
"wifi",
"witg",
"work",
"world",
"wrkx",
"wtf",
"x",
"xmr",
"xyz",
"zil",
"zone",
"pizza"
];
@override
AddressSource get source => AddressSource.unstoppableDomains;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.xmr, CryptoCurrency.btc];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsUnstoppableDomains;
@override
bool canHandle(String query) {
// Unstoppable Domains handle example: name.crypto
final formattedName = OpenaliasRecord.formatDomainName(query);
final domainParts = formattedName.split('.');
final name = domainParts.last;
return domainParts.length > 1 &&
domainParts.first.isNotEmpty &&
name.isNotEmpty &&
unstoppableDomains.any((domain) => name.trim() == domain);
}
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final formattedName = OpenaliasRecord.formatDomainName(query);
final result = <CryptoCurrency, String>{};
for (final currency in currencies) {
final address = await fetchUnstoppableDomainAddress(
formattedName,
currency.title,
);
if (address.isNotEmpty) {
result[currency] = address;
}
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
profileName: formattedName,
addressSource: AddressSource.unstoppableDomains,
handle: formattedName,
),
];
} catch (e) {
printV('[address resolver] Error resolving Unstoppable Domain: $e');
return [];
}
}
static Future<String> fetchUnstoppableDomainAddress(String domain, String ticker) async {
var address = '';
try {
final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records");
final response = await ProxyWrapper().get(clearnetUri: uri);
final jsonParsed = json.decode(response.body) as Map<String, dynamic>;
if (jsonParsed["records"] == null) {
throw Exception(".records response from $uri is empty");
};
final records = jsonParsed["records"] as Map<String, dynamic>;
final key = "crypto.${ticker.toUpperCase()}.address";
if (records[key] == null) {
throw Exception(".records.${key} response from $uri is empty");
}
return records[key] as String? ?? '';
} catch (e) {
printV('Unstoppable domain error: ${e.toString()}');
address = '';
}
return address;
}
}
@@ -0,0 +1,49 @@
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/parsed_address.dart';
import 'package:cake_wallet/core/address_resolver/wellknown/wellknown_record.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 WellKnownAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.wellKnown;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.nano];
@override
bool canHandle(String q) => q.contains('.') && q.contains('@');
// .well-known handle example:
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final rec = await WellKnownRecord.fetch(query, CryptoCurrency.nano);
if (rec == null || rec.address.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: {CryptoCurrency.nano: rec.address},
addressSource: AddressSource.wellKnown,
handle: query,
profileName: rec.title ?? '',
profileImageUrl: rec.imageUrl ?? '',
)
];
} catch (e) {
printV('[address resolver] Error resolving Well-Known address: $e');
return [];
}
}
}
@@ -0,0 +1,78 @@
import 'dart:convert';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class WellKnownRecord {
WellKnownRecord({
required this.address,
required this.alias,
required this.imageUrl,
required this.title,
});
final String address;
final String alias;
final String? imageUrl;
final String? title;
static const _jsonLocation = {CryptoCurrency.nano: 'nano-currency'};
static Future<WellKnownRecord?> fetch(
String username,
CryptoCurrency currency,
) async {
final location = _jsonLocation[currency];
if (location == null) {
printV('well-known: unsupported coin $currency');
return null;
}
final parts = username.split('@');
if (parts.length < 2) {
printV('well-known: missing @ in "$username"');
return null;
}
final alias = (parts.length == 3 ? parts[1] : parts[0]).trim();
final domain = parts.last.trim();
final encoded = Uri.encodeComponent(alias.isEmpty ? '_' : alias);
final uri = Uri.https(
domain,
'/.well-known/$location.json',
{'names': encoded},
);
try {
final res = await ProxyWrapper().get(
clearnetUri: uri,
headers: {'Accept': 'application/json'},
);
final cType = res.headers['content-type']?.toLowerCase() ?? '';
if (res.statusCode != 200 || !cType.contains('application/json')) {
printV('well-known: $uri → ${res.statusCode} $cType');
return null;
}
final body = json.decode(res.body) as Map<String, dynamic>;
final items = body['names'] as List<dynamic>? ?? const [];
for (final raw in items.whereType<Map>()) {
if (raw['name'].toString().toLowerCase() == alias.toLowerCase()) {
return WellKnownRecord(
address: raw['address']?.toString() ?? '',
alias: alias,
imageUrl: raw['image']?.toString(),
title: raw['name']?.toString(),
);
}
}
printV('well-known: alias "$alias" not found in list');
} catch (e) {
printV('well-known: network / JSON error → $e');
}
return null;
}
}
@@ -0,0 +1,59 @@
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/parsed_address.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/entities/emoji_string_extension.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 YatAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.yatRecord;
@override
List<CryptoCurrency> get supportedCurrencies =>
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.eth, CryptoCurrency.ltc];
@override
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final result = <CryptoCurrency, String>{};
for (final cur in currencies) {
final records = await YatService.fetchYatAddress(query, cur.title);
if (records.isEmpty) continue;
final chosen = cur == CryptoCurrency.xmr
? records.firstWhere((r) => r.isMoneroSub, orElse: () => records.first)
: records.first;
result[cur] = chosen.address;
}
if (result.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.yatRecord,
handle: query,
)
];
} catch (e) {
printV('[address resolver] Error resolving Yat address: $e');
return [];
}
}
}
@@ -0,0 +1,19 @@
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
class YatRecord {
YatRecord({
required this.address,
required this.tag,
});
final String address;
final String tag;
factory YatRecord.fromJson(Map<String, dynamic> json, String tag) =>
YatRecord(address: (json['address'] ?? '').toString(), tag: tag);
bool get isMoneroSub => tag == YatService.MONERO_SUB_ADDRESS;
@override
String toString() => 'YatRecord(tag: $tag, address: $address)';
}
@@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:cake_wallet/core/address_resolver/yat/yat_record.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class YatService {
static bool isDevMode = false;
static String get apiUrl =>
YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl;
static const apiReleaseUrl = "https://a.y.at";
static const apiDevUrl = 'https://a.yat.fyi';
static String lookupEmojiUrl(String emojiId) =>
"$apiUrl/emoji_id/$emojiId/payment";
static const String MONERO_SUB_ADDRESS = '0x1002';
static const String MONERO_STD_ADDRESS = '0x1001';
static const tags = {
'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS",
'BTC': '0x1003',
'LTC': '0x1019'
};
static Future<List<YatRecord>> fetchYatAddress(
String emojiId,
String ticker,
) async {
final tagQuery = tags[ticker.toUpperCase()];
if (tagQuery == null) return const [];
final uri = Uri.parse(
lookupEmojiUrl(emojiId.replaceAll(' ', '')),
).replace(queryParameters: {'tags': tagQuery});
try {
final response = await ProxyWrapper().get(clearnetUri: uri);
final body = json.decode(response.body) as Map<String, dynamic>;
final res = body['result'];
if (res == null) return const [];
final records = <YatRecord>[];
if (res is Map) {
res.forEach((tag, data) {
if (data is Map<String, dynamic>) {
records.add(YatRecord.fromJson(data, tag.toString()));
}
});
}
else if (res is List) {
for (final item in res) {
if (item is Map<String, dynamic>) {
final tag = item['tag']?.toString() ?? '';
records.add(YatRecord.fromJson(item, tag));
}
}
}
return records;
} catch (_) {
return const [];
}
}
}
@@ -0,0 +1,75 @@
import 'dart:convert';
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/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/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
class ZanoAliasAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.zanoAlias;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zano];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZanoAlias;
@override
bool canHandle(String query) => query.startsWith('@'); // zano handle example: @username
static const _zanoUrl = 'http://37.27.100.59:10500/json_rpc';
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
if (!currencies.contains(CryptoCurrency.zano)) return [];
final formattedName = query.replaceFirst("@", "");
if (formattedName.isEmpty) return [];
final zanoAddress = await fetchZanoAliasAddress(formattedName);
if (zanoAddress == null || zanoAddress.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: {CryptoCurrency.zano: zanoAddress},
addressSource: AddressSource.zanoAlias,
handle: query,
),
];
} catch (e) {
printV('[address resolver] Error resolving Zano Alias: $e');
return [];
}
}
static Future<String?> fetchZanoAliasAddress(String alias) async {
try {
final uri = Uri.parse(_zanoUrl);
final response = await ProxyWrapper().post(
clearnetUri: uri,
body: json.encode({
'id': 0,
'jsonrpc': '2.0',
'method': 'get_alias_details',
'params': {'alias': alias},
}),
);
final jsonParsed = json.decode(response.body) as Map<String, dynamic>;
return jsonParsed['result']?['alias_details']?['address'] as String?;
} catch (e) {
printV('[address resolver] Zano Alias error: $e');
return null;
}
}
}
@@ -0,0 +1,49 @@
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/parsed_address.dart';
import 'package:cake_wallet/core/address_resolver/zcash/zcash_names_record.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 ZcashNameAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.zcashName;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
@override
bool canHandle(String query) {
final lowerText = query.toLowerCase();
return lowerText.endsWith(".zec") || lowerText.endsWith(".zcash");
} // Zcash handle example: name.zec
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final address = await ZcashNamesRecord.fetchZcashNamesAddress(query);
if (address == null || address.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: {CryptoCurrency.zec: address},
profileName: query,
addressSource: AddressSource.zcashName,
handle: query,
),
];
} catch (e) {
printV('[address resolver] Error resolving Zcash Name: $e');
return [];
}
}
}
@@ -0,0 +1,74 @@
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/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/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
class ZcashMeAddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.zcashAddress;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
@override
bool canHandle(String query) {
final value = query.trim().toLowerCase();
return value.startsWith('zcash.me/') && value.split('/').last.isNotEmpty;
} // zcash.me profile example: zcash.me/username
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final handle = query.trim().split('/').last;
if (handle.isEmpty) return [];
final address = await _fetchZcashAddress(handle);
if (address == null || address.isEmpty) return [];
return [
ParsedAddress(
parsedAddressByCurrencyMap: {CryptoCurrency.zec: address},
profileName: handle,
addressSource: AddressSource.zcashAddress,
handle: query,
),
];
} catch (e) {
printV('[address resolver] Error resolving zcash.me address: $e');
return [];
}
}
Future<String?> _fetchZcashAddress(String handle) async {
final url = Uri.parse('https://zcash.me/$handle');
try {
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode == 200) {
final addressRegex = RegExp(
r'(t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300})',
caseSensitive: true,
);
final match = addressRegex.firstMatch(response.body);
return match?.group(0);
}
} catch (e) {
printV('[address resolver] Error fetching zcash.me profile: $e');
}
return null;
}
}
+56 -31
View File
@@ -12,29 +12,54 @@ const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
: super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
final RegExp lightningInvoiceRegex = RegExp(
r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$',
caseSensitive: false);
if (lightningInvoiceRegex.hasMatch(txt)) return true;
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
final RegExp lightningInvoiceRegex = RegExp(
r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$',
caseSensitive: false);
if (lightningInvoiceRegex.hasMatch(txt)) return true;
return BitcoinAddressUtils.validateAddress(
address: txt,
network: type == CryptoCurrency.btc
? isTestnet
? BitcoinNetwork.testnet
: BitcoinNetwork.mainnet
: LitecoinNetwork.mainnet,
);
}
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
length: getLength(type),
);
return BitcoinAddressUtils.validateAddress(
address: txt,
network: type == CryptoCurrency.btc
? isTestnet
? BitcoinNetwork.testnet
: BitcoinNetwork.mainnet
: LitecoinNetwork.mainnet,
);
}
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
length: getLength(type),
);
static const List<CryptoCurrency> reliableValidateCurrencies = [
CryptoCurrency.xmr,
CryptoCurrency.btc,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.trx,
CryptoCurrency.nano,
CryptoCurrency.sol,
CryptoCurrency.zano,
CryptoCurrency.ada,
CryptoCurrency.xrp,
CryptoCurrency.xhv,
CryptoCurrency.zaddr,
CryptoCurrency.zec,
CryptoCurrency.dcr,
CryptoCurrency.rvn,
CryptoCurrency.near,
CryptoCurrency.rune,
CryptoCurrency.scrt,
CryptoCurrency.stx,
CryptoCurrency.kmd,
CryptoCurrency.doge,
CryptoCurrency.btcln,
];
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
var pattern = "";
@@ -146,13 +171,13 @@ class AddressValidator extends TextValidator {
pattern = '[0-9a-zA-Z.]+';
case CryptoCurrency.zec:
pattern =
'(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
'(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
case CryptoCurrency.dcr:
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
@@ -344,7 +369,7 @@ class AddressValidator extends TextValidator {
'|([0-9a-zA-Z]{106})';
case CryptoCurrency.btc:
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
case CryptoCurrency.ltc:
pattern = '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
@@ -394,4 +419,4 @@ class AddressValidator extends TextValidator {
return pattern != null ? "($pattern)" : null;
}
}
}
+3 -2
View File
@@ -1,11 +1,12 @@
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com';
const _fiatApiOnionAuthority = 'kfkyguqtz5vcnbvar5pjgddkaeawbo4j3r4fj3e22k3tzqageplosiid.onion';
const _fiatApiClearNetAuthority = 'prices.cakewallet.com';
const _fiatApiOnionAuthority = '46wisoe2uwipcj2j4og6smiq7hbmj34fkkrquwlzbbsqtgat7bf3erid.onion';
const _fiatApiPath = '/v2/rates';
Future<double> _fetchPrice(String crypto, String fiat, bool torOnly) async {
-55
View File
@@ -1,55 +0,0 @@
import 'dart:convert';
import 'package:cake_wallet/entities/yat_record.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class YatService {
static bool isDevMode = false;
static String get apiUrl =>
YatService.isDevMode ? YatService.apiDevUrl : YatService.apiReleaseUrl;
static const apiReleaseUrl = "https://a.y.at";
static const apiDevUrl = 'https://a.yat.fyi';
static String lookupEmojiUrl(String emojiId) =>
"$apiUrl/emoji_id/$emojiId/payment";
static const String MONERO_SUB_ADDRESS = '0x1002';
static const String MONERO_STD_ADDRESS = '0x1001';
static const tags = {
'XMR': "$MONERO_STD_ADDRESS,$MONERO_SUB_ADDRESS",
'BTC': '0x1003',
'LTC': '0x1019'
};
Future<List<YatRecord>> fetchYatAddress(String emojiId, String ticker) async {
final formattedTicker = ticker.toUpperCase();
final formattedEmojiId = emojiId.replaceAll(' ', '');
final tag = tags[formattedTicker];
final uri = Uri.parse(lookupEmojiUrl(formattedEmojiId)).replace(
queryParameters: <String, dynamic>{
"tags": tag
});
final yatRecords = <YatRecord>[];
try {
final response = await ProxyWrapper().get(clearnetUri: uri);
final resBody = json.decode(response.body) as Map<String, dynamic>;
final results = resBody["result"] as Map<dynamic, dynamic>;
// Favour a subaddress over a standard address.
final yatRecord = (
results[MONERO_SUB_ADDRESS] ??
results[MONERO_STD_ADDRESS] ??
results[tag]) as Map<String, dynamic>;
if (yatRecord.isNotEmpty) {
yatRecords.add(YatRecord.fromJson(yatRecord));
}
return yatRecords;
} catch (_) {
return yatRecords;
}
}
}
+17 -10
View File
@@ -6,6 +6,9 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
@@ -29,7 +32,6 @@ import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
import 'package:cake_wallet/entities/bridge_transfer.dart';
@@ -37,7 +39,6 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/entities/template.dart';
@@ -200,7 +201,6 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/device_info.dart';
@@ -901,6 +901,7 @@ Future<void> setup({
getIt.get<AppStore>(),
getIt.get<SendTemplateViewModel>(),
getIt.get<FiatConversionStore>(),
getIt.get<AddressResolverService>(),
getIt.get<BalanceViewModel>(),
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
@@ -1102,7 +1103,10 @@ Future<void> setup({
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
(ContactRecord? contact, _) => ContactViewModel(_contactSource,
getIt.get<AppStore>(),
getIt.get<AddressResolverService>(),
contact: contact));
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) =>
@@ -1251,7 +1255,10 @@ Future<void> setup({
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) {
return ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>(), paymentRequest);
return ExchangePage(getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
getIt.get<AddressResolverService>(),
paymentRequest);
});
getIt.registerFactoryParam<NewSwapPage, PaymentRequest?, CryptoCurrency?>(
@@ -1259,6 +1266,7 @@ Future<void> setup({
return NewSwapPage(
getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
getIt.get<AddressResolverService>(),
paymentRequest,
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
initialCurrency: initialCurrency,
@@ -1500,7 +1508,7 @@ Future<void> setup({
getIt.registerFactoryParam<NewBuySellAmountPage, BuySellPageMode, void>((mode, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: mode)));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>(), getIt.get<AddressResolverService>()));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
@@ -1598,10 +1606,9 @@ Future<void> setup({
getIt.registerFactory(() => YatService());
getIt.registerFactory(() => AddressResolver(
yatService: getIt.get<YatService>(),
wallet: getIt.get<AppStore>().wallet!,
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<AddressResolverService>(
() => AddressResolverService(settingsStore: getIt.get<SettingsStore>()),
);
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
-54
View File
@@ -1,54 +0,0 @@
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:ens_dart/ens_dart.dart';
import 'package:web3dart/web3dart.dart';
class EnsRecord {
static Future<String> fetchEnsAddress(String name, {WalletBase? wallet}) async {
Web3Client? _client;
if (wallet != null && (isEVMCompatibleChain(wallet.type))) {
_client = evm!.getWeb3Client(wallet);
}
if (_client == null) {
late final client = ProxyWrapper().getHttpIOClient();
_client = Web3Client("https://ethereum-rpc.publicnode.com", client);
}
try {
final ens = Ens(client: _client);
if (wallet != null) {
switch (wallet.type) {
case WalletType.monero:
return await ens.withName(name).getCoinAddress(CoinType.XMR);
case WalletType.bitcoin:
return await ens.withName(name).getCoinAddress(CoinType.BTC);
case WalletType.litecoin:
return await ens.withName(name).getCoinAddress(CoinType.LTC);
case WalletType.haven:
return await ens.withName(name).getCoinAddress(CoinType.XHV);
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
default:
return (await ens.withName(name).getAddress()).hex;
}
}
final addr = await ens.withName(name).getAddress();
return addr.hex;
} catch (e) {
printV(e);
return "";
}
}
}
-65
View File
@@ -1,65 +0,0 @@
import 'dart:convert';
import 'package:cw_core/utils/proxy_wrapper.dart';
class FioAddressProvider {
static const apiAuthority = 'fio.blockpane.com';
static const availCheck = '/v1/chain/avail_check';
static const getAddress = '/v1/chain/get_pub_address';
static Future<bool> checkAvail(String fioAddress) async {
bool isFioRegistered = false;
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{"fio_name": fioAddress};
final uri = Uri.https(apiAuthority, availCheck);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
if (response.statusCode != 200) {
return isFioRegistered;
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
isFioRegistered = responseJSON['is_registered'] as int == 1;
return isFioRegistered;
}
static Future<String> getPubAddress(String fioAddress, String token) async {
final headers = {'Content-Type': 'application/json'};
final body = <String, String>{
"fio_address": fioAddress,
"chain_code": token.toUpperCase(),
"token_code": token.toUpperCase(),
};
final uri = Uri.https(apiAuthority, getAddress);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(body),
);
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] as String;
final message = responseJSON['message'] as String;
throw Exception('${error}\n$message');
}
if (response.statusCode != 200) {
throw Exception('Unexpected response http status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final String pubAddress = responseJSON['public_address'] as String;
return pubAddress;
}
}
@@ -6,6 +6,7 @@ class ListItemCheckbox extends ListItem {
required super.keyValue,
required super.label,
this.subtitle,
this.subtitleColor,
this.iconPath,
this.onTap,
this.showArrow = false,
@@ -18,5 +19,6 @@ class ListItemCheckbox extends ListItem {
final bool value;
final String? subtitle;
final String? iconPath;
final Color? subtitleColor;
final bool showArrow;
}

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