dev
dart 211 lines 6.15 KB
Raw
1 import "package:cake_wallet/nano/nano.dart";
2 import "package:cw_core/amount/amount_sanitizer.dart";
3 import "package:cw_core/amount/money.dart";
4 import "package:cw_core/crypto_currency.dart";
5 import "package:cw_core/format_fixed.dart";
6 import "package:cw_core/lnurl.dart";
7 import "package:cw_core/payment_uris.dart";
8 import "package:cw_core/utils/print_verbose.dart";
9
10 class PaymentRequest {
11 PaymentRequest(
12 this.address,
13 this.amount,
14 this.note,
15 this.scheme,
16 this.pjUri, {
17 this.callbackUrl,
18 this.callbackMessage,
19 this.contractAddress,
20 this.chainId,
21 this.rawTokenAmount,
22 });
23
24 factory PaymentRequest.fromString(String input) {
25 try {
26 return PaymentRequest.fromBolt11(input);
27 } catch (_) {
28 try {
29 return PaymentRequest.fromUri(Uri.parse(input));
30 } catch (_) {
31 return PaymentRequest(input, "", "", "", null);
32 }
33 }
34 }
35
36 factory PaymentRequest.fromBolt11(String invoice) {
37 final amount = getBolt11Amount(invoice) ?? Money.zero(CryptoCurrency.btcln);
38 return PaymentRequest(invoice, amount.toString(), "", "lightning", null);
39 }
40
41 factory PaymentRequest.fromUri(Uri? uri) {
42 var address = "";
43 var amount = "";
44 var note = "";
45 var scheme = "";
46 String? walletType;
47 String? callbackUrl;
48 String? callbackMessage;
49 String? pjUri;
50 String? contractAddress;
51 int? chainId;
52 String? rawTokenAmount;
53
54 if (uri != null) {
55 if (uri.queryParameters["pj"] != null) {
56 pjUri = uri.toString();
57 }
58
59 address = uri.queryParameters["address"] ?? uri.path;
60 try {
61 final lnAmount = getBolt11Amount(uri.path) ?? Money.zero(CryptoCurrency.btcln);
62
63 if (lnAmount != 0) {
64 amount = lnAmount.toString();
65 }
66 } catch (_) {}
67 if (amount.isEmpty) {
68 amount = uri.queryParameters["tx_amount"] ?? uri.queryParameters["amount"] ?? "";
69 }
70 note = uri.queryParameters["tx_description"] ?? uri.queryParameters["message"] ?? "";
71 scheme = uri.scheme;
72 callbackUrl = uri.queryParameters["callback"];
73 callbackMessage = uri.queryParameters["callbackMessage"];
74 walletType = uri.queryParameters["type"];
75
76 if (scheme == "ethereum") {
77 final paymentUri = ERC681URI.fromUri(uri);
78 final hasExplicitChainId = uri.path.contains("@");
79
80 address = paymentUri.address;
81 amount = paymentUri.amount;
82 contractAddress = paymentUri.contractAddress;
83 chainId = hasExplicitChainId ? paymentUri.chainId : null;
84 rawTokenAmount = paymentUri.rawTokenAmount;
85 } else if (scheme == "tron") {
86 final token = uri.queryParameters["token"];
87 if (token != null && token.isNotEmpty) {
88 contractAddress = token;
89 }
90 } else if (scheme == "solana") {
91 final splToken = uri.queryParameters["spl-token"];
92 if (splToken != null && splToken.isNotEmpty) {
93 contractAddress = splToken;
94 }
95 }
96 }
97
98 if (scheme == "nano-gpt") {
99 scheme = walletType ?? "nano";
100 }
101
102 if (nano != null) {
103 if (amount.isNotEmpty) {
104 if (!_isAlreadyUsableAmount(amount)) {
105 if (address.contains("nano")) {
106 amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerNano);
107 } else if (address.contains("ban")) {
108 amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerBanano);
109 }
110 }
111 }
112 }
113
114 return PaymentRequest(
115 address,
116 amount,
117 note,
118 scheme,
119 pjUri,
120 callbackUrl: callbackUrl,
121 callbackMessage: callbackMessage,
122 contractAddress: contractAddress,
123 chainId: chainId,
124 rawTokenAmount: rawTokenAmount,
125 );
126 }
127
128 PaymentRequest copyWith({
129 String? address,
130 String? amount,
131 String? note,
132 String? scheme,
133 String? pjUri,
134 String? callbackUrl,
135 String? callbackMessage,
136 String? contractAddress,
137 int? chainId,
138 String? rawTokenAmount,
139 }) =>
140 PaymentRequest(
141 address ?? this.address,
142 amount ?? this.amount,
143 note ?? this.note,
144 scheme ?? this.scheme,
145 pjUri ?? this.pjUri,
146 callbackUrl: callbackUrl ?? this.callbackUrl,
147 callbackMessage: callbackMessage ?? this.callbackMessage,
148 contractAddress: contractAddress ?? this.contractAddress,
149 chainId: chainId ?? this.chainId,
150 rawTokenAmount: rawTokenAmount ?? this.rawTokenAmount,
151 );
152
153 final String address;
154 final String amount;
155 final String note;
156 final String scheme;
157 final String? pjUri;
158 final String? callbackUrl;
159 final String? callbackMessage;
160 final String? contractAddress;
161 final int? chainId;
162 final String? rawTokenAmount;
163
164 String? resolveTokenAmount(CryptoCurrency token) {
165 if (amount.isNotEmpty) {
166 return amount;
167 }
168
169 if (rawTokenAmount == null || rawTokenAmount!.isEmpty) {
170 return null;
171 }
172
173 final raw = BigInt.tryParse(rawTokenAmount!);
174 if (raw == null) {
175 return null;
176 }
177
178 // 1e9 whole tokens is beyond any real payment, so a larger result means the QR
179 // came from an old app version that encoded with 18 decimals regardless of token.
180 if (token.decimals != 18 && raw > BigInt.from(10).pow(token.decimals + 9)) {
181 printV("resolveTokenAmount: reading $rawTokenAmount with legacy 18 decimals");
182 return formatFixed(raw, 18);
183 }
184
185 return formatFixed(raw, token.decimals);
186 }
187
188 /// Checks if the amount string is already in a usable format (e.g., "123.45") and doesn't need to be converted from raw format.
189 ///
190 /// This was causing an error for us when we scan Nano QRs with amounts in them, the amounts are already in usable format so when the parsing was done, it returns 0 wrongly.
191 static bool _isAlreadyUsableAmount(String amount) {
192 if (amount.isEmpty) {
193 return false;
194 }
195
196 final parsed = double.tryParse(amount.sanitized());
197 if (parsed == null) {
198 return false;
199 }
200 if (amount.contains(".") && parsed > 0 && parsed < 1000000000) {
201 return true;
202 }
203
204 // If it's a small integer (less than 1 billion), it's likely already usable
205 if (parsed == parsed.toInt() && parsed < 1000000000) {
206 return true;
207 }
208
209 return false;
210 }
211 }