mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-14 09:14:05 +01:00
CW-1523-AnyPay Redesign (#3383)
* feat: Add network selector and compatible networks screen to send page for EVMs * feat: add redesigned payanything flow and handle cases for evms, solana and tron * feat: adding the swap part to the entire flow * refactor: update navigation and error handling in send and swap flows * adjust structuring and use the chain badge icons, also fix crypto and currency values not consistent on single network picker * fix: modify guard for non-evm chains and minor cleanup * use multipicker layout for receive section, streamline send to available balances and adjust colors as needed * handle btc case for picker, update style and images across * fix all pending issues with scanning qr and network handling for both evm and non evm, plus token and amount resolving * fix all pending issues with scanning qr and network handling for both evm and non evm, plus token and amount resolving * fix: expand fetch for tokens across networks and fix other issues regarding scanning from homepage * auto-reformat * fix conflict * handle evm qr scan from homepage and add guards for swap and switch flows * chore: Cleanup * chore: set estimated fee to 0 when switching wallet * fix merge conflicts * fix to review comments and reformatting for lints * fix: root cause for token bug reported * chore: cleanup * feat: add improved anypay swap screen which focuses on the receive amount fix: fiat input toggle erro (also present on prod) reformat: anypay related widgets * feat: add alert for unsupported networks in payment requests * fix: prevent rate and provider from showing until amount is calculated and loaded fix: remove send icon in rate widget and align to center fix: adjust network badge in swap confirm widget * feat: embed tokens for solana and tron in QR generated for swap from external * test: add unit tests for Tron, Solana, and ERC681 URI handling and detection * fix: review comments * fix: handle more edgecases * feat: enhance ERC681URI to support raw token amounts and decimals * refactor: streamline EVM chain ID retrieval in send page and recipient network row * fix: minor review comments --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
parent
931b6fb9d2
commit
90194f7136
@@ -11,4 +11,9 @@ class Debounce {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(duration, action);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
import 'package:cw_core/amount/amount_sanitizer.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:cake_wallet/nano/nano.dart';
|
||||
import "package:cake_wallet/nano/nano.dart";
|
||||
import "package:cw_core/amount/amount_sanitizer.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
import "package:cw_core/lnurl.dart";
|
||||
import "package:cw_core/payment_uris.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
|
||||
class PaymentRequest {
|
||||
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
|
||||
{this.callbackUrl, this.callbackMessage, this.contractAddress});
|
||||
PaymentRequest(
|
||||
this.address,
|
||||
this.amount,
|
||||
this.note,
|
||||
this.scheme,
|
||||
this.pjUri, {
|
||||
this.callbackUrl,
|
||||
this.callbackMessage,
|
||||
this.contractAddress,
|
||||
this.chainId,
|
||||
this.rawTokenAmount,
|
||||
});
|
||||
|
||||
factory PaymentRequest.fromString(String input) {
|
||||
try {
|
||||
return PaymentRequest.fromBolt11(input);
|
||||
} catch (_) {
|
||||
return PaymentRequest.fromUri(Uri.parse(input));
|
||||
try {
|
||||
return PaymentRequest.fromUri(Uri.parse(input));
|
||||
} catch (_) {
|
||||
return PaymentRequest(input, "", "", "", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factory PaymentRequest.fromBolt11(String invoice) {
|
||||
final amount = getBolt11Amount(invoice) ?? Money.zero(CryptoCurrency.btcln);
|
||||
return PaymentRequest(invoice, amount.toString(), '', 'lightning', null);
|
||||
return PaymentRequest(invoice, amount.toString(), "", "lightning", null);
|
||||
}
|
||||
|
||||
factory PaymentRequest.fromUri(Uri? uri) {
|
||||
@@ -32,13 +48,15 @@ class PaymentRequest {
|
||||
String? callbackMessage;
|
||||
String? pjUri;
|
||||
String? contractAddress;
|
||||
int? chainId;
|
||||
String? rawTokenAmount;
|
||||
|
||||
if (uri != null) {
|
||||
if (uri.queryParameters['pj'] != null) {
|
||||
if (uri.queryParameters["pj"] != null) {
|
||||
pjUri = uri.toString();
|
||||
}
|
||||
|
||||
address = uri.queryParameters['address'] ?? uri.path;
|
||||
address = uri.queryParameters["address"] ?? uri.path;
|
||||
try {
|
||||
final lnAmount = getBolt11Amount(uri.path) ?? Money.zero(CryptoCurrency.btcln);
|
||||
|
||||
@@ -47,27 +65,30 @@ class PaymentRequest {
|
||||
}
|
||||
} catch (_) {}
|
||||
if (amount.isEmpty) {
|
||||
amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
|
||||
amount = uri.queryParameters["tx_amount"] ?? uri.queryParameters["amount"] ?? "";
|
||||
}
|
||||
note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
|
||||
note = uri.queryParameters["tx_description"] ?? uri.queryParameters["message"] ?? "";
|
||||
scheme = uri.scheme;
|
||||
callbackUrl = uri.queryParameters['callback'];
|
||||
callbackMessage = uri.queryParameters['callbackMessage'];
|
||||
walletType = uri.queryParameters['type'];
|
||||
callbackUrl = uri.queryParameters["callback"];
|
||||
callbackMessage = uri.queryParameters["callbackMessage"];
|
||||
walletType = uri.queryParameters["type"];
|
||||
|
||||
if (scheme == "ethereum") {
|
||||
final paymentUri = ERC681URI.fromUri(uri);
|
||||
final hasExplicitChainId = uri.path.contains("@");
|
||||
|
||||
address = paymentUri.address;
|
||||
amount = paymentUri.amount;
|
||||
contractAddress = paymentUri.contractAddress;
|
||||
chainId = hasExplicitChainId ? paymentUri.chainId : null;
|
||||
rawTokenAmount = paymentUri.rawTokenAmount;
|
||||
} else if (scheme == "tron") {
|
||||
final token = uri.queryParameters['token'];
|
||||
final token = uri.queryParameters["token"];
|
||||
if (token != null && token.isNotEmpty) {
|
||||
contractAddress = token;
|
||||
}
|
||||
} else if (scheme == "solana") {
|
||||
final splToken = uri.queryParameters['spl-token'];
|
||||
final splToken = uri.queryParameters["spl-token"];
|
||||
if (splToken != null && splToken.isNotEmpty) {
|
||||
contractAddress = splToken;
|
||||
}
|
||||
@@ -99,6 +120,8 @@ class PaymentRequest {
|
||||
callbackUrl: callbackUrl,
|
||||
callbackMessage: callbackMessage,
|
||||
contractAddress: contractAddress,
|
||||
chainId: chainId,
|
||||
rawTokenAmount: rawTokenAmount,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,23 +133,53 @@ class PaymentRequest {
|
||||
final String? callbackUrl;
|
||||
final String? callbackMessage;
|
||||
final String? contractAddress;
|
||||
final int? chainId;
|
||||
final String? rawTokenAmount;
|
||||
|
||||
String? resolveTokenAmount(CryptoCurrency token) {
|
||||
if (amount.isNotEmpty) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
if (rawTokenAmount == null || rawTokenAmount!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final raw = BigInt.tryParse(rawTokenAmount!);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1e9 whole tokens is beyond any real payment, so a larger result means the QR
|
||||
// came from an old app version that encoded with 18 decimals regardless of token.
|
||||
if (token.decimals != 18 && raw > BigInt.from(10).pow(token.decimals + 9)) {
|
||||
printV("resolveTokenAmount: reading $rawTokenAmount with legacy 18 decimals");
|
||||
return formatFixed(raw, 18);
|
||||
}
|
||||
|
||||
return formatFixed(raw, token.decimals);
|
||||
}
|
||||
|
||||
/// Checks if the amount string is already in a usable format (e.g., "123.45") and doesn't need to be converted from raw format.
|
||||
///
|
||||
/// This was causing an error for us when we scan Nano QRs with amounts in them, the amounts are already in usable format so when the parsing was done, it returns 0 wrongly.
|
||||
static bool _isAlreadyUsableAmount(String amount) {
|
||||
if (amount.isEmpty) return false;
|
||||
if (amount.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final parsed = double.tryParse(amount.sanitized());
|
||||
if (parsed == null) return false;
|
||||
|
||||
// Check if the amount contains a decimal point and is a reasonable number,
|
||||
// it's likely already in usable format rather than raw format
|
||||
// Raw amounts are typically very large integers without decimal points
|
||||
if (amount.contains('.') && parsed > 0 && parsed < 1000000000) return true;
|
||||
if (parsed == null) {
|
||||
return false;
|
||||
}
|
||||
if (amount.contains(".") && parsed > 0 && parsed < 1000000000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a small integer (less than 1 billion), it's likely already usable
|
||||
if (parsed == parsed.toInt() && parsed < 1000000000) return true;
|
||||
if (parsed == parsed.toInt() && parsed < 1000000000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,24 @@ class TokenUtilities {
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? findSolanaTokenMint(CryptoCurrency currency) {
|
||||
if (currency is SPLToken) return currency.mintAddress;
|
||||
|
||||
for (final token in loadDefaultSolTokensForSwap()) {
|
||||
if (_matchesCurrency(token, currency)) return token.mintAddress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? findTronTokenContract(CryptoCurrency currency) {
|
||||
if (currency is TronToken) return currency.contractAddress;
|
||||
|
||||
for (final token in loadDefaultTronTokensForSwap()) {
|
||||
if (_matchesCurrency(token, currency)) return token.contractAddress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isNativeToken(CryptoCurrency currency) {
|
||||
final title = currency.title.toLowerCase();
|
||||
final tag = currency.tag?.toLowerCase();
|
||||
@@ -289,9 +307,7 @@ class TokenUtilities {
|
||||
final allTokens = <CryptoCurrency>[baseCurrency];
|
||||
final addedAddresses = <String>{};
|
||||
|
||||
// Handle EVM networks
|
||||
if (isEVMCompatibleChain(network)) {
|
||||
// First, collect all user tokens
|
||||
final userTokens = await _getUserTokensForNetwork(baseCurrency);
|
||||
for (final token in userTokens) {
|
||||
if (token is Erc20Token) {
|
||||
@@ -302,7 +318,17 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
// Then add tokens from CryptoCurrency.all that don't duplicate user tokens
|
||||
if (evm != null) {
|
||||
final chainId = evm!.getChainIdByWalletType(network);
|
||||
for (final token in evm!.getDefaultTokensByChainId(chainId)) {
|
||||
final address = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(address)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tokens from CryptoCurrency.all that don't duplicate user tokens
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
// Match by tag for POL/BASE, match by title==tag for ETH
|
||||
final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
|
||||
@@ -320,10 +346,7 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Solana network
|
||||
else if (network == WalletType.solana) {
|
||||
} else if (network == WalletType.solana) {
|
||||
final userSolTokens = await loadAllUniqueSolTokens();
|
||||
for (final token in userSolTokens) {
|
||||
final mintAddress = token.mintAddress.toLowerCase();
|
||||
@@ -332,6 +355,13 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
for (final token in loadDefaultSolTokensForSwap()) {
|
||||
final mintAddress = token.mintAddress.toLowerCase();
|
||||
if (addedAddresses.add(mintAddress)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
if (currency.tag?.toLowerCase() == 'sol') {
|
||||
if (currency is SPLToken) {
|
||||
@@ -356,6 +386,13 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
for (final token in loadDefaultTronTokensForSwap()) {
|
||||
final contractAddress = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(contractAddress)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
if (currency.tag?.toLowerCase() == 'trx') {
|
||||
if (currency is TronToken) {
|
||||
|
||||
Reference in New Issue
Block a user