Files
hash-wallet/lib/core/universal_address_detector.dart
T
jwinterm 04203654f1 Rename Flutter project + binary: cake_wallet -> hash_wallet
The big one. Touches 760+ files. Locks in the user's decision to break
import-line compatibility with upstream cake-tech/cake_wallet in exchange
for a binary named hash_wallet on every platform.

Changes:
- pubspec_description.yaml: project name cake_wallet -> hash_wallet
- 762 Dart files: every 'package:cake_wallet/...' import rewritten to
  'package:hash_wallet/...' (lib/, cw_*/lib/, tool/, test/, integration_test/)
- linux/CMakeLists.txt: BINARY_NAME cake_wallet -> hash_wallet
- windows/CMakeLists.txt: project + BINARY_NAME (CakeWallet -> HashWallet)
- windows/runner/Runner.rc: company / file / product strings to Hash Wallet
  + Such Software LLC copyright
- macos/Runner.xcodeproj/project_base.pbxproj: cake_wallet_new.app refs
  -> hash_wallet.app (macOS output bundle name)
- com.suchsoftware.HashWallet.yml (Flatpak): all binary path refs
  cake_wallet -> hash_wallet (e.g. /app/hash_wallet/hash_wallet)
- linux/com.suchsoftware.HashWallet.desktop: Exec=hash_wallet
- scripts/android/copy_monero_deps.sh: CW_DIR target path
- configure_hash_wallet.sh: error message typo (old filename)

INTENTIONALLY UNCHANGED:
- cw_* subpackage names (cw_core, cw_bitcoin, cw_wownero, etc.) — they
  remain as the historical 'cake wallet' naming; renaming them would
  cascade through every sub-package's internal structure
- Method channel name 'com.cake_wallet/native_utils' in iOS/macOS/Android
  AppDelegates and MainActivity — it's a string identifier that has to
  match the Dart side exactly. Renaming requires both-sides change; it's
  internal-only and not user-visible.
- ghcr.io/cake-tech/cake_wallet:debian13-... docker image references in
  build scripts — that's Cake Labs' published image we use as the build
  environment, not ours.

User data directory now follows the new binary name:
  ~/.local/share/hash_wallet/  (was ~/.local/share/cake_wallet/)
  ~/.config/hash_wallet/       (was ~/.config/cake_wallet/)
Existing users on the old name will need migration if any exist.
2026-05-16 11:33:25 -04:00

292 lines
8.6 KiB
Dart

import 'package:hash_wallet/utils/payment_request.dart';
import 'package:hash_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;
}