mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'dev' into cw-1497-transaction-history-improved-layout
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -1736,37 +1734,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 +2085,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 +2116,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);
|
||||
@@ -2434,20 +2407,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 +3181,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 +3762,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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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});
|
||||
|
||||
|
||||
@@ -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,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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-70
@@ -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
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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';
|
||||
@@ -198,7 +199,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';
|
||||
@@ -899,6 +899,7 @@ Future<void> setup({
|
||||
getIt.get<AppStore>(),
|
||||
getIt.get<SendTemplateViewModel>(),
|
||||
getIt.get<FiatConversionStore>(),
|
||||
getIt.get<AddressResolverService>(),
|
||||
getIt.get<BalanceViewModel>(),
|
||||
getIt.get<ContactListViewModel>(),
|
||||
_transactionDescriptionBox,
|
||||
@@ -1100,7 +1101,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, _) =>
|
||||
@@ -1249,7 +1253,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?>(
|
||||
@@ -1257,6 +1264,7 @@ Future<void> setup({
|
||||
return NewSwapPage(
|
||||
getIt.get<ExchangeViewModel>(),
|
||||
getIt.get<AuthService>(),
|
||||
getIt.get<AddressResolverService>(),
|
||||
paymentRequest,
|
||||
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
|
||||
initialCurrency: initialCurrency,
|
||||
@@ -1494,7 +1502,7 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
|
||||
|
||||
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>;
|
||||
@@ -1592,10 +1600,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));
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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,
|
||||
}) {
|
||||
String address = '';
|
||||
String name = formattedName;
|
||||
String note = '';
|
||||
|
||||
for (RRecord element in txtRecord) {
|
||||
String record = element.data;
|
||||
|
||||
if (record.contains("oa1:$ticker") && record.contains("recipient_address")) {
|
||||
record = record.replaceAll('\"', "");
|
||||
|
||||
final dataList = record.split(";");
|
||||
|
||||
address = dataList
|
||||
.where((item) => (item.contains("recipient_address")))
|
||||
.toString()
|
||||
.replaceAll("oa1:$ticker recipient_address=", "")
|
||||
.replaceAll("(", "")
|
||||
.replaceAll(")", "")
|
||||
.trim();
|
||||
|
||||
final recipientName = dataList
|
||||
.where((item) => (item.contains("recipient_name")))
|
||||
.toString()
|
||||
.replaceAll("(", "")
|
||||
.replaceAll(")", "")
|
||||
.trim();
|
||||
|
||||
if (recipientName.isNotEmpty) {
|
||||
name = recipientName.replaceAll("recipient_name=", "");
|
||||
}
|
||||
|
||||
final description = dataList
|
||||
.where((item) => (item.contains("tx_description")))
|
||||
.toString()
|
||||
.replaceAll("(", "")
|
||||
.replaceAll(")", "")
|
||||
.trim();
|
||||
|
||||
if (description.isNotEmpty) {
|
||||
note = description.replaceAll("tx_description=", "");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return OpenaliasRecord(address: address, name: name, description: note);
|
||||
}
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/core/yat_service.dart';
|
||||
import 'package:cake_wallet/entities/emoji_string_extension.dart';
|
||||
import 'package:cake_wallet/entities/ens_record.dart';
|
||||
import 'package:cake_wallet/entities/lnurlpay_record.dart';
|
||||
import 'package:cake_wallet/entities/fio_address_provider.dart';
|
||||
import 'package:cake_wallet/entities/openalias_record.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/entities/unstoppable_domain_address.dart';
|
||||
import 'package:cake_wallet/entities/wellknown_record.dart';
|
||||
import 'package:cake_wallet/entities/zano_alias.dart';
|
||||
import 'package:cake_wallet/entities/zcash_names_record.dart';
|
||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:cake_wallet/mastodon/mastodon_api.dart';
|
||||
import 'package:cake_wallet/nostr/nostr_api.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/twitter/twitter_api.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';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'bip_353_record.dart';
|
||||
|
||||
class AddressResolver {
|
||||
AddressResolver({required this.yatService, required this.wallet, required this.settingsStore})
|
||||
: walletType = wallet.type;
|
||||
|
||||
final YatService yatService;
|
||||
final WalletType walletType;
|
||||
final WalletBase wallet;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
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"
|
||||
];
|
||||
|
||||
static String? extractAddressByType(
|
||||
{required String raw,
|
||||
required CryptoCurrency type,
|
||||
bool requireSurroundingWhitespaces = true}) {
|
||||
var addressPattern = AddressValidator.getAddressFromStringPattern(type);
|
||||
|
||||
if (addressPattern == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requireSurroundingWhitespaces)
|
||||
addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX";
|
||||
|
||||
final match = RegExp(addressPattern, multiLine: true).firstMatch(raw);
|
||||
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 String extractUnstoppableDomain(String raw) {
|
||||
// sort by length to avoid matching shorter tld instead of longer
|
||||
final domains = List<String>.from(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 '';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
String _stripHtmlTags(String value) {
|
||||
return value
|
||||
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'<[^>]*>'), '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
Future<ParsedAddress> resolve(BuildContext context, String text, CryptoCurrency currency) async {
|
||||
final ticker = currency.title;
|
||||
try {
|
||||
if (text.startsWith("zcash.me")) {
|
||||
final parts = text.split("/");
|
||||
final handle = parts.last;
|
||||
if (parts.length == 2 && handle.isNotEmpty) {
|
||||
final extractZcashAddress = await _fetchZcashAddress(handle);
|
||||
if (extractZcashAddress != null) {
|
||||
return ParsedAddress.zcashAddress(
|
||||
address: extractZcashAddress,
|
||||
name: handle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// twitter handle example: @username
|
||||
if (text.startsWith('@') && !text.substring(1).contains('@')) {
|
||||
if (currency == CryptoCurrency.zano && settingsStore.lookupsZanoAlias) {
|
||||
final formattedName = text.substring(1);
|
||||
final zanoAddress = await ZanoAlias.fetchZanoAliasAddress(formattedName);
|
||||
if (zanoAddress != null && zanoAddress.isNotEmpty) {
|
||||
return ParsedAddress.zanoAddress(
|
||||
address: zanoAddress,
|
||||
name: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (settingsStore.lookupsTwitter) {
|
||||
final formattedName = text.substring(1);
|
||||
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
|
||||
final addressFromBio = extractAddressByType(
|
||||
raw: twitterUser.description,
|
||||
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency));
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.fetchTwitterAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name);
|
||||
} else {
|
||||
final domain = extractUnstoppableDomain(twitterUser.description);
|
||||
if (domain.isNotEmpty) {
|
||||
final parsedAddressFromDomain = await resolve(context, domain, currency);
|
||||
if (parsedAddressFromDomain.addresses.isNotEmpty &&
|
||||
parsedAddressFromDomain.addresses.first != domain) {
|
||||
return ParsedAddress(
|
||||
addresses: parsedAddressFromDomain.addresses,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
parseFrom: ParseFrom.twitter,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final addressFromLocation = extractAddressByType(
|
||||
raw: twitterUser.location,
|
||||
type: CryptoCurrency.fromString(ticker,
|
||||
walletCurrency: wallet.currency), requireSurroundingWhitespaces: false);
|
||||
if (addressFromLocation != null && addressFromLocation.isNotEmpty) {
|
||||
return ParsedAddress.fetchTwitterAddress(
|
||||
address: addressFromLocation,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name);
|
||||
} else {
|
||||
final domain = extractUnstoppableDomain(twitterUser.location);
|
||||
if (domain.isNotEmpty) {
|
||||
final parsedAddressFromDomain = await resolve(context, domain, currency);
|
||||
if (parsedAddressFromDomain.addresses.isNotEmpty &&
|
||||
parsedAddressFromDomain.addresses.first != domain) {
|
||||
return ParsedAddress(
|
||||
addresses: parsedAddressFromDomain.addresses,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
parseFrom: ParseFrom.twitter,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final pinnedTweet = twitterUser.pinnedTweet?.text;
|
||||
if (pinnedTweet != null) {
|
||||
final addressFromPinnedTweet = extractAddressByType(
|
||||
raw: pinnedTweet,
|
||||
type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency),
|
||||
requireSurroundingWhitespaces: true);
|
||||
if (addressFromPinnedTweet != null) {
|
||||
return ParsedAddress.fetchTwitterAddress(
|
||||
address: addressFromPinnedTweet,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name);
|
||||
} else {
|
||||
final domain = extractUnstoppableDomain(pinnedTweet);
|
||||
if (domain.isNotEmpty) {
|
||||
final parsedAddressFromDomain = await resolve(context, domain, currency);
|
||||
if (parsedAddressFromDomain.addresses.isNotEmpty &&
|
||||
parsedAddressFromDomain.addresses.first != domain) {
|
||||
return ParsedAddress(
|
||||
addresses: parsedAddressFromDomain.addresses,
|
||||
name: text,
|
||||
profileImageUrl: twitterUser.profileImageUrl,
|
||||
profileName: twitterUser.name,
|
||||
parseFrom: ParseFrom.twitter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mastodon example: @username@hostname.xxx
|
||||
if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) {
|
||||
if (settingsStore.lookupsMastodon) {
|
||||
final subText = text.substring(1);
|
||||
final hostNameIndex = subText.indexOf('@');
|
||||
final hostName = subText.substring(hostNameIndex + 1);
|
||||
final userName = subText.substring(0, hostNameIndex);
|
||||
|
||||
final mastodonUser =
|
||||
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
|
||||
|
||||
if (mastodonUser != null) {
|
||||
final mastodonNote = _stripHtmlTags(mastodonUser.note);
|
||||
String? addressFromBio = extractAddressByType(raw: mastodonNote, type: currency, requireSurroundingWhitespaces: true);
|
||||
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.fetchMastodonAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username);
|
||||
} else {
|
||||
final pinnedPosts =
|
||||
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
|
||||
|
||||
if (pinnedPosts.isNotEmpty) {
|
||||
final userPinnedPostsText = pinnedPosts.map((item) => _stripHtmlTags(item.content)).join('\n');
|
||||
String? addressFromPinnedPost =
|
||||
extractAddressByType(raw: userPinnedPostsText, type: currency,
|
||||
requireSurroundingWhitespaces: true);
|
||||
|
||||
if (addressFromPinnedPost != null && addressFromPinnedPost.isNotEmpty) {
|
||||
return ParsedAddress.fetchMastodonAddress(
|
||||
address: addressFromPinnedPost,
|
||||
name: text,
|
||||
profileImageUrl: mastodonUser.profileImageUrl,
|
||||
profileName: mastodonUser.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .well-known scheme:
|
||||
if (text.contains('.') && text.contains('@')) {
|
||||
if (settingsStore.lookupsWellKnown) {
|
||||
final record =
|
||||
await WellKnownRecord.fetchAddressAndName(formattedName: text, currency: currency);
|
||||
if (record != null) {
|
||||
return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text);
|
||||
}
|
||||
}
|
||||
|
||||
if (walletType == WalletType.bitcoin) {
|
||||
final record =
|
||||
await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency);
|
||||
if (record != null) {
|
||||
return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) {
|
||||
final bool isFioRegistered = await FioAddressProvider.checkAvail(text);
|
||||
if (isFioRegistered) {
|
||||
final address = await FioAddressProvider.getPubAddress(text, ticker);
|
||||
return ParsedAddress.fetchFioAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
if (text.hasOnlyEmojis) {
|
||||
if (settingsStore.lookupsYatService) {
|
||||
if (walletType != WalletType.haven) {
|
||||
final addresses = await yatService.fetchYatAddress(text, ticker);
|
||||
return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isNormalAddress = extractAddressByType(raw: text, type: currency)?.isNotEmpty ?? false;
|
||||
|
||||
if (text.length <= 30 && !isNormalAddress) {
|
||||
final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text);
|
||||
if (thorChainAddress != null && thorChainAddress.isNotEmpty) {
|
||||
String? address =
|
||||
thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null);
|
||||
if (address != null) {
|
||||
return ParsedAddress.thorChainAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final formattedName = OpenaliasRecord.formatDomainName(text);
|
||||
final domainParts = formattedName.split('.');
|
||||
final name = domainParts.last;
|
||||
|
||||
if (domainParts.length <= 1 || domainParts.first.isEmpty || name.isEmpty) {
|
||||
return ParsedAddress(addresses: [text]);
|
||||
}
|
||||
|
||||
if (unstoppableDomains.any((domain) => name.trim() == domain)) {
|
||||
if (settingsStore.lookupsUnstoppableDomains) {
|
||||
final address = await fetchUnstoppableDomainAddress(text, ticker);
|
||||
if (address.isNotEmpty) {
|
||||
return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(text, ticker);
|
||||
|
||||
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
|
||||
if (chosenAddress != null) {
|
||||
try {
|
||||
final dnsProof = await Bip353Record.fetchDnsProof(text);
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof);
|
||||
} catch (e) {
|
||||
printV('Bip353Record.fetchBip353AddressAddress error: $e');
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final lowerText = text.toLowerCase();
|
||||
if (lowerText.endsWith(".zec") || lowerText.endsWith(".zcash")) {
|
||||
if (settingsStore.lookupsZcashNames) {
|
||||
final address = await ZcashNamesRecord.fetchZcashNamesAddress(text);
|
||||
if (address != null && address.isNotEmpty) {
|
||||
return ParsedAddress.zcashNameAddress(address: address, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (text.endsWith(".eth")) {
|
||||
if (settingsStore.lookupsENS) {
|
||||
final address = await EnsRecord.fetchEnsAddress(text, wallet: wallet);
|
||||
if (address.isNotEmpty && address != "0x0000000000000000000000000000000000000000") {
|
||||
return ParsedAddress.fetchEnsAddress(name: text, address: address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedName.contains(".")) {
|
||||
if (settingsStore.lookupsOpenAlias) {
|
||||
final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName);
|
||||
|
||||
if (txtRecord != null) {
|
||||
final record = await OpenaliasRecord.fetchAddressAndName(
|
||||
formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord);
|
||||
return ParsedAddress.fetchOpenAliasAddress(record: record, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isEmailFormat(text)) {
|
||||
final nostrProfile = await NostrProfileHandler.queryProfile(context, text);
|
||||
if (nostrProfile?.relays != null) {
|
||||
final nostrUserData =
|
||||
await NostrProfileHandler.processRelays(context, nostrProfile!, text);
|
||||
|
||||
if (nostrUserData != null) {
|
||||
String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency);
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.nostrAddress(
|
||||
address: addressFromBio,
|
||||
name: text,
|
||||
profileImageUrl: nostrUserData.picture,
|
||||
profileName: nostrUserData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
}
|
||||
|
||||
return ParsedAddress(addresses: [text]);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (match != null) {
|
||||
return match.group(0);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error fetching zcash.me profile: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import 'package:cake_wallet/entities/openalias_record.dart';
|
||||
import 'package:cake_wallet/entities/yat_record.dart';
|
||||
|
||||
enum ParseFrom {
|
||||
unstoppableDomains,
|
||||
openAlias,
|
||||
yatRecord,
|
||||
fio,
|
||||
notParsed,
|
||||
twitter,
|
||||
ens,
|
||||
contact,
|
||||
mastodon,
|
||||
nostr,
|
||||
thorChain,
|
||||
wellKnown,
|
||||
zanoAlias,
|
||||
zcashAddress,
|
||||
zcashName,
|
||||
bip353,
|
||||
lnurlpay,
|
||||
}
|
||||
|
||||
class ParsedAddress {
|
||||
ParsedAddress({
|
||||
required this.addresses,
|
||||
this.name = '',
|
||||
this.description = '',
|
||||
this.profileImageUrl = '',
|
||||
this.profileName = '',
|
||||
this.parseFrom = ParseFrom.notParsed,
|
||||
this.bip353DnsProof,
|
||||
});
|
||||
|
||||
factory ParsedAddress.fetchEmojiAddress({
|
||||
List<YatRecord>? addresses,
|
||||
required String name,
|
||||
}) {
|
||||
if (addresses?.isEmpty ?? true) {
|
||||
return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: addresses!.map((e) => e.address).toList(),
|
||||
name: name,
|
||||
parseFrom: ParseFrom.yatRecord,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchUnstoppableDomainAddress({
|
||||
String? address,
|
||||
required String name,
|
||||
}) {
|
||||
if (address?.isEmpty ?? true) {
|
||||
return ParsedAddress(addresses: [name]);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: [address!],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.unstoppableDomains,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchBip353AddressAddress ({
|
||||
required String address,
|
||||
required String name,
|
||||
String? dnsProof,
|
||||
}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.bip353,
|
||||
bip353DnsProof: dnsProof,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchOpenAliasAddress(
|
||||
{required OpenaliasRecord record, required String name}) {
|
||||
if (record.address.isEmpty) {
|
||||
return ParsedAddress(addresses: [name]);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: [record.address],
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
parseFrom: ParseFrom.openAlias,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchFioAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.fio,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchTwitterAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName,
|
||||
String? description}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
description: description ?? '',
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
parseFrom: ParseFrom.twitter,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchMastodonAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.mastodon,
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchContactAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.contact,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchEnsAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.ens,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.nostrAddress(
|
||||
{required String address,
|
||||
required String name,
|
||||
required String profileImageUrl,
|
||||
required String profileName}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.nostr,
|
||||
profileImageUrl: profileImageUrl,
|
||||
profileName: profileName,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.thorChainAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.thorChain,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.zanoAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.zanoAlias,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.zcashAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.zcashAddress,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.zcashNameAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.zcashName,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchWellKnownAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.wellKnown,
|
||||
);
|
||||
}
|
||||
|
||||
factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.lnurlpay,
|
||||
);
|
||||
}
|
||||
|
||||
final List<String> addresses;
|
||||
final String name;
|
||||
final String description;
|
||||
final String profileImageUrl;
|
||||
final String profileName;
|
||||
final ParseFrom parseFrom;
|
||||
final String? bip353DnsProof;
|
||||
}
|
||||
@@ -102,7 +102,13 @@ class PreferencesKey {
|
||||
static const lookupsOpenAlias = 'looks_up_open_alias';
|
||||
static const lookupsENS = 'looks_up_ens';
|
||||
static const lookupsZcashNames = 'looks_up_zcash_names';
|
||||
static const lookupsZcashAddress = 'looks_up_zcash_address';
|
||||
static const lookupsWellKnown = 'looks_up_well_known';
|
||||
static const lookupsFio = 'looks_up_fio';
|
||||
static const lookupsNostr = 'looks_up_nostr';
|
||||
static const lookupsThorChain = 'looks_up_thor_chain';
|
||||
static const lookupsBip353 = 'looks_up_bip353';
|
||||
static const lookupsLNUrl = 'looks_up_lnurl';
|
||||
static const useBlinkProtection = 'use_blink_protection';
|
||||
static const usePayjoin = 'use_payjoin';
|
||||
static const showPayjoinCard = 'show_payjoin_card';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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.name,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String address;
|
||||
|
||||
static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency) async {
|
||||
String jsonLocation = "";
|
||||
switch (currency) {
|
||||
case CryptoCurrency.nano:
|
||||
jsonLocation = "nano-currency";
|
||||
break;
|
||||
// TODO: add other currencies
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
// split the string by the @ symbol:
|
||||
try {
|
||||
final List<String> splitStrs = username.split("@");
|
||||
String name = splitStrs.first.toLowerCase();
|
||||
final String domain = splitStrs.last;
|
||||
|
||||
if (splitStrs.length == 3) {
|
||||
// for username like @alice@domain.org instead of alice@domain.org
|
||||
name = splitStrs[1];
|
||||
}
|
||||
|
||||
if (name.isEmpty) {
|
||||
name = "_";
|
||||
}
|
||||
|
||||
// lookup domain/.well-known/nano-currency.json and check if it has a nano address:
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"),
|
||||
headers: <String, String>{"Accept": "application/json"},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// Access the first element in the names array and retrieve its address
|
||||
final List<dynamic> names = decoded["names"] as List<dynamic>;
|
||||
for (final dynamic item in names) {
|
||||
if (item["name"].toLowerCase() == name) {
|
||||
return item["address"] as String;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV("error checking well-known username: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String formatDomainName(String name) {
|
||||
String formattedName = name;
|
||||
|
||||
if (name.contains("@")) {
|
||||
formattedName = name.replaceAll("@", ".");
|
||||
}
|
||||
|
||||
return formattedName;
|
||||
}
|
||||
|
||||
static Future<WellKnownRecord?> fetchAddressAndName({
|
||||
required String formattedName,
|
||||
required CryptoCurrency currency,
|
||||
}) async {
|
||||
String name = formattedName;
|
||||
|
||||
printV("formattedName: $formattedName");
|
||||
|
||||
final address = await checkWellKnownUsername(formattedName, currency);
|
||||
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return WellKnownRecord(address: address, name: name);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
class YatRecord {
|
||||
YatRecord({
|
||||
required this.category,
|
||||
required this.address,
|
||||
});
|
||||
|
||||
YatRecord.fromJson(Map<String, dynamic> json)
|
||||
: address = json['address'] as String,
|
||||
category = json['category'] as String;
|
||||
|
||||
String category;
|
||||
String address;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class ZanoAlias {
|
||||
static Future<String?> fetchZanoAliasAddress(String alias) async {
|
||||
try {
|
||||
final uri = Uri.parse("http://37.27.100.59:10500/json_rpc");
|
||||
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('Zano Alias error: ${e.toString()}');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+24
-22
@@ -692,29 +692,18 @@ class CWEVM extends EVM {
|
||||
);
|
||||
}
|
||||
|
||||
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
|
||||
Erc20Token token,
|
||||
BigInt balanceWei,
|
||||
) async {
|
||||
Future<double> _fetchFiatApiPriceForToken(Erc20Token token) async {
|
||||
try {
|
||||
final settingsStore = getIt.get<SettingsStore>();
|
||||
final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
|
||||
|
||||
final price = await FiatConversionService.fetchPrice(
|
||||
return await FiatConversionService.fetchPrice(
|
||||
crypto: token,
|
||||
fiat: FiatCurrency.usd,
|
||||
torOnly: torOnly,
|
||||
);
|
||||
|
||||
final hasValidFiatPrice = price > 0;
|
||||
|
||||
final decimals = token.decimal;
|
||||
final balance = balanceWei.toDouble() / math.pow(10, decimals);
|
||||
final usdValue = balance * price;
|
||||
|
||||
return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
|
||||
} catch (e) {
|
||||
return (usdValue: 0.0, hasValidFiatPrice: false);
|
||||
} catch (_) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,21 +719,34 @@ class CWEVM extends EVM {
|
||||
|
||||
final List<Future<void>> tokenChecks = [];
|
||||
|
||||
final whitelistedContracts =
|
||||
wallet.getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
|
||||
|
||||
for (final item in result.newTokens) {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
final isWhitelisted = whitelistedContracts.contains(token.contractAddress.toLowerCase());
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(
|
||||
token,
|
||||
item.balanceWei,
|
||||
);
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
final moralisPrice = item.moralisUsdPrice;
|
||||
final moralisValue = item.moralisUsdValue ?? 0.0;
|
||||
final hasMoralisPrice = moralisPrice != null && moralisPrice > 0;
|
||||
|
||||
final fiatApiPrice = await _fetchFiatApiPriceForToken(token);
|
||||
final hasFiatApiPrice = fiatApiPrice > 0;
|
||||
|
||||
final isImpersonator =
|
||||
hasFiatApiPrice && !hasMoralisPrice && !isWhitelisted && !item.verifiedContract;
|
||||
|
||||
final isSpam = isPropertiesSuspicious ||
|
||||
token.isPotentialScam ||
|
||||
isImpersonator ||
|
||||
(!hasMoralisPrice && !hasFiatApiPrice);
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
token.enabled =
|
||||
hasMoralisPrice && hasFiatApiPrice && (moralisValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
await wallet.addErc20Token(token);
|
||||
})());
|
||||
|
||||
@@ -29,6 +29,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.usdcArb,
|
||||
CryptoCurrency.usdtArb,
|
||||
CryptoCurrency.trx,
|
||||
CryptoCurrency.usdttrc20,
|
||||
];
|
||||
|
||||
static const _baseURL = 'chainflip-broker.io';
|
||||
@@ -315,6 +317,11 @@ class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
// Chainflip uses 'tron' as the network slug, but Cake uses tag 'TRX' (and null
|
||||
// for native TRX), so these can't go through the generic title.tag logic below.
|
||||
if (currency == CryptoCurrency.trx) return 'trx.tron';
|
||||
if (currency == CryptoCurrency.usdttrc20) return 'usdt.tron';
|
||||
|
||||
final tag = currency.tag?.toLowerCase();
|
||||
final title = currency.title.toLowerCase();
|
||||
|
||||
@@ -330,6 +337,7 @@ class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
'ETHEREUM' => 'Ethereum',
|
||||
'ARBITRUM' => 'Arbitrum',
|
||||
'SOLANA' => 'Solana',
|
||||
'TRON' => 'Tron',
|
||||
_ => name
|
||||
};
|
||||
|
||||
@@ -350,6 +358,8 @@ class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
'eth.arb' => CryptoCurrency.arbEth,
|
||||
'usdc.arb' => CryptoCurrency.usdcArb,
|
||||
'usdt.arb' => CryptoCurrency.usdtArb,
|
||||
'trx.tron' => CryptoCurrency.trx,
|
||||
'usdt.tron' => CryptoCurrency.usdttrc20,
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
@@ -575,10 +575,17 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
|
||||
final token = supported.firstWhereOrNull((t) => t.assetId == assetId);
|
||||
|
||||
if (token == null) return null;
|
||||
final title = token.symbol;
|
||||
|
||||
final title = token.symbol.toUpperCase()
|
||||
.replaceAll(RegExp(r'\s*\([^)]*\)'), '');
|
||||
|
||||
final normalizedNetwork = _normalizeNearBlockchainToTag(token.blockchain);
|
||||
final tag =
|
||||
normalizedNetwork == title.toUpperCase() ? null : normalizedNetwork;
|
||||
|
||||
final isNativeAsset = assetId.contains(':native:coin');
|
||||
|
||||
final tag = isNativeAsset || normalizedNetwork == title
|
||||
? null
|
||||
: normalizedNetwork;
|
||||
|
||||
return (title, tag);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
|
||||
@@ -263,15 +264,34 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
_addressFocusNode.addListener(() async {
|
||||
if (!_addressFocusNode.hasFocus && _addressControllers[_selectedOutput].text.isNotEmpty) {
|
||||
final output = widget.sendViewModel.outputs[_selectedOutput];
|
||||
output.fetchParsedAddress(context).then((val) {
|
||||
if (_addressControllers[_selectedOutput].text != output.extractedAddress) {
|
||||
_addressControllers[_selectedOutput].text = output.extractedAddress;
|
||||
}
|
||||
});
|
||||
await _resolveAddressForOutput(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _resolveAddressForOutput(Output output) async {
|
||||
final result = await widget.sendViewModel.resolveAddressForOutput(output);
|
||||
if (result == null) return;
|
||||
|
||||
final confirmed = await showParsedAddressConfirmationAlert(context, result);
|
||||
|
||||
if (confirmed) {
|
||||
output.applyAddressLookupResult(result);
|
||||
} else {
|
||||
output.resetParsedAddress();
|
||||
return;
|
||||
}
|
||||
|
||||
final outputIndex = widget.sendViewModel.outputs.indexOf(output);
|
||||
if (outputIndex >= 0 &&
|
||||
outputIndex < _addressControllers.length &&
|
||||
output.extractedAddress.isNotEmpty &&
|
||||
_addressControllers[outputIndex].text != output.extractedAddress) {
|
||||
_addressControllers[outputIndex].text = output.extractedAddress;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (_) {
|
||||
@@ -377,7 +397,7 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
focusNode: _addressFocusNode,
|
||||
onURIScanned: (uri) async {
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
await _resolveAddressForOutput(output);
|
||||
|
||||
// Process the payment through the new flow
|
||||
await _handlePaymentFlow(
|
||||
@@ -404,7 +424,7 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
_justHandledPasteButton = true;
|
||||
try {
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
await _resolveAddressForOutput(output);
|
||||
|
||||
final address = output.isParsedAddress
|
||||
? output.extractedAddress
|
||||
@@ -1429,3 +1449,28 @@ class SendHelpPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> showParsedAddressConfirmationAlert(
|
||||
BuildContext context,
|
||||
ParsedAddress parsedAddress,
|
||||
) async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).address_detected,
|
||||
headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName,
|
||||
headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty
|
||||
? parsedAddress.addressSource.iconPath
|
||||
: parsedAddress.profileImageUrl,
|
||||
alertContent: S.of(context).extracted_address_content(
|
||||
'${parsedAddress.handle} (${parsedAddress.addressSource.label})',
|
||||
),
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(true),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirmed ?? false;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/core/amount_validator.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
|
||||
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
@@ -23,7 +23,6 @@ import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
@@ -52,7 +51,7 @@ import 'package:mobx/mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class NewSwapPage extends StatefulWidget {
|
||||
NewSwapPage(this.exchangeViewModel, this.authService, this.initialPaymentRequest,
|
||||
NewSwapPage(this.exchangeViewModel, this.authService,this.adrResService, this.initialPaymentRequest,
|
||||
{required this.walletSwitcherViewModel, CryptoCurrency? initialCurrency}) {
|
||||
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
@@ -68,6 +67,7 @@ class NewSwapPage extends StatefulWidget {
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final WalletSwitcherViewModel walletSwitcherViewModel;
|
||||
final AuthService authService;
|
||||
final AddressResolverService adrResService;
|
||||
final PaymentRequest? initialPaymentRequest;
|
||||
late final String? depositWalletName;
|
||||
late final String? receiveWalletName;
|
||||
@@ -423,8 +423,11 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||
printV("$domain");
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
return extractAddressFromParsed(context, parsedAddress);
|
||||
final parsedAddress = await widget.adrResService.resolve(
|
||||
query: domain,
|
||||
wallet: widget.exchangeViewModel.wallet,
|
||||
currency: currency);
|
||||
return parsedAddress.isNotEmpty ? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? '' : '';
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/lnurlpay_record.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
@@ -5,13 +5,12 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class L2SendExternalModal extends StatefulWidget {
|
||||
const L2SendExternalModal({super.key, required this.sendViewModel});
|
||||
@@ -180,11 +179,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: NewPrimaryButton(
|
||||
onPressed:(){
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: output.address),
|
||||
);
|
||||
},
|
||||
onPressed: () => ClipboardUtil.copyToClipboard(context, output.address),
|
||||
text: S.of(context).copy,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
|
||||
@@ -227,13 +227,14 @@ class SendTransactionDetails extends StatelessWidget {
|
||||
decimals: 2)
|
||||
: sendViewModel.pendingTransactionFeeFiatAmountFormatted;
|
||||
|
||||
final showAddress = !sendViewModel.outputs.any((e) =>
|
||||
RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
|
||||
RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
|
||||
(e.isParsedAddress &&
|
||||
e.parsedAddress.addresses.isNotEmpty &&
|
||||
RegExp(AddressValidator.lnurlMatcher)
|
||||
.hasMatch(e.parsedAddress.addresses.firstOrNull?.toLowerCase() ?? "")));
|
||||
final showAddress = !sendViewModel.outputs.any((e) =>
|
||||
RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
|
||||
RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
|
||||
(e.isParsedAddress &&
|
||||
e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency] != null &&
|
||||
e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.isNotEmpty &&
|
||||
RegExp(AddressValidator.lnurlMatcher)
|
||||
.hasMatch(e.parsedAddress.parsedAddressByCurrencyMap[sendViewModel.selectedCryptoCurrency]!.toLowerCase())));
|
||||
|
||||
final outputs = sendViewModel.outputs;
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SwapSendExternalModal extends StatefulWidget {
|
||||
const SwapSendExternalModal(
|
||||
@@ -45,7 +45,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (uri == null) return SizedBox.shrink();
|
||||
if (widget.address.isEmpty) return SizedBox.shrink();
|
||||
final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54);
|
||||
|
||||
return PopScope(
|
||||
@@ -139,7 +139,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
child: QrImage(
|
||||
size: animatedSize,
|
||||
embeddedImagePath: widget.from.iconPath,
|
||||
data: uri.toString(),
|
||||
data: uri?.toString() ?? widget.address,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -175,7 +175,7 @@ class _SwapSendExternalModalState extends State<SwapSendExternalModal> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: NewPrimaryButton(
|
||||
onPressed: ()=> Clipboard.setData(ClipboardData(text:widget.address)),
|
||||
onPressed: () => ClipboardUtil.copyToClipboard(context, widget.address),
|
||||
text: S.of(context).copy,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/nostr/nostr_user.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nostr_tools/nostr_tools.dart';
|
||||
|
||||
class NostrProfileHandler {
|
||||
static final relayToDomainMap = {
|
||||
'relay.snort.social': 'snort.social',
|
||||
};
|
||||
|
||||
static Nip05 _nip05 = Nip05();
|
||||
|
||||
static Future<ProfilePointer?> queryProfile(BuildContext context, String nip05Address) async {
|
||||
var profile = await _nip05.queryProfile(nip05Address);
|
||||
if (profile?.pubkey != null) {
|
||||
if (profile?.relays?.isNotEmpty == true) {
|
||||
return profile;
|
||||
} else {
|
||||
await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relays_message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<UserMetadata?> processRelays(
|
||||
BuildContext context,
|
||||
ProfilePointer profile,
|
||||
String nip05Address,
|
||||
) async {
|
||||
String userDomain = _extractDomain(nip05Address);
|
||||
const int metaData = 0;
|
||||
|
||||
for (String relayUrl in profile.relays ?? []) {
|
||||
final relayDomain = _getDomainFromRelayUrl(relayUrl);
|
||||
final formattedRelayDomain = relayToDomainMap[relayDomain] ?? relayDomain;
|
||||
if (formattedRelayDomain == userDomain) {
|
||||
final userDomainData = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaData]);
|
||||
if (userDomainData != null) {
|
||||
return userDomainData;
|
||||
}
|
||||
}
|
||||
}
|
||||
await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relay_on_domain);
|
||||
|
||||
String? chosenRelayUrl =
|
||||
await _showRelayChoiceDialog(context, profile.relays ?? []);
|
||||
if (chosenRelayUrl != null) {
|
||||
final userData = await _fetchInfoFromRelay(chosenRelayUrl, profile.pubkey, [metaData]);
|
||||
if (userData != null) {
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<UserMetadata?> _fetchInfoFromRelay(
|
||||
String relayUrl, String userPubKey, List<int> kinds) async {
|
||||
try {
|
||||
final relay = RelayApi(relayUrl: relayUrl);
|
||||
final stream = await relay.connect();
|
||||
|
||||
relay.sub([
|
||||
Filter(
|
||||
kinds: kinds,
|
||||
authors: [userPubKey],
|
||||
)
|
||||
]);
|
||||
|
||||
await for (var message in stream) {
|
||||
if (message.type == 'EVENT') {
|
||||
final event = message.message as Event;
|
||||
|
||||
final eventContent = json.decode(event.content) as Map<String, dynamic>;
|
||||
|
||||
final userMetadata = UserMetadata.fromJson(eventContent);
|
||||
relay.close();
|
||||
return userMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
relay.close();
|
||||
return null;
|
||||
} catch (e) {
|
||||
printV('[!] Error with relay $relayUrl: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _showErrorDialog(
|
||||
BuildContext context, String title, String errorMessage) async {
|
||||
if (context.mounted) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: title,
|
||||
alertContent: errorMessage,
|
||||
buttonText: S.of(dialogContext).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static String _extractDomain(String nip05Address) {
|
||||
var parts = nip05Address.split('@');
|
||||
return parts.length == 2 ? parts[1] : '';
|
||||
}
|
||||
|
||||
static String _getDomainFromRelayUrl(String relayUrl) {
|
||||
try {
|
||||
var uri = Uri.parse(relayUrl);
|
||||
return uri.host;
|
||||
} catch (e) {
|
||||
printV('Error parsing URL: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> _showRelayChoiceDialog(
|
||||
BuildContext context,
|
||||
List<String> relays,
|
||||
) async {
|
||||
String? selectedRelay;
|
||||
|
||||
if (context.mounted) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Picker<String>(
|
||||
selectedAtIndex: 0,
|
||||
title: S.of(dialogContext).choose_relay,
|
||||
items: relays,
|
||||
onItemSelected: (String relay) => selectedRelay = relay,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return selectedRelay;
|
||||
}
|
||||
}
|
||||
@@ -387,9 +387,11 @@ class CWSolana extends Solana {
|
||||
tokenChecks.add((() async {
|
||||
final token = item.token;
|
||||
|
||||
final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
|
||||
|
||||
final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
|
||||
|
||||
final isSpam = !fiatResult.hasValidFiatPrice;
|
||||
final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
|
||||
|
||||
token.isPotentialScam = isSpam;
|
||||
token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:cake_wallet/buy/sell_buy_states.dart';
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
|
||||
@@ -16,7 +16,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
|
||||
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/src/widgets/trail_button.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
@@ -28,9 +27,10 @@ import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class BuySellPage extends BasePage {
|
||||
BuySellPage(this.buySellViewModel);
|
||||
BuySellPage(this.buySellViewModel, this._resolver);
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
final AddressResolverService _resolver;
|
||||
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
@@ -300,7 +300,7 @@ class BuySellPage extends BasePage {
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
|
||||
if (cryptoAddressController != address) {
|
||||
if (cryptoAddressController.text != address) {
|
||||
cryptoCurrencyKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
@@ -320,8 +320,10 @@ class BuySellPage extends BasePage {
|
||||
_cryptoAddressFocus.addListener(() async {
|
||||
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
|
||||
final domain = cryptoAddressController.text;
|
||||
buySellViewModel.cryptoCurrencyAddress = await fetchParsedAddress(
|
||||
context, domain, buySellViewModel.cryptoCurrency);
|
||||
final parsed = await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
|
||||
if (parsed.isNotEmpty) {
|
||||
buySellViewModel.cryptoCurrencyAddress = parsed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -410,8 +412,23 @@ class BuySellPage extends BasePage {
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
onPushPasteButton: (context) async {},
|
||||
onPushAddressBookButton: (context) async {},
|
||||
onPushPasteButton: (context) async {
|
||||
final domain = cryptoCurrencyKey.currentState!.addressController.text;
|
||||
final parsed =
|
||||
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
|
||||
if (parsed.isNotEmpty) {
|
||||
buySellViewModel.cryptoCurrencyAddress = parsed;
|
||||
}
|
||||
},
|
||||
|
||||
onPushAddressBookButton: (context) async {
|
||||
final domain = cryptoCurrencyKey.currentState!.addressController.text;
|
||||
final parsed =
|
||||
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
|
||||
if (parsed.isNotEmpty) {
|
||||
buySellViewModel.cryptoCurrencyAddress = parsed;
|
||||
}
|
||||
},
|
||||
fillColor: buySellViewModel.isBuyAction
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
@@ -537,9 +554,10 @@ class BuySellPage extends BasePage {
|
||||
String domain,
|
||||
CryptoCurrency currency,
|
||||
) async {
|
||||
final parsedAddress =
|
||||
await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||
return address;
|
||||
final parsedAddresses = await _resolver.resolve(
|
||||
query: domain, wallet: buySellViewModel.wallet, currency: currency);
|
||||
return parsedAddresses.isNotEmpty
|
||||
? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? ''
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +340,7 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||
updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
|
||||
canReorder: widget.contactListViewModel.isEditable,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 250),
|
||||
itemBuilder: (context, index) {
|
||||
final contact = contacts[index];
|
||||
final contactContent =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
@@ -27,7 +28,15 @@ class ContactPage extends BasePage {
|
||||
_nameController.text = contactViewModel.name;
|
||||
_addressController.text = contactViewModel.address;
|
||||
_nameController.addListener(() => contactViewModel.name = _nameController.text);
|
||||
_addressController.addListener(() => contactViewModel.address = _addressController.text);
|
||||
_addressController.addListener(() {
|
||||
final address = _addressController.text;
|
||||
|
||||
if (address != contactViewModel.address) {
|
||||
contactViewModel.displayName = '';
|
||||
}
|
||||
|
||||
contactViewModel.address = address;
|
||||
});
|
||||
|
||||
autorun((_) => _currencyTypeController.text = "${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}");
|
||||
}
|
||||
@@ -98,7 +107,8 @@ class ContactPage extends BasePage {
|
||||
focusNode: addressFocusNode,
|
||||
displayName: contactViewModel.displayName,
|
||||
addressController: _addressController,
|
||||
onEditingComplete: ()=> contactViewModel.extractParsedAddress(context),
|
||||
onEditingComplete: () {},
|
||||
onPushPasteButton: (context) => _extractParsedAddress(context),
|
||||
validator: AddressValidator(type: contactViewModel.currency!),
|
||||
selectedCurrency: contactViewModel.currency!,
|
||||
);
|
||||
@@ -129,8 +139,7 @@ class ContactPage extends BasePage {
|
||||
builder: (_) => PrimaryButton(
|
||||
onPressed: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
await contactViewModel.extractParsedAddress(context);
|
||||
_extractParsedAddress(context);
|
||||
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
@@ -150,6 +159,16 @@ class ContactPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _extractParsedAddress(BuildContext context) async {
|
||||
final parsedAddress = await contactViewModel.extractParsedAddress(context);
|
||||
if (parsedAddress == null) return;
|
||||
|
||||
final confirmed = await showParsedAddressConfirmationAlert(context, parsedAddress);
|
||||
if (!confirmed) return;
|
||||
|
||||
contactViewModel.applyParsedAddress(parsedAddress);
|
||||
}
|
||||
|
||||
void _presentCurrencyPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
builder: (_) => CurrencyPicker(
|
||||
@@ -203,4 +222,29 @@ class ContactPage extends BasePage {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> showParsedAddressConfirmationAlert(
|
||||
BuildContext context,
|
||||
ParsedAddress parsedAddress,
|
||||
) async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).address_detected,
|
||||
headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName,
|
||||
headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty
|
||||
? parsedAddress.addressSource.iconPath
|
||||
: parsedAddress.profileImageUrl,
|
||||
alertContent: S.of(context).extracted_address_content(
|
||||
'${parsedAddress.handle} (${parsedAddress.addressSource.label})',
|
||||
),
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(true),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirmed ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,15 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
subTitle: "Turn your old phone into your new hardware wallet with our new app",
|
||||
image: 'assets/images/cupcake.png',
|
||||
),
|
||||
AppsWidget(
|
||||
isWide: true,
|
||||
isLink: true,
|
||||
isCake: true,
|
||||
onTap: () => _launchUrl("radar.chat"),
|
||||
title: "Radar",
|
||||
subTitle: "Your messages. Your Bitcoin. Together, at last",
|
||||
image: 'assets/images/radar.png',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, top: 16, bottom: 8),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
@@ -11,8 +12,6 @@ import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
@@ -41,7 +40,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
|
||||
|
||||
class ExchangePage extends BasePage {
|
||||
ExchangePage(this.exchangeViewModel, this.authService, this.initialPaymentRequest) {
|
||||
ExchangePage(this.exchangeViewModel, this.authService, this.adrResService, this.initialPaymentRequest) {
|
||||
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
: null;
|
||||
@@ -52,6 +51,7 @@ class ExchangePage extends BasePage {
|
||||
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final AuthService authService;
|
||||
final AddressResolverService adrResService;
|
||||
final PaymentRequest? initialPaymentRequest;
|
||||
final depositKey = GlobalKey<ExchangeCardState>();
|
||||
final receiveKey = GlobalKey<ExchangeCardState>();
|
||||
@@ -655,8 +655,8 @@ class ExchangePage extends BasePage {
|
||||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
return extractAddressFromParsed(context, parsedAddress);
|
||||
final parsedAddresses = await adrResService.resolve(query: domain, wallet:exchangeViewModel.wallet, currency: currency);
|
||||
return parsedAddresses.isNotEmpty ? parsedAddresses.first.parsedAddressByCurrencyMap[currency] ?? '' : '';
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
|
||||
@@ -139,68 +139,71 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(walletTypeToDisplayName(item)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
walletTypeToDisplayTicker(item),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
const SizedBox(width: 12),
|
||||
Text(walletTypeToDisplayName(item)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
walletTypeToDisplayTicker(item),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -780,7 +780,6 @@ class SendPage extends BasePage {
|
||||
}
|
||||
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
}
|
||||
|
||||
Output _defineCurrentOutput() {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'choose_yat_address_alert.dart';
|
||||
|
||||
Future<String> extractAddressFromParsed(
|
||||
BuildContext? context,
|
||||
ParsedAddress parsedAddress) async {
|
||||
if (context == null || !context.mounted) return parsedAddress.addresses.first;
|
||||
|
||||
var title = '';
|
||||
var content = '';
|
||||
var address = '';
|
||||
var profileImageUrl = '';
|
||||
var profileName = '';
|
||||
|
||||
switch (parsedAddress.parseFrom) {
|
||||
case ParseFrom.unstoppableDomains:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).address_from_domain(parsedAddress.name);
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.ens:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (ENS)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.openAlias:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (OpenAlias)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.wellKnown:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Well-Known)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.fio:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (FIO)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.twitter:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.mastodon:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.nostr:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)');
|
||||
address = parsedAddress.addresses.first;
|
||||
profileImageUrl = parsedAddress.profileImageUrl;
|
||||
profileName = parsedAddress.profileName;
|
||||
break;
|
||||
case ParseFrom.thorChain:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (ThorChain)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.zanoAlias:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zano Alias)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.zcashAddress:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash.me)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.zcashName:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Zcash Names)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.bip353:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.lnurlpay:
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)');
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
case ParseFrom.yatRecord:
|
||||
if (parsedAddress.name.isEmpty) {
|
||||
title = S.of(context).yat_error;
|
||||
content = S.of(context).yat_error_content;
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
}
|
||||
|
||||
title = S.of(context).address_detected;
|
||||
content = S.of(context).address_from_yat(parsedAddress.name);
|
||||
|
||||
if (parsedAddress.addresses.length == 1) {
|
||||
address = parsedAddress.addresses.first;
|
||||
break;
|
||||
}
|
||||
|
||||
content += S.of(context).choose_address;
|
||||
|
||||
address = await showPopUp<String?>(
|
||||
context: context,
|
||||
builder: (context) => PopScope(
|
||||
child: ChooseYatAddressAlert(
|
||||
alertTitle: title,
|
||||
alertContent: content,
|
||||
addresses: parsedAddress.addresses,
|
||||
),
|
||||
canPop: false,
|
||||
),
|
||||
) ??
|
||||
'';
|
||||
|
||||
if (address.isEmpty) {
|
||||
return parsedAddress.name;
|
||||
}
|
||||
|
||||
return address;
|
||||
case ParseFrom.contact:
|
||||
case ParseFrom.notParsed:
|
||||
return parsedAddress.addresses.first;
|
||||
}
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: title,
|
||||
headerTitleText: profileName.isEmpty ? null : profileName,
|
||||
headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl,
|
||||
alertContent: content,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
@@ -594,7 +594,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
controller: addressController,
|
||||
onURIScanned: (uri) async {
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
|
||||
// Process the payment through the new flow
|
||||
await _handlePaymentFlow(
|
||||
@@ -620,7 +619,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
_justHandledPasteButton = true;
|
||||
try {
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
|
||||
final address =
|
||||
output.isParsedAddress ? output.extractedAddress : output.address;
|
||||
@@ -643,7 +641,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
output.resetParsedAddress();
|
||||
},
|
||||
onSelectedContact: (contact) {
|
||||
output.loadContact(contact);
|
||||
},
|
||||
validator: validator,
|
||||
selectedCurrency: sendViewModel.selectedCryptoCurrency,
|
||||
@@ -1033,8 +1030,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
if (current.isEmpty) return;
|
||||
if (_justHandledPasteButton || _lastHandledAddress == current) return;
|
||||
|
||||
await output.fetchParsedAddress(context);
|
||||
|
||||
// If it's a URI with params, go through URI flow
|
||||
if (current.contains('=')) {
|
||||
try {
|
||||
|
||||
@@ -92,11 +92,9 @@ class SendTemplateCard extends StatelessWidget {
|
||||
],
|
||||
onPushPasteButton: (context) async {
|
||||
template.output.resetParsedAddress();
|
||||
await template.output.fetchParsedAddress(context);
|
||||
},
|
||||
onPushAddressBookButton: (context) async {
|
||||
template.output.resetParsedAddress();
|
||||
await template.output.fetchParsedAddress(context);
|
||||
},
|
||||
buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/settings/connection_sync_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
@@ -20,47 +21,21 @@ class DomainLookupsPage extends BasePage {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
SettingsSwitcherCell(
|
||||
title: 'Twitter',
|
||||
value: _connectionsSyncViewModel.lookupTwitter,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsTwitter(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Mastodon',
|
||||
value: _connectionsSyncViewModel.looksUpMastodon,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsMastodon(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Yat service',
|
||||
value: _connectionsSyncViewModel.looksUpYatService,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsYatService(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Unstoppable Domains',
|
||||
value: _connectionsSyncViewModel.looksUpUnstoppableDomains,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsUnstoppableDomains(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'OpenAlias',
|
||||
value: _connectionsSyncViewModel.looksUpOpenAlias,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsOpenAlias(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Ethereum Name Service',
|
||||
value: _connectionsSyncViewModel.looksUpENS,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsENS(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Zcash Names',
|
||||
value: _connectionsSyncViewModel.lookupsZcashNames,
|
||||
onValueChange: (_, bool value) =>
|
||||
_connectionsSyncViewModel.setLookupsZcashNames(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: '.well-known',
|
||||
value: _connectionsSyncViewModel.looksUpWellKnown,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsWellKnown(value)),
|
||||
SettingsSwitcherCell(
|
||||
title: 'Zano Aliases',
|
||||
value: _connectionsSyncViewModel.lookupsZanoAlias,
|
||||
onValueChange: (_, bool value) => _connectionsSyncViewModel.setLookupsZanoAlias(value)),
|
||||
|
||||
//if (!isHaven) it does not work correctly
|
||||
],
|
||||
children: _connectionsSyncViewModel.domainLookupSources
|
||||
.map(
|
||||
(source) => SettingsSwitcherCell(
|
||||
title: source.label,
|
||||
leading: source.iconPath.isNotEmpty ? CakeImageWidget(
|
||||
imageUrl: source.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
) : SizedBox(width: 24, height: 24),
|
||||
value: _connectionsSyncViewModel.lookupValue(source),
|
||||
onValueChange: (_, bool value) =>
|
||||
_connectionsSyncViewModel.setLookupValue(source, value),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -10,6 +10,7 @@ class FilteredList extends StatefulWidget {
|
||||
this.canReorder = true,
|
||||
this.shrinkWrap = false,
|
||||
this.physics,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
final ObservableList<dynamic> list;
|
||||
@@ -18,6 +19,7 @@ class FilteredList extends StatefulWidget {
|
||||
final bool canReorder;
|
||||
final bool shrinkWrap;
|
||||
final ScrollPhysics? physics;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
@override
|
||||
FilteredListState createState() => FilteredListState();
|
||||
@@ -48,6 +50,7 @@ class FilteredListState extends State<FilteredList> {
|
||||
),
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
physics: widget.physics ?? const BouncingScrollPhysics(),
|
||||
padding: widget.padding,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
itemCount: widget.list.length,
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user