@swapkit/toolboxes 4.0.0-beta.36 → 4.0.0-beta.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import { SwapKitError } from "@swapkit/helpers";
2
+ import { TronWeb } from "tronweb";
3
+ import type { TronGridAccountResponse } from "../types.js";
4
+
5
+ const TRONGRID_API_BASE = "https://api.trongrid.io";
6
+
7
+ /**
8
+ * Fetch account information including TRC20 balances from TronGrid API
9
+ */
10
+ export async function fetchAccountFromTronGrid(address: string) {
11
+ try {
12
+ const response = await fetch(`${TRONGRID_API_BASE}/v1/accounts/${address}`);
13
+
14
+ if (!response.ok) {
15
+ throw new Error(`TronGrid API error: ${response.status} ${response.statusText}`);
16
+ }
17
+
18
+ const data = (await response.json()) as TronGridAccountResponse;
19
+
20
+ if (!(data.success && data.data) || data.data.length === 0) {
21
+ throw new Error("Invalid response from TronGrid API");
22
+ }
23
+
24
+ // Convert search address to hex format for comparison
25
+ let searchAddressHex: string;
26
+ try {
27
+ // If address is base58, convert to hex
28
+ searchAddressHex = TronWeb.address.toHex(address).toLowerCase();
29
+ } catch {
30
+ // If conversion fails, assume it's already hex
31
+ searchAddressHex = address.toLowerCase();
32
+ }
33
+
34
+ // Find the account that matches the requested address
35
+ const account = data.data.find((acc) => {
36
+ return acc.address.toLowerCase() === searchAddressHex;
37
+ });
38
+
39
+ if (!account) {
40
+ return;
41
+ }
42
+
43
+ // Return simplified object with balance and trc20 array
44
+ return {
45
+ balance: account.balance,
46
+ trc20: account.trc20 || [],
47
+ };
48
+ } catch (error) {
49
+ throw new SwapKitError("toolbox_tron_trongrid_api_error", {
50
+ message: error instanceof Error ? error.message : "Unknown error",
51
+ address,
52
+ });
53
+ }
54
+ }
package/src/tron/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  TronTransferParams,
10
10
  TronContract,
11
11
  TronTransaction,
12
+ TronSignedTransaction,
12
13
  } from "./types";
13
14
  export { trc20ABI } from "./helpers/trc20.abi";
14
15
 
