Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
+16 -16
View File
@@ -3,37 +3,37 @@ import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/keyable.dart';
import 'package:hive/hive.dart';
part 'anonpay_invoice_info.g.dart';
part 'anonpay_invoice_info.part.dart';
@HiveType(typeId: AnonpayInvoiceInfo.typeId)
// @HiveType(typeId: AnonpayInvoiceInfo.typeId)
class AnonpayInvoiceInfo extends HiveObject with Keyable implements AnonpayInfoBase {
@HiveField(0)
// @HiveField(0)
final String invoiceId;
@HiveField(1)
// @HiveField(1)
String status;
@HiveField(2)
// @HiveField(2)
final double? fiatAmount;
@HiveField(3)
// @HiveField(3)
final String? fiatEquiv;
@HiveField(4)
// @HiveField(4)
final double? amountTo;
@HiveField(5)
// @HiveField(5)
final String coinTo;
@HiveField(6)
// @HiveField(6)
final String address;
@HiveField(7)
// @HiveField(7)
final String clearnetUrl;
@HiveField(8)
// @HiveField(8)
final String onionUrl;
@HiveField(9)
// @HiveField(9)
final String clearnetStatusUrl;
@HiveField(10)
// @HiveField(10)
final String onionStatusUrl;
@HiveField(11)
// @HiveField(11)
final DateTime createdAt;
@HiveField(12)
// @HiveField(12)
final String walletId;
@HiveField(13)
// @HiveField(13)
final String provider;
static const typeId = ANONPAY_INVOICE_INFO_TYPE_ID;
@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'anonpay_invoice_info.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AnonpayInvoiceInfoAdapter extends TypeAdapter<AnonpayInvoiceInfo> {
@override
final int typeId = 10;
@override
AnonpayInvoiceInfo read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AnonpayInvoiceInfo(
invoiceId: fields[0] as String,
clearnetUrl: fields[7] as String,
onionUrl: fields[8] as String,
clearnetStatusUrl: fields[9] as String,
onionStatusUrl: fields[10] as String,
status: fields[1] as String,
fiatAmount: fields[2] as double?,
fiatEquiv: fields[3] as String?,
amountTo: fields[4] as double?,
coinTo: fields[5] as String,
address: fields[6] as String,
createdAt: fields[11] as DateTime,
walletId: fields[12] as String,
provider: fields[13] as String,
);
}
@override
void write(BinaryWriter writer, AnonpayInvoiceInfo obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.invoiceId)
..writeByte(1)
..write(obj.status)
..writeByte(2)
..write(obj.fiatAmount)
..writeByte(3)
..write(obj.fiatEquiv)
..writeByte(4)
..write(obj.amountTo)
..writeByte(5)
..write(obj.coinTo)
..writeByte(6)
..write(obj.address)
..writeByte(7)
..write(obj.clearnetUrl)
..writeByte(8)
..write(obj.onionUrl)
..writeByte(9)
..write(obj.clearnetStatusUrl)
..writeByte(10)
..write(obj.onionStatusUrl)
..writeByte(11)
..write(obj.createdAt)
..writeByte(12)
..write(obj.walletId)
..writeByte(13)
..write(obj.provider);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnonpayInvoiceInfoAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+60 -31
View File
@@ -156,13 +156,12 @@ class CWBitcoin extends Bitcoin {
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
cryptoAmount: out.cryptoAmountMoney,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo.isNotEmpty ? out.memo : null,
extra: out.extra,
))
@@ -178,7 +177,7 @@ class CWBitcoin extends Bitcoin {
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addressesByReceiveType
.map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress(
.map<ElectrumSubAddress>((addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: addr.address,
@@ -191,7 +190,7 @@ class CWBitcoin extends Bitcoin {
}
@override
Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority,
Future<Money> estimateFakeSendAllTxAmount(WalletBase wallet, TransactionPriority priority,
{UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
try {
final sk = ECPrivate.random();
@@ -231,7 +230,7 @@ class CWBitcoin extends Bitcoin {
return estimatedTx.amount;
} catch (_) {
return 0;
return Money.zero(wallet.currency);
}
}
@@ -245,10 +244,6 @@ class CWBitcoin extends Bitcoin {
String formatterBitcoinAmountToString({required int amount}) =>
bitcoinAmountToString(amount: amount);
@override
double formatterBitcoinAmountToDouble({required int amount}) =>
bitcoinAmountToDouble(amount: amount);
@override
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
@@ -261,7 +256,10 @@ class CWBitcoin extends Bitcoin {
List<BitcoinUnspent> getUnspents(Object wallet,
{UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.unspentCoins.where((element) {
final coins = bitcoinWallet.unspentCoinsForCurrentAccount;
return coins.where((element) {
switch (coinTypeToSpendFrom) {
case UnspentCoinType.mweb:
return element.bitcoinAddressRecord.type == SegwitAddresType.mweb;
@@ -322,6 +320,12 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp;
}
@override
bool hasSelectedLightning(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.addressPageType is LightningAddressType;
}
@override
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) {
switch (option) {
@@ -557,9 +561,9 @@ class CWBitcoin extends Bitcoin {
@override
HardwareWalletService getTrezorHardwareWalletService(
trezor.TrezorConnect connect, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(connect);
return LitecoinTrezorService(connect);
trezor.TrezorConnect? connect, trezor.TrezorClient? client, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(client!);
return LitecoinTrezorService(connect!);
}
@override
@@ -692,28 +696,20 @@ class CWBitcoin extends Bitcoin {
}
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
if (pendingTransaction is PendingLightningTransaction) return outputs;
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
if (!pendingTx.hasSilentPayment) {
if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) {
return outputs;
}
final updatedOutputs = outputs.map((output) {
try {
final pendingOut = pendingTx.outputs[outputs.indexOf(output)];
final updatedOutput = output;
final stealthAddresses = pendingTransaction.stealthAddresses;
if (stealthAddresses.length != outputs.length) {
printV("well, that shouldn't happen");
return outputs;
}
updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey)
.toAddress(BitcoinNetwork.mainnet);
return updatedOutput;
} catch (_) {}
return output;
}).toList();
return updatedOutputs;
for (var i = 0; i < outputs.length; i++) {
outputs[i].stealthAddress = stealthAddresses[i];
}
return outputs;
}
@override
@@ -926,4 +922,37 @@ class CWBitcoin extends Bitcoin {
}
return null;
}
@override
Money balanceForAccount(Object wallet, int accountIndex) {
final bitcoinWallet = wallet as ElectrumWallet;
final balance = bitcoinWallet.balanceForAccount(accountIndex);
return balance.confirmed + balance.unconfirmed;
}
@override
Map<int, Object> accountBalancesSnapshot(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return Map<int, Object>.of(bitcoinWallet.accountBalances);
}
@override
List<TransactionInfo> getCurrentAccountBitcoinTransactions(Object wallet) =>
(wallet as ElectrumWallet).currentAccountBitcoinTransactions;
@override
Future<void> setCurrentAccount(Object wallet, int accountIndex) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.setCurrentAccount(accountIndex);
}
@override
Future<Account> getCurrentAccount(Object wallet) async {
final electrumWallet = wallet as ElectrumWallet;
final acc = await electrumWallet.loadCurrentAccount();
return Account(
id: acc.accountIndex,
label: acc.label,
balance: electrumWallet.balanceForAccount(acc.accountIndex).confirmed.toString());
}
}
+7 -4
View File
@@ -5,6 +5,7 @@ import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/provider_types.dart';
import 'package:hash_wallet/exchange/limits.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
enum ProviderRecommendation { bestRate, lowKyc, successRate }
@@ -13,11 +14,11 @@ extension RecommendationTitle on ProviderRecommendation {
String get title {
switch (this) {
case ProviderRecommendation.bestRate:
return 'BEST RATE';
return S.current.best_rate;
case ProviderRecommendation.lowKyc:
return 'LOW KYC';
return S.current.low_kyc;
case ProviderRecommendation.successRate:
return 'HIGHEST SUCCESS RATE';
return S.current.highest_success_rate;
}
}
}
@@ -123,7 +124,8 @@ class Quote extends SelectableOption {
set setLimits(Limits limits) => this.limits = limits;
factory Quote.fromMoonPayJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType,
{String? customPaymentMethodType}) {
final rate = isBuyAction
? json['quoteCurrencyPrice'] as double? ?? 0.0
: json['baseCurrencyPrice'] as double? ?? 0.0;
@@ -149,6 +151,7 @@ class Quote extends SelectableOption {
transactionFee: transactionFee,
payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0,
paymentType: paymentType,
customPaymentMethodType: customPaymentMethodType,
recommendations: [],
quoteId: json['signature'] as String? ?? '',
provider: ProvidersHelper.getProviderByType(ProviderType.moonpay),
@@ -0,0 +1,32 @@
class MoonPayPaymentMethod {
MoonPayPaymentMethod({
required this.type,
required this.displayName,
required this.iconUrl,
required this.active,
this.limitAmount,
this.limitCurrencyCode,
});
factory MoonPayPaymentMethod.fromJson(Map<String, dynamic> json) {
final availability = json["availability"] as Map<String, dynamic>? ?? {};
final limits = json["limits"] as Map<String, dynamic>?;
final perTransaction = limits?["perTransaction"] as Map<String, dynamic>?;
return MoonPayPaymentMethod(
type: json["type"] as String? ?? "",
displayName: json["displayName"] as String? ?? "",
iconUrl: json["iconUrl"] as String? ?? "",
active: availability["active"] as bool? ?? false,
limitAmount: (perTransaction?["limit"] as num?)?.toDouble(),
limitCurrencyCode: perTransaction?["currencyCode"] as String?,
);
}
final String type;
final String displayName;
final String iconUrl;
final bool active;
final double? limitAmount;
final String? limitCurrencyCode;
}
+201 -223
View File
@@ -1,41 +1,38 @@
import 'dart:convert';
import 'dart:developer';
import "dart:convert";
import "dart:developer";
import 'package:hash_wallet/.secrets.g.dart' as secrets;
import 'package:hash_wallet/buy/buy_exception.dart';
import 'package:hash_wallet/buy/buy_provider.dart';
import 'package:hash_wallet/buy/buy_provider_description.dart';
import 'package:hash_wallet/buy/buy_quote.dart';
import 'package:hash_wallet/order/order.dart';
import 'package:hash_wallet/order/order_source_description.dart';
import 'package:hash_wallet/buy/pairs_utils.dart';
import 'package:hash_wallet/buy/payment_method.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/exchange/trade_state.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/palette.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/themes/core/material_base_theme.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import "package:hash_wallet/.secrets.g.dart" as secrets;
import "package:hash_wallet/buy/buy_exception.dart";
import "package:hash_wallet/buy/buy_provider.dart";
import "package:hash_wallet/buy/buy_provider_description.dart";
import "package:hash_wallet/buy/buy_quote.dart";
import "package:hash_wallet/buy/moonpay/moonpay_payment_methods.dart";
import "package:hash_wallet/buy/pairs_utils.dart";
import "package:hash_wallet/buy/payment_method.dart";
import "package:hash_wallet/entities/fiat_currency.dart";
import "package:hash_wallet/exchange/trade_state.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/order/order.dart";
import "package:hash_wallet/order/order_source_description.dart";
import "package:hash_wallet/palette.dart";
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/utils/proxy_wrapper.dart";
import "package:flutter/material.dart";
import "package:url_launcher/url_launcher.dart";
class MoonPayProvider extends BuyProvider {
MoonPayProvider({
required AppStore appStore,
required WalletBase wallet,
bool isTestEnvironment = false,
required super.wallet,
super.isTestEnvironment = false,
}) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
this._appStore = appStore,
_appStore = appStore,
super(
wallet: wallet,
isTestEnvironment: isTestEnvironment,
hardwareWalletVM: null,
supportedCryptoList: supportedCryptoToFiatPairs(
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
@@ -44,159 +41,207 @@ class MoonPayProvider extends BuyProvider {
final AppStore _appStore;
static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
static const _baseSellProductUrl = 'sell.moonpay.com';
static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.such.software';
static const _apiUrl = 'https://api.moonpay.com';
static const _baseUrl = 'api.moonpay.com';
static const _currenciesPath = '/v3/currencies';
static const _buyQuote = '/buy_quote';
static const _sellQuote = '/sell_quote';
static const _baseSellTestUrl = "sell-sandbox.moonpay.com";
static const _baseSellProductUrl = "sell.moonpay.com";
static const _baseBuyTestUrl = "buy-staging.moonpay.com";
static const _baseBuyProductUrl = "buy.moonpay.com";
static const _cIdBaseUrl = "exchange-helper.such.software";
static const _apiUrl = "https://api.moonpay.com";
static const _baseUrl = "api.moonpay.com";
static const _currenciesPath = "/v3/currencies";
static const _buyQuote = "/buy_quote";
static const _sellQuote = "/sell_quote";
static const _paymentMethodsPath = "/api/moonpay/payment-methods";
static const _transactionsSuffix = '/v1/transactions';
static const _transactionsSuffix = "/v1/transactions";
static const List<CryptoCurrency> _notSupportedCrypto = [];
static const List<FiatCurrency> _notSupportedFiat = [];
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
static const Map<PaymentType, String> _moonPayPaymentMethodStrings = {
PaymentType.creditCard: 'credit_debit_card',
PaymentType.debitCard: 'credit_debit_card',
PaymentType.ach: 'ach_bank_transfer',
PaymentType.applePay: 'apple_pay',
PaymentType.googlePay: 'google_pay',
PaymentType.sepa: 'sepa_bank_transfer',
PaymentType.paypal: 'paypal',
PaymentType.sepaOpenBankingPayment: 'sepa_open_banking_payment',
PaymentType.gbpOpenBankingPayment: 'gbp_open_banking_payment',
PaymentType.lowCostAch: 'low_cost_ach',
PaymentType.mobileWallet: 'mobile_wallet',
PaymentType.pixInstantPayment: 'pix_instant_payment',
PaymentType.yellowCardBankTransfer: 'yellow_card_bank_transfer',
PaymentType.fiatBalance: 'fiat_balance',
PaymentType.revolutPay: 'revolut_pay',
PaymentType.moonpayCashApp: 'cash_app',
PaymentType.moonpayBalance: 'moonpay_balance',
};
static final Map<String, PaymentType> _paymentTypeByMoonPayString = {
for (final entry in _moonPayPaymentMethodStrings.entries) entry.value: entry.key,
};
String? normalizePaymentMethod(PaymentType paymentMethod) =>
_moonPayPaymentMethodStrings[paymentMethod];
PaymentType _getPaymentTypeByString(String? paymentMethod) =>
_paymentTypeByMoonPayString[paymentMethod] ?? PaymentType.unknown;
final String baseBuyUrl;
final String baseSellUrl;
@override
String get providerDescription =>
'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
"MoonPay offers a fast and simple way to buy and sell cryptocurrencies";
@override
String get title => 'MoonPay';
String get title => "MoonPay";
@override
String get lightIcon => 'assets/images/moonpay_light.png';
String get lightIcon => "assets/images/moonpay_light.png";
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
String get darkIcon => "assets/images/moonpay_dark.png";
@override
bool get isAggregator => false;
static String get _apiKey => secrets.moonPayApiKey;
String get _apiKey => isTestEnvironment ? secrets.moonPaySandboxApiKey : secrets.moonPayApiKey;
String get currencyCode =>
walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
String get trackUrl => "$baseBuyUrl/transaction_receipt?transactionId=";
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
static String themeToMoonPayTheme(MaterialThemeBase theme) {
switch (theme.type) {
case ThemeType.light:
return 'light';
case ThemeType.dark:
return 'dark';
}
}
Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
Future<String> getMoonpaySignedQuery(String query) async {
final uri =
Uri.https(_cIdBaseUrl, "/api/moonpay", isTestEnvironment ? {"useSandbox": "true"} : null);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
body: json.encode({'query': query}),
headers: {"Content-Type": "application/json", "x-api-key": _exchangeHelperApiKey},
body: json.encode({"query": query}),
);
if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
printV(jsonDecode(response.body) as Map<String, dynamic>);
return (jsonDecode(response.body) as Map<String, dynamic>)["query"] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
}
}
Future<Map<String, dynamic>> fetchFiatCredentials(
String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await ProxyWrapper().get(
clearnetUri: url,
headers: {'accept': 'application/json'},
"Provider currently unavailable. Status: ${response.statusCode} ${response.body}",
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
printV('MoonPay does not support fiat: $fiatCurrency');
return {};
}
} catch (e) {
printV('MoonPay Error fetching fiat currencies: $e');
return {};
}
}
Future<List<MoonPayPaymentMethod>> fetchMoonPayPaymentMethods({
required String currencyCode,
String transactionType = "buy",
}) async {
final String? countryCode = FiatCurrency.tryDeserialize(raw: currencyCode)?.apiCountryCode;
if (countryCode == null) {
throw Exception("failed to determine country code for currency: $currencyCode");
}
final params = <String, String>{
"currencyCode": currencyCode,
"countryCode": countryCode,
"transactionType": transactionType,
if (countryCode == "USA") "stateOfResidence": "NY",
};
final uri = Uri.https(_cIdBaseUrl, _paymentMethodsPath, params);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"x-api-key": _exchangeHelperApiKey,
},
);
if (response.statusCode != 200) {
if (response.statusCode == 500) {
throw Exception("failed to fetch payment methods. Status: 500 Internal Server Error");
}
throw Exception(
"failed to fetch payment methods. Status: ${response.statusCode} ${response.body}");
}
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final list = decoded["payment-methods"] as List? ?? [];
return list.map((e) => MoonPayPaymentMethod.fromJson(e as Map<String, dynamic>)).toList();
}
@override
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
final List<PaymentMethod> paymentMethods = [];
if (isBuyAction) {
final fiatBuyCredentials =
await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null);
if (fiatBuyCredentials.isNotEmpty) {
final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
try {
final moonPayMethods = await fetchMoonPayPaymentMethods(
currencyCode: fiatCurrency,
transactionType: isBuyAction ? "buy" : "sell",
);
for (final method in moonPayMethods) {
if (!method.active) {
continue;
}
final paymentType = _getPaymentTypeByString(method.type);
paymentMethods.add(PaymentMethod.fromMoonPayJson(
fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
return paymentMethods;
method,
paymentType,
),);
}
} catch (e) {
printV("Error fetching payment methods: $e");
}
return paymentMethods;
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async {
Future<List<Quote>?> fetchQuote({
required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode,
}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
} else {
paymentMethod = 'credit_debit_card';
paymentMethod = normalizePaymentMethod(paymentType) ?? customPaymentMethodType;
}
final action = isBuyAction ? 'buy' : 'sell';
final action = isBuyAction ? "buy" : "sell";
final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
final baseCurrencyCode =
isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
final params = {
'baseCurrencyCode': baseCurrencyCode,
'baseCurrencyAmount': amount.toStringAsFixed(2),
'paymentMethod': paymentMethod,
'areFeesIncluded': 'false',
"baseCurrencyCode": baseCurrencyCode,
"baseCurrencyAmount": amount.toStringAsFixed(2),
if (paymentMethod != null) "paymentMethod": paymentMethod,
"areFeesIncluded": "false",
'apiKey': _apiKey
};
log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
log("MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod");
final quotePath = isBuyAction ? _buyQuote : _sellQuote;
final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
final path = "$_currenciesPath/$formattedCryptoCurrency$quotePath";
final url = Uri.https(_baseUrl, path, params);
try {
final response = await ProxyWrapper().get(clearnetUri: url);
@@ -216,19 +261,30 @@ class MoonPayProvider extends BuyProvider {
}
final paymentMethods = data['paymentMethod'] as String?;
final quote =
Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
final quote = Quote.fromMoonPayJson(
data,
isBuyAction,
_getPaymentTypeByString(paymentMethods ?? paymentMethod),
customPaymentMethodType: paymentMethod,
);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
printV('Moon Pay: Error fetching buy quote: ');
}
try {
final errorData = jsonDecode(response.body) as Map<String, dynamic>;
final errorMessage = errorData["message"] as String?;
printV("Error fetching buy quote: $errorMessage");
return null;
} catch (e) {
printV("Error parsing error response: $e");
return null;
}
} catch (e) {
printV('Moon Pay: Error fetching buy quote: $e');
printV("Exception while fetching buy quote: $e");
return null;
}
}
@@ -242,20 +298,21 @@ class MoonPayProvider extends BuyProvider {
required String cryptoCurrencyAddress,
String? countryCode}) async {
final Map<String, String> params = {
'theme': themeToMoonPayTheme(_appStore.themeStore.currentTheme),
'language': _appStore.settingsStore.languageCode,
'colorCode': _appStore.themeStore.currentTheme.isDark
"theme": _appStore.themeStore.currentTheme.type.name,
"language": _appStore.settingsStore.languageCode,
"colorCode": _appStore.themeStore.currentTheme.isDark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
'baseCurrencyAmount': amount.toStringAsFixed(2),
'walletAddress': cryptoCurrencyAddress,
'lockAmount': 'false',
'showAllCurrencies': 'false',
'showWalletAddressForm': 'false',
"baseCurrencyCode": isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
"baseCurrencyAmount": amount.toStringAsFixed(2),
"walletAddress": cryptoCurrencyAddress,
"lockAmount": "false",
"showAllCurrencies": "false",
"showWalletAddressForm": "false",
if (isBuyAction)
'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
"enabledPaymentMethods": normalizePaymentMethod(quote.paymentType) ??
quote.customPaymentMethodType ??
"credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment",
if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
};
@@ -272,20 +329,18 @@ class MoonPayProvider extends BuyProvider {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
throw Exception("Could not launch URL");
}
} catch (e) {
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
}
@@ -297,18 +352,13 @@ class MoonPayProvider extends BuyProvider {
required Map<String, String> params,
String? amount,
}) async {
if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
if (_apiKey.isNotEmpty) params["apiKey"] = _apiKey;
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
final originalUri = Uri.https(baseUrl, '', params);
final originalUri = Uri.https(baseUrl, "", params);
if (isTestEnvironment) return originalUri;
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
final query = await getMoonpaySignedQuery("?${originalUri.query}");
return Uri.parse(query);
}
Future<Order> findOrderById(String id) async {
@@ -350,76 +400,4 @@ class MoonPayProvider extends BuyProvider {
return currency.toString().toLowerCase();
}
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.creditCard:
return 'credit_debit_card';
case PaymentType.debitCard:
return 'credit_debit_card';
case PaymentType.ach:
return 'ach_bank_transfer';
case PaymentType.applePay:
return 'apple_pay';
case PaymentType.googlePay:
return 'google_pay';
case PaymentType.sepa:
return 'sepa_bank_transfer';
case PaymentType.paypal:
return 'paypal';
case PaymentType.sepaOpenBankingPayment:
return 'sepa_open_banking_payment';
case PaymentType.gbpOpenBankingPayment:
return 'gbp_open_banking_payment';
case PaymentType.lowCostAch:
return 'low_cost_ach';
case PaymentType.mobileWallet:
return 'mobile_wallet';
case PaymentType.pixInstantPayment:
return 'pix_instant_payment';
case PaymentType.yellowCardBankTransfer:
return 'yellow_card_bank_transfer';
case PaymentType.fiatBalance:
return 'fiat_balance';
case PaymentType.revolutPay:
return 'revolut_pay';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'ach_bank_transfer':
return PaymentType.ach;
case 'apple_pay':
return PaymentType.applePay;
case 'credit_debit_card':
return PaymentType.creditCard;
case 'fiat_balance':
return PaymentType.fiatBalance;
case 'gbp_open_banking_payment':
return PaymentType.gbpOpenBankingPayment;
case 'google_pay':
return PaymentType.googlePay;
case 'low_cost_ach':
return PaymentType.lowCostAch;
case 'mobile_wallet':
return PaymentType.mobileWallet;
case 'paypal':
return PaymentType.paypal;
case 'pix_instant_payment':
return PaymentType.pixInstantPayment;
case 'sepa_bank_transfer':
return PaymentType.sepa;
case 'sepa_open_banking_payment':
return PaymentType.sepaOpenBankingPayment;
case 'yellow_card_bank_transfer':
return PaymentType.yellowCardBankTransfer;
case 'revolut_pay':
return PaymentType.revolutPay;
default:
return PaymentType.unknown;
}
}
}
+87 -9
View File
@@ -1,4 +1,6 @@
import "package:hash_wallet/buy/moonpay/moonpay_payment_methods.dart";
import 'package:hash_wallet/core/selectable_option.dart';
import "package:hash_wallet/entities/calculate_fiat_amount.dart";
enum PaymentType {
all,
@@ -33,6 +35,8 @@ enum PaymentType {
fiatBalance,
bancontact,
pixPay,
moonpayCashApp,
moonpayBalance,
unknown,
}
@@ -103,6 +107,10 @@ extension PaymentTypeTitle on PaymentType {
return 'Bancontact';
case PaymentType.pixPay:
return 'PIX Pay';
case PaymentType.moonpayCashApp:
return 'Cash App';
case PaymentType.moonpayBalance:
return 'MoonPay Balance';
default:
return null;
}
@@ -115,7 +123,7 @@ extension PaymentTypeTitle on PaymentType {
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card.svg';
return 'assets/new-ui/buy_payment_methods/debit_card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_light.svg';
case PaymentType.skrill:
@@ -132,17 +140,21 @@ extension PaymentTypeTitle on PaymentType {
String? get darkIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_dark.svg';
return 'assets/new-ui/buy_payment_methods/all_methods.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card_dark.svg';
return 'assets/new-ui/buy_payment_methods/debit_card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_dark.svg';
return 'assets/new-ui/buy_payment_methods/bank_transfer.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_dark.svg';
return 'assets/new-ui/buy_payment_methods/apple_pay.svg';
case PaymentType.googlePay:
return "assets/new-ui/buy_payment_methods/google_pay.svg";
case PaymentType.paypal:
return "assets/new-ui/buy_payment_methods/paypal.svg";
case PaymentType.revolutPay:
return 'assets/images/revolut_dark.svg';
default:
@@ -150,6 +162,12 @@ extension PaymentTypeTitle on PaymentType {
}
}
bool get isMonochromeIcon => [
"assets/new-ui/buy_payment_methods/all_methods.svg",
"assets/new-ui/buy_payment_methods/debit_card.svg",
"assets/new-ui/buy_payment_methods/bank_transfer.svg"
].contains(darkIconPath);
String? get description {
switch (this) {
default:
@@ -165,6 +183,8 @@ class PaymentMethod extends SelectableOption {
required this.customIconPath,
this.customDescription,
this.customPaymentMethodType,
this.customBadges = const [],
this.customBottomLeftSubTitle,
}) : super(title: paymentMethodType.title ?? customTitle);
final PaymentType paymentMethodType;
@@ -172,8 +192,16 @@ class PaymentMethod extends SelectableOption {
final String customIconPath;
final String? customDescription;
final String? customPaymentMethodType;
final List<String> customBadges;
final String? customBottomLeftSubTitle;
bool isSelected = false;
@override
List<String> get badges => customBadges;
@override
String? get bottomLeftSubTitle => customBottomLeftSubTitle;
@override
String? get description => paymentMethodType.description ?? customDescription;
@@ -190,14 +218,64 @@ class PaymentMethod extends SelectableOption {
return PaymentMethod(
paymentMethodType: PaymentType.all,
customTitle: 'All Payment Methods',
customIconPath: 'assets/images/dollar_coin.svg');
customIconPath: 'assets/new-ui/buy_payment_methods/all_methods.svg');
}
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
return PaymentMethod(
paymentMethodType: type ?? PaymentType.unknown,
customPaymentMethodType: json['paymentTypeId'] as String?,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: json['icon'] as String? ?? 'assets/new-ui/buy_payment_methods/debit_card.svg',
customDescription: json['description'] as String?);
}
factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: json['paymentMethod'] as String,
customIconPath: 'assets/images/card.png');
customTitle: paymentMethod,
customIconPath: 'assets/new-ui/buy_payment_methods/debit_card.svg');
}
factory PaymentMethod.fromMoonPayJson(MoonPayPaymentMethod method, PaymentType paymentType) {
final List<String> badges = [];
String? limitSubtitle;
if (paymentType == PaymentType.moonpayCashApp) {
badges.add('New');
}
if (method.limitAmount != null && method.limitCurrencyCode != null) {
limitSubtitle =
'max: ${formatWithCommas(method.limitAmount.toString())} ${method.limitCurrencyCode!.toUpperCase()}';
}
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: method.displayName,
customIconPath: method.iconUrl.isNotEmpty ? method.iconUrl : 'assets/images/card.png',
customPaymentMethodType: method.type,
customBadges: badges,
customBottomLeftSubTitle: limitSubtitle,
);
}
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
final logos = json['logos'] as Map<String, dynamic>;
return PaymentMethod(
paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: logos['dark'] as String? ?? 'assets/new-ui/buy_payment_methods/debit_card.svg',
customDescription: json['description'] as String?);
}
factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?);
return PaymentMethod(
paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['payment_method'] as String? ?? 'Unknown',
customIconPath: 'assets/new-ui/buy_payment_methods/debit_card.svg',
);
}
static PaymentType? getPaymentTypeId(String? type) {
+60
View File
@@ -0,0 +1,60 @@
import "package:hash_wallet/store/settings_store.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class CiBuildOverlay extends StatelessWidget {
const CiBuildOverlay({required this.child, this.settingsStore, super.key});
static const _commit = String.fromEnvironment("CI_COMMIT");
static const _branch = String.fromEnvironment("CI_BRANCH");
static const isCiBuild = _commit != "" || _branch != "";
final Widget child;
final SettingsStore? settingsStore;
@override
Widget build(BuildContext context) {
if (!isCiBuild) {
return child;
}
final shortCommit = _commit.length > 7 ? _commit.substring(0, 7) : _commit;
final label = [shortCommit, _branch].where((part) => part.isNotEmpty).join(" · ");
return Stack(
fit: StackFit.passthrough,
textDirection: TextDirection.ltr,
children: [
child,
Positioned(
top: 0,
left: 0,
right: 0,
child: Observer(
builder: (_) => settingsStore?.showCiBuildOverlay ?? true
? IgnorePointer(
child: ExcludeSemantics(
child: SafeArea(
bottom: false,
child: ColoredBox(
color: Colors.black54,
child: Text(
label,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
),
),
)
: const SizedBox.shrink(),
),
),
],
);
}
}
@@ -0,0 +1,21 @@
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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,134 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/bip_353/bip_353_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/ens/ens_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/fio/fio_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/lnurl_pay/lnurl_pay_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/mastodon/mastodon_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/nostr/nostr_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/openalias/openalias_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/twitter/twitter_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/wellknown/wellknown_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/zano/zano_alias_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/zcash/zcash_address_provider.dart';
import 'package:hash_wallet/core/address_resolver/zcash/zcash_me_address_provider.dart';
import 'package:hash_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.
// Hash Bags privacy rule: only domain-anchored resolvers, where the lookup goes to the domain
// the user typed (DNS for OpenAlias/BIP-353, the domain itself for .well-known/LNURL) or to a
// public chain RPC (ENS). Never add a third-party lookup (X, Mastodon, Nostr, Yat, Unstoppable,
// FIO, ThorChain, Zcash/Zano names) without an off-by-default setting and a PRIVACY.md update.
static const supportedSources = [
AddressSource.lnurlPay,
AddressSource.openAlias,
AddressSource.ens,
AddressSource.wellKnown,
AddressSource.bip353,
];
List<AddressLookupProvider> _buildProviders() {
final allProviders = <AddressLookupProvider>[
LNUrlPayAddressProvider(),
TwitterAddressProvider(),
MastodonAddressProvider(),
UnstoppableAddressProvider(),
OpenaliasAddressProvider(),
EnsAddressProvider(),
ZcashNameAddressProvider(),
ZcashMeAddressProvider(),
WellKnownAddressProvider(),
ZanoAliasAddressProvider(),
Bip353AddressProvider(),
FioAddressProvider(),
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,105 @@
import 'package:hash_wallet/core/address_resolver/unstoppable/unstoppable_address_provider.dart';
import 'package:hash_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:hash_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,70 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/bip_353/bip_353_record.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
class Bip353AddressProvider extends AddressLookupProvider {
@override
AddressSource get source => AddressSource.bip353;
@override
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.btc];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsBip353;
@override
bool canHandle(String query) {
final value = query.trim();
return value.contains('@') && value.contains('.') && !value.startsWith('@');
}
@override
Future<List<ParsedAddress>> resolve({
required String query,
required List<CryptoCurrency> currencies,
required WalletBase wallet,
}) async {
try {
final result = <CryptoCurrency, String>{};
String? dnsProof;
for (final cur in currencies) {
final bip353AddressMap = await Bip353Record.fetchUriByCryptoCurrency(query, cur.title);
if (bip353AddressMap == null || bip353AddressMap.isEmpty) continue;
final spAddress = bip353AddressMap['sp'];
final address = bip353AddressMap['address'];
final chosenAddress = spAddress?.isNotEmpty == true ? spAddress : address;
if (chosenAddress != null && chosenAddress.isNotEmpty) {
result[cur] = chosenAddress;
}
}
if (result.isEmpty) return [];
try {
dnsProof = await Bip353Record.fetchDnsProof(query);
} catch (e) {
printV('Bip353Record.fetchDnsProof error: $e');
}
return [
ParsedAddress(
parsedAddressByCurrencyMap: result,
addressSource: AddressSource.bip353,
handle: query,
bip353DnsProof: dnsProof ?? '',
)
];
} catch (e) {
printV('[address resolver] Error resolving BIP353 address: $e');
return [];
}
}
}
@@ -2,13 +2,9 @@ import 'dart:convert';
import 'dart:isolate';
import 'package:basic_utils/basic_utils.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/widgets/alert_with_picker_option.dart';
import 'package:hash_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({
@@ -103,69 +99,4 @@ class Bip353Record {
return result;
}
}
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,48 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/ens/ens_record.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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.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,68 @@
import "package:hash_wallet/evm/evm.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: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 registryEns = Ens(client: _client).withName(name);
final coinType = getEnsCoinType(cur);
if (coinType == null) {
printV("Unsupported currency for ENS: $cur");
return "";
}
try {
// Check if the ENS name has its resolver set. If not, return an empty string.
final resolverAddress = await registryEns.getResolverAddress(registryEns.nodeHash);
if (isZeroAddress(resolverAddress)) {
printV("No resolver set for ENS name: $name");
return "";
}
final resolverEns = Ens(client: _client,address: resolverAddress).withName(name);
if (coinType == CoinType.ETH) {
return (await resolverEns.getAddress()).hex;
} else {
return await resolverEns.getCoinAddress(coinType);
}
} catch (e) {
printV(e);
return "";
}
}
static bool isZeroAddress(EthereumAddress address) =>
address.addressBytes.every((byte) => byte == 0);
static CoinType? getEnsCoinType(CryptoCurrency cur) => switch (cur) {
CryptoCurrency.xmr => CoinType.XMR,
CryptoCurrency.btc => CoinType.BTC,
CryptoCurrency.ltc => CoinType.LTC,
CryptoCurrency.eth => CoinType.ETH,
CryptoCurrency.maticpoly => CoinType.MATIC,
_ => null,
};
}
@@ -0,0 +1,125 @@
import 'dart:convert';
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_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,67 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/mastodon/mastodon_api.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_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 [];
}
}
}
@@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:hash_wallet/core/address_resolver/mastodon/mastodon_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
class MastodonAPI {
static const httpsScheme = 'https';
static const userPath = '/api/v1/accounts/lookup';
static const statusesPath = '/api/v1/accounts/:id/statuses';
static Future<MastodonUser?> lookupUserByUserName(
{required String userName, required String apiHost}) async {
try {
final queryParams = {'acct': userName};
final uri = Uri(
scheme: httpsScheme,
host: apiHost,
path: userPath,
queryParameters: queryParams,
);
final response = await ProxyWrapper().get(clearnetUri: uri);
if (response.statusCode != 200) return null;
final Map<String, dynamic> responseJSON = json.decode(response.body) as Map<String, dynamic>;
return MastodonUser.fromJson(responseJSON);
} catch (e) {
printV('Error in lookupUserByUserName: $e');
return null;
}
}
static Future<List<PinnedPost>> getPinnedPosts({
required String userId,
required String apiHost,
}) async {
try {
final queryParams = {'pinned': 'true'};
final uri = Uri(
scheme: httpsScheme,
host: apiHost,
path: statusesPath.replaceAll(':id', userId),
queryParameters: queryParams,
);
final response = await ProxyWrapper().get(clearnetUri: uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final List<dynamic> responseJSON = json.decode(response.body) as List<dynamic>;
return responseJSON.map((json) => PinnedPost.fromJson(json as Map<String, dynamic>)).toList();
} catch (e) {
printV('Error in getPinnedPosts: $e');
throw e;
}
}
}
@@ -0,0 +1,38 @@
class MastodonUser {
String id;
String username;
String profileImageUrl;
String acct;
String note;
MastodonUser({
required this.id,
required this.username,
required this.profileImageUrl,
required this.acct,
required this.note,
});
factory MastodonUser.fromJson(Map<String, dynamic> json) {
return MastodonUser(
id: json['id'] as String,
username: json['username'] as String? ?? '',
acct: json['acct'] as String,
note: json['note'] as String,
profileImageUrl: json['avatar'] as String? ?? '');
}
}
class PinnedPost {
final String id;
final String content;
PinnedPost({required this.id, required this.content});
factory PinnedPost.fromJson(Map<String, dynamic> json) {
return PinnedPost(
id: json['id'] as String,
content: json['content'] as String,
);
}
}
@@ -0,0 +1,66 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/nostr/nostr_api.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_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:hash_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,34 @@
class UserMetadata {
final String name;
final String lnurl;
final String email;
final String picture;
final String about;
final String nip05;
final String banner;
final String website;
UserMetadata({
required this.name,
required this.lnurl,
required this.email,
required this.picture,
required this.about,
required this.nip05,
required this.banner,
required this.website,
});
factory UserMetadata.fromJson(Map<String, dynamic> json) {
return UserMetadata(
name: json['name'] as String? ?? '',
lnurl: json['lud06'] as String? ?? '',
email: json['lud16'] as String? ?? '',
picture: json['picture'] as String? ?? '',
about: json['about'] as String? ?? '',
nip05: json['nip05'] as String? ?? '',
banner: json['banner'] as String? ?? '',
website: json['website'] as String? ?? '',
);
}
}
@@ -0,0 +1,64 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/openalias/openalias_record.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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:hash_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,96 @@
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_utils.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/twitter/twitter_api.dart';
import 'package:hash_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_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,
);
}
}
@@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
import 'package:hash_wallet/core/address_resolver/twitter/twitter_user.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class TwitterApi {
static const twitterBearerToken = secrets.twitterBearerToken;
static const httpsScheme = 'https';
static const apiHost = 'api.twitter.com';
static const userPath = '/2/users/by/username/';
static Future<TwitterUser> lookupUserByName({required String userName}) async {
final queryParams = {
'user.fields': 'description,profile_image_url,location',
'expansions': 'pinned_tweet_id',
'tweet.fields': 'note_tweet'
};
final headers = {'authorization': 'Bearer $twitterBearerToken'};
final uri = Uri(
scheme: httpsScheme,
host: apiHost,
path: userPath + userName,
queryParameters: queryParams);
final response =
await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) {
throw Exception('HTTP request failed: $error');
});
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final Map<String, dynamic> responseJSON = jsonDecode(response.body) as Map<String, dynamic>;
if (responseJSON['errors'] != null &&
!responseJSON['errors'][0]['detail']
.toString()
.contains("Could not find tweet with pinned_tweet_id")) {
throw Exception(responseJSON['errors'][0]['detail']);
}
return TwitterUser.fromJson(responseJSON, _getPinnedTweet(responseJSON));
}
static Tweet? _getPinnedTweet(Map<String, dynamic> responseJSON) {
try {
final tweetId = responseJSON['data']['pinned_tweet_id'] as String?;
if (tweetId == null || responseJSON['includes'] == null) return null;
final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List);
final pinnedTweetData = tweetIncludes.firstWhere(
(tweet) => tweet['id'] == tweetId,
orElse: () => null,
) as Map<String, dynamic>?;
if (pinnedTweetData == null) return null;
final pinnedTweetText =
(pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String;
return Tweet(id: tweetId, text: pinnedTweetText);
} catch (e) {
return null;
}
}
}
@@ -0,0 +1,49 @@
class TwitterUser {
TwitterUser(
{required this.id,
required this.username,
required this.name,
required this.description,
required this.location,
required this.profileImageUrl,
this.pinnedTweet});
final String id;
final String username;
final String name;
final String description;
final String location;
final String profileImageUrl;
final Tweet? pinnedTweet;
factory TwitterUser.fromJson(Map<String, dynamic> json, [Tweet? pinnedTweet]) {
final profileImageUrl = json['data']['profile_image_url'] as String? ?? '';
final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200');
return TwitterUser(
id: json['data']['id'] as String,
username: json['data']['username'] as String? ?? '',
name: json['data']['name'] as String,
description: json['data']['description'] as String? ?? '',
location: json['data']['location'] as String? ?? '',
profileImageUrl: scaledProfileImageUrl,
pinnedTweet: pinnedTweet,
);
}
}
class Tweet {
Tweet({
required this.id,
required this.text,
});
final String id;
final String text;
factory Tweet.fromJson(Map<String, dynamic> json) {
return Tweet(
id: json['id'] as String,
text: json['text'] as String,
);
}
}
@@ -0,0 +1,237 @@
import 'dart:convert';
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/openalias/openalias_record.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/wellknown/wellknown_record.dart';
import 'package:hash_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,74 @@
import 'dart:convert';
import 'package:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_wallet/core/address_resolver/zcash/zcash_names_record.dart';
import 'package:hash_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:hash_wallet/core/address_resolver/address_lookup_provider.dart';
import 'package:hash_wallet/core/address_resolver/address_sources.dart';
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
import 'package:hash_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.lookupsZcashAddress;
@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;
}
}
@@ -0,0 +1,74 @@
import 'dart:async';
import 'dart:convert';
import 'package:hash_wallet/core/address_validator.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
class ZcashNamesRecord {
static final Uri _mainnetEndpoint = Uri.parse('https://main.zcashnames.com');
static final RegExp _nameRegExp = RegExp(r'^[A-Za-z0-9_-]{1,63}$');
static Future<String?> fetchZcashNamesAddress(String input) async {
final name = _extractName(input);
if (name == null) return null;
try {
final response = await ProxyWrapper().post(
clearnetUri: _mainnetEndpoint,
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({
'jsonrpc': '2.0',
'id': 1,
'method': 'resolve',
'params': {'query': name},
}),
);
if (response.statusCode != 200) {
printV('ZcashNames: non-200 status ${response.statusCode} for $name');
return null;
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) return null;
if (decoded['error'] != null) return null;
final result = decoded['result'];
if (result is! Map<String, dynamic>) return null;
final address = result['address'];
if (address is! String || address.isEmpty) return null;
if (!_isValidZcashAddress(address)) {
printV('ZcashNames: returned address failed validation for $name');
return null;
}
return address;
} catch (e) {
printV('ZcashNames: lookup failed for $name: $e');
return null;
}
}
static String? _extractName(String input) {
final lower = input.toLowerCase().trim();
String? raw;
if (lower.endsWith('.zcash')) {
raw = lower.substring(0, lower.length - '.zcash'.length);
} else if (lower.endsWith('.zec')) {
raw = lower.substring(0, lower.length - '.zec'.length);
}
if (raw == null || raw.isEmpty) return null;
if (!_nameRegExp.hasMatch(raw)) return null;
return raw;
}
static bool _isValidZcashAddress(String address) {
final pattern = AddressValidator.getAddressFromStringPattern(CryptoCurrency.zec);
if (pattern == null) return false;
return RegExp('^(?:$pattern)\$').hasMatch(address);
}
}
+39 -18
View File
@@ -8,8 +8,11 @@ const BEFORE_REGEX = '(^|\\s)';
const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
: super(
AddressValidator({
required CryptoCurrency type,
bool isTestnet = false,
String? network,
}) : super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
@@ -28,11 +31,36 @@ class AddressValidator extends TextValidator {
);
}
: null,
pattern: getPattern(type, isTestnet: isTestnet),
pattern: getPattern(type, isTestnet: isTestnet, network: network),
length: getLength(type),
);
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
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, String? network}) {
var pattern = "";
if (type is Erc20Token) {
pattern = '0x[0-9a-zA-Z]+';
@@ -80,6 +108,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.eth:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
case CryptoCurrency.mana:
case CryptoCurrency.matic:
case CryptoCurrency.maticpoly:
@@ -141,13 +170,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.hbar:
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}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
case CryptoCurrency.dcr:
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
@@ -236,6 +259,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.avaxc:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
return [42];
case CryptoCurrency.bch:
return [42, 54, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35];
@@ -314,6 +338,8 @@ class AddressValidator extends TextValidator {
static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
static String get mWebAddressPattern => MwebAddress.regex.pattern;
static String get zcashAddressPattern =>
"t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300}";
static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
@@ -345,6 +371,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.maticpoly:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
case CryptoCurrency.arb:
pattern = '0x[0-9a-zA-Z]+';
case CryptoCurrency.nano:
@@ -360,13 +387,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zano:
pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
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}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
default:
if (type.tag == CryptoCurrency.eth.title) {
pattern = '0x[0-9a-zA-Z]+';
+27 -41
View File
@@ -1,7 +1,9 @@
import 'package:hash_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:hash_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
class AmountParsingProxy {
@@ -24,7 +26,7 @@ class AmountParsingProxy {
if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
return cryptoCurrency
.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals))
.toString();
.toStringWithPrecision(useBaseUnit: true);
}
return amount.withMaxDecimals(cryptoCurrency.decimals);
@@ -35,55 +37,39 @@ class AmountParsingProxy {
}
}
/// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency]
String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) =>
asDisplayString(Money.fromInt(amount, cryptoCurrency));
return cryptoCurrency.formatAmount(BigInt.from(amount));
}
/// [getDisplayCryptoString] turns the input [amount] into the preferred representation of [cryptoCurrency]
///
/// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000
/// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1
/// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that
String asDisplayString(Money amount) =>
amount.toStringWithPrecision(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8);
String getDisplayCryptoStringFromBigInt(BigInt amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
/// [asDisplayStringWithSymbol] turns the input [amount] into the preferred representation of
/// [cryptoCurrency] following the symbol of the unit
///
/// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000 sats
/// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1 BTC
/// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that
String asDisplayStringWithSymbol(Money amount) =>
amount.toStringWithSymbol(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8);
return cryptoCurrency.formatAmount(amount);
}
/// [parseCryptoString] turns the the display representation [string] into `Money` with [cryptoCurrency]
Money parseCryptoString(String amount, CryptoCurrency cryptoCurrency) =>
Money.parse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency));
/// [getCryptoStringFromDouble] turns the input [amount] into the preferred representation of [cryptoCurrency] and
String getDisplayCryptoStringFromDouble(double amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return "$amount";
}
return cryptoCurrency.formatAmount(BigInt.from(amount));
}
/// [parseCryptoString] turns the input [string] into a `BigInt` presentation of the [cryptoCurrency]
BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return BigInt.parse(amount);
}
return cryptoCurrency.parseAmount(amount);
}
/// [tryParseCryptoString] tries to turn the display representation [string] into a `BigInt` presentation of the [cryptoCurrency]
BigInt? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
if (useSatoshi(cryptoCurrency)) {
return BigInt.tryParse(amount);
}
return cryptoCurrency.tryParseAmount(amount);
}
/// [tryParseCryptoString] tries to turn the display representation [string] into `Money` with [cryptoCurrency]
Money? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) =>
Money.tryParse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency));
/// [getCryptoSymbol] returns the correct Symbol related to the presentation
String getCryptoSymbol(CryptoCurrency cryptoCurrency) =>
useSatoshi(cryptoCurrency) ? "sats" : cryptoCurrency.title.safeSubString(0, 8);
bool useSatoshi(CryptoCurrency cryptoCurrency) =>
bool useSatoshi(Currency cryptoCurrency) =>
([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) &&
displayMode == BitcoinAmountDisplayMode.satoshi) ||
(CryptoCurrency.btcln == cryptoCurrency &&
+174
View File
@@ -0,0 +1,174 @@
import "package:hash_wallet/core/universal_address_detector.dart";
import "package:hash_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
sealed class ChainBinding {
const ChainBinding();
}
class ExplicitEvmChain extends ChainBinding {
const ExplicitEvmChain(this.chainId);
final int chainId;
}
class ChainlessEvm extends ChainBinding {
const ChainlessEvm();
}
class NoEvmBinding extends ChainBinding {
const NoEvmBinding();
}
class AnyPayRequest {
AnyPayRequest({
required this.rawInput,
required this.paymentRequest,
required this.detection,
required this.chainBinding,
});
final String rawInput;
final PaymentRequest paymentRequest;
final AddressDetectionResult detection;
final ChainBinding chainBinding;
String get address => paymentRequest.address;
String get amount => paymentRequest.amount;
String? get contractAddress => paymentRequest.contractAddress;
bool get hasContract => contractAddress != null && contractAddress!.isNotEmpty;
bool get isLightning => detection.detectedCurrency == CryptoCurrency.btcln;
}
class WalletSnapshot {
WalletSnapshot({
required this.type,
required this.currentWalletIsEvm,
required this.currentChainId,
required this.wallets,
required this.supportedEvmChains,
required this.hasEvmProxy,
required this.hasSolanaProxy,
required this.hasTronProxy,
});
final WalletType type;
final bool currentWalletIsEvm;
final int? currentChainId;
final List<WalletInfo> wallets;
final Map<int, WalletType> supportedEvmChains;
final bool hasEvmProxy;
final bool hasSolanaProxy;
final bool hasTronProxy;
List<WalletInfo> walletsOfType(WalletType walletType) =>
wallets.where((wallet) => wallet.type == walletType).toList();
}
sealed class AnyPayResolution {
const AnyPayResolution();
}
class NoTokenRequested extends AnyPayResolution {
const NoTokenRequested();
}
class TokenResolved extends AnyPayResolution {
const TokenResolved({required this.token, this.chainId, this.amountOverride});
final CryptoCurrency token;
final int? chainId;
final String? amountOverride;
}
class TokenUnknown extends AnyPayResolution {
const TokenUnknown(this.contract);
final String contract;
}
sealed class AnyPayDecision {
const AnyPayDecision();
}
class AnyPayApplyToCurrentWallet extends AnyPayDecision {
const AnyPayApplyToCurrentWallet({
this.token,
this.amountOverride,
this.fallbackCurrency,
});
final CryptoCurrency? token;
final String? amountOverride;
final CryptoCurrency? fallbackCurrency;
}
class AnyPayEvmNetworkChoice extends AnyPayDecision {
const AnyPayEvmNetworkChoice();
}
class AnyPayCrossChainPayment extends AnyPayDecision {
const AnyPayCrossChainPayment({
required this.targetWalletType,
required this.wallets,
this.targetChainId,
this.token,
this.amountOverride,
this.canSwap = true,
});
final WalletType targetWalletType;
final int? targetChainId;
final List<WalletInfo> wallets;
final CryptoCurrency? token;
final String? amountOverride;
final bool canSwap;
bool get hasCompatibleWallet => wallets.isNotEmpty;
}
class AnyPayUnsupportedNetwork extends AnyPayDecision {
const AnyPayUnsupportedNetwork(this.chainId);
final int chainId;
}
class AnyPayUnsupportedToken extends AnyPayDecision {
const AnyPayUnsupportedToken(this.contract);
final String contract;
}
class AnyPayEmptyInput extends AnyPayDecision {
const AnyPayEmptyInput();
}
class AnyPayEvaluation {
AnyPayEvaluation({required this.request, required this.decision});
final AnyPayRequest request;
final AnyPayDecision decision;
}
class AnyPaySwapIntent {
AnyPaySwapIntent({
required this.request,
required this.receiveCurrency,
required this.targetWalletType,
this.receiveAmount,
});
final AnyPayRequest request;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Money? receiveAmount;
String get recipientAddress => request.address;
}
+71
View File
@@ -0,0 +1,71 @@
import "package:hash_wallet/core/anypay/anypay_models.dart";
import "package:hash_wallet/core/universal_address_detector.dart";
import "package:hash_wallet/utils/payment_request.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/payment_uris.dart";
class AnyPayParser {
static AnyPayRequest fromRaw(String input) {
final paymentRequest = PaymentRequest.fromString(input);
return AnyPayRequest(
rawInput: input,
paymentRequest: paymentRequest,
detection: UniversalAddressDetector.detectAddress(input),
chainBinding: _bindingFor(paymentRequest),
);
}
static AnyPayRequest fromPaymentRequest(PaymentRequest request) {
final raw = _reconstructRawInput(request);
return AnyPayRequest(
rawInput: raw,
paymentRequest: request,
detection: UniversalAddressDetector.detectAddress(raw),
chainBinding: _bindingFor(request),
);
}
static String _reconstructRawInput(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme.isEmpty || scheme == "lightning") {
return request.address;
}
if (scheme == "ethereum") {
return ERC681URI(
address: request.address,
amount: request.amount,
contractAddress: request.contractAddress,
chainId: request.chainId ?? 1,
rawTokenAmount: request.rawTokenAmount,
).toString();
}
final amount = request.amount.isNotEmpty ? "?amount=${request.amount}" : "";
return "${request.scheme}:${request.address}$amount";
}
static ChainBinding _bindingFor(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme == "ethereum") {
final chainId = request.chainId;
return chainId != null ? ExplicitEvmChain(chainId) : const ChainlessEvm();
}
if (scheme.isNotEmpty) {
try {
final chainId = getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
if (chainId != null) {
return ExplicitEvmChain(chainId);
}
} catch (_) {}
}
return const NoEvmBinding();
}
}
+109
View File
@@ -0,0 +1,109 @@
import "package:hash_wallet/core/anypay/anypay_models.dart";
import "package:hash_wallet/core/anypay/anypay_router.dart";
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:hash_wallet/utils/token_utilities.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayResolver {
AnyPayResolver({AnyPayTokenLookup? tokenLookup})
: _tokenLookup = tokenLookup ?? const TokenUtilitiesLookup();
final AnyPayTokenLookup _tokenLookup;
Future<AnyPayResolution> resolve(AnyPayRequest request, WalletSnapshot snapshot) async {
if (!request.hasContract) {
return const NoTokenRequested();
}
final contract = request.contractAddress!;
final detectedType = request.detection.detectedWalletType;
if (detectedType != null && isEVMCompatibleChain(detectedType)) {
return _resolveEvm(request, snapshot, contract);
}
final token = await _tokenLookup.findTokenByAddress(
walletType: detectedType ?? snapshot.type,
address: contract,
);
if (token == null) {
return TokenUnknown(contract);
}
return TokenResolved(
token: token,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
Future<AnyPayResolution> _resolveEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
String contract,
) async {
final requestedChainId = AnyPayRouter.requestedEvmChainId(request, snapshot) ?? 1;
final lookupType = snapshot.supportedEvmChains[requestedChainId];
if (lookupType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: lookupType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: requestedChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
// The QRs from our old app versions omit the chainId on mainnet, so a contract the target
// network does not know may still belong to another EVM network.
if (request.chainBinding is ChainlessEvm) {
final reboundChainId = await _tokenLookup.findEvmChainIdForContract(
contract,
excludingChainId: requestedChainId,
);
final reboundType =
reboundChainId != null ? snapshot.supportedEvmChains[reboundChainId] : null;
if (reboundType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: reboundType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: reboundChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
}
return TokenUnknown(contract);
}
}
abstract class AnyPayTokenLookup {
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
});
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId});
}
class TokenUtilitiesLookup implements AnyPayTokenLookup {
const TokenUtilitiesLookup();
@override
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
}) =>
TokenUtilities.findTokenByAddress(walletType: walletType, address: address);
@override
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) =>
TokenUtilities.findEvmChainIdForContract(contractAddress, excludingChainId: excludingChainId);
}
+135
View File
@@ -0,0 +1,135 @@
import "package:hash_wallet/core/address_validator.dart";
import "package:hash_wallet/core/anypay/anypay_models.dart";
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayRouter {
static final List<RegExp> _swapIncompatibleAddressPatterns = [
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"),
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"),
RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"),
];
static bool _isSwapIncompatibleAddress(String address) =>
_swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address));
static AnyPayDecision route(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
if (request.rawInput.trim().isEmpty) {
return const AnyPayEmptyInput();
}
final detection = request.detection;
final detectedType = detection.detectedWalletType;
if (!detection.isValid || detectedType == null) {
return const AnyPayApplyToCurrentWallet();
}
if (isEVMCompatibleChain(detectedType)) {
if (!snapshot.hasEvmProxy) {
return const AnyPayApplyToCurrentWallet();
}
return _routeEvm(request, snapshot, resolution);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
if (detectedType == snapshot.type) {
return AnyPayApplyToCurrentWallet(
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
return AnyPayCrossChainPayment(
targetWalletType: detectedType,
wallets: snapshot.walletsOfType(detectedType),
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
canSwap: !_isSwapIncompatibleAddress(request.address),
);
}
static AnyPayDecision _routeEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
final requestedChainId = requestedEvmChainId(request, snapshot);
if (requestedChainId == null) {
if (snapshot.currentWalletIsEvm) {
return const AnyPayApplyToCurrentWallet();
}
return const AnyPayEvmNetworkChoice();
}
if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) {
return AnyPayUnsupportedNetwork(requestedChainId);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
int targetChainId = requestedChainId;
if (resolution is TokenResolved && resolution.chainId != null) {
targetChainId = resolution.chainId!;
}
final token = resolution is TokenResolved ? resolution.token : null;
final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null;
if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) {
return AnyPayApplyToCurrentWallet(
token: token,
amountOverride: amountOverride,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
final targetWalletType = snapshot.supportedEvmChains[targetChainId]!;
return AnyPayCrossChainPayment(
targetWalletType: targetWalletType,
targetChainId: targetChainId,
wallets: snapshot.walletsOfType(targetWalletType),
token: token,
amountOverride: amountOverride,
);
}
static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) {
if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) {
return null;
}
return request.detection.detectedCurrency;
}
static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) {
final binding = request.chainBinding;
if (binding is ExplicitEvmChain) {
return binding.chainId;
}
if (binding is ChainlessEvm) {
// the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request
return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1;
}
return null;
}
}
+138
View File
@@ -0,0 +1,138 @@
import "package:hash_wallet/core/anypay/anypay_models.dart";
import "package:hash_wallet/core/anypay/anypay_parser.dart";
import "package:hash_wallet/core/anypay/anypay_resolver.dart";
import "package:hash_wallet/core/anypay/anypay_router.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/new-ui/services/wallet_switch_service.dart";
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayService {
AnyPayService({
required this.appStore,
required this.walletSwitchService,
AnyPayResolver? resolver,
}) : resolver = resolver ?? AnyPayResolver();
final AppStore appStore;
final WalletSwitchService walletSwitchService;
final AnyPayResolver resolver;
Future<AnyPayEvaluation> evaluateRawInput(String input) => _evaluate(AnyPayParser.fromRaw(input));
Future<AnyPayEvaluation> evaluatePaymentRequest(PaymentRequest request) =>
_evaluate(AnyPayParser.fromPaymentRequest(request));
Future<AnyPayEvaluation> evaluateForEvmChain(AnyPayRequest request, int chainId) => _evaluate(
AnyPayRequest(
rawInput: request.rawInput,
paymentRequest: request.paymentRequest,
detection: request.detection,
chainBinding: ExplicitEvmChain(chainId),
),
);
Future<AnyPayEvaluation> _evaluate(AnyPayRequest request) async {
try {
final snapshot = await _buildWalletSnapshot();
final resolution = await resolver.resolve(request, snapshot);
return AnyPayEvaluation(
request: request,
decision: AnyPayRouter.route(request, snapshot, resolution),
);
} catch (e) {
printV("anypay evaluation failed: $e");
return AnyPayEvaluation(
request: request,
decision: const AnyPayApplyToCurrentWallet(),
);
}
}
Future<WalletSnapshot> _buildWalletSnapshot() async {
final wallet = appStore.wallet!;
final currentWalletIsEvm = isEVMCompatibleChain(wallet.type);
final supportedEvmChains = <int, WalletType>{};
if (evm != null) {
for (final chain in evm!.getAllChains()) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
if (walletType != null) {
supportedEvmChains[chain.chainId] = walletType;
}
}
}
return WalletSnapshot(
type: wallet.type,
currentWalletIsEvm: currentWalletIsEvm,
currentChainId: currentWalletIsEvm && evm != null
? evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type)
: null,
wallets: await WalletInfo.getAll(),
supportedEvmChains: supportedEvmChains,
hasEvmProxy: evm != null,
hasSolanaProxy: false,
hasTronProxy: false,
);
}
AnyPaySwapIntent buildSwapIntent(
AnyPayRequest request, {
required WalletType targetWalletType,
int? targetChainId,
CryptoCurrency? token,
}) {
final receiveCurrency = token ??
(targetChainId != null
? walletTypeToCryptoCurrency(targetWalletType, chainId: targetChainId)
: request.detection.detectedCurrency ?? walletTypeToCryptoCurrency(targetWalletType));
final amountValue = token != null
? request.paymentRequest.resolveTokenAmount(token)
: (request.amount.isNotEmpty ? request.amount : null);
return AnyPaySwapIntent(
request: request,
receiveCurrency: receiveCurrency,
targetWalletType: targetWalletType,
receiveAmount: amountValue != null ? Money.tryParse(amountValue, receiveCurrency) : null,
);
}
Future<bool> switchWalletForPayment(WalletInfo walletInfo, {int? chainId}) async {
try {
await walletSwitchService.switchToWallet(walletInfo);
} catch (e) {
printV("wallet switch failed: $e");
return false;
}
final wallet = appStore.wallet!;
if (chainId != null && evm != null && isEVMCompatibleChain(wallet.type)) {
try {
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await evm!.selectChain(wallet, chainId, node: node);
} catch (e, s) {
printV("switchWalletForPayment chain select failed: $e\n$s");
return false;
}
}
try {
await wallet.updateBalance();
} catch (e) {
printV("balance refresh after wallet switch failed: $e");
}
return true;
}
}
+1
View File
@@ -36,6 +36,7 @@ class AuthService with Store {
static const List<String> _alwaysAuthenticateRoutes = [
Routes.showKeys,
Routes.seed,
Routes.backup,
Routes.setupPin,
Routes.setup_2faPage,
+6 -2
View File
@@ -208,10 +208,14 @@ class BackgroundSync {
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
tx.date.add(Duration(minutes: 1)).toIso8601String());
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
final action = tx.additionalInfo['isAutoShield'] == true
? "Autoshield"
: tx.direction == TransactionDirection.incoming
? "Received"
: "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification(
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}");
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
}
printV(
"${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}");
+7
View File
@@ -18,6 +18,9 @@ import 'package:hash_wallet/core/key_service.dart';
import 'package:hash_wallet/entities/encrypt.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/secret_store_key.dart';
import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
import 'package:cw_core/wallet_info.dart';
import 'package:hash_wallet/exchange/trade_legacy.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
@@ -113,6 +116,10 @@ class $BackupService {
Future<void> verifyWallets() async {
await performHiveMigration(); // for backups made before sqlite migration
await performTradeHiveMigration(_secureStorage);
await performErc20TokenHiveMigration();
await performSplTokenHiveMigration();
await performTronTokenHiveMigration();
correctWallets = (await WalletInfo.getAll())
.where((info) => availableWalletTypes.contains(info.type))
.toList();
+49 -3
View File
@@ -147,6 +147,21 @@ class BackupMetadata {
}
}
class IncompatibleBackupAppException implements Exception {
IncompatibleBackupAppException({
required this.sourceAppName,
required this.currentAppName,
});
final String sourceAppName;
final String currentAppName;
@override
String toString() {
return 'This backup was created in $sourceAppName and cannot be restored in $currentAppName.';
}
}
class BackupServiceV3 extends $BackupService {
BackupServiceV3(super.secureStorage, super.transactionDescriptionBox, super.keyService,
super.sharedPreferences);
@@ -201,7 +216,8 @@ class BackupServiceV3 extends $BackupService {
}
}
Future<void> importBackupFile(File file, String password, {String nonce = secrets.backupSalt}) {
Future<void> importBackupFile(File file, String password,
{String nonce = secrets.backupSalt, bool checkBackupApp = true}) {
final version = getVersionFile(file);
switch (version) {
case BackupVersion.unknown:
@@ -214,12 +230,12 @@ class BackupServiceV3 extends $BackupService {
case BackupVersion.v2:
return super.importBackupV2(file.readAsBytesSync(), password);
case BackupVersion.v3:
return importBackupFileV3(file, password, nonce: nonce);
return importBackupFileV3(file, password, nonce: nonce, checkBackupApp: checkBackupApp);
}
}
Future<void> importBackupFileV3(File file, String password,
{String nonce = secrets.backupSalt}) async {
{String nonce = secrets.backupSalt, bool checkBackupApp = true}) async {
// Overall design of v3 backup is the following:
// 1. backup.zip - plaintext zip file that user can open with any archive manager
// 2. backup.zip/README.txt - text file to let user know what is inside of this file
@@ -228,6 +244,11 @@ class BackupServiceV3 extends $BackupService {
final inputStream = InputFileStream(file.path);
final archive = ZipDecoder().decodeStream(inputStream);
if (checkBackupApp) {
await _throwIfBackupWasCreatedInAnotherApp(archive);
}
final metadataFile = archive.findFile('metadata.json');
if (metadataFile == null) {
throw Exception('Invalid v3 backup: missing metadata.json');
@@ -497,6 +518,31 @@ This backup was created on ${DateTime.now().toIso8601String()}
return file;
}
Future<void> _throwIfBackupWasCreatedInAnotherApp(Archive archive) async {
final readmeFile = archive.findFile('README.txt');
if (readmeFile == null) return;
final readmeBytes = readmeFile.rawContent?.readBytes();
if (readmeBytes == null) return;
final readmeString = utf8.decode(readmeBytes, allowMalformed: true);
final sourceAppName = _extractBackupAppName(readmeString);
if (sourceAppName == null) return;
final currentAppName = (await PackageInfo.fromPlatform()).appName;
if (sourceAppName == currentAppName) return;
throw IncompatibleBackupAppException(
sourceAppName: sourceAppName,
currentAppName: currentAppName,
);
}
String? _extractBackupAppName(String readme) {
final match = RegExp(r'^This is a (.+) backup\.', multiLine: true).firstMatch(readme);
return match?.group(1)?.trim();
}
static const chunkSize = 24 * 1024 * 1024; // 24MiB
File setVersionFile(File file, BackupVersion version) {
+339
View File
@@ -0,0 +1,339 @@
import 'dart:io';
import 'package:hash_wallet/entities/transaction_description.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/backup/backup_page.dart';
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:hash_wallet/utils/share_util.dart';
import 'package:hash_wallet/utils/show_bar.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:hash_wallet/view_model/dashboard/action_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
class CsvExportService {
CsvExportService({required this.transactionDescriptionBox});
final Box<TransactionDescription> transactionDescriptionBox;
static const _columns = [
'record_type',
'date_time',
'type',
'amount',
'currency',
'fee',
'tx_id',
'address',
'status',
'note',
'from_amount',
'from_currency',
'to_amount',
'to_currency',
'trade_id',
'provider',
'confirmations',
];
static const _utf8Bom = '';
static const _formulaTriggers = {'=', '+', '-', '@', '\t', '\r'};
static final _plainNumber = RegExp(r'^-?\d+(\.\d+)?$');
String buildCsvContent(List<ActionListItem> items) {
final buf = StringBuffer();
buf.write(_utf8Bom);
buf.writeln(_columns.join(','));
final primaryAddress = _primaryAddressOf(items);
for (final item in items) {
if (item is DateSectionItem) continue;
final row = _buildRow(item, primaryAddress);
if (row != null) buf.writeln(row);
}
return buf.toString();
}
String _primaryAddressOf(List<ActionListItem> items) {
for (final item in items) {
if (item is TransactionListItem) {
return item.balanceViewModel.wallet.walletAddresses.primaryAddress;
}
}
return '';
}
String _noteFor(TransactionListItem item, String primaryAddress) {
final txHash = item.transaction.txHash;
final descriptionKey = '${txHash}_$primaryAddress';
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == txHash);
return description?.note ?? '';
}
String? _buildRow(ActionListItem item, String primaryAddress) {
if (item is TransactionListItem) return _transactionRow(item, primaryAddress);
if (item is TradeListItem) return _tradeRow(item);
if (item is OrderListItem) return _orderRow(item);
if (item is AnonpayTransactionListItem) return _anonpayRow(item);
if (item is PayjoinTransactionListItem) return _payjoinRow(item);
return null;
}
String _transactionRow(TransactionListItem item, String primaryAddress) {
final tx = item.transaction;
final type = tx.direction == TransactionDirection.incoming ? 'incoming' : 'outgoing';
final status = tx.isPending ? 'pending' : 'confirmed';
// Prefer tx.to/tx.from; fall back to address lists for chains that don't populate them.
final address = tx.direction == TransactionDirection.incoming
? (tx.from?.isNotEmpty == true ? tx.from! : (tx.inputAddresses?.firstOrNull ?? ''))
: (tx.to?.isNotEmpty == true ? tx.to! : (tx.outputAddresses?.firstOrNull ?? ''));
final fee = tx.fee != null && !tx.fee!.isZero ? tx.fee.toString() : '';
return _row([
'transaction',
_isoDate(tx.date),
type,
tx.amount.toString(),
tx.amount.currency.symbol,
fee,
tx.id,
address,
status,
_noteFor(item, primaryAddress),
'',
'',
'',
'',
'',
'',
tx.confirmations.toString(),
]);
}
String _tradeRow(TradeListItem item) {
final trade = item.trade;
return _row([
'trade',
_isoDate(trade.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)),
'swap',
'',
'',
trade.fee?.toString() ?? '',
trade.outputTransaction ?? '',
trade.payoutAddress ?? '',
trade.state.title,
trade.memo ?? '',
trade.amount,
trade.from?.name ?? '',
trade.receiveAmount ?? '',
trade.to?.name ?? '',
trade.id,
trade.provider.title,
'',
]);
}
String _orderRow(OrderListItem item) {
final order = item.order;
return _row([
'order',
_isoDate(order.createdAt),
order.source.title,
order.amountFormatted(),
order.from ?? '',
'',
order.transferId,
'',
order.state.title,
'',
order.amountFormatted(),
order.from ?? '',
order.receiveAmount ?? '',
order.to ?? '',
order.id,
order.providerTitle,
'',
]);
}
String _anonpayRow(AnonpayTransactionListItem item) {
final tx = item.transaction;
final amount = tx.fiatAmount?.toString() ?? tx.amountTo?.toString() ?? '';
final currency = tx.fiatEquiv ?? tx.coinTo;
return _row([
'anonpay',
_isoDate(tx.createdAt),
'anonymous_payment',
amount,
currency,
'',
tx.invoiceId,
tx.address,
tx.status,
'',
'',
'',
'',
'',
'',
tx.provider,
'',
]);
}
String _payjoinRow(PayjoinTransactionListItem item) {
final session = item.session;
final type = session.isSenderSession ? 'send' : 'receive';
final amount = session.rawAmount ?? '0';
return _row([
'payjoin',
_isoDate(session.inProgressSince ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)),
type,
amount,
'BTC',
'',
session.txId ?? '',
'',
item.status,
'',
'',
'',
'',
'',
'',
'',
'',
]);
}
String _row(List<String> fields) => fields.map(escapeField).join(',');
String escapeField(String field) {
final value = _neutralizeFormula(field);
if (value.contains(',') ||
value.contains('"') ||
value.contains('\n') ||
value.contains('\r')) {
return '"${value.replaceAll('"', '""')}"';
}
return value;
}
String _neutralizeFormula(String field) {
if (field.isEmpty || !_formulaTriggers.contains(field[0])) return field;
if (_plainNumber.hasMatch(field)) return field;
return "'$field";
}
String _isoDate(DateTime dt) => dt.toUtc().toIso8601String();
Future<void> exportToCsv(List<ActionListItem> items, BuildContext context) async {
final dataItems =
items.whereType<ActionListItem>().where((e) => e is! DateSectionItem).toList();
if (dataItems.isEmpty) {
await showBar<void>(context, S.current.csv_nothing_to_export);
return;
}
final now = DateTime.now();
final fileName = 'cake_wallet_export_${DateFormat('yyyyMMdd_HHmmss').format(now)}.csv';
late File csvFile;
final exportFuture = Future(() async {
final content = buildCsvContent(items);
csvFile = await _writeTempFile(fileName, content);
});
showPersistentActionOverlay(context, exportFuture, text: S.current.generating_csv);
await exportFuture;
if (!context.mounted) return;
if (Platform.isAndroid) {
_showAndroidExportDialog(context, csvFile, fileName);
} else if (Platform.isIOS) {
await _shareFile(csvFile, fileName, context);
} else {
await _saveFileDesktop(csvFile, fileName);
}
}
void _showAndroidExportDialog(BuildContext context, File csvFile, String fileName) {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.current.export_csv,
alertContent: S.current.select_destination,
rightButtonText: S.current.save_to_downloads,
leftButtonText: S.current.share,
actionRightButton: () async {
await _saveToDownloads(fileName, csvFile);
Navigator.of(dialogContext).pop();
await showBar<void>(context, S.current.file_saved);
await csvFile.delete();
},
actionLeftButton: () async {
Navigator.of(dialogContext).pop();
await _shareFile(csvFile, fileName, context);
},
);
},
);
}
Future<void> _shareFile(File file, String fileName, BuildContext context) async {
await ShareUtil.shareFile(filePath: file.path, fileName: fileName, context: context);
if (await file.exists()) await file.delete();
}
Future<void> _saveFileDesktop(File csvFile, String fileName) async {
final outputPath = await FilePicker.platform.saveFile(
dialogTitle: 'Save CSV export',
fileName: fileName,
lockParentWindow: true,
);
if (outputPath == null) return;
await csvFile.copy(outputPath);
await csvFile.delete();
}
Future<File> _writeTempFile(String fileName, String content) async {
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/$fileName');
if (file.existsSync()) file.deleteSync();
await file.writeAsString(content, flush: true);
return file;
}
Future<void> _saveToDownloads(String fileName, File file) async {
if (!Platform.isAndroid) return;
const downloadsPath = '/storage/emulated/0/Download';
final dest = File('$downloadsPath/$fileName');
if (dest.existsSync()) dest.deleteSync();
await file.copy(dest.path);
}
}
+21 -15
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import "dart:convert";
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:cw_core/utils/proxy_wrapper.dart";
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
try {
@@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) {
}
class _LNURLPResponseDTO {
const _LNURLPResponseDTO(
this.callback,
this.maxSendable,
this.minSendable,
this.tag,
this.metadata,
);
factory _LNURLPResponseDTO.fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String,
);
final String callback;
final int maxSendable;
final int minSendable;
final String tag;
final String metadata;
const _LNURLPResponseDTO(
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String);
}
class _LNURLPCallbackResponseDTO {
final String pr;
const _LNURLPCallbackResponseDTO(this.pr);
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
factory _LNURLPCallbackResponseDTO.fromJson(Map<String, dynamic> map) =>
_LNURLPCallbackResponseDTO(map["pr"] as String);
final String pr;
}
+7 -9
View File
@@ -14,7 +14,6 @@ class NodeSwitchingService {
NodeSwitchingService({
required this.appStore,
required this.settingsStore,
required this.nodeSource,
});
static const int _healthCheckIntervalSeconds = 30;
@@ -38,7 +37,6 @@ class NodeSwitchingService {
final AppStore appStore;
final SettingsStore settingsStore;
final Box<Node> nodeSource;
final Map<WalletType, List<dynamic>> _usedNodeKeys = {};
@@ -118,13 +116,13 @@ class NodeSwitchingService {
WalletType walletType,
) async {
for (final node in nodes) {
if (!_usedNodeKeys[walletType]!.contains(node.key)) {
if (!_usedNodeKeys[walletType]!.contains(node.id)) {
final isActive = await node.requestNode();
if (isActive) {
return node;
} else {
printV('Node ${node.uriRaw} is not active. Marking as used.');
_usedNodeKeys[walletType]!.add(node.key);
_usedNodeKeys[walletType]!.add(node.id);
}
}
}
@@ -157,7 +155,7 @@ class NodeSwitchingService {
final currentNode = settingsStore.getCurrentNode(nodeWalletType, chainId: chainId);
// Get all trusted nodes for this wallet type
final trustedNodes = nodeSource.values
final trustedNodes = (await Node.getAll())
.where((node) => node.type == nodeWalletType && node.isEnabledForAutoSwitching)
.toList();
@@ -171,8 +169,8 @@ class NodeSwitchingService {
_usedNodeKeys.putIfAbsent(nodeWalletType, () => []);
// Add current node to used list if not already there
if (!_usedNodeKeys[nodeWalletType]!.contains(currentNode.key)) {
_usedNodeKeys[nodeWalletType]!.add(currentNode.key);
if (!_usedNodeKeys[nodeWalletType]!.contains(currentNode.id)) {
_usedNodeKeys[nodeWalletType]!.add(currentNode.id);
}
// Try to find an active unused node
@@ -199,8 +197,8 @@ class NodeSwitchingService {
}
// Ensure the selected node is marked as used
if (!_usedNodeKeys[nodeWalletType]!.contains(nextNode.key)) {
_usedNodeKeys[nodeWalletType]!.add(nextNode.key);
if (!_usedNodeKeys[nodeWalletType]!.contains(nextNode.id)) {
_usedNodeKeys[nodeWalletType]!.add(nextNode.id);
}
printV(
+1 -1
View File
@@ -42,7 +42,7 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi
}
if (syncStatus is SyncedSyncStatus) {
return "";
return S.current.sync_synced;
}
if (syncStatus is FailedSyncStatus) {
+6
View File
@@ -238,6 +238,12 @@ class UniversalAddressDetector {
currency: CryptoCurrency.doge,
),
// Zcash (transparent, Sapling shielded, unified)
_DetectionPattern(
pattern: RegExp("^(?:${AddressValidator.zcashAddressPattern})\$"),
currency: CryptoCurrency.zec,
),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
+196 -304
View File
@@ -6,11 +6,14 @@ import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:hash_wallet/anypay/anypay_api.dart';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:hash_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:hash_wallet/core/anypay/anypay_service.dart';
import 'package:hash_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:hash_wallet/core/auth_service.dart';
import 'package:hash_wallet/core/backup_service_v3.dart';
import 'package:hash_wallet/core/csv_export_service.dart';
import 'package:hash_wallet/core/key_service.dart';
import 'package:hash_wallet/core/new_wallet_arguments.dart';
import 'package:hash_wallet/core/new_wallet_type_arguments.dart';
@@ -28,7 +31,6 @@ import 'package:hash_wallet/entities/contact.dart';
import 'package:hash_wallet/entities/contact_record.dart';
import 'package:hash_wallet/entities/exchange_api_mode.dart';
import 'package:hash_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/qr_view_data.dart';
import 'package:hash_wallet/entities/template.dart';
@@ -40,20 +42,29 @@ import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:hash_wallet/exchange/trade.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/nano/nano.dart';
import 'package:hash_wallet/new-ui/model/charts/price_store.dart';
import 'package:hash_wallet/new-ui/new_dashboard.dart';
import 'package:hash_wallet/new-ui/pages/about_page.dart';
import 'package:hash_wallet/new-ui/pages/account_customizer.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:hash_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:hash_wallet/new-ui/pages/charts_page.dart';
import 'package:hash_wallet/new-ui/pages/coin_control_page.dart';
import 'package:hash_wallet/new-ui/pages/addresses_page.dart';
import 'package:hash_wallet/new-ui/pages/home_page.dart';
import 'package:hash_wallet/new-ui/pages/send_page.dart';
import "package:hash_wallet/new-ui/services/wallet_switch_service.dart";
import 'package:hash_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:hash_wallet/new-ui/pages/receive_page.dart';
import "package:hash_wallet/new-ui/pages/seed/pre_seed_page.dart";
import "package:hash_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import 'package:hash_wallet/new-ui/viewmodels/charts/charts_bloc.dart';
import 'package:hash_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
import 'package:hash_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:hash_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:hash_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:hash_wallet/new-ui/pages/swap_page.dart';
@@ -64,22 +75,11 @@ import 'package:hash_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:hash_wallet/src/screens/auth/auth_page.dart';
import 'package:hash_wallet/src/screens/backup/backup_page.dart';
import 'package:hash_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:hash_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:hash_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:hash_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:hash_wallet/src/screens/buy/webview_page.dart';
import 'package:hash_wallet/src/screens/contact/contact_list_page.dart';
import 'package:hash_wallet/src/screens/contact/contact_page.dart';
import 'package:hash_wallet/src/screens/dashboard/dashboard_page.dart';
import 'package:hash_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:hash_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:hash_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/balance/balance_page.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:hash_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:hash_wallet/src/screens/dev/exchange_provider_logs_page.dart';
import 'package:hash_wallet/src/screens/dev/monero_background_sync.dart';
@@ -90,43 +90,24 @@ import 'package:hash_wallet/src/screens/dev/qr_tools_page.dart';
import 'package:hash_wallet/src/screens/dev/secure_preferences_page.dart';
import 'package:hash_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:hash_wallet/src/screens/dev/socket_health_logs_page.dart';
import 'package:hash_wallet/src/screens/exchange/exchange_page.dart';
import 'package:hash_wallet/src/screens/exchange/exchange_template_page.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
import 'package:hash_wallet/src/screens/faq/faq_page.dart';
import 'package:hash_wallet/src/screens/integrations/deuro/savings_page.dart';
import 'package:hash_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart';
import 'package:hash_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
import 'package:hash_wallet/src/screens/nano/nano_change_rep_page.dart';
import 'package:hash_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart';
import 'package:hash_wallet/src/screens/nano_accounts/nano_account_list_page.dart';
import 'package:hash_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
import 'package:hash_wallet/src/screens/nodes/node_create_or_edit_page.dart';
import 'package:hash_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:hash_wallet/src/screens/order_details/order_details_page.dart';
import 'package:hash_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:hash_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:hash_wallet/src/screens/receive/address_list_page.dart';
import 'package:hash_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:hash_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:hash_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:hash_wallet/src/screens/receive/receive_page.dart';
import 'package:hash_wallet/src/screens/rescan/rescan_page.dart';
import 'package:hash_wallet/src/screens/restore/restore_from_backup_page.dart';
import 'package:hash_wallet/src/screens/restore/restore_options_page.dart';
import 'package:hash_wallet/src/screens/restore/wallet_restore_choose_derivation.dart';
import 'package:hash_wallet/src/screens/restore/wallet_restore_page.dart';
import 'package:hash_wallet/src/screens/seed/pre_seed_page.dart';
import 'package:hash_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:hash_wallet/src/screens/seed/wallet_seed_page.dart';
import 'package:hash_wallet/src/screens/send/send_page.dart';
import 'package:hash_wallet/src/screens/send/send_template_page.dart';
import 'package:hash_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:hash_wallet/src/screens/settings/background_sync_page.dart';
import 'package:hash_wallet/src/screens/settings/connection_sync_page.dart';
import 'package:hash_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart';
import 'package:hash_wallet/src/screens/settings/display_settings_page.dart';
import 'package:hash_wallet/src/screens/settings/domain_lookups_page.dart';
import 'package:hash_wallet/src/screens/settings/manage_nodes_page.dart';
@@ -138,7 +119,7 @@ import 'package:hash_wallet/src/screens/settings/privacy_page.dart';
import 'package:hash_wallet/src/screens/settings/security_backup_page.dart';
import 'package:hash_wallet/src/screens/settings/silent_payments_logs_page.dart';
import 'package:hash_wallet/src/screens/settings/silent_payments_settings.dart';
import 'package:hash_wallet/src/screens/settings/trocador_providers_page.dart';
import 'package:hash_wallet/new-ui/pages/wallet_accounts_page.dart';
import 'package:hash_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
import 'package:hash_wallet/src/screens/setup_2fa/setup_2fa.dart';
import 'package:hash_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
@@ -146,7 +127,6 @@ import 'package:hash_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart';
import 'package:hash_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart';
import 'package:hash_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
import 'package:hash_wallet/src/screens/start_tor/start_tor_page.dart';
import 'package:hash_wallet/src/screens/subaddress/address_edit_or_create_page.dart';
import 'package:hash_wallet/src/screens/support/support_page.dart';
import 'package:hash_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:hash_wallet/src/screens/support_other_links/support_other_links_page.dart';
@@ -154,10 +134,8 @@ import 'package:hash_wallet/src/screens/trade_details/trade_details_page.dart';
import 'package:hash_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:hash_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:hash_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:hash_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:hash_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:hash_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:hash_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
@@ -181,19 +159,14 @@ import 'package:hash_wallet/store/dashboard/trade_filter_store.dart';
import 'package:hash_wallet/store/bridge_transfers_store.dart';
import 'package:hash_wallet/store/dashboard/trades_store.dart';
import 'package:hash_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:hash_wallet/store/node_list_store.dart';
import 'package:hash_wallet/store/seed_settings_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/templates/exchange_template_store.dart';
import 'package:hash_wallet/store/templates/send_template_store.dart';
import 'package:hash_wallet/store/wallet_list_store.dart';
import 'package:hash_wallet/store/yat/yat_store.dart';
import 'package:hash_wallet/themes/core/theme_store.dart';
import 'package:hash_wallet/utils/device_info.dart';
import 'package:hash_wallet/utils/payment_request.dart';
import 'package:hash_wallet/view_model/advanced_privacy_settings_view_model.dart';
import 'package:hash_wallet/view_model/animated_ur_model.dart';
import 'package:hash_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:hash_wallet/view_model/anonpay_details_view_model.dart';
import 'package:hash_wallet/view_model/auth_view_model.dart';
import 'package:hash_wallet/view_model/backup_view_model.dart';
@@ -201,13 +174,12 @@ import 'package:hash_wallet/view_model/bridge/bridge_details_view_model.dart';
import 'package:hash_wallet/view_model/bridge/bridge_history_view_model.dart';
import 'package:hash_wallet/view_model/buy/buy_amount_view_model.dart';
import 'package:hash_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:hash_wallet/view_model/buy/buy_view_model.dart';
import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:hash_wallet/view_model/contact_list/contact_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/home_settings_view_model.dart';
import "package:hash_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/sign_view_model.dart';
@@ -223,17 +195,19 @@ import 'package:hash_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/trezor_view_model.dart';
import 'package:hash_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:hash_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:hash_wallet/view_model/link_view_model.dart';
import 'package:hash_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:hash_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import "package:hash_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
import 'package:hash_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:hash_wallet/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_list_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/nano_account_list/nano_account_list_view_model.dart';
import 'package:hash_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:hash_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:hash_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:hash_wallet/view_model/order_details_view_model.dart';
import 'package:hash_wallet/view_model/payjoin_details_view_model.dart';
import 'package:hash_wallet/view_model/payment/payment_view_model.dart';
@@ -242,7 +216,6 @@ import 'package:hash_wallet/view_model/restore/restore_wallet.dart';
import 'package:hash_wallet/view_model/restore_from_backup_view_model.dart';
import 'package:hash_wallet/view_model/seed_settings_view_model.dart';
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
import 'package:hash_wallet/view_model/send/send_template_view_model.dart';
import 'package:hash_wallet/view_model/send/send_view_model.dart';
import 'package:hash_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:hash_wallet/view_model/settings/connection_sync_view_model.dart';
@@ -262,6 +235,7 @@ import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_details_view_
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:hash_wallet/view_model/bridge/bridge_view_model.dart';
import 'package:hash_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
@@ -302,11 +276,12 @@ import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'new-ui/widgets/addresses_page/address_info.dart';
import 'src/screens/buy/buy_sell_page.dart';
final getIt = GetIt.instance;
var _isSetupFinished = false;
late Box<Node> _nodeSource;
late Box<Node> _powNodeSource;
// late Box<Node> _nodeSource;
// late Box<Node> _powNodeSource;
late Box<Contact> _contactSource;
late Box<Template> _templates;
late Box<ExchangeTemplate> _exchangeTemplates;
@@ -316,8 +291,6 @@ late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
late Box<PayjoinSession> _payjoinSessionSource;
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
Future<void> setup({
required Box<Node> nodeSource,
required Box<Node> powNodeSource,
required Box<Contact> contactSource,
required Box<Template> templates,
required Box<ExchangeTemplate> exchangeTemplates,
@@ -329,8 +302,6 @@ Future<void> setup({
required SecureStorage secureStorage,
required GlobalKey<NavigatorState> navigatorKey,
}) async {
_nodeSource = nodeSource;
_powNodeSource = powNodeSource;
_contactSource = contactSource;
_templates = templates;
_exchangeTemplates = exchangeTemplates;
@@ -353,8 +324,6 @@ Future<void> setup({
const isBitcoinBuyEnabled = false;
final settingsStore = await SettingsStoreBase.load(
nodeSource: _nodeSource,
powNodeSource: _powNodeSource,
isBitcoinBuyEnabled: isBitcoinBuyEnabled,
);
@@ -390,18 +359,16 @@ Future<void> setup({
}
void _registerCoreSingletons(SettingsStore settingsStore) {
getIt.registerFactory<Box<Node>>(() => _nodeSource);
getIt.registerFactory<Box<Node>>(() => _powNodeSource, instanceName: Node.boxName + "pow");
getIt.registerSingleton(AuthenticationStore());
getIt.registerSingleton<WalletListStore>(WalletListStore());
getIt.registerSingleton(NodeListStoreBase.instance);
// getIt.registerSingleton(NodeListStoreBase.instance);
getIt.registerSingleton<SettingsStore>(settingsStore);
getIt.registerSingleton<AppStore>(AppStore(
authenticationStore: getIt.get<AuthenticationStore>(),
walletList: getIt.get<WalletListStore>(),
settingsStore: getIt.get<SettingsStore>(),
nodeListStore: getIt.get<NodeListStore>(),
// nodeListStore: getIt.get<NodeListStore>(),
themeStore: getIt.get<ThemeStore>()));
getIt.registerSingleton<TradesStore>(TradesStore(appStore: getIt.get<AppStore>()));
getIt.registerSingleton<OrdersStore>(
@@ -413,11 +380,6 @@ void _registerCoreSingletons(SettingsStore settingsStore) {
getIt.registerSingleton<OrderFilterStore>(OrderFilterStore());
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
getIt.registerSingleton<SendTemplateStore>(SendTemplateStore(templateSource: _templates));
getIt.registerSingleton<ExchangeTemplateStore>(
ExchangeTemplateStore(templateSource: _exchangeTemplates));
getIt.registerSingleton<YatStore>(
YatStore(appStore: getIt.get<AppStore>(), secureStorage: getIt.get<SecureStorage>())..init());
getIt.registerSingleton<AnonpayTransactionsStore>(
AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource));
getIt.registerSingleton<SeedSettingsStore>(SeedSettingsStore());
@@ -429,23 +391,28 @@ void _registerCoreSingletons(SettingsStore settingsStore) {
case HardwareWalletType.ledger:
return getIt<LedgerViewModel>();
case HardwareWalletType.trezor:
return getIt<TrezorViewModel>();
return getIt<TrezorConnectViewModel>();
case HardwareWalletType.cupcake:
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
throw Exception(
"This should not have happened, because airgapped Wallets don't need View Models");
"This should not have happened, because airgapped Wallets don't need View Models",
);
}
});
getIt.registerLazySingleton(() => LedgerViewModel());
getIt.registerLazySingleton(() => LedgerViewModel(getIt<AppStore>()));
getIt.registerLazySingleton(() => BitboxViewModel());
getIt.registerLazySingleton(BitboxViewModel.new);
getIt.registerLazySingleton(
() => TrezorConnect("hashbags://trezor_connect", appName: "Hash Bags"));
getIt.registerLazySingleton(() => TrezorViewModel(getIt<TrezorConnect>()));
() => TrezorConnect("hashbags://trezor_connect", appName: "Hash Bags"),
);
getIt.registerLazySingleton(
() => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
);
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
@@ -551,7 +518,6 @@ void _registerCoreViewModels(SettingsStore settingsStore) {
getIt.registerFactory<WalletAddressListViewModel>(() => WalletAddressListViewModel(
appStore: getIt.get<AppStore>(),
yatStore: getIt.get<YatStore>(),
fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactory(() => BalanceViewModel(
@@ -562,7 +528,6 @@ void _registerCoreViewModels(SettingsStore settingsStore) {
getIt.registerFactory(
() => ExchangeViewModel(
getIt.get<AppStore>(),
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<SharedPreferences>(),
getIt.get<ContactListViewModel>(),
@@ -583,6 +548,13 @@ void _registerCoreViewModels(SettingsStore settingsStore) {
getIt.registerFactory(() => DashboardViewModel(
tradeMonitor: getIt.get<TradeMonitor>(),
balanceViewModel: getIt.get<BalanceViewModel>(),
accountListViewModelFactory: () {
final wallet = getIt.get<AppStore>().wallet;
if (wallet == null) return null;
final hasAccounts = wallet.hasAccountsSupport;
if (!hasAccounts) return null;
return getIt.get<WalletAccountListViewModel>();
},
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
ordersStore: getIt.get<OrdersStore>(),
@@ -590,24 +562,30 @@ void _registerCoreViewModels(SettingsStore settingsStore) {
orderFilterStore: getIt.get<OrderFilterStore>(),
transactionFilterStore: getIt.get<TransactionFilterStore>(),
settingsStore: settingsStore,
yatStore: getIt.get<YatStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
getIt.registerFactoryParam<CardCustomizerBloc, bool, BitcoinAmountDisplayMode?>(
(lightningMode, displayMode) {
getIt.registerFactoryParam<CardCustomizerBloc, CardCustomizerBlocParams, void>(
(params, _) {
final wallet = getIt.get<AppStore>().wallet!;
return CardCustomizerBloc(wallet,
lightningMode: lightningMode,
canHide: params.canHide,
lightningMode: params.lightningMode,
displaySats: wallet.type == WalletType.bitcoin &&
(displayMode == BitcoinAmountDisplayMode.satoshi ||
(displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode)));
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshi ||
(params.amountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning && params.lightningMode)));
});
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerLazySingleton<PriceStore>(() => PriceStore(settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<ChartsBloc>(
() => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
getIt.registerFactory<ChartsPage>(() => ChartsPage(
chartsBloc: getIt.get<ChartsBloc>(),
));
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
@@ -775,17 +753,6 @@ void _registerDashboardAndSendPages(AppStore appStore) {
return walletKitService;
});
getIt.registerFactory(() => BalancePage(
nftViewModel: getIt.get<NFTViewModel>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<DashboardPage>(() => DashboardPage(
bottomSheetService: getIt.get<BottomSheetService>(),
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
));
getIt.registerFactory<NewDashboard>(() => NewDashboard(
dashboardViewModel: getIt.get<DashboardViewModel>(),
@@ -797,27 +764,6 @@ void _registerDashboardAndSendPages(AppStore appStore) {
nftViewModel: getIt.get<NFTViewModel>(),
));
getIt.registerFactory<DesktopSidebarWrapper>(() {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
return DesktopSidebarWrapper(
bottomSheetService: getIt.get<BottomSheetService>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
desktopSidebarViewModel: getIt.get<DesktopSidebarViewModel>(),
child: getIt.get<DesktopDashboardPage>(param1: _navigatorKey),
desktopNavigatorKey: _navigatorKey,
);
});
getIt.registerFactoryParam<DesktopDashboardPage, GlobalKey<NavigatorState>, void>(
(desktopKey, _) => DesktopDashboardPage(
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
desktopKey: desktopKey,
));
getIt.registerFactory<TransactionsPage>(
() => TransactionsPage(dashboardViewModel: getIt.get<DashboardViewModel>()));
getIt.registerFactory<Setup2FAInfoPage>(() => Setup2FAInfoPage());
getIt.registerFactory<Setup2FAPage>(
@@ -829,43 +775,9 @@ void _registerDashboardAndSendPages(AppStore appStore) {
getIt.registerFactory<Modify2FAPage>(
() => Modify2FAPage(setup2FAViewModel: getIt.get<Setup2FAViewModel>()));
getIt.registerFactory<DesktopSettingsPage>(
() => DesktopSettingsPage(getIt.get<DashboardViewModel>()));
getIt.registerFactoryParam<ReceiveOptionViewModel, ReceivePageOption?, void>(
(pageOption, _) => ReceiveOptionViewModel(getIt.get<AppStore>().wallet!, pageOption));
getIt.registerFactoryParam<AnonInvoicePageViewModel, List<dynamic>, void>((args, _) {
final address = args.first as String;
final pageOption = args.last as ReceivePageOption;
return AnonInvoicePageViewModel(
getIt.get<AnonPayApi>(),
address,
getIt.get<SettingsStore>(),
getIt.get<AppStore>().wallet!,
_anonpayInvoiceInfoSource,
getIt.get<SharedPreferences>(),
pageOption,
);
});
getIt.registerFactoryParam<AnonPayInvoicePage, List<dynamic>, void>((List<dynamic> args, _) {
final pageOption = args.last as ReceivePageOption;
return AnonPayInvoicePage(getIt.get<AnonInvoicePageViewModel>(param1: args),
getIt.get<ReceiveOptionViewModel>(param1: pageOption));
});
getIt.registerFactory<ReceivePage>(
() => ReceivePage(addressListViewModel: getIt.get<WalletAddressListViewModel>()));
getIt.registerFactory<AddressPage>(
() => AddressPage(
addressListViewModel: getIt<WalletAddressListViewModel>(),
dashboardViewModel: getIt<DashboardViewModel>(),
receiveOptionViewModel: getIt<ReceiveOptionViewModel>(),
),
);
getIt.registerFactoryParam<NewReceivePage, bool?, CryptoCurrency?>((param1, param2) =>
NewReceivePage(
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
@@ -878,10 +790,6 @@ void _registerDashboardAndSendPages(AppStore appStore) {
(WalletAddressListItem? item, _) =>
WalletAddressEditOrCreateViewModel(wallet: getIt.get<AppStore>().wallet!, item: item));
getIt.registerFactoryParam<AddressEditOrCreatePage, dynamic, void>((dynamic item, _) =>
AddressEditOrCreatePage(
addressEditOrCreateViewModel:
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactoryParam<AddressLabelInputPopup, dynamic, void>((dynamic item, _) =>
AddressLabelInputPopup(
@@ -897,17 +805,12 @@ void _registerDashboardAndSendPages(AppStore appStore) {
walletAddressEditOrCreateViewModel:
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactory<SendTemplateViewModel>(() => SendTemplateViewModel(
getIt.get<AppStore>().wallet!,
getIt.get<AppStore>(),
getIt.get<SendTemplateStore>(),
getIt.get<FiatConversionStore>()));
getIt.registerFactoryParam<SendViewModel, UnspentCoinType?, void>(
(coinTypeToSpendFrom, _) => SendViewModel(
getIt.get<AppStore>(),
getIt.get<SendTemplateViewModel>(),
getIt.get<FiatConversionStore>(),
getIt.get<AddressResolverService>(),
getIt.get<BalanceViewModel>(),
getIt.get<ContactListViewModel>(),
_transactionDescriptionBox,
@@ -920,14 +823,7 @@ void _registerDashboardAndSendPages(AppStore appStore) {
getIt.get<FeesViewModel>()),
);
getIt.registerFactoryParam<SendPage, PaymentRequest?, UnspentCoinType?>(
(PaymentRequest? initialPaymentRequest, coinTypeToSpendFrom) => SendPage(
sendViewModel: getIt.get<SendViewModel>(param1: coinTypeToSpendFrom),
authService: getIt.get<AuthService>(),
initialPaymentRequest: initialPaymentRequest,
paymentViewModel: getIt.get<PaymentViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
));
getIt.registerFactoryParam<NewSendPage, SendPageParams?, void>((params, _) {
params ??= SendPageParams();
@@ -936,13 +832,12 @@ void _registerDashboardAndSendPages(AppStore appStore) {
authService: getIt.get<AuthService>(),
params: params,
contactListViewModel: getIt.get<ContactListViewModel>(),
paymentViewModel: getIt.get<PaymentViewModel>(),
anyPayService: getIt.get<AnyPayService>(),
linkViewModel: getIt.get<LinkViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
);
});
getIt.registerFactory(
() => SendTemplatePage(sendTemplateViewModel: getIt.get<SendTemplateViewModel>()));
}
void _registerWalletListAndAccounts() {
@@ -1009,44 +904,65 @@ void _registerWalletListAndAccounts() {
getIt.registerFactory<MoneroAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero) {
return MoneroAccountListViewModel(wallet, getIt.get<SettingsStore>());
return MoneroAccountListViewModel(wallet);
}
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
});
getIt.registerFactory(
() => MoneroAccountListPage(accountListViewModel: getIt.get<MoneroAccountListViewModel>()));
getIt.registerFactory(
() => NanoAccountListPage(accountListViewModel: getIt.get<NanoAccountListViewModel>()));
/*getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
if (wallet is MoneroWallet) {
return MoneroAccountEditOrCreateViewModel(wallet.accountList);
getIt.registerFactory<BitcoinAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
if (wallet.type == WalletType.bitcoin) {
return BitcoinAccountListViewModel(wallet);
}
// FIXME: throw exception.
return null;
throw Exception(
'Unexpected wallet type: ${wallet.type} for generate Bitcoin AccountListViewModel');
});
getIt.registerFactory(() => MoneroAccountEditOrCreatePage(
moneroAccountCreationViewModel:
getIt.get<MoneroAccountEditOrCreateViewModel>()));*/
getIt.registerFactory<WalletAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
switch (wallet.type) {
case WalletType.bitcoin:
return getIt.get<BitcoinAccountListViewModel>();
case WalletType.monero:
case WalletType.wownero:
return getIt.get<MoneroAccountListViewModel>();
default:
throw Exception('Unsupported wallet type for WalletAccountListViewModel: ${wallet.type}');
}
});
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel, AccountListItem?, void>(
(AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(getIt.get<AppStore>().wallet!),
wownero?.getAccountList(getIt.get<AppStore>().wallet!),
wallet: getIt.get<AppStore>().wallet!,
accountListItem: account));
(AccountListItem? account, _) {
final wallet = getIt.get<AppStore>().wallet!;
getIt.registerFactoryParam<MoneroAccountEditOrCreatePage, AccountListItem?, void>(
(AccountListItem? account, _) => MoneroAccountEditOrCreatePage(
moneroAccountCreationViewModel:
getIt.get<MoneroAccountEditOrCreateViewModel>(param1: account)));
if (wallet.type == WalletType.monero) {
return MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(wallet),
null,
wallet: wallet,
accountListItem: account,
);
}
if (wallet.type == WalletType.wownero) {
return MoneroAccountEditOrCreateViewModel(
null,
wownero!.getAccountList(wallet),
wallet: wallet,
accountListItem: account,
);
}
throw StateError("Account creation is unavailable for ${wallet.type}");
});
getIt.registerFactory<BitcoinAccountEditOrCreateViewModel>(
() => BitcoinAccountEditOrCreateViewModel(
wallet: getIt.get<AppStore>().wallet!,
),
);
getIt.registerFactoryParam<NanoAccountEditOrCreateViewModel, NanoAccount?, void>(
(NanoAccount? account, _) =>
@@ -1055,10 +971,6 @@ void _registerWalletListAndAccounts() {
wallet: getIt.get<AppStore>().wallet!,
accountListItem: account));
getIt.registerFactoryParam<NanoAccountEditOrCreatePage, NanoAccount?, void>(
(NanoAccount? account, _) => NanoAccountEditOrCreatePage(
nanoAccountCreationViewModel:
getIt.get<NanoAccountEditOrCreateViewModel>(param1: account)));
}
void _registerSettingsAndNodes(List<WalletInfo> walletList) {
@@ -1103,13 +1015,17 @@ void _registerSettingsAndNodes(List<WalletInfo> walletList) {
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
getIt.registerFactory(() => ShowKeysDisclaimerPage(
authService: getIt.get<AuthService>(), settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>(
(Map<String, String> urQr, _) => AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>((ContactRecord? contact, _) =>
ContactViewModel(_contactSource, getIt.get<AppStore>(), getIt.get<AddressResolverService>(),
contact: contact));
getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>(
(CryptoCurrency? cur, _) =>
@@ -1125,26 +1041,21 @@ void _registerSettingsAndNodes(List<WalletInfo> walletList) {
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
getIt.registerFactoryParam<NewAddressesPage, bool, void>(
(showHidden, _) => NewAddressesPage(
showHidden: showHidden,
getIt.registerFactoryParam<NodeListViewModel, bool, void>((isPow, _) {
final appStore = getIt.get<AppStore>();
return NodeListViewModel(appStore, isPow);
});
getIt.registerFactoryParam<NewAddressesPage, AddressesPageArgs, void>(
(args, _) => NewAddressesPage(
showHidden: args.showHidden,
popOnSelection: args.popOnSelection,
addressListViewModel: getIt<WalletAddressListViewModel>(),
dashboardViewModel: getIt<DashboardViewModel>(),
),
);
getIt.registerFactory(() {
final appStore = getIt.get<AppStore>();
return NodeListViewModel(_nodeSource, appStore);
});
getIt.registerFactory(() {
final appStore = getIt.get<AppStore>();
return PowNodeListViewModel(_powNodeSource, appStore);
});
getIt.registerFactory(
() => ConnectionSyncViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
@@ -1153,9 +1064,32 @@ void _registerSettingsAndNodes(List<WalletInfo> walletList) {
getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(),
getIt.get<AuthService>(), getIt.get<AppStore>().wallet!.isHardwareWallet));
getIt.registerFactory<WalletAccountEditOrCreateViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!;
switch (wallet.type) {
case WalletType.bitcoin:
return getIt.get<BitcoinAccountEditOrCreateViewModel>();
case WalletType.monero:
case WalletType.wownero:
return getIt.get<MoneroAccountEditOrCreateViewModel>();
default:
throw Exception(
'Unsupported wallet type for WalletAccountEditOrCreateViewModel: ${wallet.type}');
}
});
getIt.registerFactoryParam<WalletAccountsPage, DashboardViewModel, void>(
(dashboardViewModel, _) => WalletAccountsPage(
dashboardViewModel: dashboardViewModel,
accountListViewModel:
dashboardViewModel.accountListViewModel ?? getIt.get<WalletAccountListViewModel>(),
accountEditOrCreateViewModel: getIt.get<WalletAccountEditOrCreateViewModel>(),
),
);
getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>()));
getIt.registerFactory(() => TrocadorProvidersPage(getIt.get<TrocadorProvidersViewModel>()));
getIt.registerFactory(() => DomainLookupsPage(getIt.get<ConnectionSyncViewModel>()));
@@ -1186,8 +1120,8 @@ void _registerSettingsAndNodes(List<WalletInfo> walletList) {
final WalletType type = args['type'] as WalletType? ?? getIt.get<AppStore>().wallet!.type;
final bool isPow = args['isPow'] as bool? ?? false;
final Node? editingNode = args['editingNode'] as Node?;
final nodeSourceArgs = isPow ? _powNodeSource : _nodeSource;
return NodeCreateOrEditViewModel(nodeSourceArgs, type, editingNode, getIt.get<SettingsStore>());
return NodeCreateOrEditViewModel(isPow, type, getIt.get<SettingsStore>(),
editingNode: editingNode);
});
getIt.registerFactoryParam<NodeCreateOrEditPage, Node?, bool?>(
@@ -1216,7 +1150,6 @@ void _registerExchangeAndSwap() {
isTestEnvironment: kDebugMode || kProfileMode,
));
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
getIt.registerFactory<FeesViewModel>(
() => FeesViewModel(
@@ -1235,33 +1168,22 @@ void _registerExchangeAndSwap() {
),
);
getIt.registerFactoryParam<ExchangePage, PaymentRequest?, void>(
(PaymentRequest? paymentRequest, __) {
return ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>(), paymentRequest);
});
getIt.registerFactoryParam<NewSwapPage, PaymentRequest?, CryptoCurrency?>(
(PaymentRequest? paymentRequest, CryptoCurrency? initialCurrency) {
return NewSwapPage(
getIt.get<ExchangeViewModel>(),
getIt.get<AuthService>(),
getIt.get<AddressResolverService>(),
paymentRequest,
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
initialCurrency: initialCurrency,
);
});
getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>()));
getIt.registerFactory(
() => ExchangeTradePage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(() =>
ExchangeTradeExternalSendPage(exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>()));
getIt.registerFactory(() => BackgroundSyncPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>()));
getIt.registerFactoryParam<SwapConfirmationBottomSheet, PaymentFlowResult, void>(
(paymentFlowResult, _) => SwapConfirmationBottomSheet(
@@ -1281,6 +1203,16 @@ void _registerExchangeAndSwap() {
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => WalletSwitchService(
walletLoadingService: getIt.get<WalletLoadingService>(),
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => AnyPayService(
appStore: getIt.get<AppStore>(),
walletSwitchService: getIt.get<WalletSwitchService>(),
));
getIt.registerFactory(() => WalletSwitcherViewModel(
appStore: getIt.get<AppStore>(),
walletLoadingService: getIt.get<WalletLoadingService>(),
@@ -1348,8 +1280,6 @@ void _registerRestoreAndBackup() {
getIt.registerFactory(() => RescanPage(getIt.get<RescanViewModel>()));
getIt.registerFactory(() => FaqPage(getIt.get<SettingsStore>()));
getIt.registerFactoryParam<WalletRestoreViewModel, WalletType, Map<String, dynamic>?>(
(type, additionalParams) {
final restoredWallet = additionalParams?['restoredWallet'] as RestoredWallet?;
@@ -1374,6 +1304,9 @@ void _registerRestoreAndBackup() {
param1: derivations,
)));
getIt.registerFactory<CsvExportService>(
() => CsvExportService(transactionDescriptionBox: _transactionDescriptionBox));
getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>((params, _) {
final transactionInfo = params[0] as TransactionInfo;
final canReplaceByFee = params[1] as bool? ?? false;
@@ -1389,10 +1322,12 @@ void _registerRestoreAndBackup() {
);
});
getIt.registerFactoryParam<TransactionDetailsModal, TransactionInfo, void>((transactionInfo, _) =>
TransactionDetailsModal(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: [transactionInfo, false])));
getIt.registerFactoryParam<TransactionDetailsModal, TransactionInfo, bool?>(
(transactionInfo, highlightNoteField) => TransactionDetailsModal(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: [transactionInfo, false]),
highlightNoteField: highlightNoteField ?? false,
));
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
@@ -1417,10 +1352,7 @@ void _registerRestoreAndBackup() {
);
});
getIt.registerFactory<PreSeedPage>(() => PreSeedPage());
getIt.registerFactoryParam<TransactionSuccessPage, String, void>(
(content, _) => TransactionSuccessPage(content: content));
getIt.registerFactory<PreSeedPage>(() => PreSeedPage(getIt.get<AppStore>().wallet!));
getIt.registerFactoryParam<TradeDetailsViewModel, Trade, void>(
(trade, _) => TradeDetailsViewModel(tradeForDetails: trade, appStore: getIt.get<AppStore>()));
@@ -1456,40 +1388,12 @@ void _registerRestoreAndBackup() {
void _registerBuyAndSupport() {
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<BuySellViewModel, NewBuySellParams, void>((params, _) => BuySellViewModel(
params: params, getIt.get<AppStore>(), fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
getIt.registerFactoryParam<NewBuySellAmountPage, NewBuySellParams, void>((params, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: params)));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
final confirmOption = args[2] as void Function(BuildContext contex)?;
return BuyOptionsPage(items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
});
getIt
.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableOption>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
return PaymentMethodOptionsPage(items: items, pickAnOption: pickAnOption);
});
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
return BuyViewModel(_ordersSource, getIt.get<OrdersStore>(), getIt.get<SettingsStore>(),
getIt.get<BuyAmountViewModel>(),
wallet: wallet!);
});
getIt.registerFactoryParam<BuyWebViewPage, List<dynamic>, void>((List<dynamic> args, _) {
final url = args.first as String;
final buyViewModel = args[1] as BuyViewModel;
return BuyWebViewPage(
buyViewModel: buyViewModel, ordersStore: getIt.get<OrdersStore>(), url: url);
});
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
final wallet = getIt.get<AppStore>().wallet;
@@ -1522,15 +1426,12 @@ void _registerBuyAndSupport() {
);
});
getIt.registerFactoryParam<UnspentCoinsListPage, UnspentCoinType?, void>(
(coinTypeToSpendFrom, _) => UnspentCoinsListPage(
unspentCoinsListViewModel:
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom)));
getIt.registerFactoryParam<NewCoinControlPage, UnspentCoinType?, void>((coinTypeToSpendFrom, _) =>
NewCoinControlPage(
unspentCoinsListViewModel:
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom)));
getIt.registerFactoryParam<NewCoinControlPage, UnspentCoinType?, bool?>(
(coinTypeToSpendFrom, canEdit) => NewCoinControlPage(
unspentCoinsListViewModel:
getIt.get<UnspentCoinsListViewModel>(param1: coinTypeToSpendFrom),
canEdit: canEdit ?? true,
));
getIt.registerFactoryParam<UnspentCoinsDetailsViewModel, UnspentCoinsItem,
UnspentCoinsListViewModel>(
@@ -1548,8 +1449,9 @@ void _registerBuyAndSupport() {
}
void _registerDevToolsAndBridge(AppStore appStore) {
getIt.registerFactory(() => AddressResolver(
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));
@@ -1560,11 +1462,6 @@ void _registerDevToolsAndBridge(AppStore appStore) {
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get<AppStore>().wallet!));
getIt.registerFactory(() =>
DesktopWalletSelectionDropDown(getIt.get<WalletListViewModel>(), getIt.get<AuthService>()));
getIt.registerFactory(() => DesktopSidebarViewModel());
getIt.registerFactoryParam<AnonpayDetailsViewModel, AnonpayInvoiceInfo, void>(
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsViewModel(
anonPayApi: getIt.get<AnonPayApi>(),
@@ -1580,10 +1477,6 @@ void _registerDevToolsAndBridge(AppStore appStore) {
themeStore: getIt.get<ThemeStore>(),
));
getIt.registerFactoryParam<AnonPayReceivePage, AnonPayReceivePageArgs, void>(
(AnonPayReceivePageArgs anonpayReceivePageArgs, _) =>
AnonPayReceivePage(args: anonpayReceivePageArgs));
getIt.registerFactoryParam<AnonpayDetailsPage, AnonpayInvoiceInfo, void>(
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
@@ -1608,13 +1501,8 @@ void _registerDevToolsAndBridge(AppStore appStore) {
);
getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
if (isPow) {
return ManageNodesPage(isPow,
powNodeListViewModel: getIt.get<PowNodeListViewModel>(),
dashboardViewModel: getIt.get<DashboardViewModel>());
}
return ManageNodesPage(isPow,
nodeListViewModel: getIt.get<NodeListViewModel>(),
nodeListViewModel: getIt.get<NodeListViewModel>(param1: isPow),
dashboardViewModel: getIt.get<DashboardViewModel>());
});
@@ -1624,9 +1512,14 @@ void _registerDevToolsAndBridge(AppStore appStore) {
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
getIt.registerFactory(() =>
NFTSendViewModel(appStore, getIt.get<ContactListViewModel>(param1: CryptoCurrency.sol)));
getIt.registerFactory(() => SignViewModel(getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
getIt.registerFactoryParam<SeedVerificationPage, bool, void>((bool isNewWalletCreated, _) =>
SeedVerificationPage(getIt.get<WalletSeedViewModel>(),
isNewWalletCreated: isNewWalletCreated));
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
@@ -1700,7 +1593,6 @@ void _registerDevToolsAndBridge(AppStore appStore) {
getIt.registerLazySingleton(() => NodeSwitchingService(
appStore: getIt.get<AppStore>(),
settingsStore: getIt.get<SettingsStore>(),
nodeSource: _nodeSource,
));
getIt.registerFactoryParam<BridgeAmountPage, CryptoCurrency, void>(
+5 -2
View File
@@ -1,13 +1,16 @@
import 'package:cw_core/amount/amount_sanitizer.dart';
String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = false}) {
if (price == null || cryptoAmount == null) {
return '0.00';
}
cryptoAmount = cryptoAmount.replaceAll(',', '.');
cryptoAmount = cryptoAmount.sanitized();
final _amount = double.tryParse(cryptoAmount);
if (_amount == null || _amount.isNaN) return '0.00';
if (_amount == null || !_amount.isFinite) return '0.00';
final _result = price * _amount;
if (!_result.isFinite) return '0.00';
final result = _result < 0 ? _result * -1 : _result;
if (result == 0.0) {
@@ -7,6 +7,10 @@ String calculateFiatAmountRaw({required double cryptoAmount, double? price}) {
final result = price * cryptoAmount;
if (!result.isFinite) {
return '0.00';
}
if (result == 0.0) {
return '0.00';
}
+7 -7
View File
@@ -3,9 +3,9 @@ import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/keyable.dart';
import 'package:hive/hive.dart';
part 'contact.g.dart';
part 'contact.part.dart';
@HiveType(typeId: Contact.typeId)
// @HiveType(typeId: Contact.typeId)
class Contact extends HiveObject with Keyable {
Contact(
{required this.name,
@@ -22,19 +22,19 @@ class Contact extends HiveObject with Keyable {
static const typeId = CONTACT_TYPE_ID;
static const boxName = 'Contacts';
@HiveField(0, defaultValue: '')
// @HiveField(0, defaultValue: '')
String name;
@HiveField(1, defaultValue: '')
// @HiveField(1, defaultValue: '')
String address;
@HiveField(2, defaultValue: 0)
// @HiveField(2, defaultValue: 0)
late int raw;
@HiveField(3)
// @HiveField(3)
DateTime lastChange;
@HiveField(4, defaultValue: "")
// @HiveField(4, defaultValue: "")
String displayName;
CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
+50
View File
@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'contact.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ContactAdapter extends TypeAdapter<Contact> {
@override
final int typeId = 0;
@override
Contact read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Contact(
name: fields[0] == null ? '' : fields[0] as String,
address: fields[1] == null ? '' : fields[1] as String,
lastChange: fields[3] as DateTime?,
displayName: fields[4] == null ? '' : fields[4] as String,
)..raw = fields[2] == null ? 0 : fields[2] as int;
}
@override
void write(BinaryWriter writer, Contact obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.address)
..writeByte(2)
..write(obj.raw)
..writeByte(3)
..write(obj.lastChange)
..writeByte(4)
..write(obj.displayName);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ContactAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
}
+189 -314
View File
@@ -9,14 +9,16 @@ import 'package:hash_wallet/entities/exchange_api_mode.dart';
import 'package:hash_wallet/entities/fiat_api_mode.dart';
import 'package:hash_wallet/entities/fiat_currency.dart';
import 'package:hash_wallet/entities/fs_migration.dart';
import 'package:hash_wallet/entities/node_list.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/entities/secret_store_key.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/new-ui/model/charts/charts_asset.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:hash_wallet/entities/sync_status_display_mode.dart';
import 'package:cw_core/node_list.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -25,7 +27,6 @@ import 'package:cw_core/wallet_type.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/erc20_token.dart';
const newCakeWalletMoneroUri =
@@ -46,11 +47,10 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com';
const tronDefaultNodeUri = 'api.trongrid.io';
const tronDefaultNodeUri = 'trx.nownodes.io';
const newCakeWalletBitcoinUri =
'electrum.blockstream.info:50002'; // Hash Bags: default flipped to Blockstream
const wowneroDefaultNodeUri = 'node.wownero.org:443';
const zanoDefaultNodeUri = '37.27.100.59:10500';
const moneroWorldNodeUri = '.moneroworld.com';
const decredDefaultUri = "default-spv-nodes";
const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
@@ -62,15 +62,13 @@ Future<void> defaultSettingsMigration(
{required int version,
required SharedPreferences sharedPreferences,
required SecureStorage secureStorage,
required Box<Node> nodes,
required Box<Node> powNodes,
required Box<Contact> contactSource}) async {
if (Platform.isIOS) {
await ios_migrate_v1(contactSource);
}
// check current nodes for nullability regardless of the version
await checkCurrentNodes(nodes, powNodes, sharedPreferences);
// await checkCurrentNodes(sharedPreferences);
final isNewInstall =
sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion) == null;
@@ -98,15 +96,18 @@ Future<void> defaultSettingsMigration(
case 1:
await sharedPreferences.setString(
PreferencesKey.currentFiatCurrencyKey, FiatCurrency.usd.toString());
await sharedPreferences.setInt(PreferencesKey.currentTransactionPriorityKeyLegacy,
monero!.getDefaultTransactionPriority().raw);
if (monero != null) {
await sharedPreferences.setInt(
PreferencesKey.currentTransactionPriorityKeyLegacy,
monero!.getDefaultTransactionPriority().raw);
}
await sharedPreferences.setInt(
PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw);
await sharedPreferences.setBool('save_recipient_address', true);
await resetToDefault(nodes);
await resetToDefault();
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.monero,
currentNodePreferenceKey: PreferencesKey.currentNodeIdKey,
@@ -114,14 +115,12 @@ Future<void> defaultSettingsMigration(
trusted: true,
);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoin,
currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey,
useSSL: true,
);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.litecoin,
currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey,
@@ -129,9 +128,8 @@ Future<void> defaultSettingsMigration(
);
break;
case 2:
await replaceNodesMigration(nodes: nodes);
await replaceNodesMigration();
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
@@ -146,13 +144,12 @@ Future<void> defaultSettingsMigration(
);
break;
case 3:
await updateNodeTypes(nodes: nodes);
await addWalletNodeList(nodes: nodes, type: WalletType.bitcoin);
await updateNodeTypes();
await addWalletNodeList(type: WalletType.bitcoin);
break;
case 4:
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoin,
newDefaultUri: newCakeWalletBitcoinUri,
@@ -179,7 +176,6 @@ Future<void> defaultSettingsMigration(
case 11:
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
@@ -190,33 +186,32 @@ Future<void> defaultSettingsMigration(
break;
case 12:
await checkCurrentNodes(nodes, powNodes, sharedPreferences);
// await checkCurrentNodes(sharedPreferences);
break;
case 13:
await resetBitcoinElectrumServer(nodes, sharedPreferences);
await resetBitcoinElectrumServer(sharedPreferences);
break;
case 15:
await addWalletNodeList(nodes: nodes, type: WalletType.litecoin);
await addWalletNodeList(type: WalletType.litecoin);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.litecoin,
currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey,
);
await checkCurrentNodes(nodes, powNodes, sharedPreferences);
// await checkCurrentNodes(sharedPreferences);
break;
case 16:
await checkCurrentNodes(nodes, powNodes, sharedPreferences);
// await checkCurrentNodes(sharedPreferences);
break;
case 17:
break;
case 18:
addWalletNodeList(nodes: nodes, type: WalletType.monero);
addWalletNodeList(type: WalletType.monero);
break;
case 19:
@@ -226,25 +221,22 @@ Future<void> defaultSettingsMigration(
await migrateExchangeStatus(sharedPreferences);
break;
case 21:
await addWalletNodeList(nodes: nodes, type: WalletType.ethereum);
await addWalletNodeList(type: WalletType.ethereum);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.ethereum,
currentNodePreferenceKey: PreferencesKey.currentEthereumNodeIdKey,
);
break;
case 22:
await addWalletNodeList(nodes: nodes, type: WalletType.nano);
await addNanoPowNodeList(nodes: powNodes);
await addWalletNodeList(type: WalletType.nano);
await addNanoPowNodeList();
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.nano,
currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey,
);
await _changeDefaultNode(
nodes: powNodes,
sharedPreferences: sharedPreferences,
type: WalletType.nano,
currentNodePreferenceKey: PreferencesKey.currentNanoPowNodeIdKey,
@@ -252,18 +244,16 @@ Future<void> defaultSettingsMigration(
);
break;
case 23:
await addWalletNodeList(nodes: nodes, type: WalletType.bitcoinCash);
await addWalletNodeList(type: WalletType.bitcoinCash);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoinCash,
currentNodePreferenceKey: PreferencesKey.currentBitcoinCashNodeIdKey,
);
break;
case 24:
await addWalletNodeList(nodes: nodes, type: WalletType.polygon);
await addWalletNodeList(type: WalletType.polygon);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.polygon,
currentNodePreferenceKey: PreferencesKey.currentPolygonNodeIdKey,
@@ -287,7 +277,6 @@ Future<void> defaultSettingsMigration(
break;
case 29:
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoin,
newDefaultUri: newCakeWalletBitcoinUri,
@@ -300,7 +289,7 @@ Future<void> defaultSettingsMigration(
await disableServiceStatusFiatDisabled(sharedPreferences);
break;
case 31:
await updateNanoNodeList(nodes: nodes);
await updateNanoNodeList();
break;
case 32:
await updateBtcNanoWalletInfos();
@@ -308,10 +297,9 @@ Future<void> defaultSettingsMigration(
case 33:
break;
case 34:
addWalletNodeList(nodes: nodes, type: WalletType.bitcoin);
addWalletNodeList(type: WalletType.bitcoin);
case 35:
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoin,
newDefaultUri: newCakeWalletBitcoinUri,
@@ -321,9 +309,8 @@ Future<void> defaultSettingsMigration(
);
break;
case 36:
await addWalletNodeList(nodes: nodes, type: WalletType.wownero);
await addWalletNodeList(type: WalletType.wownero);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.wownero,
currentNodePreferenceKey: PreferencesKey.currentWowneroNodeIdKey,
@@ -337,9 +324,8 @@ Future<void> defaultSettingsMigration(
await fixBtcDerivationPaths();
break;
case 39:
_fixNodesUseSSLFlag(nodes);
_fixNodesUseSSLFlag();
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.nano,
newDefaultUri: nanoDefaultNodeUri,
@@ -349,7 +335,7 @@ Future<void> defaultSettingsMigration(
);
break;
case 40:
await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes);
await removeMoneroWorld(sharedPreferences: sharedPreferences);
break;
case 41:
_changeExchangeProviderAvailability(
@@ -357,13 +343,13 @@ Future<void> defaultSettingsMigration(
providerName: "SwapTrade",
enabled: false,
);
addWalletNodeList(nodes: nodes, type: WalletType.bitcoin);
addWalletNodeList(type: WalletType.bitcoin);
break;
case 42:
_fixNodesUseSSLFlag(nodes);
_fixNodesUseSSLFlag();
break;
case 43:
_fixNodesUseSSLFlag(nodes);
_fixNodesUseSSLFlag();
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "THORChain",
@@ -376,9 +362,8 @@ Future<void> defaultSettingsMigration(
);
break;
case 44:
_fixNodesUseSSLFlag(nodes);
_fixNodesUseSSLFlag();
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bitcoin,
newDefaultUri: newCakeWalletBitcoinUri,
@@ -388,20 +373,18 @@ Future<void> defaultSettingsMigration(
);
break;
case 45:
addWalletNodeList(nodes: nodes, type: WalletType.polygon);
addWalletNodeList(nodes: nodes, type: WalletType.ethereum);
addWalletNodeList(type: WalletType.polygon);
addWalletNodeList(type: WalletType.ethereum);
break;
case 46:
await _fixNodesUseSSLFlag(nodes);
await addWalletNodeList(nodes: nodes, type: WalletType.litecoin);
await _fixNodesUseSSLFlag();
await addWalletNodeList(type: WalletType.litecoin);
await _updateNode(
nodes: nodes,
currentUri: "ethereum.publicnode.com",
newUri: "ethereum-rpc.publicnode.com",
useSSL: true,
);
await _updateNode(
nodes: nodes,
currentUri: "polygon-bor.publicnode.com",
newUri: "polygon-bor-rpc.publicnode.com",
useSSL: true,
@@ -428,42 +411,36 @@ Future<void> defaultSettingsMigration(
);
break;
case 50:
migrateExistingNodesToUseAutoSwitching(nodes: nodes, powNodes: powNodes);
migrateExistingNodesToUseAutoSwitching();
break;
case 51:
await addWalletNodeList(nodes: nodes, type: WalletType.dogecoin);
await addWalletNodeList(type: WalletType.dogecoin);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.dogecoin,
currentNodePreferenceKey: PreferencesKey.currentDogecoinNodeIdKey,
);
break;
case 52:
await addWalletNodeList(nodes: nodes, type: WalletType.base);
await addWalletNodeList(type: WalletType.base);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.base,
currentNodePreferenceKey: PreferencesKey.currentBaseNodeIdKey,
);
break;
case 53:
await addWalletNodeList(nodes: nodes, type: WalletType.arbitrum);
await addWalletNodeList(type: WalletType.arbitrum);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.arbitrum,
currentNodePreferenceKey: PreferencesKey.currentArbitrumNodeIdKey,
);
break;
case 54:
// Hash Bags: upstream backed up Wownero seeds here ahead of dropping Wownero.
break;
case 55:
// Hash Bags: zcash removed. The original migration here added
// zcash node list + set default zcash node. We no longer load
// zcash_node_list.yml (it was deleted), so skip the whole step.
break;
case 56:
await sharedPreferences.setString(
PreferencesKey.syncStatusDisplayMode, SyncStatusDisplayMode.blocksRemaining.name);
@@ -471,9 +448,8 @@ Future<void> defaultSettingsMigration(
case 57:
await _addXautTokenToExistingEthereumWallets();
await addWalletNodeList(nodes: nodes, type: WalletType.bsc);
await addWalletNodeList(type: WalletType.bsc);
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.bsc,
currentNodePreferenceKey: PreferencesKey.currentBscNodeIdKey,
@@ -516,13 +492,55 @@ Future<void> defaultSettingsMigration(
break;
case 65:
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.base,
currentNodePreferenceKey: PreferencesKey.currentBaseNodeIdKey,
oldUri: ['base.nownodes.io'],
);
break;
case 66:
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.arbitrum,
currentNodePreferenceKey: PreferencesKey.currentArbitrumNodeIdKey,
oldUri: ["arbitrum.nownodes.io"]);
break;
case 67:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "LetsExchange",
enabled: true,
);
break;
case 68:
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
currentNodePreferenceKey: PreferencesKey.currentNodeIdKey,
useSSL: true,
oldUri: ['nodes.hashvault.pro:18081'],
);
break;
case 69:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "Exolix",
enabled: false,
);
break;
case 70:
break;
case 71:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "Swaps.XYZ",
enabled: false,
);
break;
case 72:
await createDefaultChartsData();
break;
default:
break;
}
@@ -539,12 +557,13 @@ Future<void> defaultSettingsMigration(
}
Future<void> _updateNode({
required Box<Node> nodes,
required String currentUri,
String? newUri,
bool? useSSL,
}) async {
for (Node node in nodes.values) {
final nodes = await Node.getAll();
for (Node node in nodes) {
if (node.uriRaw == currentUri) {
if (newUri != null) {
node.uriRaw = newUri;
@@ -560,7 +579,6 @@ Future<void> _updateNode({
/// generic function for changing any wallet default node
/// instead of making a new function for each change
Future<void> _changeDefaultNode({
required Box<Node> nodes,
required SharedPreferences sharedPreferences,
required WalletType type,
required String currentNodePreferenceKey,
@@ -570,19 +588,21 @@ Future<void> _changeDefaultNode({
List<String>?
oldUri, // ignore, if you want to force replace the node regardless of the user's current node
}) async {
List<Node> nodes = await Node.getAll();
final currentNodeId = sharedPreferences.getInt(currentNodePreferenceKey);
final bool shouldReplace;
if (currentNodeId == null) {
shouldReplace = true;
} else {
final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId);
shouldReplace = oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true;
final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId);
shouldReplace =
currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true);
}
if (shouldReplace) {
newDefaultUri ??= _getDefaultNodeUri(type);
var newNodeId =
nodes.values.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.key;
newDefaultUri ??= (await getDefaultNodeFromFiles(type)).uriRaw;
var newNodeId = nodes.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.id;
// new node doesn't exist, then add it
if (newNodeId == null) {
@@ -593,43 +613,11 @@ Future<void> _changeDefaultNode({
trusted: trusted,
);
await nodes.add(newNode);
newNodeId = newNode.key;
await newNode.save();
newNodeId = newNode.id;
}
await sharedPreferences.setInt(currentNodePreferenceKey, newNodeId as int);
}
}
String _getDefaultNodeUri(WalletType type) {
switch (type) {
case WalletType.monero:
return newCakeWalletMoneroUri;
case WalletType.bitcoin:
return newCakeWalletBitcoinUri;
case WalletType.litecoin:
return cakeWalletLitecoinElectrumUri;
case WalletType.ethereum:
return ethereumDefaultNodeUri;
case WalletType.nano:
return nanoDefaultNodeUri;
case WalletType.bitcoinCash:
return cakeWalletBitcoinCashDefaultNodeUri;
case WalletType.polygon:
return polygonDefaultNodeUri;
case WalletType.wownero:
return wowneroDefaultNodeUri;
case WalletType.dogecoin:
return dogecoinDefaultNodeUri;
case WalletType.base:
return baseDefaultNodeUri;
case WalletType.arbitrum:
return arbitrumDefaultNodeUri;
case WalletType.bsc:
return bscDefaultNodeUri;
case WalletType.banano:
case WalletType.none:
return '';
await sharedPreferences.setInt(currentNodePreferenceKey, newNodeId);
}
}
@@ -647,8 +635,9 @@ Future<void> _changeExchangeProviderAvailability(SharedPreferences sharedPrefere
);
}
Future<void> _fixNodesUseSSLFlag(Box<Node> nodes) async {
for (Node node in nodes.values) {
Future<void> _fixNodesUseSSLFlag() async {
final nodes = await Node.getAll();
for (Node node in nodes) {
switch (node.uriRaw) {
case cakeWalletLitecoinElectrumUri:
case cakeWalletBitcoinElectrumUri:
@@ -661,7 +650,8 @@ Future<void> _fixNodesUseSSLFlag(Box<Node> nodes) async {
}
}
Future<void> updateNanoNodeList({required Box<Node> nodes}) async {
Future<void> updateNanoNodeList() async {
final nodes = await Node.getAll();
final nodeList = await loadDefaultNodes(WalletType.nano);
var listOfNewEndpoints = <String>[
"app.natrium.io",
@@ -672,14 +662,13 @@ Future<void> updateNanoNodeList({required Box<Node> nodes}) async {
];
// add new nodes:
for (final node in nodeList) {
if (listOfNewEndpoints.contains(node.uriRaw) && !nodes.values.contains(node)) {
await nodes.add(node);
if (listOfNewEndpoints.contains(node.uriRaw) && !nodes.contains(node)) {
await node.save();
}
}
// update the nautilus node:
final nautilusNode =
nodes.values.firstWhereOrNull((element) => element.uriRaw == "node.perish.co");
final nautilusNode = nodes.firstWhereOrNull((element) => element.uriRaw == "node.perish.co");
if (nautilusNode != null) {
nautilusNode.uriRaw = "node.nautilus.io";
nautilusNode.path = "/api";
@@ -699,7 +688,12 @@ Future<void> disableServiceStatusFiatDisabled(SharedPreferences sharedPreference
}
}
Future<void> _updateMoneroPriority(SharedPreferences sharedPreferences) async {
if (monero == null) {
return;
}
final currentPriority =
await sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority) ??
monero!.getDefaultTransactionPriority().serialize();
@@ -790,7 +784,7 @@ Future<void> validateBitcoinSavedTransactionPriority(SharedPreferences sharedPre
}
}
Future<void> replaceNodesMigration({required Box<Node> nodes}) async {
Future<void> replaceNodesMigration() async {
final replaceNodes = <String, Node>{
'eu-node.cakewallet.io:18081':
Node(uri: 'xmr-node-eu.cakewallet.com:18081', type: WalletType.monero),
@@ -799,7 +793,8 @@ Future<void> replaceNodesMigration({required Box<Node> nodes}) async {
'node.xmr.ru:13666': Node(uri: 'node.monero.net:18081', type: WalletType.monero)
};
nodes.values.forEach((Node node) async {
List<Node> nodes = await Node.getAll();
nodes.forEach((Node node) async {
final nodeToReplace = replaceNodes[node.uri];
if (nodeToReplace != null) {
@@ -811,16 +806,11 @@ Future<void> replaceNodesMigration({required Box<Node> nodes}) async {
});
}
Node? getBitcoinTestnetDefaultElectrumServer({required Box<Node> nodes}) {
return nodes.values
.firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ??
nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin);
}
Future<Node?> getBitcoinTestnetDefaultElectrumServer() async {
final nodes = await Node.getAll();
Node? getDefaultNode({required Box<Node> nodes, required WalletType type}) {
final defaultUri = _getDefaultNodeUri(type);
return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == defaultUri) ??
nodes.values.firstWhereOrNull((node) => node.type == type);
return nodes.firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ??
nodes.firstWhereOrNull((node) => node.type == WalletType.bitcoin);
}
Future<void> insecureStorageMigration({
@@ -931,8 +921,9 @@ Future<void> rewriteSecureStoragePin({required SecureStorage secureStorage}) asy
);
}
Future<void> updateNodeTypes({required Box<Node> nodes}) async {
nodes.values.forEach((node) async {
Future<void> updateNodeTypes() async {
List<Node> nodes = await Node.getAll();
nodes.forEach((node) async {
if (node.type == null) {
node.type = WalletType.monero;
await node.save();
@@ -983,11 +974,15 @@ Future<void> generateBackupPassword(SecureStorage secureStorage) async {
Future<void> changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPreferences) async {
final legacyTransactionPriority =
sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!;
await sharedPreferences.setInt(
PreferencesKey.moneroTransactionPriority, legacyTransactionPriority);
await sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority,
bitcoin!.getMediumTransactionPriority().serialize());
sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy);
if (legacyTransactionPriority != null) {
await sharedPreferences.setInt(
PreferencesKey.moneroTransactionPriority, legacyTransactionPriority);
}
if (bitcoin != null) {
await sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority,
bitcoin!.getMediumTransactionPriority().serialize());
}
}
Future<void> fixBtcDerivationPaths() async {
@@ -1020,153 +1015,14 @@ Future<void> updateBtcNanoWalletInfos() async {}
// }
// }
Future<void> checkCurrentNodes(
Box<Node> nodeSource, Box<Node> powNodeSource, SharedPreferences sharedPreferences) async {
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentBitcoinElectrumSeverId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final currentLitecoinElectrumSeverId =
sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
final currentBaseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey);
final currentArbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey);
final currentBscNodeId = sharedPreferences.getInt(PreferencesKey.currentBscNodeIdKey);
final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
final currentBitcoinCashNodeId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey);
final currentDogecoinNodeId = sharedPreferences.getInt(PreferencesKey.currentDogecoinNodeIdKey);
final currentWowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey);
final currentMoneroNode =
nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId);
final currentBitcoinElectrumServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinElectrumSeverId);
final currentLitecoinElectrumServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentLitecoinElectrumSeverId);
final currentEthereumNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentEthereumNodeId);
final currentPolygonNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId);
final currentBaseNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentBaseNodeId);
final currentArbitrumNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentArbitrumNodeId);
final currentBscNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentBscNodeId);
final currentNanoNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId);
final currentNanoPowNodeServer =
powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId);
final currentBitcoinCashNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId);
final currentDogecoinNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentDogecoinNodeId);
final currentWowneroNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId);
if (currentMoneroNode == null) {
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
await nodeSource.add(newCakeWalletNode);
await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int);
}
if (currentBitcoinElectrumServer == null) {
final cakeWalletElectrum = Node(
uri: cakeWalletBitcoinElectrumUri,
type: WalletType.bitcoin,
useSSL: false,
isEnabledForAutoSwitching: true);
await nodeSource.add(cakeWalletElectrum);
final cakeWalletElectrumTestnet =
Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
await nodeSource.add(cakeWalletElectrumTestnet);
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int);
}
if (currentLitecoinElectrumServer == null) {
final cakeWalletElectrum =
Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin, useSSL: false);
await nodeSource.add(cakeWalletElectrum);
await sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int);
}
if (currentEthereumNodeServer == null) {
final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, node.key as int);
}
if (currentNanoNodeServer == null) {
final node = Node(uri: nanoDefaultNodeUri, useSSL: true, type: WalletType.nano);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, node.key as int);
}
if (currentNanoPowNodeServer == null) {
Node? node = powNodeSource.values
.firstWhereOrNull((node) => node.uri.toString() == nanoDefaultPowNodeUri);
if (node == null) {
node = Node(uri: nanoDefaultPowNodeUri, useSSL: true, type: WalletType.nano);
await powNodeSource.add(node);
}
await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, node.key as int);
}
if (currentBitcoinCashNodeServer == null) {
final node =
Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash, useSSL: false);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int);
}
if (currentDogecoinNodeServer == null) {
final node = Node(uri: dogecoinDefaultNodeUri, type: WalletType.dogecoin, useSSL: true);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentDogecoinNodeIdKey, node.key as int);
}
if (currentPolygonNodeServer == null) {
final node = Node(uri: polygonDefaultNodeUri, type: WalletType.polygon);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int);
}
if (currentBaseNodeServer == null) {
final node = Node(uri: baseDefaultNodeUri, type: WalletType.base);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentBaseNodeIdKey, node.key as int);
}
if (currentArbitrumNodeServer == null) {
final node = Node(uri: arbitrumDefaultNodeUri, type: WalletType.arbitrum);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentArbitrumNodeIdKey, node.key as int);
}
if (currentBscNodeServer == null) {
final node = Node(uri: bscDefaultNodeUri, type: WalletType.bsc);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentBscNodeIdKey, node.key as int);
}
if (currentWowneroNodeServer == null) {
final node = Node(uri: wowneroDefaultNodeUri, type: WalletType.wownero);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int);
}
}
Future<void> resetBitcoinElectrumServer(
Box<Node> nodeSource, SharedPreferences sharedPreferences) async {
Future<void> resetBitcoinElectrumServer(SharedPreferences sharedPreferences) async {
final nodeSource = await Node.getAll();
final currentElectrumSeverId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final oldElectrumServer = nodeSource.values
final oldElectrumServer = nodeSource
.firstWhereOrNull((node) => node.uri.toString().contains('electrumx.cakewallet.com'));
var cakeWalletNode = nodeSource.values
.firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri);
var cakeWalletNode =
nodeSource.firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri);
if (cakeWalletNode == null) {
cakeWalletNode = Node(
@@ -1177,12 +1033,12 @@ Future<void> resetBitcoinElectrumServer(
// final cakeWalletElectrumTestnet =
// Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
// await nodeSource.add(cakeWalletElectrumTestnet);
await nodeSource.add(cakeWalletNode);
await cakeWalletNode.save();
}
if (currentElectrumSeverId == oldElectrumServer?.key) {
if (currentElectrumSeverId == oldElectrumServer?.id) {
await sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.key as int);
PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.id);
}
await oldElectrumServer?.delete();
@@ -1200,38 +1056,41 @@ Future<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async {
await sharedPreferences.remove(PreferencesKey.disableExchangeKey);
}
Future<void> addNanoPowNodeList({required Box<Node> nodes}) async {
Future<void> addNanoPowNodeList() async {
final nodeList = await loadDefaultNanoPowNodes();
final nodes = await Node.getAllPow();
for (var node in nodeList) {
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await nodes.add(node);
if (nodes.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await node.save();
}
}
}
Node? getNanoDefaultPowNode({required Box<Node> nodes}) {
return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ??
nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano));
Future<Node?> getNanoDefaultPowNode() async {
final nodes = await Node.getAll();
return nodes.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ??
nodes.firstWhereOrNull((node) => (node.type == WalletType.nano));
}
Future<void> addWalletNodeList({required Box<Node> nodes, required WalletType type}) async {
Future<void> addWalletNodeList({required WalletType type}) async {
final nodes = await Node.getAll();
final List<Node> nodeList = await loadDefaultNodes(type);
for (var node in nodeList) {
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await nodes.add(node);
if (nodes.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await node.save();
}
}
}
Future<void> removeMoneroWorld(
{required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
Future<void> removeMoneroWorld({required SharedPreferences sharedPreferences}) async {
final nodes = await Node.getAll();
const cakeWalletMoneroNodeUriPattern = '.moneroworld.com';
final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final currentMoneroNode = nodes.values.firstWhere((node) => node.key == currentMoneroNodeId);
final currentMoneroNode = nodes.firstWhere((node) => node.id == currentMoneroNodeId);
final needToReplaceCurrentMoneroNode =
currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern);
nodes.values.forEach((node) async {
nodes.forEach((node) async {
if (node.type == WalletType.monero &&
node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) {
await node.delete();
@@ -1240,7 +1099,6 @@ Future<void> removeMoneroWorld(
if (needToReplaceCurrentMoneroNode) {
await _changeDefaultNode(
nodes: nodes,
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
@@ -1250,8 +1108,7 @@ Future<void> removeMoneroWorld(
}
}
Future<void> migrateExistingNodesToUseAutoSwitching(
{required Box<Node> nodes, required Box<Node> powNodes}) async {
Future<void> migrateExistingNodesToUseAutoSwitching() async {
final listOfDefaultNodesWithAutoSwitching = [
'bitcoincash.stackwallet.com:50002',
'bch.aftrek.org:50002',
@@ -1285,12 +1142,23 @@ Future<void> migrateExistingNodesToUseAutoSwitching(
'zano.cakewallet.com:11211',
'electrum.cakewallet.com:50002',
];
for (var node in [...nodes.values.toList(), ...powNodes.values.toList()]) {
final nodes = await Node.getAll();
for (var node in nodes) {
if (listOfDefaultNodesWithAutoSwitching.contains(node.uriRaw)) {
node.isEnabledForAutoSwitching = true;
await node.save();
}
}
final powNodes = await Node.getAllPow();
for (var node in powNodes) {
if (listOfDefaultNodesWithAutoSwitching.contains(node.uriRaw)) {
node.isEnabledForAutoSwitching = true;
node.isPow = true;
await node.save();
}
}
}
Future<void> _addXautTokenToExistingEthereumWallets() async {
@@ -1308,24 +1176,31 @@ Future<void> _addXautTokenToExistingEthereumWallets() async {
final ethereumWallets =
allWallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
const ethereumChainId = 1;
for (final walletInfo in ethereumWallets) {
final sanitizedName = walletInfo.name.replaceAll(' ', '_');
final boxName = '${sanitizedName}_${Erc20Token.ethereumBoxName}';
final existingToken = await Erc20Token.getByContract(
walletInfo.name,
ethereumChainId,
xautToken.contractAddress,
);
Box<Erc20Token> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<Erc20Token>(boxName);
} else {
tokenBox = await CakeHive.openBox<Erc20Token>(boxName);
}
if (existingToken != null) continue;
final xautAddress = xautToken.contractAddress;
if (!tokenBox.containsKey(xautAddress)) {
await tokenBox.put(xautAddress, xautToken);
}
await Erc20Token.copyWith(
xautToken,
walletName: walletInfo.name,
chainId: ethereumChainId,
).save();
}
} catch (e) {
printV('Error in XAUT migration: $e');
}
}
Future<void> createDefaultChartsData() async {
await ChartsAsset(asset: CryptoCurrency.btc, isFavorite: true).insert();
await ChartsAsset(asset: CryptoCurrency.xmr, isFavorite: false).insert();
await ChartsAsset(asset: CryptoCurrency.eth, isFavorite: false).insert();
}
-52
View File
@@ -1,52 +0,0 @@
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_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.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 "";
}
}
}
+187 -64
View File
@@ -1,18 +1,36 @@
import 'package:cw_core/currency.dart';
import 'package:cw_core/enumerable_item.dart';
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/enumerable_item.dart";
class FiatCurrency extends EnumerableItem<String> with Serializable<String> implements Currency {
const FiatCurrency({
required String symbol,
required this.symbol,
required this.countryCode,
required this.fullName,
required this.emoji,
this.decimals = 2,
this.isoCountryCode,
}) : super(title: symbol, raw: symbol);
final String countryCode;
final String? isoCountryCode;
@override
final String fullName;
@override
final int decimals;
@override
String get apiString => "fiat.$title";
@override
final String symbol;
String get apiCountryCode => isoCountryCode ?? countryCode.toUpperCase();
final String emoji;
static List<FiatCurrency> get all => _all.values.toList();
static List<FiatCurrency> get currenciesAvailableToBuyWith => [
@@ -60,67 +78,164 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
kes,
];
static const amd = FiatCurrency(symbol: 'AMD', countryCode: "arm", fullName: "Armenian Dram");
static const ars = FiatCurrency(symbol: 'ARS', countryCode: "arg", fullName: "Argentine Peso");
static const aud = FiatCurrency(symbol: 'AUD', countryCode: "aus", fullName: "Australian Dollar");
static const bdt = FiatCurrency(symbol: 'BDT', countryCode: "bgd", fullName: "Bangladeshi Taka");
static const bgn = FiatCurrency(symbol: 'BGN', countryCode: "bgr", fullName: "Bulgarian Lev");
static const brl = FiatCurrency(symbol: 'BRL', countryCode: "bra", fullName: "Brazilian Real");
static const cad = FiatCurrency(symbol: 'CAD', countryCode: "cad", fullName: "Canadian Dollar");
static const chf = FiatCurrency(symbol: 'CHF', countryCode: "che", fullName: "Swiss Franc");
static const clp = FiatCurrency(symbol: 'CLP', countryCode: "chl", fullName: "Chilean Peso");
static const cny = FiatCurrency(symbol: 'CNY', countryCode: "chn", fullName: "Chinese Yuan");
static const cop = FiatCurrency(symbol: 'COP', countryCode: "col", fullName: "Colombian Peso");
static const czk = FiatCurrency(symbol: 'CZK', countryCode: "czk", fullName: "Czech Koruna");
static const dkk = FiatCurrency(symbol: 'DKK', countryCode: "dnk", fullName: "Danish Krone");
static const egp = FiatCurrency(symbol: 'EGP', countryCode: "egy", fullName: "Egyptian Pound");
static const eur = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
static const gbp = FiatCurrency(symbol: 'GBP', countryCode: "gbr", fullName: "Pound Sterling");
static const ghs = FiatCurrency(symbol: 'GHS', countryCode: "gha", fullName: "Ghanaian Cedi");
static const gtq =
FiatCurrency(symbol: 'GTQ', countryCode: "gtm", fullName: "Guatemalan Quetzal");
static const hkd = FiatCurrency(symbol: 'HKD', countryCode: "hkg", fullName: "Hong Kong Dollar");
static const hrk = FiatCurrency(symbol: 'HRK', countryCode: "hrv", fullName: "Croatian Kuna");
static const huf = FiatCurrency(symbol: 'HUF', countryCode: "hun", fullName: "Hungarian Forint");
static const idr = FiatCurrency(symbol: 'IDR', countryCode: "idn", fullName: "Indonesian Rupiah");
static const ils =
FiatCurrency(symbol: 'ILS', countryCode: "isr", fullName: "Israeli New Shekel");
static const inr = FiatCurrency(symbol: 'INR', countryCode: "ind", fullName: "Indian Rupee");
static const irr = FiatCurrency(symbol: 'IRR', countryCode: "irn", fullName: "Iranian Rial");
static const isk =
FiatCurrency(symbol: 'ISK', countryCode: "isl", fullName: "Icelandic Krona Króna");
static const jpy = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen");
static const krw = FiatCurrency(symbol: 'KRW', countryCode: "kor", fullName: "South Korean Won");
static const mad = FiatCurrency(symbol: 'MAD', countryCode: "mar", fullName: "Moroccan Dirham");
static const mxn = FiatCurrency(symbol: 'MXN', countryCode: "mex", fullName: "Mexican Peso");
static const myr = FiatCurrency(symbol: 'MYR', countryCode: "mys", fullName: "Malaysian Ringgit");
static const ngn = FiatCurrency(symbol: 'NGN', countryCode: "nga", fullName: "Nigerian Naira");
static const nok = FiatCurrency(symbol: 'NOK', countryCode: "nor", fullName: "Norwegian Krone");
static const nzd =
FiatCurrency(symbol: 'NZD', countryCode: "nzl", fullName: "New Zealand Dollar");
static const php = FiatCurrency(symbol: 'PHP', countryCode: "phl", fullName: "Philippine Peso");
static const pkr = FiatCurrency(symbol: 'PKR', countryCode: "pak", fullName: "Pakistani Rupee");
static const pln =
FiatCurrency(symbol: 'PLN', countryCode: "pol", fullName: "Poland Zloty złoty");
static const ron = FiatCurrency(symbol: 'RON', countryCode: "rou", fullName: "Romanian Leu");
static const rub = FiatCurrency(symbol: 'RUB', countryCode: "rus", fullName: "Russian Ruble");
static const sar = FiatCurrency(symbol: 'SAR', countryCode: "sau", fullName: "Saudi Riyal");
static const sek = FiatCurrency(symbol: 'SEK', countryCode: "swe", fullName: "Swedish Krona");
static const sgd = FiatCurrency(symbol: 'SGD', countryCode: "sgp", fullName: "Singapore Dollar");
static const thb =
FiatCurrency(symbol: 'THB', countryCode: "tha", fullName: "New Thaiwan Dollar");
static const twd = FiatCurrency(symbol: 'TWD', countryCode: "twn", fullName: "Thai Baht");
static const uah = FiatCurrency(symbol: 'UAH', countryCode: "ukr", fullName: "Ukrainian Hryvnia");
static const usd =
FiatCurrency(symbol: 'USD', countryCode: "usa", fullName: "United States Dollar");
static const vef =
FiatCurrency(symbol: 'VEF', countryCode: "ven", fullName: "Venezuelan Bolivar Bolívar");
static const vnd =
FiatCurrency(symbol: 'VND', countryCode: "vnm", fullName: "Vietnamese Dong đồng");
static const zar =
FiatCurrency(symbol: 'ZAR', countryCode: "saf", fullName: "South African Rand");
static const tur = FiatCurrency(symbol: 'TRY', countryCode: "tur", fullName: "Turkish Lira");
static const kes = FiatCurrency(symbol: 'KES', countryCode: "ken", fullName: "Kenyan Shillings");
static const amd =
FiatCurrency(symbol: "AMD", countryCode: "arm", fullName: "Armenian Dram", emoji: "🇦🇲");
static const ars =
FiatCurrency(symbol: "ARS", countryCode: "arg", fullName: "Argentine Peso", emoji: "🇦🇷");
static const aud =
FiatCurrency(symbol: "AUD", countryCode: "aus", fullName: "Australian Dollar", emoji: "🇦🇺");
static const bdt =
FiatCurrency(symbol: "BDT", countryCode: "bgd", fullName: "Bangladeshi Taka", emoji: "🇧🇩");
static const bgn =
FiatCurrency(symbol: "BGN", countryCode: "bgr", fullName: "Bulgarian Lev", emoji: "🇧🇬");
static const brl =
FiatCurrency(symbol: "BRL", countryCode: "bra", fullName: "Brazilian Real", emoji: "🇧🇷");
static const cad = FiatCurrency(
symbol: "CAD",
countryCode: "cad",
fullName: "Canadian Dollar",
emoji: "🇨🇦",
isoCountryCode: "CAN",
);
static const chf =
FiatCurrency(symbol: "CHF", countryCode: "che", fullName: "Swiss Franc", emoji: "🇨🇭");
static const clp =
FiatCurrency(symbol: "CLP", countryCode: "chl", fullName: "Chilean Peso", emoji: "🇨🇱");
static const cny =
FiatCurrency(symbol: "CNY", countryCode: "chn", fullName: "Chinese Yuan", emoji: "🇨🇳");
static const cop =
FiatCurrency(symbol: "COP", countryCode: "col", fullName: "Colombian Peso", emoji: "🇨🇴");
static const czk = FiatCurrency(
symbol: "CZK",
countryCode: "czk",
fullName: "Czech Koruna",
emoji: "🇨🇿",
isoCountryCode: "CZE",
);
static const dkk =
FiatCurrency(symbol: "DKK", countryCode: "dnk", fullName: "Danish Krone", emoji: "🇩🇰");
static const egp =
FiatCurrency(symbol: "EGP", countryCode: "egy", fullName: "Egyptian Pound", emoji: "🇪🇬");
static const eur = FiatCurrency(
symbol: "EUR",
countryCode: "eur",
fullName: "Euro",
emoji: "🇪🇺",
isoCountryCode: "DEU",
);
static const gbp =
FiatCurrency(symbol: "GBP", countryCode: "gbr", fullName: "Pound Sterling", emoji: "🇬🇧");
static const ghs =
FiatCurrency(symbol: "GHS", countryCode: "gha", fullName: "Ghanaian Cedi", emoji: "🇬🇭");
static const gtq = FiatCurrency(
symbol: "GTQ",
countryCode: "gtm",
fullName: "Guatemalan Quetzal",
emoji: "🇬🇹",
);
static const hkd =
FiatCurrency(symbol: "HKD", countryCode: "hkg", fullName: "Hong Kong Dollar", emoji: "🇭🇰");
static const hrk =
FiatCurrency(symbol: "HRK", countryCode: "hrv", fullName: "Croatian Kuna", emoji: "🇭🇷");
static const huf =
FiatCurrency(symbol: "HUF", countryCode: "hun", fullName: "Hungarian Forint", emoji: "🇭🇺");
static const idr =
FiatCurrency(symbol: "IDR", countryCode: "idn", fullName: "Indonesian Rupiah", emoji: "🇮🇩");
static const ils = FiatCurrency(
symbol: "ILS",
countryCode: "isr",
fullName: "Israeli New Shekel",
emoji: "🇮🇱",
);
static const inr =
FiatCurrency(symbol: "INR", countryCode: "ind", fullName: "Indian Rupee", emoji: "🇮🇳");
static const irr =
FiatCurrency(symbol: "IRR", countryCode: "irn", fullName: "Iranian Rial", emoji: "🇮🇷");
static const isk = FiatCurrency(
symbol: "ISK",
countryCode: "isl",
fullName: "Icelandic Krona Króna",
emoji: "🇮🇸",
);
static const jpy =
FiatCurrency(symbol: "JPY", countryCode: "jpn", fullName: "Japanese Yen", emoji: "🇯🇵");
static const krw =
FiatCurrency(symbol: "KRW", countryCode: "kor", fullName: "South Korean Won", emoji: "🇰🇷");
static const mad =
FiatCurrency(symbol: "MAD", countryCode: "mar", fullName: "Moroccan Dirham", emoji: "🇲🇦");
static const mxn =
FiatCurrency(symbol: "MXN", countryCode: "mex", fullName: "Mexican Peso", emoji: "🇲🇽");
static const myr =
FiatCurrency(symbol: "MYR", countryCode: "mys", fullName: "Malaysian Ringgit", emoji: "🇲🇾");
static const ngn =
FiatCurrency(symbol: "NGN", countryCode: "nga", fullName: "Nigerian Naira", emoji: "🇳🇬");
static const nok =
FiatCurrency(symbol: "NOK", countryCode: "nor", fullName: "Norwegian Krone", emoji: "🇳🇴");
static const nzd = FiatCurrency(
symbol: "NZD",
countryCode: "nzl",
fullName: "New Zealand Dollar",
emoji: "🇳🇿",
);
static const php =
FiatCurrency(symbol: "PHP", countryCode: "phl", fullName: "Philippine Peso", emoji: "🇵🇭");
static const pkr =
FiatCurrency(symbol: "PKR", countryCode: "pak", fullName: "Pakistani Rupee", emoji: "🇵🇰");
static const pln = FiatCurrency(
symbol: "PLN",
countryCode: "pol",
fullName: "Poland Zloty złoty",
emoji: "🇵🇱",
);
static const ron =
FiatCurrency(symbol: "RON", countryCode: "rou", fullName: "Romanian Leu", emoji: "🇷🇴");
static const rub =
FiatCurrency(symbol: "RUB", countryCode: "rus", fullName: "Russian Ruble", emoji: "🇷🇺");
static const sar =
FiatCurrency(symbol: "SAR", countryCode: "sau", fullName: "Saudi Riyal", emoji: "🇸🇦");
static const sek =
FiatCurrency(symbol: "SEK", countryCode: "swe", fullName: "Swedish Krona", emoji: "🇸🇪");
static const sgd =
FiatCurrency(symbol: "SGD", countryCode: "sgp", fullName: "Singapore Dollar", emoji: "🇸🇬");
static const thb = FiatCurrency(
symbol: "THB",
countryCode: "tha",
fullName: "Thai Baht",
emoji: "🇹🇭",
);
static const twd = FiatCurrency(
symbol: "TWD", countryCode: "twn", fullName: "New Thaiwan Dollar", emoji: "🇹🇼");
static const uah =
FiatCurrency(symbol: "UAH", countryCode: "ukr", fullName: "Ukrainian Hryvnia", emoji: "🇺🇦");
static const usd = FiatCurrency(
symbol: "USD",
countryCode: "usa",
fullName: "United States Dollar",
emoji: "🇺🇸",
);
static const vef = FiatCurrency(
symbol: "VEF",
countryCode: "ven",
fullName: "Venezuelan Bolivar Bolívar",
emoji: "🇻🇪",
);
static const vnd = FiatCurrency(
symbol: "VND",
countryCode: "vnm",
fullName: "Vietnamese Dong đồng",
emoji: "🇻🇳",
);
static const zar = FiatCurrency(
symbol: "ZAR",
countryCode: "saf",
fullName: "South African Rand",
emoji: "🇿🇦",
isoCountryCode: "ZAF",
);
static const tur =
FiatCurrency(symbol: "TRY", countryCode: "tur", fullName: "Turkish Lira", emoji: "🇹🇷");
static const kes =
FiatCurrency(symbol: "KES", countryCode: "ken", fullName: "Kenyan Shillings", emoji: "🇰🇪");
static final _all = {
FiatCurrency.amd.raw: FiatCurrency.amd,
@@ -178,6 +293,8 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
static FiatCurrency deserialize({required String raw}) => _all[raw] ?? FiatCurrency.usd;
static FiatCurrency? tryDeserialize({required String raw}) => _all[raw];
@override
bool operator ==(Object other) => other is FiatCurrency && other.raw == raw;
@@ -192,4 +309,10 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
@override
String get iconPath => "assets/images/flags/$countryCode.png";
@override
Money parseAmount(String value) => Money.parse(value, this);
@override
Money? tryParseAmount(String value) => Money.tryParse(value, this);
}
@@ -1,5 +1,6 @@
import 'package:bitbox_flutter/usb/bitbox_device.dart' as bitbox;
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:trezor_flutter/trezor_flutter.dart' as trezor;
abstract class HardwareWalletDevice {
String get name;
@@ -41,6 +42,21 @@ class BitboxHardwareWalletDevice extends HardwareWalletDevice {
HardwareWalletConnectionType get connectionType => HardwareWalletConnectionType.usb;
}
class TrezorHardwareWalletDevice extends HardwareWalletDevice {
final trezor.TrezorDevice device;
TrezorHardwareWalletDevice(this.device);
@override
String get name => device.name;
@override
HardwareWalletDeviceType get type => device.deviceInfo.toGeneric();
@override
HardwareWalletConnectionType get connectionType => device.connectionType.toGeneric();
}
enum HardwareWalletDeviceType {
ledgerBlue,
ledgerNanoS,
@@ -50,7 +66,12 @@ enum HardwareWalletDeviceType {
ledgerStax,
ledgerFlex,
BitBox02,
BitBox02Nova;
BitBox02Nova,
trezorModelOne,
trezorModelT,
trezorSafe3,
trezorSafe5,
trezorSafe7;
}
enum HardwareWalletConnectionType {
@@ -60,7 +81,7 @@ enum HardwareWalletConnectionType {
qr;
}
extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
extension LedgerToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
HardwareWalletDeviceType toGeneric() {
switch (this) {
case ledger.LedgerDeviceType.blue:
@@ -81,7 +102,24 @@ extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
}
}
extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
extension TrezorToGenericHardwareWalletDeviceType on trezor.TrezorDeviceType {
HardwareWalletDeviceType toGeneric() {
switch (this) {
case trezor.TrezorDeviceType.modelOne:
return HardwareWalletDeviceType.trezorModelOne;
case trezor.TrezorDeviceType.modelT:
return HardwareWalletDeviceType.trezorModelT;
case trezor.TrezorDeviceType.safe3:
return HardwareWalletDeviceType.trezorSafe3;
case trezor.TrezorDeviceType.safe5:
return HardwareWalletDeviceType.trezorSafe5;
case trezor.TrezorDeviceType.safe7:
return HardwareWalletDeviceType.trezorSafe7;
}
}
}
extension LedgerToGenericHardwareWalletConnectionType on ledger.ConnectionType {
HardwareWalletConnectionType toGeneric() {
switch (this) {
case ledger.ConnectionType.usb:
@@ -91,3 +129,14 @@ extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
}
}
}
extension TrezorToGenericHardwareWalletConnectionType on trezor.ConnectionType {
HardwareWalletConnectionType toGeneric() {
switch (this) {
case trezor.ConnectionType.usb:
return HardwareWalletConnectionType.usb;
case trezor.ConnectionType.ble:
return HardwareWalletConnectionType.ble;
}
}
}
+15 -5
View File
@@ -1,7 +1,15 @@
import 'package:hash_wallet/generated/locales.dart';
import 'package:devicelocale/devicelocale.dart';
import "package:flutter/widgets.dart";
import 'package:intl/intl.dart';
// Flutter's Material delegates match on the language subtag, so "pt_BR" must be split into two
Locale localeFromLanguageCode(String code) {
final parts = code.split("_");
return parts.length == 2 ? Locale(parts[0], parts[1]) : Locale(code);
}
class LanguageService {
static const Map<String, String> supportedLocales = {
'en': 'English',
@@ -20,7 +28,6 @@ class LanguageService {
'ru': 'Русский (Russian)',
'uk': 'Українська (Ukrainian)',
'zh': '简体中文 (Chinese Simplified)',
'zh_TW': '繁體中文 (Chinese Traditional)',
'hr': 'Hrvatski (Croatian)',
'it': 'Italiano (Italian)',
'th': 'ภาษาไทย (Thai)',
@@ -55,7 +62,6 @@ class LanguageService {
'ru': 'rus',
'uk': 'ukr',
'zh': 'chn',
'zh_TW': 'twn',
'hr': 'hrv',
'it': 'ita',
'th': 'tha',
@@ -87,12 +93,16 @@ class LanguageService {
static Future<String> localeDetection() async {
try {
var locale = await Devicelocale.currentLocale ?? '';
locale = Intl.shortLocale(locale);
final locale = Intl.canonicalizedLocale(await Devicelocale.currentLocale ?? "");
if (list.keys.contains(locale)) {
return locale;
}
final language = Intl.shortLocale(locale);
if (list.keys.contains(language)) {
return language;
}
return LanguageService.defaultLocale;
} catch (_) {
return LanguageService.defaultLocale;
+1 -1
View File
@@ -82,7 +82,7 @@ class MainActions {
canShow: (viewModel) => viewModel.hasTradeAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledTradeAction) return;
await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
await Navigator.of(context).pushNamed(Routes.buySellPage);
},
);
}
+25 -24
View File
@@ -1,14 +1,9 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:hash_wallet/utils/feature_flag.dart';
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:flutter/material.dart";
class NewMainActions {
final Key key;
final String Function(BuildContext context) name;
final String image;
final VoidCallback onTap;
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
NewMainActions._({
required this.key,
@@ -18,48 +13,54 @@ class NewMainActions {
this.isEnabled,
this.canShow,
});
final Key key;
final String Function(BuildContext context) name;
final String image;
final VoidCallback onTap;
final bool Function(DashboardViewModel viewModel)? isEnabled;
final bool Function(DashboardViewModel viewModel)? canShow;
static List<NewMainActions> all = [
homeAction,
walletsAction,
contactsAction,
appsAction,
//chartsAction,
if (FeatureFlag.isChartsEnabled) chartsAction,
];
static NewMainActions homeAction = NewMainActions._(
name: (context) => S.of(context).home,
image: 'assets/new-ui/navbar/home.svg',
key: ValueKey('dashboard_page_home_action_button_key'),
image: "assets/new-ui/navbar/home.svg",
key: const ValueKey("dashboard_page_home_action_button_key"),
onTap: () {},
);
static NewMainActions walletsAction = NewMainActions._(
name: (context) => S.of(context).wallets,
image: 'assets/new-ui/navbar/wallets.svg',
key: ValueKey('dashboard_page_wallets_action_button_key'),
image: "assets/new-ui/navbar/wallets.svg",
key: const ValueKey("dashboard_page_wallets_action_button_key"),
onTap: () {},
);
static NewMainActions contactsAction = NewMainActions._(
name: (context) => S.of(context).contacts,
image: 'assets/new-ui/navbar/contacts.svg',
key: ValueKey('dashboard_page_contacts_action_button_key'),
image: "assets/new-ui/navbar/contacts.svg",
key: const ValueKey("dashboard_page_contacts_action_button_key"),
onTap: () {},
);
static NewMainActions appsAction = NewMainActions._(
name: (context) => S.of(context).apps,
image: 'assets/new-ui/navbar/apps.svg',
key: ValueKey('dashboard_page_apps_action_button_key'),
image: "assets/new-ui/navbar/apps.svg",
key: const ValueKey("dashboard_page_apps_action_button_key"),
canShow: (dashboardVM) => dashboardVM.showApps,
onTap: () {},
);
// static NewMainActions chartsAction = NewMainActions._(
// name: (context) => 'Charts', //TODO S.of(context).charts,
// image: 'assets/new-ui/navbar/charts.svg',
// key: ValueKey('dashboard_page_charts_action_button_key'),
// onTap: () {},
// );
static NewMainActions chartsAction = NewMainActions._(
name: (context) => S.of(context).charts,
image: "assets/new-ui/navbar/charts.svg",
key: const ValueKey("dashboard_page_charts_action_button_key"),
onTap: () {},
);
}
@@ -6,6 +6,7 @@ class ListItemCheckbox extends ListItem {
required super.keyValue,
required super.label,
this.subtitle,
this.subtitleColor,
this.iconPath,
this.onTap,
this.showArrow = false,
@@ -18,5 +19,6 @@ class ListItemCheckbox extends ListItem {
final bool value;
final String? subtitle;
final String? iconPath;
final Color? subtitleColor;
final bool showArrow;
}
@@ -2,32 +2,47 @@ import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:flutter/material.dart';
class ListItemRegularRow extends ListItem {
const ListItemRegularRow(
{required super.keyValue,
required super.label,
this.subtitle,
this.trailingText,
this.iconPath,
this.onTap,
this.trailingIconPath,
this.showArrow = true,
this.bottomWidget,
this.trailingWidget,
this.truncateTrailingText = false,
this.foregroundColor,
this.trailingIconSize,
this.copyableText});
const ListItemRegularRow({
required super.keyValue,
required super.label,
this.subtitle,
this.trailingText,
this.iconPath,
this.badgeIconPath,
this.onTap,
this.trailingIconPath,
this.showArrow = true,
this.bottomWidget,
this.trailingWidget,
this.truncateTrailingText = false,
this.foregroundColor,
this.trailingIconSize,
this.copyableText,
this.leadingIconErrorWidget,
this.leadingIconSize,
this.badgeIconSize,
this.iconColor,
this.secondaryLabel,
this.subtitleColor,
});
final String? subtitle;
final String? trailingText;
final String? iconPath;
final String? trailingIconPath;
final String? badgeIconPath;
final String? copyableText;
final VoidCallback? onTap;
final String? secondaryLabel;
final bool showArrow;
final Widget? bottomWidget;
final Widget? trailingWidget;
final bool truncateTrailingText;
final Color? subtitleColor;
final Color? foregroundColor;
final double? trailingIconSize;
final Widget? leadingIconErrorWidget;
final double? leadingIconSize;
final double? badgeIconSize;
final Color? iconColor;
}
@@ -5,12 +5,16 @@ class ListItemToggle extends ListItem {
const ListItemToggle({
required super.keyValue,
required super.label,
this.subtitle,
this.iconPath,
required this.value,
required this.onChanged,
this.leadingEndWidget,
});
final String? subtitle;
final String? iconPath;
final ValueChanged<bool> onChanged;
final Widget? leadingEndWidget;
final bool value;
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:hash_wallet/core/utilities.dart';
import 'package:hash_wallet/entities/preferences_key.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/node_list.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:shared_preferences/shared_preferences.dart';
const Map<WalletType, String> nodePreferenceKeys = {
WalletType.monero: PreferencesKey.currentNodeIdKey,
WalletType.bitcoin: PreferencesKey.currentBitcoinElectrumSererIdKey,
WalletType.litecoin: PreferencesKey.currentLitecoinElectrumSererIdKey,
WalletType.ethereum: PreferencesKey.currentEthereumNodeIdKey,
WalletType.polygon: PreferencesKey.currentPolygonNodeIdKey,
WalletType.base: PreferencesKey.currentBaseNodeIdKey,
WalletType.arbitrum: PreferencesKey.currentArbitrumNodeIdKey,
WalletType.bsc: PreferencesKey.currentBscNodeIdKey,
WalletType.nano: PreferencesKey.currentNanoNodeIdKey,
WalletType.bitcoinCash: PreferencesKey.currentBitcoinCashNodeIdKey,
WalletType.dogecoin: PreferencesKey.currentDogecoinNodeIdKey,
WalletType.wownero: PreferencesKey.currentWowneroNodeIdKey,
};
const Map<WalletType, String> powNodePreferenceKeys = {
WalletType.nano: PreferencesKey.currentNanoPowNodeIdKey
};
Future<void> checkNodeForWalletType(SharedPreferences sharedPreferences,
SettingsStore settingsStore, WalletType type, bool isPow) async {
final preferenceKey = isPow ? powNodePreferenceKeys[type] : nodePreferenceKeys[type];
if (preferenceKey == null) {
return;
}
final currentId = await sharedPreferences.getInt(preferenceKey);
final nodes =
isPow ? await Node.getAllForWalletTypePow(type) : await Node.getAllForWalletType(type);
final currentNode = nodes.firstWhereOrNull((item) => item.id == currentId);
if (currentNode == null) {
final newNode = (isPow
? await Node.getDefaultPowForWalletType(type)
: await Node.getDefaultForWalletType(type)) ??
await getDefaultNodeFromFiles(type, isPow: isPow);
final newId = await newNode.save();
sharedPreferences.setInt(preferenceKey, newId);
settingsStore.nodes[type] = newNode;
} else {
settingsStore.nodes[type] = currentNode;
}
}
Future<void> checkCurrentNodes(
SharedPreferences sharedPreferences, SettingsStore settingsStore) async {
for (final walletType in nodePreferenceKeys.keys) {
await checkNodeForWalletType(sharedPreferences, settingsStore, walletType, false);
}
for (final walletType in powNodePreferenceKeys.keys) {
await checkNodeForWalletType(sharedPreferences, settingsStore, walletType, true);
}
}
-119
View File
@@ -1,120 +1 @@
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import "package:yaml/yaml.dart";
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
Future<List<Node>> loadDefaultNodes(WalletType type) async {
String path;
switch (type) {
case WalletType.monero:
path = 'assets/node_list.yml';
break;
case WalletType.bitcoin:
path = 'assets/bitcoin_electrum_server_list.yml';
break;
case WalletType.litecoin:
path = 'assets/litecoin_electrum_server_list.yml';
break;
// TODO: (refactoring) each wallet would have its path, so `wallet.nodePath` would be decided based on chain id in Evm wallet
case WalletType.ethereum:
path = 'assets/ethereum_server_list.yml';
break;
case WalletType.nano:
path = 'assets/nano_node_list.yml';
break;
case WalletType.bitcoinCash:
path = 'assets/bitcoin_cash_electrum_server_list.yml';
break;
case WalletType.polygon:
path = 'assets/polygon_node_list.yml';
break;
case WalletType.wownero:
path = 'assets/wownero_node_list.yml';
break;
case WalletType.dogecoin:
path = 'assets/dogecoin_electrum_server_list.yml';
break;
case WalletType.base:
path = 'assets/base_node_list.yml';
break;
case WalletType.arbitrum:
path = 'assets/arbitrum_node_list.yml';
break;
case WalletType.bsc:
path = 'assets/bsc_node_list.yml';
break;
case WalletType.banano:
case WalletType.none:
path = '';
break;
}
final nodesRaw = await rootBundle.loadString(path);
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = type;
nodes.add(node);
}
}
return nodes;
}
Future<List<Node>> loadDefaultNanoPowNodes() async {
final powNodesRaw = await rootBundle.loadString('assets/nano_pow_node_list.yml');
final loadedPowNodes = loadYaml(powNodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedPowNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = WalletType.nano;
nodes.add(node);
}
}
return nodes;
}
Future<void> resetToDefault(Box<Node> nodeSource) async {
final wowneroNodes = await loadDefaultNodes(WalletType.wownero);
final moneroNodes = await loadDefaultNodes(WalletType.monero);
final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin);
final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin);
final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash);
final ethereumNodes = await loadDefaultNodes(WalletType.ethereum);
final nanoNodes = await loadDefaultNodes(WalletType.nano);
final polygonNodes = await loadDefaultNodes(WalletType.polygon);
final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin);
final baseNodes = await loadDefaultNodes(WalletType.base);
final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum);
final bscNodes = await loadDefaultNodes(WalletType.bsc);
final nodes = wowneroNodes +
moneroNodes +
bitcoinElectrumServerList +
litecoinElectrumServerList +
ethereumNodes +
bitcoinCashElectrumServerList +
nanoNodes +
polygonNodes +
dogecoinElectrumServerList +
baseNodes +
arbitrumNodes +
bscNodes;
await nodeSource.clear();
await nodeSource.addAll(nodes);
}
Future<void> resetPowToDefault(Box<Node> powNodeSource) async {
final nanoPowNodes = await loadDefaultNanoPowNodes();
final nodes = nanoPowNodes;
await powNodeSource.clear();
await powNodeSource.addAll(nodes);
}
-88
View File
@@ -1,88 +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);
}
}
-138
View File
@@ -1,138 +0,0 @@
import 'package:hash_wallet/core/address_validator.dart';
import 'package:hash_wallet/entities/ens_record.dart';
import 'package:hash_wallet/entities/lnurlpay_record.dart';
import 'package:hash_wallet/entities/openalias_record.dart';
import 'package:hash_wallet/entities/parsed_address.dart';
import 'package:hash_wallet/entities/wellknown_record.dart';
import 'package:hash_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';
import 'package:flutter/cupertino.dart';
import 'bip_353_record.dart';
/// Resolves human-readable names typed into the address field.
///
/// Privacy contract: every lookup here either queries DNS (OpenAlias,
/// BIP-353), the domain the user explicitly typed (.well-known, LNURL-pay),
/// or the wallet's own configured RPC node (ENS). Resolvers that shipped the
/// typed input to unrelated third-party services (Twitter, Mastodon, Nostr,
/// Yat, Unstoppable Domains, FIO, ThorChain names, zcash.me, Zano aliases)
/// were removed deliberately — do not reintroduce one without updating
/// PRIVACY.md and gating it behind an off-by-default setting.
class AddressResolver {
AddressResolver({required this.wallet, required this.settingsStore}) : walletType = wallet.type;
final WalletType walletType;
final WalletBase wallet;
final SettingsStore settingsStore;
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 '';
});
}
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);
}
Future<ParsedAddress> resolve(BuildContext context, String text, CryptoCurrency currency) async {
final ticker = currency.title;
try {
// user@domain identifiers: resolved against the domain the user typed.
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);
}
}
}
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]);
}
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);
}
}
}
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);
}
}
}
} catch (e) {
printV(e.toString());
}
return ParsedAddress(addresses: [text]);
}
}
-203
View File
@@ -1,203 +0,0 @@
import 'package:hash_wallet/entities/openalias_record.dart';
import 'package:hash_wallet/entities/yat_record.dart';
enum ParseFrom {
unstoppableDomains,
openAlias,
yatRecord,
fio,
notParsed,
twitter,
ens,
contact,
mastodon,
nostr,
thorChain,
wellKnown,
zanoAlias,
zcashAddress,
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.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;
}
+19 -2
View File
@@ -1,6 +1,10 @@
import "package:cw_core/wallet_type.dart";
class PreferencesKey {
static const currentWalletType = 'current_wallet_type';
static const currentWalletName = 'current_wallet_name';
static const accountsHomePromoDismissed = 'accounts_home_promo_dismissed';
static String educationDismissed(String educationId) => "education_dismissed_$educationId";
static const currentNodeIdKey = 'current_node_id';
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
@@ -11,6 +15,7 @@ class PreferencesKey {
static const currentBaseNodeIdKey = 'current_node_id_base';
static const currentArbitrumNodeIdKey = 'current_node_id_arbitrum';
static const currentBscNodeIdKey = 'current_node_id_bsc';
static const currentRobinhoodNodeIdKey = "current_node_id_robinhood";
static const currentNanoNodeIdKey = 'current_node_id_nano';
static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow';
static const currentDecredNodeIdKey = 'current_node_id_decred';
@@ -63,7 +68,8 @@ class PreferencesKey {
static const zanoTransactionPriority = 'current_fee_priority_zano';
static const wowneroTransactionPriority = 'current_fee_priority_wownero';
static const decredTransactionPriority = 'current_fee_priority_decred';
static const zcashTransactionPriority = 'current_fee_priority_decred';
static const zcashTransactionPriority = 'current_fee_priority_zcash';
static const dogecoinTransactionPriority = 'current_fee_priority_dogecoin';
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
static const mwebCardDisplay = 'mwebCardDisplay';
@@ -89,6 +95,7 @@ class PreferencesKey {
static const useBaseScan = 'use_base_scan';
static const useArbiScan = 'use_arbitrum_scan';
static const useBscScan = 'use_bscscan';
static const useRobinhoodScan = "use_robinhood_scan";
static const useTronGrid = 'use_trongrid';
static const useMempoolFeeAPI = 'use_mempool_fee_api';
static const evmHiddenChainIds = 'evm_hidden_chain_ids';
@@ -101,7 +108,14 @@ class PreferencesKey {
static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain';
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';
@@ -109,7 +123,6 @@ class PreferencesKey {
static const showDecredInfoCard = 'show_decred_info_card';
static const forceDecentralizedExchanges = 'force_decentralized_exchanges';
static const decentralizedExchangesPromptDismissed = 'decentralized_exchanges_prompt_dismissed';
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
@@ -138,4 +151,8 @@ class PreferencesKey {
static const backgroundImage = 'background_image';
static const mwebAdDismissed = "mweb_ad_dismissed";
static const balanceHideCounter = "balance_hide_counter";
static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
static const showCiBuildOverlay = "show_ci_build_overlay";
static String deprecationPopupViewed(WalletType type) =>
"deprecation_popup_viewed_${walletTypeToString(type)}";
}
+23 -9
View File
@@ -1,26 +1,40 @@
import 'dart:math';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/new-ui/pages/scan_page.dart';
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:fast_scanner/fast_scanner.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:ur/ur_decoder.dart';
var isQrScannerShown = false;
Future<String?> presentQRScanner(BuildContext context) async {
Future<String?> presentQRScanner(BuildContext context,
{bool showHelp = false, bool showManualInput = true, bool useModal = false}) async {
isQrScannerShown = true;
try {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) {
return BarcodeScannerSimple();
},
),
);
final result = useModal
? await CupertinoScaffold.showCupertinoModalBottomSheet<String?>(
context: context,
builder: (context) => ScanPage(
showHelp: showHelp,
showManualInput: showManualInput,
))
: await Navigator.of(context).push<String>(
CupertinoPageRoute(
builder: (context) {
return ScanPage(
showHelp: showHelp,
showManualInput: showManualInput,
);
},
),
);
isQrScannerShown = false;
return result;
} catch (e) {
@@ -55,7 +69,7 @@ class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonText: S.of(context).ok,
buttonAction: () {
Navigator.of(context).pop();
},
+116
View File
@@ -0,0 +1,116 @@
import "package:hash_wallet/entities/solana_nft_asset_model.dart";
import "package:hash_wallet/entities/wallet_nft_response.dart";
import "package:cw_core/imported_nft.dart";
class SavedNFTs {
Future<Map<String, SolanaNFTAssetModel>> solanaNFTs(String walletName) async {
final rows = await ImportedNFT.getAllForWallet(walletName, ImportedNFT.solanaChain);
return {
for (final row in rows)
row.identifier: SolanaNFTAssetModel(
address: row.identifier,
mint: row.identifier,
name: row.name,
symbol: row.symbol,
description: row.description,
imageOriginalUrl: row.imageUrl,
isOwned: row.isOwned,
),
};
}
Future<void> addSolana(String walletName, SolanaNFTAssetModel asset) async {
final mint = asset.mint;
if (mint == null || mint.isEmpty) {
return;
}
await _rowForSolana(walletName, mint, asset).save();
}
Future<void> refreshSolana(String walletName, List<SolanaNFTAssetModel> assets) async {
for (final asset in assets) {
final mint = asset.mint;
if (mint == null) {
continue;
}
await ImportedNFT.updateMetadata(_rowForSolana(walletName, mint, asset));
}
}
Future<List<NFTAssetModel>> evmNFTs(String walletName, String chain) async {
final rows = await ImportedNFT.getAllForWallet(walletName, chain);
return rows
.map(
(row) => NFTAssetModel(
tokenAddress: _tokenAddressOf(row.identifier),
tokenId: _tokenIdOf(row.identifier),
name: row.name,
symbol: row.symbol,
normalizedMetadata: NormalizedMetadata(
name: row.name,
description: row.description,
image: row.imageUrl,
),
),
)
.toList();
}
Future<void> addEvm(String walletName, String chain, NFTAssetModel asset) async {
final identifier = evmIdentifier(asset);
if (identifier == null) {
return;
}
await ImportedNFT(
walletName: walletName,
chain: chain,
identifier: identifier,
name: asset.name,
symbol: asset.symbol,
description: asset.normalizedMetadata?.description,
imageUrl: asset.normalizedMetadata?.imageUrl,
).save();
}
Future<void> removeSolana(String walletName, String mint) =>
ImportedNFT.deleteOne(walletName, ImportedNFT.solanaChain, mint);
static String? evmIdentifier(NFTAssetModel asset) {
final address = asset.tokenAddress;
final tokenId = asset.tokenId;
if (address == null || address.isEmpty) {
return null;
}
return "$address:${tokenId ?? ""}";
}
ImportedNFT _rowForSolana(String walletName, String mint, SolanaNFTAssetModel asset) =>
ImportedNFT(
walletName: walletName,
chain: ImportedNFT.solanaChain,
identifier: mint,
name: asset.name,
symbol: asset.symbol,
description: asset.description,
imageUrl: asset.imageOriginalUrl,
isOwned: asset.isOwned,
);
String _tokenAddressOf(String identifier) => identifier.split(":").first;
String _tokenIdOf(String identifier) {
final parts = identifier.split(":");
return parts.length > 1 ? parts.sublist(1).join(":") : "";
}
}
+46 -41
View File
@@ -1,3 +1,6 @@
import "package:cw_core/utils/ipfs_url.dart";
import "package:cw_core/utils/nft_text.dart";
class SolanaNFTAssetModel {
String? address;
String? mint;
@@ -12,46 +15,50 @@ class SolanaNFTAssetModel {
Metaplex? metaplex;
Collection? collection;
Contract? contract;
bool? isOwned;
SolanaNFTAssetModel({
this.address,
this.mint,
this.standard,
this.name,
this.symbol,
this.description,
this.imageOriginalUrl,
String? name,
String? symbol,
String? description,
String? imageOriginalUrl,
this.externalUrl,
this.metadataOriginalUrl,
this.totalSupply,
this.metaplex,
this.collection,
this.contract,
});
this.isOwned,
}) : name = sanitizeNFTText(name),
symbol = sanitizeNFTText(symbol),
description = sanitizeNFTText(description),
imageOriginalUrl = tryNormalizeIpfsUrl(imageOriginalUrl);
factory SolanaNFTAssetModel.fromJson(Map<String, dynamic> json) {
return SolanaNFTAssetModel(
address: json['address'] as String?,
mint: json['mint'] as String?,
standard: json['standard'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metadataOriginalUrl: json['metadataOriginalUrl'] as String?,
totalSupply: json['totalSupply'] as String?,
metaplex: json['metaplex'] != null
? Metaplex.fromJson(json['metaplex'] as Map<String, dynamic>)
: null,
collection: json['collection'] != null
? Collection.fromJson(json['collection'] as Map<String, dynamic>)
: null,
contract: json['contract'] != null
? Contract.fromJson(json['contract'] as Map<String, dynamic>)
: null,
);
}
factory SolanaNFTAssetModel.fromJson(Map<String, dynamic> json) => SolanaNFTAssetModel(
address: json["address"] as String?,
mint: json["mint"] as String?,
standard: json["standard"] as String?,
name: json["name"] as String?,
symbol: json["symbol"] as String?,
description: json["description"] as String?,
imageOriginalUrl: json["imageOriginalUrl"] as String?,
externalUrl: json["externalUrl"] as String?,
metadataOriginalUrl: json["metadataOriginalUrl"] as String?,
totalSupply: json["totalSupply"] as String?,
metaplex: json["metaplex"] != null
? Metaplex.fromJson(json["metaplex"] as Map<String, dynamic>)
: null,
collection: json["collection"] != null
? Collection.fromJson(json["collection"] as Map<String, dynamic>)
: null,
contract: json["contract"] != null
? Contract.fromJson(json["contract"] as Map<String, dynamic>)
: null,
isOwned: json["isOwned"] as bool?,
);
}
class Metaplex {
@@ -100,17 +107,15 @@ class Collection {
this.metaplexMint,
this.sellerFeeBasisPoints});
factory Collection.fromJson(Map<String, dynamic> json) {
return Collection(
collectionAddress: json['collectionAddress'] as String?,
name: json['name'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metaplexMint: json['metaplexMint'] as String?,
sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?,
);
}
factory Collection.fromJson(Map<String, dynamic> json) => Collection(
collectionAddress: json["collectionAddress"] as String?,
name: sanitizeNFTText(json["name"] as String?),
description: sanitizeNFTText(json["description"] as String?),
imageOriginalUrl: tryNormalizeIpfsUrl(json["imageOriginalUrl"] as String?),
externalUrl: json["externalUrl"] as String?,
metaplexMint: json["metaplexMint"] as String?,
sellerFeeBasisPoints: json["sellerFeeBasisPoints"] as int?,
);
}
class Contract {
@@ -123,8 +128,8 @@ class Contract {
factory Contract.fromJson(Map<String, dynamic> json) {
return Contract(
type: json['type'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
name: sanitizeNFTText(json["name"] as String?),
symbol: sanitizeNFTText(json["symbol"] as String?),
);
}
}
+10 -10
View File
@@ -1,9 +1,9 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'template.g.dart';
part 'template.part.dart';
@HiveType(typeId: Template.typeId)
// @HiveType(typeId: Template.typeId)
class Template extends HiveObject {
Template(
{required this.nameRaw,
@@ -18,28 +18,28 @@ class Template extends HiveObject {
static const typeId = TEMPLATE_TYPE_ID;
static const boxName = 'Template';
@HiveField(0)
// @HiveField(0)
String? nameRaw;
@HiveField(1)
// @HiveField(1)
String? addressRaw;
@HiveField(2)
// @HiveField(2)
String? cryptoCurrencyRaw;
@HiveField(3)
// @HiveField(3)
String? amountRaw;
@HiveField(4)
// @HiveField(4)
String? fiatCurrencyRaw;
@HiveField(5)
// @HiveField(5)
bool? isCurrencySelectedRaw;
@HiveField(6)
// @HiveField(6)
String? amountFiatRaw;
@HiveField(7)
// @HiveField(7)
List<Template>? additionalRecipientsRaw;
bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
+60
View File
@@ -0,0 +1,60 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'template.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TemplateAdapter extends TypeAdapter<Template> {
@override
final int typeId = 6;
@override
Template read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Template(
nameRaw: fields[0] as String?,
isCurrencySelectedRaw: fields[5] as bool?,
addressRaw: fields[1] as String?,
cryptoCurrencyRaw: fields[2] as String?,
amountRaw: fields[3] as String?,
fiatCurrencyRaw: fields[4] as String?,
amountFiatRaw: fields[6] as String?,
additionalRecipientsRaw: (fields[7] as List?)?.cast<Template>(),
);
}
@override
void write(BinaryWriter writer, Template obj) {
writer
..writeByte(8)
..writeByte(0)
..write(obj.nameRaw)
..writeByte(1)
..write(obj.addressRaw)
..writeByte(2)
..write(obj.cryptoCurrencyRaw)
..writeByte(3)
..write(obj.amountRaw)
..writeByte(4)
..write(obj.fiatCurrencyRaw)
..writeByte(5)
..write(obj.isCurrencySelectedRaw)
..writeByte(6)
..write(obj.amountFiatRaw)
..writeByte(7)
..write(obj.additionalRecipientsRaw);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TemplateAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
}
+6 -6
View File
@@ -1,9 +1,9 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'transaction_description.g.dart';
part 'transaction_description.part.dart';
@HiveType(typeId: TransactionDescription.typeId)
// @HiveType(typeId: TransactionDescription.typeId)
class TransactionDescription extends HiveObject {
TransactionDescription(
{required this.id, this.recipientAddress, this.transactionNote, this.transactionKey});
@@ -12,16 +12,16 @@ class TransactionDescription extends HiveObject {
static const boxName = 'TransactionDescriptions';
static const boxKey = 'transactionDescriptionsBoxKey';
@HiveField(0, defaultValue: '')
// @HiveField(0, defaultValue: '')
String id;
@HiveField(1)
// @HiveField(1)
String? recipientAddress;
@HiveField(2)
// @HiveField(2)
String? transactionNote;
@HiveField(3)
// @HiveField(3)
String? transactionKey;
String get note => transactionNote ?? '';
@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'transaction_description.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TransactionDescriptionAdapter extends TypeAdapter<TransactionDescription> {
@override
final int typeId = 2;
@override
TransactionDescription read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return TransactionDescription(
id: fields[0] == null ? '' : fields[0] as String,
recipientAddress: fields[1] as String?,
transactionNote: fields[2] as String?,
transactionKey: fields[3] as String?,
);
}
@override
void write(BinaryWriter writer, TransactionDescription obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.recipientAddress)
..writeByte(2)
..write(obj.transactionNote)
..writeByte(3)
..write(obj.transactionKey);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransactionDescriptionAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+9 -28
View File
@@ -1,3 +1,6 @@
import "package:cw_core/utils/ipfs_url.dart";
import "package:cw_core/utils/nft_text.dart";
class WalletNFTsResponseModel {
final int? page;
final int? pageSize;
@@ -40,8 +43,8 @@ class NFTAssetModel {
tokenAddress: json['token_address'] as String?,
tokenId: json['token_id'] as String?,
contractType: json['contract_type'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
name: sanitizeNFTText(json["name"] as String?),
symbol: sanitizeNFTText(json["symbol"] as String?),
normalizedMetadata: json['normalized_metadata'] != null
? new NormalizedMetadata.fromJson(json['normalized_metadata'] as Map<String, dynamic>)
: null,
@@ -61,33 +64,11 @@ class NormalizedMetadata {
factory NormalizedMetadata.fromJson(Map<String, dynamic> json) {
return NormalizedMetadata(
name: json['name'] as String?,
description: json['description'] as String?,
image: json['image'] as String?,
name: sanitizeNFTText(json["name"] as String?),
description: sanitizeNFTText(json["description"] as String?),
image: json["image"] as String?,
);
}
String? get imageUrl {
if (image == null) return image;
if (image!.contains('ipfs.io')) return image;
if (!image!.contains('ipfs')) return image;
// IPFS public gateway provided by Cloudflare is https://cloudflare-ipfs.com/ipfs/
//
// Here is an example of an ipfs image link:
//
// [ipfs://bafkreia2i2ctfexpovgzfff66wqhbmwwpvqjvozan7ioifzcnq76jharwu]
//https://ipfs.io/ipfs/QmTRcRXo6cXByjHYHTVxGpag6vpocrG3rxjPC9PxKAArR9/1620.png
const String ipfsPublicGateway = 'https://cloudflare-ipfs.com/ipfs/';
final ipfsPath = image?.split('//')[1];
final imageLink = '$ipfsPublicGateway$ipfsPath';
return imageLink;
}
String? get imageUrl => tryNormalizeIpfsUrl(image);
}
-93
View File
@@ -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);
}
}
-13
View File
@@ -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;
}
+103 -75
View File
@@ -105,26 +105,25 @@ class CWEVM extends EVM {
TransactionPriority? priority,
int? feeRate,
bool useBlinkProtection = true,
}) {
return EVMChainTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as EVMChainTransactionPriority?,
currency: currency,
feeRate: feeRate,
useBlinkProtection: useBlinkProtection,
);
}
}) =>
EVMChainTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmountMoney,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
memo: out.memo,
))
.toList(),
priority: priority as EVMChainTransactionPriority?,
currency: currency,
feeRate: feeRate,
useBlinkProtection: useBlinkProtection,
);
@override
Object createEVMTransactionCredentialsRaw(
@@ -144,23 +143,42 @@ class CWEVM extends EVM {
}
@override
int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount);
TransactionInfo getTransactionInfo({
required String id,
required int height,
required Money amount,
required Money fee,
required String tokenSymbol,
int exponent = 18,
required TransactionDirection direction,
required bool isPending,
required DateTime date,
required int confirmations,
String? to,
String? from,
String? evmSignatureName,
String? contractAddress,
required int chainId,
}) =>
EVMChainTransactionInfo(
id: id,
height: height,
amount: amount,
fee: fee,
tokenSymbol: tokenSymbol,
exponent: exponent,
direction: direction,
isPending: isPending,
date: date,
confirmations: confirmations,
to: to,
from: from,
evmSignatureName: evmSignatureName,
contractAddress: contractAddress,
chainId: chainId);
@override
double formatterEVMAmountToDouble({
TransactionInfo? transaction,
BigInt? amount,
int exponent = 18,
}) {
assert(transaction != null || amount != null);
if (transaction != null) {
transaction as EVMChainTransactionInfo;
return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
} else {
return (amount!) / BigInt.from(10).pow(exponent);
}
}
int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount);
@override
List<Erc20Token> getERC20Currencies(WalletBase wallet) =>
@@ -181,10 +199,13 @@ class CWEVM extends EVM {
@override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) {
final evmWallet = wallet as EVMChainWallet;
final chainName = EVMChainUtils.getDefaultTokenSymbol(evmWallet.selectedChainId).toLowerCase();
return evmWallet.getErc20Token(contractAddress, chainName);
return evmWallet.getErc20Token(contractAddress, getMoralisChainName(wallet));
}
@override
String? getMoralisChainName(WalletBase wallet) =>
EVMChainUtils.getMoralisChainName((wallet as EVMChainWallet).selectedChainId);
@override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as EVMChainTransactionInfo;
@@ -261,20 +282,16 @@ class CWEVM extends EVM {
@override
Future<PendingTransaction> createTokenApproval(
WalletBase wallet,
BigInt amount,
Money amount,
String spender,
CryptoCurrency token,
TransactionPriority? priority, {
bool useBlinkProtection = true,
}) {
final evmWallet = wallet as EVMChainWallet;
final feeCurrency = EVMChainUtils.getFeeCurrency(evmWallet.selectedChainId);
return evmWallet.createApprovalTransaction(
amount,
spender,
token,
priority as EVMChainTransactionPriority?,
feeCurrency,
useBlinkProtection: useBlinkProtection,
);
}
@@ -284,7 +301,7 @@ class CWEVM extends EVM {
WalletBase wallet,
String to,
String dataHex,
BigInt valueWei,
Money valueWei,
TransactionPriority? priority, {
bool useBlinkProtection = true,
String? sourceTokenAddress,
@@ -366,7 +383,7 @@ class CWEVM extends EVM {
// Chain-specific integrations (only for Ethereum)
@override
Future<BigInt>? getDEuroSavingsBalance(WalletBase wallet) {
Future<Money>? getDEuroSavingsBalance(WalletBase wallet) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).savingsBalance;
}
@@ -374,7 +391,15 @@ class CWEVM extends EVM {
}
@override
Future<BigInt>? getDEuroAccruedInterest(WalletBase wallet) {
Future<Money>? getDEuroSavingsV1Balance(WalletBase wallet) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).savingsBalanceV1;
}
return null;
}
@override
Future<Money>? getDEuroAccruedInterest(WalletBase wallet) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).accruedInterest;
}
@@ -415,6 +440,15 @@ class CWEVM extends EVM {
return null;
}
@override
Future<PendingTransaction>? withdrawDEuroSavingV1(
WalletBase wallet, TransactionPriority priority) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).withdrawSavingsV1(priority as EVMChainTransactionPriority);
}
return null;
}
@override
Future<PendingTransaction>? reinvestDEuroInterest(
WalletBase wallet, TransactionPriority priority) {
@@ -492,14 +526,6 @@ class CWEVM extends EVM {
return _registry.getWalletTypeByChainId(chainId);
}
@override
BigInt? getERC20AvailableBalance(Object balance) {
if (balance is EVMChainERC20Balance) {
return balance.balance;
}
return null;
}
@override
List<ChainInfo> getAllChains() {
final allChains = _registry.getAllChains();
@@ -672,29 +698,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;
}
}
@@ -710,21 +725,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);
})());
+10 -10
View File
@@ -1,9 +1,9 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'exchange_template.g.dart';
part 'exchange_template.part.dart';
@HiveType(typeId: ExchangeTemplate.typeId)
// @HiveType(typeId: ExchangeTemplate.typeId)
class ExchangeTemplate extends HiveObject {
ExchangeTemplate(
{required this.amountRaw,
@@ -18,28 +18,28 @@ class ExchangeTemplate extends HiveObject {
static const typeId = EXCHANGE_TEMPLATE_TYPE_ID;
static const boxName = 'ExchangeTemplate';
@HiveField(0)
// @HiveField(0)
String? amountRaw;
@HiveField(1)
// @HiveField(1)
String? depositCurrencyRaw;
@HiveField(2)
// @HiveField(2)
String? receiveCurrencyRaw;
@HiveField(3)
// @HiveField(3)
String? providerRaw;
@HiveField(4)
// @HiveField(4)
String? depositAddressRaw;
@HiveField(5)
// @HiveField(5)
String? receiveAddressRaw;
@HiveField(6)
// @HiveField(6)
String? depositCurrencyTitleRaw;
@HiveField(7)
// @HiveField(7)
String? receiveCurrencyTitleRaw;
String get amount => amountRaw ?? '';
+62
View File
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'exchange_template.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ExchangeTemplateAdapter extends TypeAdapter<ExchangeTemplate> {
@override
final int typeId = 7;
@override
ExchangeTemplate read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ExchangeTemplate(
amountRaw: fields[0] as String?,
depositCurrencyRaw: fields[1] as String?,
receiveCurrencyRaw: fields[2] as String?,
providerRaw: fields[3] as String?,
depositAddressRaw: fields[4] as String?,
receiveAddressRaw: fields[5] as String?,
depositCurrencyTitleRaw: fields[6] as String?,
receiveCurrencyTitleRaw: fields[7] as String?,
);
}
@override
void write(BinaryWriter writer, ExchangeTemplate obj) {
writer
..writeByte(8)
..writeByte(0)
..write(obj.amountRaw)
..writeByte(1)
..write(obj.depositCurrencyRaw)
..writeByte(2)
..write(obj.receiveCurrencyRaw)
..writeByte(3)
..write(obj.providerRaw)
..writeByte(4)
..write(obj.depositAddressRaw)
..writeByte(5)
..write(obj.receiveAddressRaw)
..writeByte(6)
..write(obj.depositCurrencyTitleRaw)
..writeByte(7)
..write(obj.receiveCurrencyTitleRaw);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ExchangeTemplateAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
@@ -19,6 +19,8 @@ abstract class ExchangeProvider {
bool get supportsOnionAddress => false;
bool get supportsMemoOrDestinationTag => true;
@override
String toString() => title;
@@ -39,7 +39,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
static const Map<String, String> kNearDummyAddresses = {
// UTXO
'LTC': 'ltc1qhdwz74m3wuuhppv2mckagqk9e2e49z5j4kucnv',
'BTC': 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080',
'BTC': 'bc1qzwdt09dgr5nle2fkv7h5s6axgjqpdyp5g5tumz',
'DOGE': 'D9t7rGQ9mE3hJ2z1w8pGQxkGmKjYwYc8pQ',
'BCH': 'qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a',
@@ -50,6 +50,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
'AVAXC': '0x1111111111111111111111111111111111111111',
'ARB': '0x1111111111111111111111111111111111111111',
'BASE': '0x1111111111111111111111111111111111111111',
"ROB": "0x1111111111111111111111111111111111111111",
// Others
'SOL': '11111111111111111111111111111111',
@@ -88,6 +89,9 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
@override
bool get supportsFixedRate => true;
@override
bool get supportsMemoOrDestinationTag => false;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.nearIntents;
@@ -242,6 +246,11 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final quoteObj = quote['quote'] as Map<String, dynamic>;
final depositAddress = quoteObj['depositAddress'] as String;
final depositMemo = quoteObj['depositMemo'] as String?;
final depositAmount = quoteObj['amountInFormatted'] as String?;
if (depositAmount == null) {
throw Exception('Deposit amount is null in quote response');
}
final quoteRequest = quote['quoteRequest'] as Map<String, dynamic>;
final fromAssetId = quoteRequest['originAsset'] as String;
@@ -264,8 +273,8 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final trade = Trade(
id: depositAddress,
// Using deposit address as trade ID
from: from,
to: to,
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
providerName: title,
state: TradeState.created,
@@ -273,7 +282,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
inputAddress: depositAddress,
payoutAddress: request.toAddress,
refundAddress: request.refundAddress,
amount: request.fromAmount,
amount: depositAmount,
receiveAmount: quoteObj['amountOutFormatted']?.toString(),
memo: depositMemo,
isSendAll: isSendAll,
@@ -508,6 +517,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
return switch (tag) {
'TRX' => 'tron',
'AVAXC' => 'avax',
"ROB" => "hood",
_ => tag?.toLowerCase(),
};
}
@@ -516,6 +526,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
return switch (blockchain) {
'tron' => 'TRX',
'avax' => 'AVAXC',
"hood" => "ROB",
_ => blockchain?.toUpperCase(),
};
}
@@ -526,6 +537,13 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final symbol = currency.title.toUpperCase();
final blockchain = _normalizeTagToNearBlockchain(currency.tag);
// Use the native Bitcoin asset routed through Omni Bridge.
if (currency == CryptoCurrency.btc) {
return supported.firstWhereOrNull(
(t) => t.assetId == '1cs_v1:btc:native:coin',
);
}
// Native asset (no contract)
final native = supported.firstWhereOrNull((t) =>
t.symbol.toUpperCase() == symbol &&
@@ -549,9 +567,14 @@ 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);
}
@@ -559,7 +582,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
String _buildDeadline() {
return DateTime.now()
.toUtc()
.add(const Duration(hours: 2))
.add(const Duration(hours: 1))
.toIso8601String()
.replaceFirst(RegExp(r'\.\d+Z$'), 'Z');
}
@@ -48,8 +48,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey;
static const clearNetAuthority = 'api.trocador.app';
static const onionApiAuthority = clearNetAuthority;
// static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion';
static const onionApiAuthority = '65bsisadnxvw4kfz7h7a3jwcyenrhluuj3kd5toslfzxbk5q4m3wy6qd.onion';
static const markup = secrets.trocadorExchangeMarkup;
static const newRatePath = '/new_rate';
static const createTradePath = '/new_trade';
@@ -235,6 +234,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
if (!isFixedRateMode) 'amount_from': request.fromAmount,
if (isFixedRateMode) 'amount_to': request.toAmount,
'address': request.toAddress,
if (request.toAddressExtraId.isNotEmpty) 'address_memo': request.toAddressExtraId,
'refund': request.refundAddress,
'refund_memo': '0',
// Hash Bags: route trade-status updates through our Cloudflare Worker
@@ -398,6 +398,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
payoutAddress: payoutAddress,
isSendAll: isSendAll,
extraId: addressProviderMemo,
toAddressExtraId: request.toAddressExtraId,
);
}
+6
View File
@@ -27,6 +27,7 @@ class Trade {
this.refundAddress,
this.walletId,
this.payoutAddress,
this.toAddressExtraId,
this.password,
this.providerId,
this.providerName,
@@ -86,6 +87,9 @@ class Trade {
String? refundAddress;
String? walletId;
String? payoutAddress;
// holds the receive address memo or destination tag that was passed for this trade
String? toAddressExtraId;
String? password;
String? providerId;
String? providerName;
@@ -221,6 +225,7 @@ class Trade {
'refundAddress': refundAddress,
'walletId': walletId,
'payoutAddress': payoutAddress,
'toAddressExtraId': toAddressExtraId,
'password': password,
'providerId': providerId,
'providerName': providerName,
@@ -264,6 +269,7 @@ class Trade {
refundAddress: row['refundAddress'] as String?,
walletId: row['walletId'] as String?,
payoutAddress: row['payoutAddress'] as String?,
toAddressExtraId: row['toAddressExtraId'] as String?,
password: row['password'] as String?,
providerId: row['providerId'] as String?,
providerName: row['providerName'] as String?,
+2
View File
@@ -8,6 +8,7 @@ class TradeRequest {
required this.refundAddress,
required this.fromAmount,
this.toAmount = '',
this.toAddressExtraId = '',
this.isFixedRate = false});
final CryptoCurrency fromCurrency;
@@ -16,5 +17,6 @@ class TradeRequest {
final String refundAddress;
final String fromAmount;
final String toAmount;
final String toAddressExtraId;
final bool isFixedRate;
}
+18
View File
@@ -1036,4 +1036,22 @@ class HaCupertinoLocalizations extends GlobalCupertinoLocalizations {
@override
// TODO: implement cancelButtonLabel
String get cancelButtonLabel => "cancelButtonLabel";
@override
String get collapsedHint => "collapsedHint";
@override
String get expandedHint => "expandedHint";
@override
String get expansionTileCollapsedHint => "expansionTileCollapsedHint";
@override
String get expansionTileCollapsedTapHint => "expansionTileCollapsedTapHint";
@override
String get expansionTileExpandedHint => "expansionTileExpandedHint";
@override
String get expansionTileExpandedTapHint => "expansionTileExpandedTapHint";
}
+18
View File
@@ -1036,4 +1036,22 @@ class YoCupertinoLocalizations extends GlobalCupertinoLocalizations {
@override
// TODO: implement cancelButtonLabel
String get cancelButtonLabel => "cancelButtonLabel";
@override
String get collapsedHint => "collapsedHint";
@override
String get expandedHint => "expandedHint";
@override
String get expansionTileCollapsedHint => "expansionTileCollapsedHint";
@override
String get expansionTileCollapsedTapHint => "expansionTileCollapsedTapHint";
@override
String get expansionTileExpandedHint => "expansionTileExpandedHint";
@override
String get expansionTileExpandedTapHint => "expansionTileExpandedTapHint";
}
+31 -39
View File
@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:hash_wallet/app_scroll_behavior.dart';
import "package:hash_wallet/ci_build_overlay.dart";
import 'package:hash_wallet/core/auth_service.dart';
import 'package:hash_wallet/core/background_sync.dart';
import 'package:hash_wallet/core/node_switching_service.dart';
@@ -16,12 +17,14 @@ import 'package:hash_wallet/entities/default_settings_migration.dart';
import 'package:hash_wallet/entities/demo_mode.dart';
import 'package:hash_wallet/entities/get_encryption_key.dart';
import 'package:hash_wallet/entities/language_service.dart';
import 'package:hash_wallet/entities/node_check.dart';
import 'package:hash_wallet/entities/template.dart';
import 'package:hash_wallet/entities/transaction_description.dart';
import 'package:hash_wallet/exchange/exchange_template.dart';
import 'package:hash_wallet/exchange/trade_legacy.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/locales/locale.dart';
import "package:hash_wallet/new-ui/widgets/money/money_settings_provider.dart";
import 'package:hash_wallet/order/order.dart';
import 'package:hash_wallet/reactions/bootstrap.dart';
import 'package:hash_wallet/router.dart' as Router;
@@ -40,15 +43,16 @@ import 'package:hash_wallet/view_model/link_view_model.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/key.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/node_legacy.dart' show performNodeHiveMigration;
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
@@ -61,6 +65,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import "package:flutter_localizations/flutter_localizations.dart";
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:quick_actions/quick_actions.dart';
@@ -180,17 +185,22 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
),
);
} else {
runApp(App(
key: topLevelKey,
initialQuickAction: initialQuickAction,
quickActionsStream: quickActionsStream.stream));
runApp(
CiBuildOverlay(
settingsStore: getIt.get<SettingsStore>(),
child: App(
key: topLevelKey,
initialQuickAction: initialQuickAction,
quickActionsStream: quickActionsStream.stream),
),
);
}
isAppRunning = true;
}, (error, stackTrace) async {
if (!isAppRunning) {
runApp(
TopLevelErrorWidget(error: error, stackTrace: stackTrace),
CiBuildOverlay(child: TopLevelErrorWidget(error: error, stackTrace: stackTrace)),
);
}
@@ -213,10 +223,6 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(ContactAdapter());
}
if (!CakeHive.isAdapterRegistered(Node.typeId)) {
CakeHive.registerAdapter(NodeAdapter());
}
if (!CakeHive.isAdapterRegistered(TransactionDescription.typeId)) {
CakeHive.registerAdapter(TransactionDescriptionAdapter());
}
@@ -257,31 +263,22 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
}
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
CakeHive.registerAdapter(SPLTokenAdapter());
}
if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
CakeHive.registerAdapter(TronTokenAdapter());
}
await performHiveMigration();
await performErc20TokenHiveMigration();
await performSplTokenHiveMigration();
await performTronTokenHiveMigration();
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey);
final contacts = await CakeHive.openBox<Contact>(Contact.boxName);
final nodes = await CakeHive.openBox<Node>(Node.boxName);
final powNodes =
await CakeHive.openBox<Node>(Node.boxName + "pow"); // must be different from Node.boxName
final transactionDescriptions = await CakeHive.openBox<TransactionDescription>(
TransactionDescription.boxName,
encryptionKey: transactionDescriptionsBoxKey);
await performTradeHiveMigration(secureStorage);
await performNodeHiveMigration();
await validateBuiltinNodes();
final orders = await CakeHive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
final templates = await CakeHive.openBox<Template>(Template.boxName);
@@ -293,8 +290,6 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
await initialSetup(
loadWallet: loadWallet,
sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes,
powNodes: powNodes,
contactSource: contacts,
ordersSource: orders,
unspentCoinsInfoSource: unspentCoinsInfoSource,
@@ -305,15 +300,13 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
secureStorage: secureStorage,
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 65,
initialMigrationVersion: 72,
);
}
Future<void> initialSetup({
required bool loadWallet,
required SharedPreferences sharedPreferences,
required Box<Node> nodes,
required Box<Node> powNodes,
required Box<Contact> contactSource,
required Box<Order> ordersSource,
// required FiatConvertationService fiatConvertationService,
@@ -327,17 +320,14 @@ Future<void> initialSetup({
required int initialMigrationVersion,
}) async {
LanguageService.loadLocaleList();
await GlobalMaterialLocalizations.delegate.load(const Locale("en"));
await defaultSettingsMigration(
secureStorage: secureStorage,
version: initialMigrationVersion,
sharedPreferences: sharedPreferences,
contactSource: contactSource,
nodes: nodes,
powNodes: powNodes,
);
await setup(
nodeSource: nodes,
powNodeSource: powNodes,
contactSource: contactSource,
ordersSource: ordersSource,
templates: templates,
@@ -349,11 +339,12 @@ Future<void> initialSetup({
navigatorKey: navigatorKey,
secureStorage: secureStorage,
);
final settingsStore = getIt<SettingsStore>();
await checkCurrentNodes(sharedPreferences, settingsStore);
await getIt.get<ResetService>().resetAuthDataOnNewInstall(sharedPreferences);
await bootstrapOffline();
final settingsStore = getIt<SettingsStore>();
if (!settingsStore.currentBuiltinTor) {
bootstrapOnline(navigatorKey, loadWallet: loadWallet);
}
@@ -420,14 +411,15 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: child!),
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
),
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
localizationsDelegates: localizationDelegates,
supportedLocales: S.delegate.supportedLocales,
locale: Locale(appStore.settingsStore.languageCode),
locale: localeFromLanguageCode(appStore.settingsStore.languageCode),
onGenerateRoute: (settings) => Router.createRoute(settings),
initialRoute: initialRoute,
scrollBehavior: AppScrollBehavior(),
+71 -26
View File
@@ -146,6 +146,10 @@ class CWMonero extends Monero {
@override
MoneroAccountList getAccountList(Object wallet) => CWMoneroAccountList(wallet);
@override
Money getAccountFullBalance(int accountIndex) =>
monero_wallet_api.getFullBalance(accountIndex: accountIndex);
@override
MoneroSubaddressList getSubaddressList(Object wallet) => CWMoneroSubaddressList(wallet);
@@ -235,10 +239,16 @@ class CWMonero extends Monero {
required String name,
required String password,
required int height,
required ledger.LedgerConnection ledgerConnection,
required HardwareWalletService hardwareWalletService,
required String? passphrase,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name, password: password, height: height, ledgerConnection: ledgerConnection);
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
passphrase: passphrase,
);
@override
WalletCredentials createMoneroRestoreWalletFromSeedCredentials(
@@ -288,6 +298,23 @@ class CWMonero extends Monero {
};
}
@override
Future<int> getNodeHeight(Object wallet) => (wallet as MoneroWallet).getNodeHeight();
@override
String getLegacySeed(Object wallet, String langName) =>
(wallet as MoneroWallet).seedLegacy(langName);
@override
bool isBackgroundSyncRunning(Object wallet) => (wallet as MoneroWallet).isBackgroundSyncRunning;
@override
Future<void> startBackgroundSync(Object wallet) => (wallet as MoneroWallet).startBackgroundSync();
@override
Future<void> stopBackgroundSync(Object wallet, String password) =>
(wallet as MoneroWallet).stopBackgroundSync(password);
@override
int? getRestoreHeight(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
@@ -300,14 +327,14 @@ class CWMonero extends Monero {
MoneroTransactionCreationCredentials(
outputs: outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmountMoney,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
))
.toList(),
priority: priority as MoneroTransactionPriority);
@@ -409,16 +436,20 @@ class CWMonero extends Monero {
}
@override
bool needExportOutputs(Object wallet, int amount) {
bool needExportOutputs(Object wallet, Money amount) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.needExportOutputs(amount);
}
@override
void monerocCheck() {
checkIfMoneroCIsFine();
bool hasUnknownKeyImages(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.hasUnknownKeyImages();
}
@override
void monerocCheck() => checkIfMoneroCIsFine();
@override
Future<void> setLedgerConnection(Object wallet, ledger.LedgerConnection connection) async {
final moneroWallet = wallet as MoneroWallet;
@@ -426,29 +457,43 @@ class CWMonero extends Monero {
}
@override
void resetLedgerConnection() {
disableLedgerExchange();
}
void resetLedgerConnection() => disableLedgerExchange();
@override
void setGlobalLedgerConnection(ledger.LedgerConnection connection) {
gLedger = connection;
}
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
@override
bool hasGlobalLedgerConnection() => gLedger != null;
@override
String? getLastLedgerCommand() => latestLedgerCommand;
bool isViewOnly() {
return isViewOnlyBySpendKey(null);
@override
void setHardwareWalletService(Object wallet, HardwareWalletService service) {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.trezorService = service as MoneroTrezorService;
}
@override
Map<String, List<int>> debugCallLength() {
return monero_wallet_api.debugCallLength();
}
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) =>
MoneroLedgerService(connection);
@override
Map<String, dynamic> getWalletCacheDebug() {
return monero_wallet_api.getWalletCacheDebug();
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorClient client) =>
MoneroTrezorService(client);
@override
Future<void> syncTrezor(Object wallet) async {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.syncTrezor();
}
bool isViewOnly() => isViewOnlyBySpendKey(null);
@override
Map<String, List<int>> debugCallLength() => monero_wallet_api.debugCallLength();
@override
Map<String, dynamic> getWalletCacheDebug() => monero_wallet_api.getWalletCacheDebug();
}
+1 -7
View File
@@ -150,13 +150,12 @@ class CWNano extends Nano {
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
cryptoAmount: out.cryptoAmountMoney,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
))
.toList(),
);
@@ -175,11 +174,6 @@ class CWNano extends Nano {
return (wallet as NanoWallet).updateTransactions();
}
@override
BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) {
return (transactionInfo as NanoTransactionInfo).amountRaw;
}
@override
String getRepresentative(Object wallet) {
return (wallet as NanoWallet).representative;
-58
View File
@@ -1,58 +0,0 @@
import 'dart:ui';
import 'package:hash_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
class LongPressPopupBuilder extends StatelessWidget {
const LongPressPopupBuilder(
{super.key, required this.child, required this.popup, this.spacing = 8});
final Widget child;
final Widget popup;
final double spacing;
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: () {
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
showPopUp(
context: context,
builder: (context) => _buildPopup(context, offset, size),
);
},
child: child,
);
}
Widget _buildPopup(BuildContext context, Offset offset, Size size) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy,
width: size.width,
height: size.height,
child: Material(
color: Colors.transparent,
child: child,
),
),
Positioned(
left: offset.dx * 2,
top: offset.dy + size.height + spacing,
child: Material(
color: Colors.transparent,
child: popup,
),
),
],
),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/db/sqlite.dart";
import "package:sqflite/sqflite.dart";
class ChartsAsset {
const ChartsAsset({required this.asset, required this.isFavorite});
factory ChartsAsset.fromJson(Map<String, dynamic> json) => ChartsAsset(
asset: currencyFromApiString(json["asset"] as String) as CryptoCurrency,
isFavorite: json["isFavorite"] != 0,
);
final CryptoCurrency asset;
final bool isFavorite;
static const tableName = "ChartsAssets";
Map<String, dynamic> toJson() => {"asset": asset.apiString, "isFavorite": isFavorite ? 1 : 0};
static Future<List<ChartsAsset>> get() async {
final json = await db!.query(tableName);
return List.generate(json.length, (index) => ChartsAsset.fromJson(json[index]));
}
Future<void> insert() async {
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<void> remove() async {
await db!.delete(tableName, where: "asset = ?", whereArgs: [asset.apiString]);
}
}

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