Files
hash-wallet/lib/core/universal_address_detector.dart
T

292 lines
8.6 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/lnurl.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/currency_for_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,
this.chainId,
});
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;
final int? chainId;
}
/// 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);
CryptoCurrency currency;
int? chainId;
if (uri.scheme.toLowerCase() == 'ethereum') {
final erc681 = ERC681URI.fromUri(uri);
chainId = erc681.chainId;
currency = getCryptoCurrencyByChainId(chainId);
} else {
currency = CryptoCurrency.fromString(uri.scheme.toLowerCase());
chainId = getChainIdByCryptoCurrency(currency);
}
final walletType = cryptoCurrencyOrTokenToWalletType(currency);
return AddressDetectionResult(
address: paymentRequest.address,
detectedCurrency: currency,
detectedWalletType: walletType,
amount: paymentRequest.amount,
note: paymentRequest.note,
scheme: paymentRequest.scheme,
pjUri: paymentRequest.pjUri,
callbackUrl: paymentRequest.callbackUrl,
callbackMessage: paymentRequest.callbackMessage,
isValid: true,
chainId: chainId,
);
} 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 (Bolt11 Invoice format)
_DetectionPattern(
pattern: RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false),
currency: CryptoCurrency.btcln,
),
// Lightning Network (Bolt12 Offer format)
_DetectionPattern(
pattern: RegExp(r'^(lightning:)?(lno1)[a-z0-9]+$', caseSensitive: false),
currency: CryptoCurrency.btcln,
),
// Lightning Address (email format)
_DetectionPattern(
pattern: RegExp(r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'),
currency: CryptoCurrency.btcln,
),
// Lightning Address (LNURL format)
_DetectionPattern(
pattern: RegExp(r'^(lightning:)?(lnurl)[a-z0-9]+$', caseSensitive: false),
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'^([qp])[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'^([DTS])[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)) {
final walletType = cryptoCurrencyOrTokenToWalletType(pattern.currency);
final chainId = getChainIdByCryptoCurrency(pattern.currency);
final amount = _getAmountFromInvoice(cleanInput, pattern.currency);
return AddressDetectionResult(
address: cleanInput,
detectedCurrency: pattern.currency,
detectedWalletType: walletType,
isValid: true,
chainId: chainId,
amount: amount,
);
}
}
return AddressDetectionResult(
address: cleanInput,
detectedCurrency: null,
isValid: false,
);
}
static String? _getAmountFromInvoice(String input, CryptoCurrency currency) {
if (currency != CryptoCurrency.btcln) return null;
try {
return getBolt11Amount(input).toString();
} catch (e) {
return null;
}
}
}
class _DetectionPattern {
const _DetectionPattern({
required this.pattern,
required this.currency,
});
final RegExp pattern;
final CryptoCurrency currency;
}