Files
hash-wallet/test/utils/anypay_matrix_test.dart
T
jwinterm dd449f2594 Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
2026-10-24 23:49:29 -04:00

345 lines
11 KiB
Dart

import "package:hash_wallet/core/universal_address_detector.dart";
import "package:hash_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);
});
});
}