feat: Pay Anything from Any Wallet (#2450)

* feat: Pay Anything from Any Wallet

* feat: Trigger payment flow when address is pasted

* chore: Remove unneeded changes

* feat Expand address checks and fix scanning bch qrs

* fix: Add support for dogecoin

* feat: More modifications for Pay from Any Wallet

This change:
- Adds the pay from any wallet flow to externally signed QR codes
- Adds fixes for review comments
- Fixes failing tests

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2025-08-21 18:37:50 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 16f058e9e2
commit 13a74133f5
39 changed files with 1228 additions and 27 deletions
+232
View File
@@ -0,0 +1,232 @@
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
class AddressDetectionResult {
AddressDetectionResult({
required this.address,
this.detectedCurrency,
this.detectedWalletType,
this.amount,
this.note,
this.scheme,
this.pjUri,
this.callbackUrl,
this.callbackMessage,
required this.isValid,
this.errorMessage,
});
final String address;
final CryptoCurrency? detectedCurrency;
final WalletType? detectedWalletType;
final String? amount;
final String? note;
final String? scheme;
final String? pjUri;
final String? callbackUrl;
final String? callbackMessage;
final bool isValid;
final String? errorMessage;
}
/// Universal address detector that can identify cryptocurrency addresses from various formats
class UniversalAddressDetector {
/// Detects cryptocurrency address from various input formats
/// Supports: raw addresses, URIs, and QR codes
static AddressDetectionResult detectAddress(String input) {
if (input.trim().isEmpty) {
return AddressDetectionResult(
address: '',
detectedCurrency: null,
isValid: false,
errorMessage: 'Empty input provided',
);
}
// Try to parse as URI first
final uriResult = _detectFromUri(input);
if (uriResult.isValid) return uriResult;
// Try to detect from raw address patterns
final rawAddressResult = _detectFromRawAddress(input);
if (rawAddressResult.isValid) return rawAddressResult;
return AddressDetectionResult(
address: input,
detectedCurrency: null,
isValid: false,
errorMessage: 'Unable to detect valid cryptocurrency address',
);
}
/// Detects address from URI format (e.g., bitcoin:address?amount=0.001)
static AddressDetectionResult _detectFromUri(String input) {
try {
final uri = Uri.parse(input);
final paymentRequest = PaymentRequest.fromUri(uri);
// Determine currency from scheme
final currency = CryptoCurrency.fromString(uri.scheme.toLowerCase());
return AddressDetectionResult(
address: paymentRequest.address,
detectedCurrency: currency,
detectedWalletType: cryptoCurrencyToWalletType(currency),
amount: paymentRequest.amount,
note: paymentRequest.note,
scheme: paymentRequest.scheme,
pjUri: paymentRequest.pjUri,
callbackUrl: paymentRequest.callbackUrl,
callbackMessage: paymentRequest.callbackMessage,
isValid: true,
);
} catch (e) {
return AddressDetectionResult(
address: input,
detectedCurrency: null,
isValid: false,
errorMessage: 'Failed to parse URI: $e',
);
}
}
/// Detect address from raw address patterns
static AddressDetectionResult _detectFromRawAddress(String input) {
final cleanInput = input.trim();
// Detection patterns ordered by specificity (most specific first)
final detectionPatterns = [
// Lightning Network
_DetectionPattern(
pattern: RegExp(r'^(lnbc|LNBC)[a-km-zA-HJ-NP-Z1-9]{1,}[a-zA-Z0-9]+$'),
currency: CryptoCurrency.btcln,
),
// Bitcoin Bech32
_DetectionPattern(
pattern: RegExp(r'^bc1[a-km-zA-HJ-NP-Z1-9]{25,39}$'),
currency: CryptoCurrency.btc,
),
// Litecoin Bech32
_DetectionPattern(
pattern: RegExp(r'^ltc1[a-z0-9]{25,50}$'),
currency: CryptoCurrency.ltc,
),
// Bitcoin Cash
_DetectionPattern(
pattern: RegExp(r'^(q|p)[a-z0-9]{41}$'),
currency: CryptoCurrency.bch,
),
// Ethereum
_DetectionPattern(
pattern: RegExp(r'^0x[a-fA-F0-9]{40}$'),
currency: CryptoCurrency.eth,
),
// Nano
_DetectionPattern(
pattern: RegExp(r'^nano_[a-km-zA-HJ-NP-Z1-9]{60}$'),
currency: CryptoCurrency.nano,
),
// Banano
_DetectionPattern(
pattern: RegExp(r'^ban_[a-km-zA-HJ-NP-Z1-9]{60}$'),
currency: CryptoCurrency.banano,
),
// Tron
_DetectionPattern(
pattern: RegExp(r'^T[a-km-zA-HJ-NP-Z1-9]{33}$'),
currency: CryptoCurrency.trx,
),
// Zano alias
_DetectionPattern(
pattern: RegExp(r'^@[\w\d.-]+$'),
currency: CryptoCurrency.zano,
),
// Monero (standard) - 95 characters
_DetectionPattern(
pattern: RegExp(r'^4[a-km-zA-HJ-NP-Z1-9]{94}$'),
currency: CryptoCurrency.xmr,
),
// Monero (integrated) - 95 characters
_DetectionPattern(
pattern: RegExp(r'^8[a-km-zA-HJ-NP-Z1-9]{94}$'),
currency: CryptoCurrency.xmr,
),
// Wownero - 97 characters
_DetectionPattern(
pattern: RegExp(r'^W[a-km-zA-HJ-NP-Z1-9]{96}$'),
currency: CryptoCurrency.wow,
),
// Zano (long addresses) - 97 characters
_DetectionPattern(
pattern: RegExp(r'^Z[a-km-zA-HJ-NP-Z1-9]{96}$'),
currency: CryptoCurrency.zano,
),
// Decred
_DetectionPattern(
pattern: RegExp(r'^(D|T|S)[ksecS][a-km-zA-HJ-NP-Z1-9]+$'),
currency: CryptoCurrency.dcr,
),
// Bitcoin P2PKH/P2SH (legacy formats)
_DetectionPattern(
pattern: RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.btc,
),
// Litecoin P2PKH/P2SH
_DetectionPattern(
pattern: RegExp(r'^(L|M|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.ltc,
),
// Solana (Base58 format, 32-44 chars)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{32,44}$'),
currency: CryptoCurrency.sol,
),
];
// Test each pattern in order of specificity
for (final pattern in detectionPatterns) {
if (pattern.pattern.hasMatch(cleanInput)) {
return AddressDetectionResult(
address: cleanInput,
detectedCurrency: pattern.currency,
detectedWalletType: cryptoCurrencyToWalletType(pattern.currency),
isValid: true,
);
}
}
return AddressDetectionResult(
address: cleanInput,
detectedCurrency: null,
isValid: false,
);
}
}
class _DetectionPattern {
const _DetectionPattern({
required this.pattern,
required this.currency,
});
final RegExp pattern;
final CryptoCurrency currency;
}