mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
CW1612: AnyPay Service Layer (#3516)
* fix android CI * feat: add anypay core models, parser and routing engine * feat: add anypay resolver, anypay service and wallet switch service * refactor: route the send page payment flow through anypay service * fix: show message when there's no swap providers for a pair and reset the trade guard on dismissal * refactor: merge the swap from network and send to network decision pages together and clean up anypay widgets * feat: reapply deep links to a currently open send flow and register the solana scheme * feat: add support for zcash address detection to anypay and other minor fixes * refactor: remove trailing icon * feat: hide swap flow for sp and mweb addresses * feat: enhance zcash address detection and deeplink handling * fix: pin bitcoin_base * refactor: Move the anypay decision navigation out of the send page and address minor review comments * refactor: Move the anypay decision navigation out of the send page and address minor review comments * feat: handle exception in switch service and mvoe balance compute to call site * refactor: combine anypay entry points to one and extract selected mode widget --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
This commit is contained in:
co-authored by
Czarek Nakamoto
parent
4cc1495686
commit
3b100eae33
@@ -177,13 +177,7 @@ class AddressValidator extends TextValidator {
|
||||
if (ZcashNetworkType.isDevNetwork(network)) {
|
||||
return null;
|
||||
}
|
||||
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:
|
||||
@@ -355,6 +349,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]+$';
|
||||
@@ -401,13 +397,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]+';
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import "package:cake_wallet/core/universal_address_detector.dart";
|
||||
import "package:cake_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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import "package:cake_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:cake_wallet/core/universal_address_detector.dart";
|
||||
import "package:cake_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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import "package:cake_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:cake_wallet/core/anypay/anypay_router.dart";
|
||||
import "package:cake_wallet/reactions/wallet_connect.dart";
|
||||
import "package:cake_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);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import "package:cake_wallet/core/address_validator.dart";
|
||||
import "package:cake_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:cake_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 (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) {
|
||||
return const AnyPayApplyToCurrentWallet();
|
||||
}
|
||||
|
||||
if (detectedType == WalletType.tron && !snapshot.hasTronProxy) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import "package:cake_wallet/core/anypay/anypay_models.dart";
|
||||
import "package:cake_wallet/core/anypay/anypay_parser.dart";
|
||||
import "package:cake_wallet/core/anypay/anypay_resolver.dart";
|
||||
import "package:cake_wallet/core/anypay/anypay_router.dart";
|
||||
import "package:cake_wallet/evm/evm.dart";
|
||||
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
|
||||
import "package:cake_wallet/reactions/wallet_connect.dart";
|
||||
import "package:cake_wallet/solana/solana.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/tron/tron.dart";
|
||||
import "package:cake_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: solana != null,
|
||||
hasTronProxy: tron != null,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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}$'),
|
||||
|
||||
Reference in New Issue
Block a user