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