@@ -11,6 +11,7 @@ import {
11
11
  import { P, match } from "ts-pattern";
12
12
 
13
13
  import { trc20ABI } from "./helpers/trc20.abi.js";
14
+ import { fetchAccountFromTronGrid } from "./helpers/trongrid.js";
14
15
  import type {
15
16
  TronCreateTransactionParams,
16
17
  TronSignedTransaction,
@@ -22,6 +23,14 @@ import type {
22
23
 
23
24
  import { TronWeb } from "tronweb";
24
25
 
26
+ // Constants for TRON resource calculation
27
+ const TRX_TRANSFER_BANDWIDTH = 268; // Bandwidth consumed by a TRX transfer
28
+ const TRC20_TRANSFER_ENERGY = 13000; // Average energy consumed by TRC20 transfer
29
+ const TRC20_TRANSFER_BANDWIDTH = 345; // Bandwidth consumed by TRC20 transfer
30
+
31
+ // Known TRON tokens
32
+ const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
33
+
25
34
  export async function getTronAddressValidator() {
26
35
  return (address: string) => {
27
36
  return TronWeb.isAddress(address);
@@ -131,50 +140,201 @@ export const createTronToolbox = async (options: TronToolboxOptions = {}) => {
131
140
  return 100_000_000; // 100 TRX in SUN
132
141
  };
133
142
 
134
- const getBalance = async (address: string, scamFilter = true) => {
135
- const { getBalance: getBalanceFromApi } = await import("../utils.js");
143
+ /**
144
+ * Get current chain parameters including resource prices
145
+ */
146
+ const getChainParameters = async () => {
147
+ try {
148
+ const parameters = await tronWeb.trx.getChainParameters();
149
+ const paramMap: Record<string, number> = {};
150
+
151
+ for (const param of parameters) {
152
+ paramMap[param.key] = param.value;
153
+ }
154
+
155
+ return {
156
+ energyFee: paramMap.getEnergyFee || 420, // SUN per energy unit
157
+ bandwidthFee: paramMap.getTransactionFee || 1000, // SUN per bandwidth unit
158
+ createAccountFee: paramMap.getCreateAccountFee || 100000, // 0.1 TRX in SUN
159
+ };
160
+ } catch {
161
+ // Return default values if unable to fetch
162
+ return {
163
+ energyFee: 420,
164
+ bandwidthFee: 1000,
165
+ createAccountFee: 100000,
166
+ };
167
+ }
168
+ };
169
+
170
+ /**
171
+ * Check if an address exists on the blockchain
172
+ */
173
+ const accountExists = async (address: string) => {
174
+ try {
175
+ const account = await tronWeb.trx.getAccount(address);
176
+ return account && Object.keys(account).length > 0;
177
+ } catch {
178
+ return false;
179
+ }
180
+ };
136
181
 
182
+ /**
183
+ * Get account resources (bandwidth and energy)
184
+ */
185
+ const getAccountResources = async (address: string) => {
137
186
  try {
138
- // Use SwapKit API for comprehensive balance fetching (includes TRX + TRC20 tokens)
139
- const apiBalances = await getBalanceFromApi(Chain.Tron)(address, scamFilter);
187
+ const resources = await tronWeb.trx.getAccountResources(address);
188
+
189
+ return {
190
+ bandwidth: {
191
+ free: resources.freeNetLimit - resources.freeNetUsed,
192
+ total: resources.NetLimit || 0,
193
+ used: resources.NetUsed || 0,
194
+ },
195
+ energy: {
196
+ total: resources.EnergyLimit || 0,
197
+ used: resources.EnergyUsed || 0,
198
+ },
199
+ };
200
+ } catch {
201
+ // Return default structure if unable to fetch
202
+ return {
203
+ bandwidth: { free: 600, total: 0, used: 0 }, // 600 free bandwidth daily
204
+ energy: { total: 0, used: 0 },
205
+ };
206
+ }
207
+ };
208
+
209
+ /**
210
+ * Get token balance and info directly from contract
211
+ */
212
+ const fetchTokenBalance = async (address: string, contractAddress: string) => {
213
+ try {
214
+ const contract = tronWeb.contract(trc20ABI, contractAddress);
140
215
 
141
- // If API returns balances, use those
142
- if (apiBalances.length > 0) {
143
- return apiBalances;
216
+ if (!contract.methods?.balanceOf) {
217
+ return 0n;
144
218
  }
145
219
 
146
- // Fallback to on-chain TRX balance if API fails or returns empty
147
- const trxBalanceInSun = await tronWeb.trx.getBalance(address);
148
- return [
149
- AssetValue.from({
150
- chain: Chain.Tron,
151
- value: trxBalanceInSun,
152
- fromBaseDecimal: 6, // TRX has 6 decimals
153
- }),
154
- ];
220
+ const balance = (await contract.methods.balanceOf(address).call())[0] as string;
221
+
222
+ return BigInt(balance || 0); // Convert to BigInt for consistency
223
+ } catch (err) {
224
+ console.warn(`balanceOf() failed for ${contractAddress}:`, err);
225
+ return 0n;
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Get token balance and info directly from contract
231
+ */
232
+ const fetchTokenMetadata = async (contractAddress: string, address: string) => {
233
+ try {
234
+ tronWeb.setAddress(address); // Set address for contract calls
235
+ const contract = tronWeb.contract(trc20ABI, contractAddress);
236
+
237
+ const [symbolRaw, decimalsRaw] = await Promise.all([
238
+ contract
239
+ .symbol()
240
+ .call()
241
+ .catch(() => "UNKNOWN"),
242
+ contract
243
+ .decimals()
244
+ .call()
245
+ .catch(() => "18"),
246
+ ]);
247
+
248
+ return {
249
+ symbol: symbolRaw ?? "UNKNOWN",
250
+ decimals: Number(decimalsRaw ?? 18),
251
+ };
252
+ } catch (error) {
253
+ warnOnce(
254
+ true,
255
+ `Failed to get token balance for ${contractAddress}: ${error instanceof Error ? error.message : error}`,
256
+ );
257
+ return null;
258
+ }
259
+ };
260
+
261
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
262
+ const getBalance = async (address: string, _scamFilter = true) => {
263
+ const fallbackBalance = [
264
+ AssetValue.from({
265
+ chain: Chain.Tron,
266
+ }),
267
+ ];
268
+ // Try primary source (TronGrid)
269
+ try {
270
+ const accountData = await fetchAccountFromTronGrid(address);
271
+ if (accountData) {
272
+ const balances: AssetValue[] = [];
273
+
274
+ // Add TRX balance
275
+ balances.push(
276
+ AssetValue.from({
277
+ chain: Chain.Tron,
278
+ value: accountData.balance,
279
+ fromBaseDecimal: 6,
280
+ }),
281
+ );
282
+
283
+ // Add TRC20 balances
284
+
285
+ for (const token of accountData.trc20) {
286
+ const [contractAddress, balance] = Object.entries(token)[0] || [];
287
+
288
+ if (!(contractAddress && balance)) continue;
289
+
290
+ const tokenMetaData = await fetchTokenMetadata(contractAddress, address);
291
+
292
+ if (!tokenMetaData) continue;
293
+
294
+ balances.push(
295
+ AssetValue.from({
296
+ asset: `TRX.${tokenMetaData.symbol}-${contractAddress}`,
297
+ value: BigInt(balance || 0),
298
+ fromBaseDecimal: tokenMetaData.decimals,
299
+ }),
300
+ );
301
+ }
302
+
303
+ return balances;
304
+ }
305
+ return fallbackBalance;
155
306
  } catch (error) {
156
307
  warnOnce(
157
308
  true,
158
- `Failed to get Tron balance for ${address}: ${error instanceof Error ? error.message : error}`,
309
+ `Tron API getBalance failed: ${error instanceof Error ? error.message : error}`,
159
310
  );
160
311
 
161
- // Final fallback: try to get just the native TRX balance
162
- try {
163
- const trxBalanceInSun = await tronWeb.trx.getBalance(address);
164
- return [
312
+ // Fallback: get TRX and USDT directly
313
+ const balances: AssetValue[] = [];
314
+
315
+ const trxBalanceInSun = await tronWeb.trx.getBalance(address);
316
+ if (trxBalanceInSun && Number(trxBalanceInSun) > 0) {
317
+ balances.push(
165
318
  AssetValue.from({
166
319
  chain: Chain.Tron,
167
320
  value: trxBalanceInSun,
168
321
  fromBaseDecimal: 6,
169
322
  }),
170
- ];
171
- } catch (fallbackError) {
172
- warnOnce(
173
- true,
174
- `Failed to get native TRX balance for ${address}: ${fallbackError instanceof Error ? fallbackError.message : fallbackError}`,
175
323
  );
176
- return [];
177
324
  }
325
+
326
+ const usdtBalance = await fetchTokenBalance(address, TRON_USDT_CONTRACT);
327
+ if (usdtBalance) {
328
+ balances.push(
329
+ AssetValue.from({
330
+ asset: `TRX.USDT-${TRON_USDT_CONTRACT}`,
331
+ value: usdtBalance,
332
+ fromBaseDecimal: 6,
333
+ }),
334
+ );
335
+ }
336
+
337
+ return balances;
178
338
  }
179
339
  };
180
340
 
@@ -218,7 +378,7 @@ export const createTronToolbox = async (options: TronToolboxOptions = {}) => {
218
378
  }
219
379
 
220
380
  const feeLimit = calculateFeeLimit();
221
- const contract = await tronWeb.contract(trc20ABI, contractAddress);
381
+ const contract = tronWeb.contract(trc20ABI, contractAddress);
222
382
 
223
383
  if (!contract.methods?.transfer) {
224
384
  throw new SwapKitError("toolbox_tron_token_transfer_failed");
@@ -239,17 +399,94 @@ export const createTronToolbox = async (options: TronToolboxOptions = {}) => {
239
399
  return txid;
240
400
  };
241
401
 
242
- const estimateTransactionFee = ({ assetValue }: TronTransferParams) => {
402
+ const estimateTransactionFee = async ({
403
+ assetValue,
404
+ recipient,
405
+ sender,
406
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
407
+ }: TronTransferParams & { sender?: string }) => {
243
408
  const isNative = assetValue.isGasAsset;
244
409
 
245
- if (isNative) {
246
- // Native TRX transfers typically consume bandwidth, which is free up to daily limit
247
- // Return a minimal fee estimation for bandwidth cost
248
- return AssetValue.from({ chain: Chain.Tron, value: 1 }); // 1 TRX
249
- }
410
+ try {
411
+ // Get sender address
412
+ const senderAddress = sender ? sender : signer ? await getAddress() : undefined;
413
+ if (!senderAddress) {
414
+ // If no signer, return conservative estimate
415
+ return isNative
416
+ ? AssetValue.from({ chain: Chain.Tron, value: 0.1, fromBaseDecimal: 0 })
417
+ : AssetValue.from({ chain: Chain.Tron, value: 15, fromBaseDecimal: 0 });
418
+ }
419
+
420
+ // Get chain parameters for current resource prices
421
+ const chainParams = await getChainParameters();
250
422
 
251
- // TRC20 transfers consume energy, estimate higher fee
252
- return AssetValue.from({ chain: Chain.Tron, value: 10 }); // 10 TRX
423
+ // Check if recipient account exists (new accounts require activation fee)
424
+ const recipientExists = await accountExists(recipient);
425
+ const activationFee = recipientExists ? 0 : chainParams.createAccountFee;
426
+
427
+ // Get account resources
428
+ const resources = await getAccountResources(senderAddress);
429
+
430
+ if (isNative) {
431
+ // Calculate bandwidth needed for TRX transfer
432
+ const bandwidthNeeded = TRX_TRANSFER_BANDWIDTH;
433
+ const availableBandwidth =
434
+ resources.bandwidth.free + (resources.bandwidth.total - resources.bandwidth.used);
435
+
436
+ let bandwidthFee = 0;
437
+ if (bandwidthNeeded > availableBandwidth) {
438
+ // Need to burn TRX for bandwidth
439
+ const bandwidthToBuy = bandwidthNeeded - availableBandwidth;
440
+ bandwidthFee = bandwidthToBuy * chainParams.bandwidthFee;
441
+ }
442
+
443
+ // Total fee in SUN
444
+ const totalFeeSun = activationFee + bandwidthFee;
445
+
446
+ return AssetValue.from({
447
+ chain: Chain.Tron,
448
+ value: totalFeeSun,
449
+ fromBaseDecimal: 6, // SUN to TRX
450
+ });
451
+ }
452
+
453
+ // TRC20 Transfer - needs both bandwidth and energy
454
+ const bandwidthNeeded = TRC20_TRANSFER_BANDWIDTH;
455
+ const energyNeeded = TRC20_TRANSFER_ENERGY;
456
+
457
+ const availableBandwidth =
458
+ resources.bandwidth.free + (resources.bandwidth.total - resources.bandwidth.used);
459
+ const availableEnergy = resources.energy.total - resources.energy.used;
460
+
461
+ let bandwidthFee = 0;
462
+ if (bandwidthNeeded > availableBandwidth) {
463
+ const bandwidthToBuy = bandwidthNeeded - availableBandwidth;
464
+ bandwidthFee = bandwidthToBuy * chainParams.bandwidthFee;
465
+ }
466
+
467
+ let energyFee = 0;
468
+ if (energyNeeded > availableEnergy) {
469
+ const energyToBuy = energyNeeded - availableEnergy;
470
+ energyFee = energyToBuy * chainParams.energyFee;
471
+ }
472
+
473
+ // Total fee in SUN
474
+ const totalFeeSun = activationFee + bandwidthFee + energyFee;
475
+
476
+ return AssetValue.from({
477
+ chain: Chain.Tron,
478
+ value: totalFeeSun,
479
+ fromBaseDecimal: 6, // SUN to TRX
480
+ });
481
+ } catch (error) {
482
+ // Fallback to conservative estimates if calculation fails
483
+ warnOnce(
484
+ true,
485
+ `Failed to calculate exact fee, using conservative estimate: ${error instanceof Error ? error.message : error}`,
486
+ );
487
+
488
+ throw new SwapKitError("toolbox_tron_fee_estimation_failed", { error });
489
+ }
253
490
  };
254
491
 
255
492
  const createTransaction = async (params: TronCreateTransactionParams) => {
package/src/tron/types.ts CHANGED
@@ -29,3 +29,70 @@ export interface TronCreateTransactionParams
29
29
  extends Omit<GenericCreateTransactionParams, "feeRate"> {
30
30
  // No additional fields needed - all inherited from GenericCreateTransactionParams
31
31
  }
32
+
33
+ // TronGrid API Types
34
+ export type TronGridTRC20Balance = Array<{
35
+ [contractAddress: string]: string; // Balance as string
36
+ }>;
37
+
38
+ export interface TronGridAccountResponse {
39
+ data: Array<{
40
+ address: string;
41
+ balance: number; // TRX balance in SUN
42
+ create_time: number;
43
+ latest_opration_time: number; // Note: typo in API response
44
+ free_net_usage: number;
45
+ net_window_size: number;
46
+ net_window_optimized: boolean;
47
+ trc20: TronGridTRC20Balance;
48
+ assetV2?: Array<{
49
+ key: string;
50
+ value: number;
51
+ }>;
52
+ frozenV2?: Array<{
53
+ type?: string;
54
+ }>;
55
+ free_asset_net_usageV2?: Array<{
56
+ key: string;
57
+ value: number;
58
+ }>;
59
+ latest_consume_free_time?: number;
60
+ owner_permission?: {
61
+ keys: Array<{
62
+ address: string;
63
+ weight: number;
64
+ }>;
65
+ threshold: number;
66
+ permission_name: string;
67
+ };
68
+ active_permission?: Array<{
69
+ operations: string;
70
+ keys: Array<{
71
+ address: string;
72
+ weight: number;
73
+ }>;
74
+ threshold: number;
75
+ id: number;
76
+ type: string;
77
+ permission_name: string;
78
+ }>;
79
+ account_resource?: {
80
+ energy_window_optimized: boolean;
81
+ energy_window_size: number;
82
+ };
83
+ }>;
84
+ success: boolean;
85
+ meta: {
86
+ at: number;
87
+ page_size: number;
88
+ };
89
+ }
90
+
91
+ export interface TronGridTokenInfo {
92
+ symbol: string;
93
+ address: string;
94
+ decimals: number;
95
+ name: string;
96
+ totalSupply: string;
97
+ owner: string;
98
+ }
@@ -1,5 +0,0 @@
1
- import"./chunk-s47y8512.js";import{AssetValue as c,BaseDecimal as p}from"@swapkit/helpers";import{SwapKitApi as u}from"@swapkit/helpers/api";var l=typeof process!=="undefined"&&process.pid?process.pid.toString(36):"",a=0;function g(){function t(){let e=Date.now(),n=a||e;return a=n,e>a?e:n+1}return l+t().toString(36)}function w(t){return async function e(n,i=!0){return(await u.getChainBalance({chain:t,address:n,scamFilter:i})).map(({identifier:r,value:o,decimal:s})=>{return new c({decimal:s||p[t],value:o,identifier:r})})}}export{g as uniqid,w as getBalance};
2
- export{g as a,w as b};
3
-
4
- //# debugId=8C6F04B3BE4067A464756E2164756E21
5
- //# sourceMappingURL=chunk-6f98phv2.js.map