mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
fix: anypay (#3507)
* fix android CI * fix: handle the parser seeing chainless QRs in the old flow as belonging to the current wallet, causing the error jack reported * fix: handle network switch when anypay is interrupted mid flow and currency is changed
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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", () {
|
||||
|
||||
@@ -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<NewSendPage> {
|
||||
|
||||
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<NewSendPage> {
|
||||
|
||||
Future<void> _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<NewSendPage> {
|
||||
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<NewSendPage> {
|
||||
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<NewSendPage> {
|
||||
}
|
||||
|
||||
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<NewSendPage> {
|
||||
_applyPaymentRequest(paymentRequest, amountOverride: amountOverride);
|
||||
}
|
||||
|
||||
Future<bool> _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<void> _completeWalletSwitch(
|
||||
WalletInfo wallet,
|
||||
PaymentFlowResult result,
|
||||
|
||||
@@ -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<int?> 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<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
|
||||
final walletKey = walletInfo.name.replaceAll(' ', '_');
|
||||
final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
|
||||
|
||||
@@ -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<void> fetchTokenForContractAddress(String contractAddress) async {
|
||||
Future<void> fetchTokenForContractAddress(String contractAddress,
|
||||
{WalletType? walletType}) async {
|
||||
final token = await TokenUtilities.findTokenByAddress(
|
||||
walletType: wallet.type,
|
||||
walletType: walletType ?? wallet.type,
|
||||
address: contractAddress,
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user