dev
dart 344 lines 11.3 KB
Raw
1 import "package:cake_wallet/core/universal_address_detector.dart";
2 import "package:cake_wallet/utils/payment_request.dart";
3 import "package:cw_core/erc20_token.dart";
4 import "package:cw_core/payment_uris.dart";
5 import "package:cw_core/wallet_type.dart";
6 import "package:flutter_test/flutter_test.dart";
7
8 void main() {
9 const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
10 const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
11 const daiEthContract = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
12 const usdtBaseContract = "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2";
13
14 final usdt = Erc20Token(
15 name: "Tether",
16 symbol: "USDT",
17 contractAddress: usdtEthContract,
18 decimal: 6,
19 );
20 final dai = Erc20Token(
21 name: "Dai",
22 symbol: "DAI",
23 contractAddress: daiEthContract,
24 decimal: 18,
25 );
26
27 group("cross generation QR matrix", () {
28 test("new app mainnet USDT QR parses with explicit chain and amount", () {
29 final uri = ERC681URI(
30 chainId: 1,
31 address: recipient,
32 amount: "200.172148",
33 contractAddress: usdtEthContract,
34 tokenDecimals: 6,
35 ).toString();
36
37 final request = PaymentRequest.fromUri(Uri.parse(uri));
38 expect(request.chainId, 1);
39 expect(request.contractAddress, usdtEthContract);
40 expect(request.resolveTokenAmount(usdt), "200.172148");
41 });
42
43 test("new app mainnet DAI QR emits both params and the amount wins", () {
44 final uri = ERC681URI(
45 chainId: 1,
46 address: recipient,
47 amount: "1.5",
48 contractAddress: daiEthContract,
49 ).toString();
50
51 expect(uri.contains("uint256=1500000000000000000"), true);
52 expect(uri.contains("amount=1.5"), true);
53
54 final request = PaymentRequest.fromUri(Uri.parse(uri));
55 expect(request.chainId, 1);
56 expect(request.resolveTokenAmount(dai), "1.5");
57 });
58
59 test("old app chainless USDT QR resolves through the legacy fallback", () {
60 final request = PaymentRequest.fromUri(
61 Uri.parse(
62 "ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000000000000000",
63 ),
64 );
65
66 expect(request.chainId, null);
67 expect(request.resolveTokenAmount(usdt), "2");
68 });
69
70 test("team lead legacy URI resolves to the intended two USDT", () {
71 final request = PaymentRequest.fromUri(
72 Uri.parse(
73 "ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=2013999999999999744",
74 ),
75 );
76
77 expect(request.chainId, 1);
78 expect(request.resolveTokenAmount(usdt), "2.013999999999999744");
79 });
80
81 test("external QR with a true six decimal uint256 resolves directly", () {
82 final request = PaymentRequest.fromUri(
83 Uri.parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
84 );
85
86 expect(request.resolveTokenAmount(usdt), "1");
87 });
88
89 test("old app native scientific value parses without a chain", () {
90 final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient?value=2.014e18"));
91
92 expect(request.chainId, null);
93 expect(request.amount, "2.014");
94 });
95
96 test("new app native QR round trips with the explicit chain", () {
97 final uri = ERC681URI(chainId: 1, address: recipient, amount: "2.5", contractAddress: null)
98 .toString();
99 expect(uri, "ethereum:$recipient@1?value=2.5e18");
100
101 final request = PaymentRequest.fromUri(Uri.parse(uri));
102 expect(request.chainId, 1);
103 expect(request.amount, "2.5");
104 });
105
106 test("new app base token QR carries the base chain", () {
107 final uri = ERC681URI(
108 chainId: 8453,
109 address: recipient,
110 amount: "3",
111 contractAddress: usdtBaseContract,
112 tokenDecimals: 6,
113 ).toString();
114
115 final request = PaymentRequest.fromUri(Uri.parse(uri));
116 expect(request.chainId, 8453);
117 expect(request.contractAddress, usdtBaseContract);
118 expect(request.amount, "3");
119 });
120 });
121
122 group("emitter invariants", () {
123 test("every emitted ethereum URI names its chain", () {
124 final uris = [
125 ERC681URI(chainId: 1, address: recipient, amount: "", contractAddress: null),
126 ERC681URI(chainId: 1, address: recipient, amount: "1", contractAddress: null),
127 ERC681URI(
128 chainId: 1,
129 address: recipient,
130 amount: "1",
131 contractAddress: usdtEthContract,
132 tokenDecimals: 6,
133 ),
134 ERC681URI(chainId: 8453, address: recipient, amount: "", contractAddress: usdtBaseContract),
135 ];
136
137 for (final uri in uris) {
138 expect(uri.toString().contains("@"), true, reason: uri.toString());
139 }
140 });
141
142 test("six decimal tokens emit no uint256", () {
143 final uri = ERC681URI(
144 chainId: 1,
145 address: recipient,
146 amount: "1.5",
147 contractAddress: usdtEthContract,
148 tokenDecimals: 6,
149 ).toString();
150
151 expect(uri.contains("uint256"), false);
152 expect(uri.contains("amount=1.5"), true);
153 });
154
155 test("a stored raw amount is passed through verbatim", () {
156 final uri = ERC681URI(
157 chainId: 1,
158 address: recipient,
159 amount: "",
160 contractAddress: usdtEthContract,
161 tokenDecimals: 6,
162 rawTokenAmount: "1000000",
163 ).toString();
164
165 expect(uri, "ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000");
166 });
167
168 test("comma decimal separators are normalized", () {
169 final uri = ERC681URI(
170 chainId: 1,
171 address: recipient,
172 amount: "2,5",
173 contractAddress: usdtEthContract,
174 tokenDecimals: 6,
175 ).toString();
176
177 expect(uri.contains("amount=2.5"), true);
178 });
179 });
180
181 group("hostile and external input", () {
182 test("EIP681 pay- prefix parses for native transfers", () {
183 final request = PaymentRequest.fromUri(Uri.parse("ethereum:pay-$recipient@1?value=1e18"));
184
185 expect(request.address, recipient);
186 expect(request.chainId, 1);
187 expect(request.amount, "1");
188 });
189
190 test("EIP681 pay- prefix parses for token transfers", () {
191 final request = PaymentRequest.fromUri(
192 Uri.parse("ethereum:pay-$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
193 );
194
195 expect(request.contractAddress, usdtEthContract);
196 expect(request.address, recipient);
197 expect(request.resolveTokenAmount(usdt), "1");
198 });
199
200 test("a trailing chain separator defaults to mainnet", () {
201 final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient@?value=1e18"));
202
203 expect(request.address, recipient);
204 expect(request.chainId, 1);
205 });
206
207 test("a non numeric chain id defaults to mainnet", () {
208 final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient@abc"));
209
210 expect(request.address, recipient);
211 expect(request.chainId, 1);
212 });
213
214 test("an ENS style target degrades to the raw name without throwing", () {
215 final request = PaymentRequest.fromUri(Uri.parse("ethereum:vitalik.eth?amount=1"));
216
217 expect(request.address, "vitalik.eth");
218 expect(request.amount, "1");
219 });
220
221 test("junk after the scheme does not throw", () {
222 final request = PaymentRequest.fromUri(Uri.parse("ethereum:junk"));
223
224 expect(request.address, "junk");
225 });
226
227 test("an empty ethereum URI does not throw", () {
228 final request = PaymentRequest.fromUri(Uri.parse("ethereum:"));
229
230 expect(request.address, "");
231 });
232
233 test("a non numeric native value yields no amount", () {
234 final request = PaymentRequest.fromUri(Uri.parse("ethereum:$recipient?value=abc"));
235
236 expect(request.address, recipient);
237 expect(request.amount, "");
238 });
239
240 test("an unknown contract function still extracts the contract", () {
241 final request = PaymentRequest.fromUri(
242 Uri.parse("ethereum:$usdtEthContract@1/approve?address=$recipient"),
243 );
244
245 expect(request.contractAddress, usdtEthContract);
246 expect(request.address, recipient);
247 });
248
249 test("plain text input falls back to an address passthrough", () {
250 final request = PaymentRequest.fromString("definitelynotauri");
251
252 expect(request.address, "definitelynotauri");
253 expect(request.scheme, "");
254 });
255 });
256
257 group("resolveTokenAmount boundaries", () {
258 test("one billion whole tokens is still read with the token decimals", () {
259 final request =
260 PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "1000000000000000");
261
262 expect(request.resolveTokenAmount(usdt), "1000000000");
263 });
264
265 test("just past the plausibility bound flips to the legacy reading", () {
266 final request =
267 PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "1000000000000001");
268
269 expect(request.resolveTokenAmount(usdt), "0.001000000000000001");
270 });
271
272 test("eighteen decimal tokens never take the legacy branch", () {
273 final request =
274 PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "50000000000000000000");
275
276 expect(request.resolveTokenAmount(dai), "50");
277 });
278
279 test("garbage raw amounts resolve to null", () {
280 final request =
281 PaymentRequest("addr", "", "", "ethereum", null, rawTokenAmount: "not a number");
282
283 expect(request.resolveTokenAmount(usdt), null);
284 });
285 });
286
287 group("send page reconstruction round trip", () {
288 test("an explicit chain request rebuilds into the same payment", () {
289 final original = PaymentRequest.fromUri(
290 Uri.parse("ethereum:$usdtBaseContract@8453/transfer?address=$recipient&uint256=3000000"),
291 );
292
293 final rebuilt = ERC681URI(
294 address: original.address,
295 amount: original.amount,
296 contractAddress: original.contractAddress,
297 chainId: original.chainId ?? 1,
298 rawTokenAmount: original.rawTokenAmount,
299 ).toString();
300
301 final reparsed = PaymentRequest.fromUri(Uri.parse(rebuilt));
302 expect(reparsed.chainId, 8453);
303 expect(reparsed.contractAddress, usdtBaseContract);
304 expect(reparsed.rawTokenAmount, "3000000");
305 expect(reparsed.address, recipient);
306 });
307
308 test("a dual param request survives reconstruction with the raw intact", () {
309 final original = PaymentRequest.fromUri(
310 Uri.parse(
311 "ethereum:$daiEthContract@1/transfer?address=$recipient&uint256=1500000000000000000&amount=1.5",
312 ),
313 );
314
315 final rebuilt = ERC681URI(
316 address: original.address,
317 amount: original.amount,
318 contractAddress: original.contractAddress,
319 chainId: original.chainId ?? 1,
320 rawTokenAmount: original.rawTokenAmount,
321 ).toString();
322
323 final reparsed = PaymentRequest.fromUri(Uri.parse(rebuilt));
324 expect(reparsed.amount, "1.5");
325 expect(reparsed.rawTokenAmount, "1500000000000000000");
326 });
327 });
328
329 group("detector supplements", () {
330 test("pay- prefixed URIs are detected as EVM payments", () {
331 final result = UniversalAddressDetector.detectAddress("ethereum:pay-$recipient@1?value=1e18");
332
333 expect(result.isValid, true);
334 expect(result.detectedWalletType, WalletType.ethereum);
335 });
336
337 test("an unsupported chain id is still surfaced for routing", () {
338 final result = UniversalAddressDetector.detectAddress("ethereum:$recipient@10?value=1e18");
339
340 expect(result.isValid, true);
341 expect(result.chainId, 10);
342 });
343 });
344 }