dev
dart 536 lines 16.9 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:developer';
4
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/crypto_currency.dart';
8 import 'package:cw_core/currency.dart';
9 import 'package:cw_core/node.dart';
10 import 'package:cw_core/utils/proxy_wrapper.dart';
11 import 'package:cw_tron/default_tron_tokens.dart';
12 import 'package:cw_tron/pending_tron_transaction.dart';
13 import 'package:cw_tron/tron_abi.dart';
14 import 'package:cw_tron/tron_balance.dart';
15 import 'package:cw_tron/tron_http_provider.dart';
16 import 'package:cw_core/tron_token.dart';
17 import 'package:cw_tron/tron_transaction_model.dart';
18 import 'package:flutter/foundation.dart';
19 import 'package:flutter/services.dart';
20 import '.secrets.g.dart' as secrets;
21 import 'package:on_chain/on_chain.dart';
22
23 class TronClient {
24 late final client = ProxyWrapper().getHttpIOClient();
25
26 TronProvider? _provider;
27 // This is an internal tracker, so we don't have to "refetch".
28 int _nativeTxEstimatedFee = 0;
29
30 Future<List<TronTransactionModel>> fetchTransactions(String address,
31 {String? contractAddress}) async {
32 try {
33 final response = await client.get(
34 Uri.https(
35 "api.trongrid.io",
36 "/v1/accounts/$address/transactions",
37 {
38 "only_confirmed": "true",
39 "limit": "200",
40 },
41 ),
42 headers: {
43 'Content-Type': 'application/json',
44 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
45 },
46 );
47 final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
48
49 if (response.statusCode >= 200 &&
50 response.statusCode < 300 &&
51 jsonResponse['status'] != false) {
52 return (jsonResponse['data'] as List).map((e) {
53 return TronTransactionModel.fromJson(e as Map<String, dynamic>);
54 }).toList();
55 }
56
57 return [];
58 } catch (e, s) {
59 log('Error getting tx: ${e.toString()}\n ${s.toString()}');
60 return [];
61 }
62 }
63
64 Future<List<TronTRC20TransactionModel>> fetchTrc20ExcludedTransactions(String address) async {
65 try {
66 final response = await client.get(
67 Uri.https(
68 "api.trongrid.io",
69 "/v1/accounts/$address/transactions/trc20",
70 {
71 "only_confirmed": "true",
72 "limit": "200",
73 },
74 ),
75 headers: {
76 'Content-Type': 'application/json',
77 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
78 },
79 );
80 final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
81
82 if (response.statusCode >= 200 &&
83 response.statusCode < 300 &&
84 jsonResponse['status'] != false) {
85 return (jsonResponse['data'] as List).map((e) {
86 return TronTRC20TransactionModel.fromJson(e as Map<String, dynamic>);
87 }).toList();
88 }
89
90 return [];
91 } catch (e, s) {
92 log('Error getting trc20 tx: ${e.toString()}\n ${s.toString()}');
93 return [];
94 }
95 }
96
97 bool connect(Node node) {
98 try {
99 final formattedUrl = '${node.isSSL ? 'https' : 'http'}://${node.uriRaw}';
100 _provider = TronProvider(TronHTTPProvider(url: formattedUrl));
101
102 return true;
103 } catch (e) {
104 return false;
105 }
106 }
107
108 Future<BigInt> getBalance(TronAddress address, {bool throwOnError = false}) async {
109 try {
110 final accountDetails = await _provider!.request(TronRequestGetAccount(address: address));
111
112 return accountDetails?.balance ?? BigInt.zero;
113 } catch (_) {
114 if (throwOnError) {
115 rethrow;
116 }
117 return BigInt.zero;
118 }
119 }
120
121 Future<int> getFeeLimit(
122 TransactionRaw rawTransaction,
123 TronAddress address,
124 TronAddress receiverAddress, {
125 int energyUsed = 0,
126 bool isEstimatedFeeFlow = false,
127 }) async {
128 try {
129 // Get the tron chain parameters.
130 final chainParams = await _provider!.request(TronRequestGetChainParameters());
131
132 final bandWidthInSun = chainParams.getTransactionFee!;
133 log('BandWidth In Sun: $bandWidthInSun');
134
135 final energyInSun = chainParams.getEnergyFee!;
136 log('Energy In Sun: $energyInSun');
137
138 final fakeTransaction = Transaction(
139 rawData: rawTransaction,
140 signature: [Uint8List(65)],
141 );
142
143 // Calculate the total size of the fake transaction, considering the required network overhead.
144 final transactionSize = fakeTransaction.length + 64;
145
146 // Assign the calculated size to the variable representing the required bandwidth.
147 int neededBandWidth = transactionSize;
148 log('Initial Needed Bandwidth: $neededBandWidth');
149
150 int neededEnergy = energyUsed;
151 log('Initial Needed Energy: $neededEnergy');
152
153 // Fetch account resources to assess the available bandwidth and energy
154 final accountResource =
155 await _provider!.request(TronRequestGetAccountResource(address: address));
156
157 neededEnergy -= accountResource.howManyEnergy.toInt();
158 log('Account resource energy: ${accountResource.howManyEnergy.toInt()}');
159 log('Needed Energy after deducting from account resource energy: $neededEnergy');
160
161 // Deduct the bandwidth from the account's available bandwidth.
162 final BigInt accountBandWidth = accountResource.howManyBandwIth;
163 log('Account resource bandwidth: ${accountResource.howManyBandwIth.toInt()}');
164
165 if (accountBandWidth >= BigInt.from(neededBandWidth) && !isEstimatedFeeFlow) {
166 log('Account has more bandwidth than required');
167 neededBandWidth = 0;
168 }
169
170 if (neededEnergy < 0) {
171 neededEnergy = 0;
172 }
173
174 final energyBurn = neededEnergy * energyInSun.toInt();
175 log('Energy Burn: $energyBurn');
176
177 final bandWidthBurn = neededBandWidth * bandWidthInSun;
178 log('Bandwidth Burn: $bandWidthBurn');
179
180 int totalBurn = energyBurn + bandWidthBurn;
181 log('Total Burn: $totalBurn');
182
183 /// If there is a note (memo), calculate the memo fee.
184 if (rawTransaction.data != null) {
185 totalBurn += chainParams.getMemoFee!;
186 }
187
188 log('Final total burn: $totalBurn');
189
190 return totalBurn;
191 } catch (_) {
192 return 0;
193 }
194 }
195
196 Future<int> getEstimatedFee(TronAddress ownerAddress) async {
197 const constantAmount = '1000';
198 // Fetch the latest Tron block
199 final block = await _provider!.request(TronRequestGetNowBlock());
200
201 // Create the transfer contract
202 final contract = TransferContract(
203 amount: TronHelper.toSun(constantAmount),
204 ownerAddress: ownerAddress,
205 toAddress: ownerAddress,
206 );
207
208 // Prepare the contract parameter for the transaction.
209 final parameter = Any(typeUrl: contract.typeURL, value: contract);
210
211 // Create a TransactionContract object with the contract type and parameter.
212 final transactionContract =
213 TransactionContract(type: contract.contractType, parameter: parameter);
214
215 // Set the transaction expiration time (maximum 24 hours)
216 final expireTime = DateTime.now().add(const Duration(minutes: 30));
217
218 // Create a raw transaction
219 TransactionRaw rawTransaction = TransactionRaw(
220 refBlockBytes: block.blockHeader.rawData.refBlockBytes,
221 refBlockHash: block.blockHeader.rawData.refBlockHash,
222 expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
223 contract: [transactionContract],
224 timestamp: block.blockHeader.rawData.timestamp,
225 );
226
227 final estimatedFee = await getFeeLimit(
228 rawTransaction,
229 ownerAddress,
230 ownerAddress,
231 isEstimatedFeeFlow: true,
232 );
233
234 _nativeTxEstimatedFee = estimatedFee;
235
236 return estimatedFee;
237 }
238
239 Future<int> getTRCEstimatedFee(TronAddress ownerAddress) async {
240 final usdtContractAddress = DefaultTronTokens().usdt.contractAddress;
241 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
242 final function = contract.functionFromName("transfer");
243
244 // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets.
245 final transferParams = [ownerAddress, BigInt.zero];
246
247 final request = await _provider!.request(
248 TronRequestTriggerConstantContract(
249 ownerAddress: ownerAddress,
250 contractAddress: TronAddress(usdtContractAddress),
251 data: function.encodeHex(transferParams),
252 ),
253 );
254
255 if (!request.isSuccess) {
256 log("Tron TRC20 error: ${request.error} \n ${request.respose}");
257 }
258
259 return getFeeLimit(
260 request.transactionRaw!,
261 ownerAddress,
262 ownerAddress,
263 energyUsed: request.energyUsed ?? 0,
264 isEstimatedFeeFlow: true,
265 );
266 }
267
268 Future<PendingTronTransaction> signTransaction({
269 required TronPrivateKey ownerPrivKey,
270 required String toAddress,
271 required Money amount,
272 required BigInt tronBalance,
273 required bool sendAll,
274 }) async {
275 // Get the owner tron address from the key
276 final ownerAddress = ownerPrivKey.publicKey().toAddress();
277
278 // Define the receiving Tron address for the transaction.
279 final receiverAddress = TronAddress(toAddress);
280
281 final isNativeTransaction = amount.currency == CryptoCurrency.trx;
282
283 Money totalAmount;
284 TransactionRaw rawTransaction;
285 if (isNativeTransaction) {
286 if (sendAll) {
287 final accountResource =
288 await _provider!.request(TronRequestGetAccountResource(address: ownerAddress));
289
290 final availableBandWidth = accountResource.howManyBandwIth.toInt();
291
292 // 269 is the current middle ground for bandwidth per transaction
293 if (availableBandWidth >= 269) {
294 totalAmount = amount;
295 } else {
296 // 5000 added here is a buffer since we're working with "estimated" value of the fee.
297 final result = amount.amount - BigInt.from(_nativeTxEstimatedFee + 5000);
298
299 totalAmount = amount.copyWith(amount: result);
300 }
301 } else {
302 totalAmount = amount;
303 }
304 rawTransaction = await _signNativeTransaction(
305 ownerAddress,
306 receiverAddress,
307 totalAmount,
308 tronBalance,
309 sendAll,
310 );
311 } else {
312 final tokenAddress = (amount.currency as TronToken).contractAddress;
313 totalAmount = amount;
314 rawTransaction = await _signTrcTokenTransaction(
315 ownerAddress,
316 receiverAddress,
317 totalAmount,
318 tokenAddress,
319 tronBalance,
320 );
321 }
322
323 final signature = ownerPrivKey.sign(rawTransaction.toBuffer());
324
325 sendTx() async => await sendTransaction(
326 rawTransaction: rawTransaction,
327 signature: signature,
328 );
329
330 return PendingTronTransaction(
331 signedTransaction: signature,
332 amount: totalAmount,
333 fee: Money(rawTransaction.feeLimit ?? BigInt.zero, CryptoCurrency.trx),
334 sendTransaction: sendTx,
335 id: rawTransaction.txID);
336 }
337
338 Future<TransactionRaw> _signNativeTransaction(
339 TronAddress ownerAddress,
340 TronAddress receiverAddress,
341 Money amount,
342 BigInt tronBalance,
343 bool sendAll,
344 ) async {
345 // This is introduce to server as a limit in cases where feeLimit is 0
346 // The transaction signing will fail if the feeLimit is explicitly 0.
347 int defaultFeeLimit = 269000;
348
349 final block = await _provider!.request(TronRequestGetNowBlock());
350 // Create the transfer contract
351 final contract = TransferContract(
352 amount: amount.amount,
353 ownerAddress: ownerAddress,
354 toAddress: receiverAddress,
355 );
356
357 // Prepare the contract parameter for the transaction.
358 final parameter = Any(typeUrl: contract.typeURL, value: contract);
359
360 // Create a TransactionContract object with the contract type and parameter.
361 final transactionContract =
362 TransactionContract(type: contract.contractType, parameter: parameter);
363
364 // Set the transaction expiration time (maximum 24 hours)
365 final expireTime = DateTime.now().add(const Duration(minutes: 30));
366
367 final rawTransaction = TransactionRaw(
368 refBlockBytes: block.blockHeader.rawData.refBlockBytes,
369 refBlockHash: block.blockHeader.rawData.refBlockHash,
370 expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
371 contract: [transactionContract],
372 timestamp: block.blockHeader.rawData.timestamp,
373 );
374
375 final feeLimit = await getFeeLimit(rawTransaction, ownerAddress, receiverAddress);
376 final feeLimitToUse = feeLimit != 0 ? feeLimit : defaultFeeLimit;
377 final tronBalanceInt = tronBalance.toInt();
378
379 if (feeLimit > tronBalanceInt) {
380 final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
381 throw Exception(
382 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up.\nTransaction fee: $feeInTrx TRX',
383 );
384 }
385
386 return rawTransaction.copyWith(feeLimit: BigInt.from(feeLimitToUse));
387 }
388
389 Future<TransactionRaw> _signTrcTokenTransaction(
390 TronAddress ownerAddress,
391 TronAddress receiverAddress,
392 Money amount,
393 String contractAddress,
394 BigInt tronBalance,
395 ) async {
396 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
397
398 final function = contract.functionFromName("transfer");
399 final transferParams = [receiverAddress, amount.amount];
400 final request = await _provider!.request(
401 TronRequestTriggerConstantContract(
402 ownerAddress: ownerAddress,
403 contractAddress: TronAddress(contractAddress),
404 data: function.encodeHex(transferParams),
405 ),
406 );
407
408 if (!request.isSuccess) {
409 log("Tron TRC20 error: ${request.error} \n ${request.respose}");
410 throw Exception('An error occurred while creating the transfer request. Please try again.');
411 }
412
413 final feeLimit = await getFeeLimit(
414 request.transactionRaw!,
415 ownerAddress,
416 receiverAddress,
417 energyUsed: request.energyUsed ?? 0,
418 );
419
420 if (feeLimit > tronBalance.toInt()) {
421 final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
422 throw Exception(
423 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX',
424 );
425 }
426
427 return request.transactionRaw!.copyWith(feeLimit: BigInt.from(feeLimit));
428 }
429
430 Future<String> sendTransaction({
431 required TransactionRaw rawTransaction,
432 required List<int> signature,
433 }) async {
434 try {
435 final transaction = Transaction(rawData: rawTransaction, signature: [signature]);
436
437 final raw = BytesUtils.toHexString(transaction.toBuffer());
438
439 final txBroadcastResult = await _provider!.request(TronRequestBroadcastHex(transaction: raw));
440
441 if (txBroadcastResult.isSuccess) {
442 return txBroadcastResult.txId!;
443 } else {
444 throw Exception(txBroadcastResult.error);
445 }
446 } catch (e) {
447 log('Send block Exception: ${e.toString()}');
448 throw Exception(e);
449 }
450 }
451
452 Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress,
453 {bool throwOnError = false, required Currency currency}) async {
454 try {
455 final ownerAddress = TronAddress(userAddress);
456
457 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
458 final function = contract.functionFromName("balanceOf");
459
460 final request = await _provider!.request(
461 TronRequestTriggerConstantContract.fromMethod(
462 ownerAddress: ownerAddress,
463 contractAddress: TronAddress(contractAddress),
464 function: function,
465 params: [ownerAddress],
466 ),
467 );
468
469 final outputResult = request.outputResult?.first ?? BigInt.zero;
470
471 return TronBalance(Money(outputResult, currency));
472 } catch (_) {
473 if (throwOnError) {
474 rethrow;
475 }
476 return TronBalance(Money.zero(currency));
477 }
478 }
479
480 Future<TronToken?> getTronToken(String contractAddress, String userAddress) async {
481 try {
482 final tokenAddress = TronAddress(contractAddress);
483
484 final ownerAddress = TronAddress(userAddress);
485
486 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
487
488 final name =
489 (await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? '';
490
491 final symbol =
492 (await getTokenDetail(contract, "symbol", ownerAddress, tokenAddress) as String?) ?? '';
493
494 final decimal =
495 (await getTokenDetail(contract, "decimals", ownerAddress, tokenAddress) as BigInt?) ??
496 BigInt.zero;
497
498 return TronToken(
499 name: name,
500 symbol: symbol,
501 contractAddress: contractAddress,
502 decimal: decimal.toInt(),
503 );
504 } catch (e) {
505 return null;
506 }
507 }
508
509 Future<dynamic> getTokenDetail(
510 ContractABI contract,
511 String functionName,
512 TronAddress ownerAddress,
513 TronAddress tokenAddress,
514 ) async {
515 final function = contract.functionFromName(functionName);
516
517 try {
518 final request = await _provider!.request(
519 TronRequestTriggerConstantContract.fromMethod(
520 ownerAddress: ownerAddress,
521 contractAddress: tokenAddress,
522 function: function,
523 params: [],
524 ),
525 );
526
527 final outputResult = request.outputResult?.first;
528
529 return outputResult;
530 } catch (_) {
531 log('Erorr fetching detail: ${_.toString()}');
532
533 return null;
534 }
535 }
536 }