diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 4ca12bdc3..effeb13a8 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -283,8 +283,8 @@ class ERC681URI extends PaymentURI { } else { final valueParam = uri.queryParameters["value"]; if (valueParam != null) { - final normalized = _normalizeToIntegerWei(valueParam); - formatedAmount = formatFixed(BigInt.parse(normalized), 18); + final normalized = BigInt.tryParse(_normalizeToIntegerWei(valueParam)); + formatedAmount = normalized != null ? formatFixed(normalized, 18) : ""; } else { formatedAmount = uri.queryParameters["amount"] ?? ""; } @@ -313,9 +313,7 @@ class ERC681URI extends PaymentURI { final targetAddress = contractAddress ?? address; uri += targetAddress; - if (chainId != 1) { - uri += "@$chainId"; - } + uri += "@$chainId"; if (contractAddress != null) { uri += "/transfer"; @@ -364,14 +362,18 @@ class ERC681URI extends PaymentURI { } } - static int _getChainID(String path) => int.parse( + static int _getChainID(String path) => + int.tryParse( RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1", - ); + ) ?? + 1; static (bool, String) _getTargetAddress(String path) { - final targetAddress = - RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!; - return (path.contains("/"), targetAddress); + // I saw in the schema (thanks Konsti) that EIP-681 allows an optional "pay-" prefix before the target address, so adding a check for it here + final cleaned = path.startsWith("pay-") ? path.substring(4) : path; + final match = RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(cleaned); + final targetAddress = match?.group(0) ?? cleaned.split("@").first.split("/").first; + return (cleaned.contains("/"), targetAddress); } /// Normalize an input amount into an integer wei string. diff --git a/cw_core/test/payment_uris_test.dart b/cw_core/test/payment_uris_test.dart index 88e5e4a45..a61bab1cf 100644 --- a/cw_core/test/payment_uris_test.dart +++ b/cw_core/test/payment_uris_test.dart @@ -78,13 +78,13 @@ void main() { const recipient = "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"; const contract = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; - test("builds a plain native transfer on mainnet", () { + test("includes the mainnet chainId on a plain native transfer", () { final uri = ERC681URI(chainId: 1, address: recipient, amount: "", contractAddress: null); - expect(uri.toString(), "ethereum:$recipient"); + expect(uri.toString(), "ethereum:$recipient@1"); }); - test("appends the chainId for non-mainnet native transfers", () { + test("appends the chainId for native transfers", () { final uri = ERC681URI(chainId: 137, address: recipient, amount: "", contractAddress: null); expect(uri.toString(), "ethereum:$recipient@137"); @@ -93,7 +93,22 @@ void main() { test("emits the native amount in ERC-681 scientific notation", () { final uri = ERC681URI(chainId: 1, address: recipient, amount: "1", contractAddress: null); - expect(uri.toString(), "ethereum:$recipient?value=1.0e18"); + expect(uri.toString(), "ethereum:$recipient@1?value=1.0e18"); + }); + + test("includes the mainnet chainId in the transfer form", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "200.172148", + contractAddress: contract, + tokenDecimals: 6, + ); + + expect( + uri.toString(), + "ethereum:$contract@1/transfer?address=$recipient&amount=200.172148", + ); }); test("builds the transfer form for a token", () { @@ -189,7 +204,7 @@ void main() { rawTokenAmount: "1000000", ); - expect(uri.toString(), "ethereum:$contract/transfer?address=$recipient&uint256=1000000"); + expect(uri.toString(), "ethereum:$contract@1/transfer?address=$recipient&uint256=1000000"); }); test("parses a scientific notation value", () { diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 1500e2ca6..b9a83c75d 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -64,6 +64,7 @@ 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/currency_for_wallet_type.dart"; +import "package:cw_core/erc20_token.dart"; import "package:cw_core/lnurl.dart"; import "package:cw_core/payment_uris.dart"; import "package:cw_core/transaction_priority.dart"; @@ -247,35 +248,17 @@ class _NewSendPageState extends State { if (widget.initialPaymentRequest != null) { if (_isInitialRequestTypeSameAsCurrentWallet()) { - _addressControllers[0].text = widget.initialPaymentRequest!.address; - // _memoControllers[0].text = widget.initialPaymentRequest!.note; - _applyNote(widget.initialPaymentRequest!.note, 0); final contractAddress = widget.initialPaymentRequest!.contractAddress; if (contractAddress != null && contractAddress.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) { return; } - final token = await TokenUtilities.findTokenByAddress( - walletType: widget.sendViewModel.wallet.type, - address: contractAddress, - ); - if (!mounted) { - return; - } - - if (token == null) { - _showUnsupportedTokenAlert(); - return; - } - await widget.sendViewModel.fetchTokenForContractAddress(contractAddress); - if (!mounted) { - return; - } - _amountControllers[0].text = - widget.initialPaymentRequest!.resolveTokenAmount(token) ?? ""; + await _applyPaymentSelectingCurrency(widget.initialPaymentRequest!, null); }); } else { + _addressControllers[0].text = widget.initialPaymentRequest!.address; + _applyNote(widget.initialPaymentRequest!.note, 0); _amountControllers[0].text = widget.initialPaymentRequest!.amount; } } else { @@ -1049,8 +1032,9 @@ class _NewSendPageState extends State { Future _handleManualNetworkSelection(ChainInfo target) async { final address = _addressControllers[_selectedOutput].text.trim(); - final amount = _amountControllers[_selectedOutput].text; final note = _memoControllers[_selectedOutput].text; + final isTokenSelected = widget.sendViewModel.selectedCryptoCurrency is Erc20Token; + final amount = isTokenSelected ? "" : _amountControllers[_selectedOutput].text; final paymentRequest = PaymentRequest(address, amount, note, "", null); await _handleEvmNetworkFlow(target, paymentRequest); } @@ -1153,7 +1137,7 @@ class _NewSendPageState extends State { if (isCrossChain) { await _handleEvmNetworkFlow(targetChain, paymentRequest); } else if (widget.sendViewModel.isEVMWallet) { - _applyPaymentRequest(paymentRequest); + await _applyPaymentSelectingCurrency(paymentRequest, null); } else { await _showEvmNetworkPicker(paymentRequest, result.walletType); } @@ -1328,8 +1312,12 @@ class _NewSendPageState extends State { String? amountOverride; final contract = paymentRequest.contractAddress; if (contract != null && contract.isNotEmpty) { + final walletType = widget.sendViewModel.wallet.type; + final lookupType = evm != null && isEVMCompatibleChain(walletType) + ? (evm!.getWalletTypeByChainId(_currentEvmChainIdOrMainnet()) ?? walletType) + : walletType; final token = await TokenUtilities.findTokenByAddress( - walletType: widget.sendViewModel.wallet.type, + walletType: lookupType, address: contract, ); if (!mounted) { @@ -1337,13 +1325,21 @@ class _NewSendPageState extends State { } if (token == null) { + final rerouted = await _rerouteChainlessContractPayment(paymentRequest); + if (rerouted || !mounted) { + return; + } _showUnsupportedTokenAlert(); return; } - await widget.sendViewModel.fetchTokenForContractAddress(contract); + await widget.sendViewModel.fetchTokenForContractAddress(contract, walletType: lookupType); amountOverride = paymentRequest.resolveTokenAmount(token); } else if (fallbackCurrency != null) { widget.sendViewModel.setSelectedCryptoCurrency(fallbackCurrency.title); + if (paymentRequest.amount.isEmpty) { + widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount(""); + _amountControllers[_selectedOutput].clear(); + } } if (!mounted) { return; @@ -1351,6 +1347,44 @@ class _NewSendPageState extends State { _applyPaymentRequest(paymentRequest, amountOverride: amountOverride); } + Future _rerouteChainlessContractPayment(PaymentRequest paymentRequest) async { + if (evm == null || paymentRequest.chainId != null) { + return false; + } + + if (paymentRequest.scheme.toLowerCase() != "ethereum") { + return false; + } + + final contract = paymentRequest.contractAddress; + if (contract == null || contract.isEmpty) { + return false; + } + + final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type) + ? _currentEvmChainIdOrMainnet() + : null; + + // QRs from old app versions omit the chainId on mainnet, so a contract the current + // network does not know may still belong to another EVM network + final chainId = await TokenUtilities.findEvmChainIdForContract( + contract, + excludingChainId: currentChainId, + ); + if (chainId == null || !mounted) { + return false; + } + + final targetChain = evm!.getChainInfoByChainId(chainId); + if (targetChain == null) { + return false; + } + + printV("chainless contract payment rerouted to chainId $chainId"); + await _handleEvmNetworkFlow(targetChain, paymentRequest); + return true; + } + Future _completeWalletSwitch( WalletInfo wallet, PaymentFlowResult result, diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index 7fedd27a0..b70a061a7 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -8,6 +8,7 @@ import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/spl_token.dart'; import 'package:cw_core/tron_token.dart'; +import "package:cw_core/utils/print_verbose.dart"; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -163,6 +164,37 @@ class TokenUtilities { return null; } + static Future findEvmChainIdForContract( + String contractAddress, { + int? excludingChainId, + }) async { + if (evm == null || contractAddress.isEmpty) { + return null; + } + + try { + for (final chain in evm!.getAllChains()) { + if (chain.chainId == excludingChainId) { + continue; + } + + final walletType = evm!.getWalletTypeByChainId(chain.chainId); + if (walletType == null) { + continue; + } + + final token = await findTokenByAddress(walletType: walletType, address: contractAddress); + if (token != null) { + return chain.chainId; + } + } + } catch (e) { + printV("findEvmChainIdForContract failed: $e"); + } + + return null; + } + static Future> _openEvmTokensBoxFor(WalletInfo walletInfo) async { final walletKey = walletInfo.name.replaceAll(' ', '_'); final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 40c04e1f9..abfec5f95 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -126,9 +126,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor super(appStore: _appStore) { outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); - unspentCoinsListViewModel - .initialSetup(); - // .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections()); + unspentCoinsListViewModel.initialSetup(); + // .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections()); reaction((_) { if (isEVMCompatibleChain(wallet.type)) { @@ -1719,9 +1718,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String? payjoinUri; @action - Future fetchTokenForContractAddress(String contractAddress) async { + Future fetchTokenForContractAddress(String contractAddress, + {WalletType? walletType}) async { final token = await TokenUtilities.findTokenByAddress( - walletType: wallet.type, + walletType: walletType ?? wallet.type, address: contractAddress, ); diff --git a/test/utils/anypay_matrix_test.dart b/test/utils/anypay_matrix_test.dart new file mode 100644 index 000000000..1bbe9fc39 --- /dev/null +++ b/test/utils/anypay_matrix_test.dart @@ -0,0 +1,344 @@ +import "package:cake_wallet/core/universal_address_detector.dart"; +import "package:cake_wallet/utils/payment_request.dart"; +import "package:cw_core/erc20_token.dart"; +import "package:cw_core/payment_uris.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"; + const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + const daiEthContract = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; + const usdtBaseContract = "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"; + + final usdt = Erc20Token( + name: "Tether", + symbol: "USDT", + contractAddress: usdtEthContract, + decimal: 6, + ); + final dai = Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: daiEthContract, + decimal: 18, + ); + + group("cross generation QR matrix", () { + test("new app mainnet USDT QR parses with explicit chain and amount", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "200.172148", + contractAddress: usdtEthContract, + tokenDecimals: 6, + ).toString(); + + final request = PaymentRequest.fromUri(Uri.parse(uri)); + expect(request.chainId, 1); + expect(request.contractAddress, usdtEthContract); + expect(request.resolveTokenAmount(usdt), "200.172148"); + }); + + test("new app mainnet DAI QR emits both params and the amount wins", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "1.5", + contractAddress: daiEthContract, + ).toString(); + + expect(uri.contains("uint256=1500000000000000000"), true); + expect(uri.contains("amount=1.5"), true); + + final request = PaymentRequest.fromUri(Uri.parse(uri)); + expect(request.chainId, 1); + expect(request.resolveTokenAmount(dai), "1.5"); + }); + + test("old app chainless USDT QR resolves through the legacy fallback", () { + final request = PaymentRequest.fromUri( + Uri.parse( + "ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000000000000000", + ), + ); + + expect(request.chainId, null); + expect(request.resolveTokenAmount(usdt), "2"); + }); + + test("team lead legacy URI resolves to the intended two USDT", () { + final request = PaymentRequest.fromUri( + Uri.parse( + "ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=2013999999999999744", + ), + ); + + expect(request.chainId, 1); + expect(request.resolveTokenAmount(usdt), "2.013999999999999744"); + }); + + test("external QR with a true six decimal uint256 resolves directly", () { + final request = PaymentRequest.fromUri( + Uri.parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"), + ); + + expect(request.resolveTokenAmount(usdt), "1"); + }); + + test("old app native scientific value parses without a chain", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient?value=2.014e18")); + + expect(request.chainId, null); + expect(request.amount, "2.014"); + }); + + test("new app native QR round trips with the explicit chain", () { + final uri = ERC681URI(chainId: 1, address: recipient, amount: "2.5", contractAddress: null) + .toString(); + expect(uri, "ethereum:$recipient@1?value=2.5e18"); + + final request = PaymentRequest.fromUri(Uri.parse(uri)); + expect(request.chainId, 1); + expect(request.amount, "2.5"); + }); + + test("new app base token QR carries the base chain", () { + final uri = ERC681URI( + chainId: 8453, + address: recipient, + amount: "3", + contractAddress: usdtBaseContract, + tokenDecimals: 6, + ).toString(); + + final request = PaymentRequest.fromUri(Uri.parse(uri)); + expect(request.chainId, 8453); + expect(request.contractAddress, usdtBaseContract); + expect(request.amount, "3"); + }); + }); + + group("emitter invariants", () { + test("every emitted ethereum URI names its chain", () { + final uris = [ + ERC681URI(chainId: 1, address: recipient, amount: "", contractAddress: null), + ERC681URI(chainId: 1, address: recipient, amount: "1", contractAddress: null), + ERC681URI( + chainId: 1, + address: recipient, + amount: "1", + contractAddress: usdtEthContract, + tokenDecimals: 6, + ), + ERC681URI(chainId: 8453, address: recipient, amount: "", contractAddress: usdtBaseContract), + ]; + + for (final uri in uris) { + expect(uri.toString().contains("@"), true, reason: uri.toString()); + } + }); + + test("six decimal tokens emit no uint256", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "1.5", + contractAddress: usdtEthContract, + tokenDecimals: 6, + ).toString(); + + expect(uri.contains("uint256"), false); + expect(uri.contains("amount=1.5"), true); + }); + + test("a stored raw amount is passed through verbatim", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "", + contractAddress: usdtEthContract, + tokenDecimals: 6, + rawTokenAmount: "1000000", + ).toString(); + + expect(uri, "ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"); + }); + + test("comma decimal separators are normalized", () { + final uri = ERC681URI( + chainId: 1, + address: recipient, + amount: "2,5", + contractAddress: usdtEthContract, + tokenDecimals: 6, + ).toString(); + + expect(uri.contains("amount=2.5"), true); + }); + }); + + group("hostile and external input", () { + test("EIP681 pay- prefix parses for native transfers", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:pay-$recipient@1?value=1e18")); + + expect(request.address, recipient); + expect(request.chainId, 1); + expect(request.amount, "1"); + }); + + test("EIP681 pay- prefix parses for token transfers", () { + final request = PaymentRequest.fromUri( + Uri.parse("ethereum:pay-$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"), + ); + + expect(request.contractAddress, usdtEthContract); + expect(request.address, recipient); + expect(request.resolveTokenAmount(usdt), "1"); + }); + + test("a trailing chain separator defaults to mainnet", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient@?value=1e18")); + + expect(request.address, recipient); + expect(request.chainId, 1); + }); + + test("a non numeric chain id defaults to mainnet", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient@abc")); + + expect(request.address, recipient); + expect(request.chainId, 1); + }); + + test("an ENS style target degrades to the raw name without throwing", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:vitalik.eth?amount=1")); + + expect(request.address, "vitalik.eth"); + expect(request.amount, "1"); + }); + + test("junk after the scheme does not throw", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:junk")); + + expect(request.address, "junk"); + }); + + test("an empty ethereum URI does not throw", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:")); + + expect(request.address, ""); + }); + + test("a non numeric native value yields no amount", () { + final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient?value=abc")); + + expect(request.address, recipient); + expect(request.amount, ""); + }); + + test("an unknown contract function still extracts the contract", () { + final request = PaymentRequest.fromUri( + Uri.parse("ethereum:$usdtEthContract@1/approve?address=$recipient"), + ); + + expect(request.contractAddress, usdtEthContract); + expect(request.address, recipient); + }); + + test("plain text input falls back to an address passthrough", () { + final request = PaymentRequest.fromString("definitelynotauri"); + + expect(request.address, "definitelynotauri"); + expect(request.scheme, ""); + }); + }); + + group("resolveTokenAmount boundaries", () { + test("one billion whole tokens is still read with the token decimals", () { + final request = + PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "1000000000000000"); + + expect(request.resolveTokenAmount(usdt), "1000000000"); + }); + + test("just past the plausibility bound flips to the legacy reading", () { + final request = + PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "1000000000000001"); + + expect(request.resolveTokenAmount(usdt), "0.001000000000000001"); + }); + + test("eighteen decimal tokens never take the legacy branch", () { + final request = + PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "50000000000000000000"); + + expect(request.resolveTokenAmount(dai), "50"); + }); + + test("garbage raw amounts resolve to null", () { + final request = + PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "not a number"); + + expect(request.resolveTokenAmount(usdt), null); + }); + }); + + group("send page reconstruction round trip", () { + test("an explicit chain request rebuilds into the same payment", () { + final original = PaymentRequest.fromUri( + Uri.parse("ethereum:$usdtBaseContract@8453/transfer?address=$recipient&uint256=3000000"), + ); + + final rebuilt = ERC681URI( + address: original.address, + amount: original.amount, + contractAddress: original.contractAddress, + chainId: original.chainId ?? 1, + rawTokenAmount: original.rawTokenAmount, + ).toString(); + + final reparsed = PaymentRequest.fromUri(Uri.parse(rebuilt)); + expect(reparsed.chainId, 8453); + expect(reparsed.contractAddress, usdtBaseContract); + expect(reparsed.rawTokenAmount, "3000000"); + expect(reparsed.address, recipient); + }); + + test("a dual param request survives reconstruction with the raw intact", () { + final original = PaymentRequest.fromUri( + Uri.parse( + "ethereum:$daiEthContract@1/transfer?address=$recipient&uint256=1500000000000000000&amount=1.5", + ), + ); + + final rebuilt = ERC681URI( + address: original.address, + amount: original.amount, + contractAddress: original.contractAddress, + chainId: original.chainId ?? 1, + rawTokenAmount: original.rawTokenAmount, + ).toString(); + + final reparsed = PaymentRequest.fromUri(Uri.parse(rebuilt)); + expect(reparsed.amount, "1.5"); + expect(reparsed.rawTokenAmount, "1500000000000000000"); + }); + }); + + group("detector supplements", () { + test("pay- prefixed URIs are detected as EVM payments", () { + final result = UniversalAddressDetector.detectAddress("ethereum:pay-$recipient@1?value=1e18"); + + expect(result.isValid, true); + expect(result.detectedWalletType, WalletType.ethereum); + }); + + test("an unsupported chain id is still surfaced for routing", () { + final result = UniversalAddressDetector.detectAddress("ethereum:$recipient@10?value=1e18"); + + expect(result.isValid, true); + expect(result.chainId, 10); + }); + }); +} diff --git a/test/utils/payment_request_test.dart b/test/utils/payment_request_test.dart index add579fda..b825b0a68 100644 --- a/test/utils/payment_request_test.dart +++ b/test/utils/payment_request_test.dart @@ -7,7 +7,8 @@ void main() { 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"); + "ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=2000000000000000000", + ); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"); @@ -17,7 +18,8 @@ void main() { test("extract address and amount from EIP681 Uri", () { final uri = Uri.parse( - "ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?value=2000000000000000000"); + "ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?value=2000000000000000000", + ); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"); @@ -34,7 +36,8 @@ void main() { test("extract address from EIP681 Uri with contract", () { final uri = Uri.parse( - "ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"); + "ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41", + ); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"); @@ -43,7 +46,8 @@ void main() { 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"); + "ethereum:0x1234567890abcdef1234567890abcdef12345678/transfer?address=0xabcdef1234567890abcdef1234567890abcdef12&uint256=1000000000000000000", + ); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.address, "0xabcdef1234567890abcdef1234567890abcdef12"); @@ -76,7 +80,8 @@ void main() { test("converts the raw uint256 using the token decimals", () { final uri = Uri.parse( - "ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=1000000"); + "ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=1000000", + ); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.resolveTokenAmount(usdt), "1"); @@ -90,8 +95,14 @@ void main() { }); test("falls back to the legacy 18 decimal reading for implausibly large amounts", () { - final paymentRequest = PaymentRequest("addr", "", "", "ethereum", null, - rawTokenAmount: "50000000000000000000"); + final paymentRequest = PaymentRequest( + "addr", + "", + "", + "ethereum", + null, + rawTokenAmount: "50000000000000000000", + ); expect(paymentRequest.resolveTokenAmount(usdt), "50"); });