Files
hash-wallet/lib/core/universal_address_detector.dart
T
c51c2dc105 Pay Anything Enhancements (#2529)
* fix: update Bitcoin and Litecoin Bech32 address patterns for improved detection for pay anything flow

* feat: add Litecoin MWEB address detection patterns for pay anything flow

* feat: Add support for ERC-681 payment scheme to QR and link to exchange send from external QR

* feat: Introduce TokenUtilities to centralize token management across viewmodels

- Added TokenUtilities class to centralize token-related operations for EVM, Solana, and Tron tokens.
- Refactored existing methods in ExchangeTradeViewModel and SendViewModel to utilize TokenUtilities.
- Updated PaymentRequest to include contractAddress.
- Enhanced payment QR scanning to fetch token details for QRs with contract addresses - ERC-681 QR scheme

* fix: Prefill amount with other data when app is brought up via payment deeplink on QR scan

* fix: Reduce trade not created errors WIP

* feat: Add exchange provider logging functionality

* feat: Enhance ERC681URI with amount normalization, prevents parsing issues with various input formats for amounts

* - Use regex from address validator for btc/ltc manual checks
- Add check for solana addresses
- Handle failures in normalizeToInteger method
- fix issue with address only being added when there's an amount

* wrap in try/catch just in case [skip ci]

* Update lib/utils/token_utilities.dart

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2025-09-24 20:24:52 +03:00

243 lines
6.9 KiB
Dart

import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/core/address_validator.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 (P2PKH, P2SH, Bech32, Silent Payments)
_DetectionPattern(
pattern: RegExp('^(?:'
'1[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|3[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|(?:bc|tb)1q[ac-hj-np-z02-9]{25,39}'
'|(?:bc|tb)1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
'|(?:bc|tb)1q[ac-hj-np-z02-9]{40,80}'
'|${AddressValidator.silentPaymentAddressPatternMainnet}'
'|${AddressValidator.silentPaymentAddressPatternTestnet}'
')\$'),
currency: CryptoCurrency.btc,
),
// Litecoin (Legacy, Bech32, MWEB)
_DetectionPattern(
pattern: RegExp('^(?:'
'L[a-km-zA-HJ-NP-Z1-9]{25,34}'
'|[3M][a-km-zA-HJ-NP-Z1-9]{25,34}'
'|ltc1q[ac-hj-np-z02-9]{25,39}'
'|ltc1p(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89})'
'|ltc1q[ac-hj-np-z02-9]{40,80}'
'|${AddressValidator.mWebAddressPattern}'
')\$'),
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,
),
// Dogecoin P2PKH
_DetectionPattern(
pattern: RegExp(r'^D[a-km-zA-HJ-NP-Z1-9]{25,34}$'),
currency: CryptoCurrency.doge,
),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,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;
}