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:
David Adegoke
2026-08-11 02:16:12 +02:00
committed by GitHub
co-authored by Robert Malikowski
parent 931b6fb9d2
commit 90194f7136
69 changed files with 4534 additions and 2682 deletions
@@ -269,5 +269,89 @@ void main() {
expect(result.errorMessage, 'Unable to detect valid cryptocurrency address');
});
});
group("EVM chainId URI Detection", () {
const recipient = "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6";
const contract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
test("detects the polygon network from an ERC-681 chainId", () {
const uri = "ethereum:$recipient@137?value=1.0e18";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedWalletType, WalletType.polygon);
expect(result.chainId, 137);
expect(result.address, recipient);
expect(result.amount, "1");
});
test("detects the base network from an ERC-681 chainId", () {
const uri = "ethereum:$recipient@8453";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedWalletType, WalletType.base);
expect(result.chainId, 8453);
expect(result.address, recipient);
});
test("extracts the recipient from an ERC-681 token transfer URI", () {
const uri = "ethereum:$contract@1/transfer?address=$recipient&uint256=1000000";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedWalletType, WalletType.ethereum);
expect(result.chainId, 1);
expect(result.address, recipient);
});
test("preserves an unsupported chainId so routing can reject it", () {
const uri = "ethereum:$recipient@999999";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedWalletType, WalletType.ethereum);
expect(result.chainId, 999999);
expect(result.address, recipient);
});
});
group("Token URI Detection", () {
test("detects a Solana pay URI with an spl-token param", () {
const address = "4Nd1mYvNQyJ8BDVXLgkvSGpVdQMZ3hxwVFkfwXNq6Wgk";
const uri =
"solana:$address?amount=1.5&spl-token=Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedCurrency, CryptoCurrency.sol);
expect(result.detectedWalletType, WalletType.solana);
expect(result.address, address);
expect(result.amount, "1.5");
});
test("detects a Tron URI with a token param", () {
const address = "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL";
const uri = "tron:$address?amount=2&token=TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedCurrency, CryptoCurrency.trx);
expect(result.detectedWalletType, WalletType.tron);
expect(result.address, address);
expect(result.amount, "2");
});
test("detects a bare Solana URI without params", () {
const address = "4Nd1mYvNQyJ8BDVXLgkvSGpVdQMZ3hxwVFkfwXNq6Wgk";
const uri = "solana:$address";
final result = UniversalAddressDetector.detectAddress(uri);
expect(result.isValid, true);
expect(result.detectedWalletType, WalletType.solana);
expect(result.address, address);
expect(result.amount, "");
});
});
});
}
+61 -8
View File
@@ -1,16 +1,18 @@
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/erc20_token.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
group('PaymentRequest', () {
group('Ethereum URIs', () {
test("extract address and amount from EIP681 Uri with contract", () {
group("PaymentRequest", () {
group("Ethereum URIs", () {
test("extract address and raw token amount from EIP681 Uri with contract", () {
final uri = Uri.parse(
"ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=2000000000000000000");
final paymentRequest = PaymentRequest.fromUri(uri);
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
expect(paymentRequest.amount, "2");
expect(paymentRequest.amount, "");
expect(paymentRequest.rawTokenAmount, "2000000000000000000");
});
test("extract address and amount from EIP681 Uri", () {
@@ -39,13 +41,14 @@ void main() {
expect(paymentRequest.amount, "");
});
test("extract address and amount from EIP681 Uri with contract and no chainId", () {
test("extract address and raw token amount from EIP681 Uri with contract and no chainId", () {
final uri = Uri.parse(
"ethereum:0x1234567890abcdef1234567890abcdef12345678/transfer?address=0xabcdef1234567890abcdef1234567890abcdef12&uint256=1000000000000000000");
final paymentRequest = PaymentRequest.fromUri(uri);
expect(paymentRequest.address, "0xabcdef1234567890abcdef1234567890abcdef12");
expect(paymentRequest.amount, "1");
expect(paymentRequest.amount, "");
expect(paymentRequest.rawTokenAmount, "1000000000000000000");
});
test("extract address from minimal EIP681 Uri", () {
@@ -56,5 +59,55 @@ void main() {
expect(paymentRequest.amount, "");
});
});
group("resolveTokenAmount", () {
final usdt = Erc20Token(
name: "Tether",
symbol: "USDT",
contractAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
decimal: 6,
);
final dai = Erc20Token(
name: "Dai",
symbol: "DAI",
contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
decimal: 18,
);
test("converts the raw uint256 using the token decimals", () {
final uri = Uri.parse(
"ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=1000000");
final paymentRequest = PaymentRequest.fromUri(uri);
expect(paymentRequest.resolveTokenAmount(usdt), "1");
});
test("converts an 18 decimal raw amount unchanged", () {
final paymentRequest =
PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "1500000000000000000");
expect(paymentRequest.resolveTokenAmount(dai), "1.5");
});
test("falls back to the legacy 18 decimal reading for implausibly large amounts", () {
final paymentRequest = PaymentRequest("addr", "", "", "ethereum", null,
rawTokenAmount: "50000000000000000000");
expect(paymentRequest.resolveTokenAmount(usdt), "50");
});
test("prefers the decimal amount over the raw token amount", () {
final paymentRequest =
PaymentRequest("addr", "2.5", "", "ethereum", null, rawTokenAmount: "1000000");
expect(paymentRequest.resolveTokenAmount(usdt), "2.5");
});
test("returns null when there is no amount at all", () {
final paymentRequest = PaymentRequest("addr", "", "", "ethereum", null);
expect(paymentRequest.resolveTokenAmount(usdt), null);
});
});
});
}
+84
View File
@@ -0,0 +1,84 @@
import "package:cake_wallet/utils/token_utilities.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/spl_token.dart";
import "package:cw_core/tron_token.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
group("TokenUtilities", () {
group("findTronTokenContract", () {
test("returns the contract from a TronToken instance", () {
final token = TronToken(
name: "Test Token",
symbol: "TST",
contractAddress: "TContractAddress123",
decimal: 6,
);
expect(TokenUtilities.findTronTokenContract(token), "TContractAddress123");
});
test("resolves a bare currency from the default token list", () {
expect(
TokenUtilities.findTronTokenContract(CryptoCurrency.usdttrc20),
"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
);
});
test("returns null for native TRX", () {
expect(TokenUtilities.findTronTokenContract(CryptoCurrency.trx), null);
});
test("does not resolve a token from another network", () {
expect(TokenUtilities.findTronTokenContract(CryptoCurrency.usdtSol), null);
});
});
group("findSolanaTokenMint", () {
test("returns the mint from an SPLToken instance", () {
final token = SPLToken(
name: "Test Token",
symbol: "TST",
mintAddress: "MintAddress123",
decimal: 6,
mint: "tst",
);
expect(TokenUtilities.findSolanaTokenMint(token), "MintAddress123");
});
test("resolves a bare currency from the default token list", () {
expect(
TokenUtilities.findSolanaTokenMint(CryptoCurrency.usdtSol),
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
);
});
test("returns null for native SOL", () {
expect(TokenUtilities.findSolanaTokenMint(CryptoCurrency.sol), null);
});
test("does not resolve a token from another network", () {
expect(TokenUtilities.findSolanaTokenMint(CryptoCurrency.usdttrc20), null);
});
});
group("findErc20TokenForSwap", () {
test("returns the same instance for an Erc20Token", () {
final token = Erc20Token(
name: "Test Token",
symbol: "TST",
contractAddress: "0x0000000000000000000000000000000000000001",
decimal: 18,
);
expect(TokenUtilities.findErc20TokenForSwap(token), same(token));
});
test("returns null for native ETH", () {
expect(TokenUtilities.findErc20TokenForSwap(CryptoCurrency.eth), null);
});
});
});
}