dev
dart 201 lines 6.61 KB
Raw
1 import "package:cake_wallet/core/anypay/anypay_models.dart";
2 import "package:cake_wallet/core/anypay/anypay_parser.dart";
3 import "package:cake_wallet/core/anypay/anypay_resolver.dart";
4 import "package:cw_core/crypto_currency.dart";
5 import "package:cw_core/erc20_token.dart";
6 import "package:cw_core/wallet_type.dart";
7 import "package:flutter_test/flutter_test.dart";
8
9 class _FakeTokenLookup implements AnyPayTokenLookup {
10 _FakeTokenLookup({this.onFindToken, this.onFindChainId});
11
12 final Future<CryptoCurrency?> Function({required WalletType walletType, required String address})?
13 onFindToken;
14 final Future<int?> Function(String contractAddress, {int? excludingChainId})? onFindChainId;
15
16 @override
17 Future<CryptoCurrency?> findTokenByAddress({
18 required WalletType walletType,
19 required String address,
20 }) {
21 if (onFindToken == null) {
22 fail("no token lookup expected");
23 }
24 return onFindToken!(walletType: walletType, address: address);
25 }
26
27 @override
28 Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) {
29 if (onFindChainId == null) {
30 fail("no chain sweep expected");
31 }
32 return onFindChainId!(contractAddress, excludingChainId: excludingChainId);
33 }
34 }
35
36 void main() {
37 const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
38 const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
39 const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
40 const usdcSolMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
41
42 final usdt = Erc20Token(
43 name: "Tether",
44 symbol: "USDT",
45 contractAddress: usdtEthContract,
46 decimal: 6,
47 );
48
49 WalletSnapshot snapshot({
50 WalletType type = WalletType.ethereum,
51 int? chainId,
52 bool hasEvmProxy = true,
53 }) =>
54 WalletSnapshot(
55 type: type,
56 currentWalletIsEvm:
57 type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.base,
58 currentChainId: chainId,
59 wallets: const [],
60 // The service builds an empty chain map when the evm proxy is absent.
61 supportedEvmChains: hasEvmProxy
62 ? const {
63 1: WalletType.ethereum,
64 137: WalletType.polygon,
65 8453: WalletType.base,
66 }
67 : const {},
68 hasEvmProxy: hasEvmProxy,
69 hasSolanaProxy: true,
70 hasTronProxy: true,
71 );
72
73 AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
74
75 test("a request without a contract needs no resolution", () async {
76 final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
77
78 final resolution = await resolver.resolve(
79 parse("ethereum:$recipient@1?value=1e18"),
80 snapshot(chainId: 1),
81 );
82
83 expect(resolution, isA<NoTokenRequested>());
84 });
85
86 test("a token on the explicit target chain resolves with its amount", () async {
87 final lookedUp = <WalletType>[];
88 final resolver = AnyPayResolver(
89 tokenLookup: _FakeTokenLookup(
90 onFindToken: ({required walletType, required address}) async {
91 lookedUp.add(walletType);
92 return walletType == WalletType.ethereum ? usdt : null;
93 },
94 ),
95 );
96
97 final resolution = await resolver.resolve(
98 parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
99 snapshot(type: WalletType.base, chainId: 8453),
100 );
101
102 final resolved = resolution as TokenResolved;
103 expect(lookedUp, [WalletType.ethereum]);
104 expect(resolved.chainId, 1);
105 expect(resolved.token, usdt);
106 expect(resolved.amountOverride, "1");
107 });
108
109 test("an unknown token on an explicit chain is not swept to other chains", () async {
110 bool sweepCalled = false;
111 final resolver = AnyPayResolver(
112 tokenLookup: _FakeTokenLookup(
113 onFindToken: ({required walletType, required address}) async => null,
114 onFindChainId: (contract, {excludingChainId}) async {
115 sweepCalled = true;
116 return 137;
117 },
118 ),
119 );
120
121 final resolution = await resolver.resolve(
122 parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
123 snapshot(chainId: 1),
124 );
125
126 expect(resolution, isA<TokenUnknown>());
127 expect(sweepCalled, false);
128 });
129
130 test("a chainless token unknown on the current chain reroutes through the sweep", () async {
131 final resolver = AnyPayResolver(
132 tokenLookup: _FakeTokenLookup(
133 onFindToken: ({required walletType, required address}) async =>
134 walletType == WalletType.ethereum ? usdt : null,
135 onFindChainId: (contract, {excludingChainId}) async {
136 expect(excludingChainId, 8453);
137 return 1;
138 },
139 ),
140 );
141
142 final resolution = await resolver.resolve(
143 parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
144 snapshot(type: WalletType.base, chainId: 8453),
145 );
146
147 final resolved = resolution as TokenResolved;
148 expect(resolved.chainId, 1);
149 expect(resolved.amountOverride, "2");
150 });
151
152 test("a chainless token unknown everywhere stays unknown", () async {
153 final resolver = AnyPayResolver(
154 tokenLookup: _FakeTokenLookup(
155 onFindToken: ({required walletType, required address}) async => null,
156 onFindChainId: (contract, {excludingChainId}) async => null,
157 ),
158 );
159
160 final resolution = await resolver.resolve(
161 parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
162 snapshot(chainId: 1),
163 );
164
165 expect((resolution as TokenUnknown).contract, usdtEthContract);
166 });
167
168 test("a solana token resolves against the detected wallet type", () async {
169 const usdcSol = CryptoCurrency.usdtSol;
170 final resolver = AnyPayResolver(
171 tokenLookup: _FakeTokenLookup(
172 onFindToken: ({required walletType, required address}) async {
173 expect(walletType, WalletType.solana);
174 expect(address, usdcSolMint);
175 return usdcSol;
176 },
177 ),
178 );
179
180 final resolution = await resolver.resolve(
181 parse("solana:$solAddress?amount=3&spl-token=$usdcSolMint"),
182 snapshot(type: WalletType.monero),
183 );
184
185 final resolved = resolution as TokenResolved;
186 expect(resolved.chainId, null);
187 expect(resolved.token, usdcSol);
188 expect(resolved.amountOverride, "3");
189 });
190
191 test("an evm contract without the evm proxy stays unknown without any lookups", () async {
192 final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
193
194 final resolution = await resolver.resolve(
195 parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
196 snapshot(hasEvmProxy: false),
197 );
198
199 expect(resolution, isA<TokenUnknown>());
200 });
201 }