@zebec-network/tron-transfer-sdk 1.0.0 → 2.0.0

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.
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  A small TypeScript SDK for sending **TRX** (TRON's native coin) and **TRC-20 tokens** such as **USDT** from one TRON address to another, on either **mainnet** or **testnet** (Nile or Shasta).
4
4
 
5
- It wraps [TronWeb](https://tronweb.network) with two functions, `transferTrx` and `transferToken`. Each one takes care of the fiddly parts of a transfer:
5
+ It wraps [TronWeb](https://tronweb.network) in a `TronTransferService` class with `transferTrx` and `transferToken` methods. The service is given a **signer**: in a web app, the user's connected wallet (TronLink and others, through the TronWallet adapter); in Node, a private key. Each transfer takes care of the fiddly parts:
6
6
 
7
7
  - **Unit conversion.** You pass amounts in whole units (`"10"` TRX, `"2.5"` USDT). The SDK converts them to on-chain units with exact integer math, so there are no floating-point rounding errors.
8
8
  - **Pre-flight checks.** Addresses, amounts and the sender's balance are checked before anything is sent. This matters for tokens: a TRC-20 transfer that fails on-chain still burns TRX in fees.
9
- - **Build, sign, broadcast.** The transaction is signed locally with the private key from your `.env`. The key never leaves your machine.
9
+ - **Build, sign, broadcast.** The SDK builds the transaction, the signer signs it (the wallet asks the user to approve), and the SDK broadcasts it. The SDK itself never holds a key.
10
10
  - **Confirmation.** By default the SDK waits until the transaction is in a block and verifies it actually succeeded. The network accepting a transaction does not mean it executed, especially for token transfers.
11
11
  - **Readable errors.** Node error messages are decoded, and every failure after broadcast carries the transaction ID.
12
12
 
@@ -18,6 +18,7 @@ It wraps [TronWeb](https://tronweb.network) with two functions, `transferTrx` an
18
18
  - [Installation](#installation)
19
19
  - [Configuration](#configuration)
20
20
  - [Quick start](#quick-start)
21
+ - [Frontend: signing with the user's wallet](#frontend-signing-with-the-users-wallet)
21
22
  - [API reference](#api-reference)
22
23
  - [Types](#types)
23
24
  - [How a transfer works](#how-a-transfer-works)
@@ -42,7 +43,7 @@ It wraps [TronWeb](https://tronweb.network) with two functions, `transferTrx` an
42
43
  | Package format | **ES modules only** (`"type": "module"`). Load it with `import`, not `require()`. |
43
44
  | TypeScript | Optional for consumers. If you use it, set `"moduleResolution"` to `"nodenext"`, `"node16"` or `"bundler"`, and turn on `"skipLibCheck": true` (see below). |
44
45
  | A TRON account | Its hex private key, funded with TRX for fees (and holding the token, for token transfers) |
45
- | TronGrid key | **Required for mainnet** (`api.trongrid.io`); optional on testnets. Get one at [trongrid.io](https://www.trongrid.io). |
46
+ | TronGrid key | **Required for mainnet** (`api.trongrid.io`), in the browser and in Node; optional on testnets. Get one at [trongrid.io](https://www.trongrid.io). |
46
47
 
47
48
  > **Why `skipLibCheck`?** TronWeb's bundled type definitions don't pass strict type-checking under `nodenext` resolution. Your own code is still fully checked; this only skips checking the `.d.ts` files inside `node_modules`.
48
49
 
@@ -56,10 +57,19 @@ npm install @zebec-network/tron-transfer-sdk
56
57
  yarn add @zebec-network/tron-transfer-sdk
57
58
  ```
58
59
 
59
- Then import it:
60
+ The package has two entry points:
61
+
62
+ | Import path | Environment | Contents |
63
+ | --------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- |
64
+ | `@zebec-network/tron-transfer-sdk` | Browser, Node | `TronTransferService`, signers, config helpers, types. No `dotenv`, no filesystem or `process.env` access. |
65
+ | `@zebec-network/tron-transfer-sdk/node` | Node only | Everything above, plus `loadConfig()` and the `.env`-driven `transferTrx()` / `transferToken()` shortcut functions. |
60
66
 
61
67
  ```ts
62
- import { transferTrx, transferToken } from '@zebec-network/tron-transfer-sdk';
68
+ // Web app
69
+ import { TronTransferService } from '@zebec-network/tron-transfer-sdk';
70
+
71
+ // Node script or backend
72
+ import { transferTrx, transferToken } from '@zebec-network/tron-transfer-sdk/node';
63
73
  ```
64
74
 
65
75
  To work on the SDK itself, see [Development](#development).
@@ -68,7 +78,9 @@ To work on the SDK itself, see [Development](#development).
68
78
 
69
79
  ## Configuration
70
80
 
71
- The SDK reads its settings from environment variables. It loads a `.env` file from the **current working directory** automatically when it's imported, via `dotenv`. Variables already set in the real environment take precedence over `.env`, so you can also configure it without a file (for example in CI or a container).
81
+ This section is about the **Node entry** (`/node`). In a web app, pass settings straight to `TronTransferService` instead; see [`TronTransferServiceConfig`](#new-trontransferservicesigner-config).
82
+
83
+ The Node entry reads its settings from environment variables. It loads a `.env` file from the **current working directory** automatically when it's imported, via `dotenv`. Variables already set in the real environment take precedence over `.env`, so you can also configure it without a file (for example in CI or a container).
72
84
 
73
85
  Copy the template and fill in your values:
74
86
 
@@ -87,12 +99,12 @@ cp .env.example .env
87
99
 
88
100
  **Per network.** Replace `<NET>` with `TESTNET` or `MAINNET`:
89
101
 
90
- | Variable | Required | Description |
91
- | -------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
- | `TRON_<NET>_FULL_HOST` | Yes | Full-node URL, e.g. `https://nile.trongrid.io` or `https://api.trongrid.io`. |
93
- | `TRON_<NET>_PRIVATE_KEY` | Yes | Sender's private key: 64 hex characters. A leading `0x` is accepted and stripped. |
94
- | `TRON_<NET>_API_KEY` | Mainnet on TronGrid: yes | TronGrid API key, sent as the `TRON-PRO-API-KEY` header. Keyless mainnet requests to `api.trongrid.io` get HTTP 429, so the SDK refuses to run without one. |
95
- | `TRON_<NET>_USDT_CONTRACT` | No | Default token for `transferToken` when no `tokenAddress` is passed. |
102
+ | Variable | Required | Description |
103
+ | -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | `TRON_<NET>_FULL_HOST` | Yes | Full-node URL, e.g. `https://nile.trongrid.io` or `https://api.trongrid.io`. |
105
+ | `TRON_<NET>_PRIVATE_KEY` | Yes | Sender's private key: 64 hex characters. A leading `0x` is accepted and stripped. |
106
+ | `TRON_<NET>_API_KEY` | Mainnet on TronGrid: yes | TronGrid API key, sent as the `TRON-PRO-API-KEY` header. Keyless TronGrid requests are limited per IP to about 1 request/s, which a token transfer can exceed, so the SDK refuses to run on mainnet TronGrid without one. |
107
+ | `TRON_<NET>_USDT_CONTRACT` | No | Default token for `transferToken` when no `tokenAddress` is passed. If unset, USDT is used when the host is a TronGrid mainnet, Nile or Shasta host. |
96
108
 
97
109
  **Tests only**
98
110
 
@@ -128,8 +140,10 @@ Configuration is read on **every call**, so each call checks only the network it
128
140
 
129
141
  ## Quick start
130
142
 
143
+ In Node, with the sender's key in `.env`:
144
+
131
145
  ```ts
132
- import { transferTrx, transferToken, TronTransferError } from '@zebec-network/tron-transfer-sdk';
146
+ import { transferTrx, transferToken, TronTransferError } from '@zebec-network/tron-transfer-sdk/node';
133
147
 
134
148
  // Send 10 TRX on the network set by TRON_NETWORK
135
149
  const trx = await transferTrx({ to: 'TXYZ...recipient', amount: '10' });
@@ -160,13 +174,169 @@ try {
160
174
 
161
175
  ---
162
176
 
177
+ ## Frontend: signing with the user's wallet
178
+
179
+ In a web app there is no `.env` private key. The user's wallet signs instead. `TronTransferService` takes any object with this shape:
180
+
181
+ ```ts
182
+ interface TronSigner {
183
+ readonly address: string | null; // connected account, or null when disconnected
184
+ signTransaction(transaction: Transaction): Promise<SignedTransaction>;
185
+ }
186
+ ```
187
+
188
+ That is exactly the interface of the [TronWallet adapters](https://github.com/web3-geek/tronwallet-adapter) (`TronLinkAdapter`, `WalletConnectAdapter`, …) and of `useWallet()` from their React hooks, so you pass them in directly. There's no glue code to write.
189
+
190
+ ### React, with `@tronweb3/tronwallet-adapter-react-hooks`
191
+
192
+ ```tsx
193
+ import { useMemo } from 'react';
194
+ import { useWallet } from '@tronweb3/tronwallet-adapter-react-hooks';
195
+ import { TronTransferService, TronTransferError } from '@zebec-network/tron-transfer-sdk';
196
+
197
+ function useTronTransfers() {
198
+ const { address, signTransaction } = useWallet();
199
+ return useMemo(
200
+ () =>
201
+ new TronTransferService(
202
+ { address, signTransaction },
203
+ { network: 'mainnet', apiKey: import.meta.env.VITE_TRONGRID_KEY }, // required on mainnet TronGrid
204
+ ),
205
+ [address, signTransaction],
206
+ );
207
+ }
208
+
209
+ function SendUsdtButton({ to, amount }: { to: string; amount: string }) {
210
+ const service = useTronTransfers();
211
+
212
+ async function send() {
213
+ try {
214
+ // The wallet pops up to approve; the promise resolves once the transfer is confirmed in a block.
215
+ const result = await service.transferToken({ to, amount });
216
+ console.log('Sent', result.txid);
217
+ } catch (error) {
218
+ if (error instanceof TronTransferError && !error.txid) {
219
+ // Nothing was broadcast: validation failed, balance too low, or the user rejected the request.
220
+ // For a rejection, `error.cause` is the wallet's original error.
221
+ }
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ return <button onClick={send}>Send {amount} USDT</button>;
227
+ }
228
+ ```
229
+
230
+ ### Plain adapter, no React
231
+
232
+ ```ts
233
+ import { TronLinkAdapter } from '@tronweb3/tronwallet-adapters';
234
+ import { TronTransferService } from '@zebec-network/tron-transfer-sdk';
235
+
236
+ const adapter = new TronLinkAdapter();
237
+ await adapter.connect();
238
+
239
+ const service = new TronTransferService(adapter, { network: 'testnet' }); // Nile by default
240
+ await service.transferTrx({ to: 'TXYZ...', amount: '1' });
241
+ const usdt = await service.getTokenBalance(); // connected account's USDT, e.g. "12.5"
242
+ ```
243
+
244
+ ### Rate limits (HTTP 429)
245
+
246
+ TronGrid can answer bursts with **HTTP 429**, even with an API key (and far more often without one, which is why mainnet requires it). The service handles this by stage:
247
+
248
+ | Stage | What the SDK does | If it still fails |
249
+ | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
250
+ | Checks and build (balances, decimals, tx build) | Retries with exponential backoff and jitter (~1 s, ~2 s, ~4 s; `rateLimitRetries`, default 3). | `code: 'RATE_LIMITED'`, **no `txid`**: nothing was sent. Safe to offer a retry. |
251
+ | Broadcast | Resends the **same signed transaction**. It has the same txid, so it can't be sent twice, and the wallet isn't asked to sign again. A duplicate on resend counts as success. | `code: 'RATE_LIMITED'` **with `txid`**: check the txid before retrying. |
252
+ | Confirmation | The transfer is already sent, so it keeps polling and slows down on 429 (up to 15 s between polls). | Confirmation timeout **with `txid`**. |
253
+
254
+ HTTP **403** (IP blocked, daily quota used up, or a bad key) is not retried: `code: 'ACCESS_DENIED'`. Token decimals are cached per service, so repeat token transfers need one fewer request.
255
+
256
+ What the UI should do:
257
+
258
+ ```ts
259
+ try {
260
+ await service.transferToken({ to, amount });
261
+ } catch (error) {
262
+ if (!(error instanceof TronTransferError)) throw error;
263
+ if (error.txid) {
264
+ // Possibly sent. Never resend automatically; show "pending" and a link to the txid on tronscan.
265
+ } else if (error.code === 'RATE_LIMITED') {
266
+ // Nothing was sent. Show "Network busy, try again in a few seconds" with a retry button.
267
+ } else if (error.code === 'NOT_SIGNED') {
268
+ // The user cancelled in their wallet.
269
+ }
270
+ }
271
+ ```
272
+
273
+ Keep the service's own traffic low, too: create one service and reuse it (for example with `useMemo`), fetch balances on demand rather than on a timer, and don't call balance methods while a transfer is running. If users still hit 429 often, check the key's quota in the TronGrid dashboard, or point `fullHost` at your own proxy or node.
274
+
275
+ ### How it behaves with a wallet
276
+
277
+ - **The address is read on every call.** If the user switches accounts, the next transfer comes from the new account. If they disconnect, calls throw `No wallet connected` before anything is built.
278
+ - **Only what was checked gets broadcast.** The SDK refuses a signed transaction whose `txID` differs from the one it built.
279
+ - **Rejection is a normal error.** If the user declines in the wallet, you get a `TronTransferError` with `code: 'NOT_SIGNED'` and no `txid`, and `cause` holds the wallet's error.
280
+ - **The wallet's selected network doesn't matter to the SDK.** Transactions are built on and broadcast to the service's `network`/`fullHost`, and the wallet just signs them. Make sure the UI's `network` matches what the user expects to spend on.
281
+ - **TronGrid API key on mainnet.** It's required, as in Node. A browser key is visible to every user, so restrict it to your app's origins in the TronGrid dashboard, or point `fullHost` at your own proxy or node (no key needed there).
282
+ - **Don't use `PrivateKeySigner` in a browser.** It's for backends, scripts and tests.
283
+
284
+ ---
285
+
163
286
  ## API reference
164
287
 
165
- Everything below is exported from the package root.
288
+ Unless noted, everything below is exported from both entry points.
289
+
290
+ ### `new TronTransferService(signer, config)`
291
+
292
+ ```ts
293
+ class TronTransferService {
294
+ constructor(signer: TronSigner, config: TronTransferServiceConfig);
295
+ readonly signer: TronSigner;
296
+ readonly config: ServiceConfig; // validated, with defaults applied
297
+ readonly tronWeb: TronWeb; // read-only client (no key), for anything the SDK doesn't cover
298
+
299
+ transferTrx(params: TransferTrxParams): Promise<TransferResult>;
300
+ transferToken(params: TransferTokenParams): Promise<TransferResult>;
301
+ getTrxBalance(address?: string): Promise<string>;
302
+ getTokenBalance(params?: TokenBalanceParams): Promise<string>;
303
+ }
304
+ ```
305
+
306
+ `config` (`TronTransferServiceConfig`). The constructor throws a plain `Error` if a value is invalid.
307
+
308
+ | Field | Type | Default | Description |
309
+ | ------------------ | ------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------ |
310
+ | `network` | `'testnet' \| 'mainnet'` | required | Network the service builds on, broadcasts to and reports. |
311
+ | `fullHost` | `string` | `https://api.trongrid.io` (mainnet), `https://nile.trongrid.io` (testnet) | Full-node URL. |
312
+ | `apiKey` | `string` | – | TronGrid key. **Required for mainnet on `api.trongrid.io`.** |
313
+ | `usdtContract` | `string` | USDT for TronGrid mainnet / Nile / Shasta hosts | Default token for `transferToken` and `getTokenBalance`. |
314
+ | `feeLimitSun` | `number` | `100000000` (100 TRX) | Default max TRX, in SUN, that one token transfer may burn. |
315
+ | `rateLimitRetries` | `number` | `3` | Retries, with backoff, for requests answered with HTTP 429. |
316
+
317
+ `transferTrx` and `transferToken` are described below; they take the same parameters as the Node shortcut functions, minus `network` (the service is bound to one).
318
+
319
+ `getTrxBalance(address?)` returns a TRX balance in whole TRX, such as `"12.5"`. `getTokenBalance({ tokenAddress?, owner? })` returns a token balance in whole tokens, using the token's `decimals()`. Both default to the signer's address and read the full node's latest state. They also work with a disconnected wallet if you pass an address.
320
+
321
+ ### `TronSigner` and `PrivateKeySigner`
322
+
323
+ `TronSigner` is the interface shown in [Frontend](#frontend-signing-with-the-users-wallet). `PrivateKeySigner` implements it with a hex private key, signing locally:
324
+
325
+ ```ts
326
+ import { PrivateKeySigner, TronTransferService } from '@zebec-network/tron-transfer-sdk';
327
+
328
+ const service = new TronTransferService(new PrivateKeySigner(process.env.KEY!), { network: 'testnet' });
329
+ ```
330
+
331
+ It throws on a malformed key. Use it on servers and in tests only.
332
+
333
+ ### `createTransferServiceFromEnv(network?)` (Node entry)
334
+
335
+ Returns a `TronTransferService` configured by `loadConfig(network)`, signing with `TRON_<NET>_PRIVATE_KEY`.
166
336
 
167
- ### `transferTrx(params)`
337
+ ### `transferTrx(params)` (Node entry)
168
338
 
169
- Sends native TRX from the account configured for the network.
339
+ Sends native TRX from the account configured in `.env`. It's a shortcut for `createTransferServiceFromEnv(params.network).transferTrx(params)`; `service.transferTrx` takes the same parameters except `network`.
170
340
 
171
341
  ```ts
172
342
  function transferTrx(params: TransferTrxParams): Promise<TransferResult>;
@@ -194,7 +364,7 @@ await transferTrx({ to: 'TXYZ...', amount: 12 }); // 12 TRX
194
364
  await transferTrx({ to: 'TXYZ...', amount: '1', waitForConfirmation: false }); // return right after broadcast
195
365
  ```
196
366
 
197
- ### `transferToken(params)`
367
+ ### `transferToken(params)` (Node entry)
198
368
 
199
369
  Sends a TRC-20 token by calling its `transfer(address,uint256)` function. The default token is the USDT contract configured for the network.
200
370
 
@@ -237,7 +407,7 @@ The error class for transfer failures. It extends `Error` and adds:
237
407
 
238
408
  The `message` includes the txid when there is one. See [Error handling](#error-handling) for the full list of failures.
239
409
 
240
- ### `loadConfig(network?)`
410
+ ### `loadConfig(network?)` (Node entry)
241
411
 
242
412
  Reads and validates configuration from the environment. `transferTrx` and `transferToken` call it for you; it's exported for advanced use.
243
413
 
@@ -249,19 +419,23 @@ It defaults to `TRON_NETWORK`, and throws `Error` on missing or invalid variable
249
419
 
250
420
  ### `createTronWeb(config)`
251
421
 
252
- Creates a TronWeb instance from a `NetworkConfig`, with the private key and API key header set. Use it to call TronWeb directly for anything the SDK doesn't cover:
422
+ Creates a TronWeb instance with the API key header set, and the private key if one is given (omit it for a read-only client). Use it to call TronWeb directly for anything the SDK doesn't cover:
253
423
 
254
424
  ```ts
255
- import { createTronWeb, loadConfig } from '@zebec-network/tron-transfer-sdk';
425
+ import { createTronWeb, loadConfig } from '@zebec-network/tron-transfer-sdk/node';
256
426
 
257
427
  const tronWeb = createTronWeb(loadConfig('testnet'));
258
428
  const account = await tronWeb.trx.getAccount('TXYZ...');
259
429
  ```
260
430
 
261
431
  ```ts
262
- function createTronWeb(config: NetworkConfig): TronWeb;
432
+ function createTronWeb(config: { fullHost: string; apiKey?: string; privateKey?: string }): TronWeb;
263
433
  ```
264
434
 
435
+ ### `resolveServiceConfig(config)`
436
+
437
+ Validates a `TronTransferServiceConfig` and applies defaults, returning a `ServiceConfig`. The service constructor calls it; it's exported so you can validate settings early, for example at app start-up.
438
+
265
439
  ### `toBaseUnits(amount, decimals)`
266
440
 
267
441
  Converts a human-readable amount into the smallest unit using exact `bigint` math. This is the same conversion the transfer functions use.
@@ -277,6 +451,10 @@ toBaseUnits('1.0000001', 6); // throws: more than 6 decimal places
277
451
 
278
452
  It rejects zero, negative, non-numeric and over-precise amounts. It also rejects numbers that JavaScript prints in exponent form (such as `1e21`); pass those as strings.
279
453
 
454
+ ### `fromBaseUnits(units, decimals)`
455
+
456
+ The reverse conversion, for displaying `rawAmount` or balances: `fromBaseUnits(1_500_000n, 6) === '1.5'`.
457
+
280
458
  ---
281
459
 
282
460
  ## Types
@@ -290,11 +468,15 @@ type TronNetwork = 'mainnet' | 'testnet';
290
468
  type Amount = string | number;
291
469
 
292
470
  interface TransferOptions {
293
- network?: TronNetwork; // default: TRON_NETWORK
294
471
  waitForConfirmation?: boolean; // default: true
295
472
  confirmationTimeoutMs?: number; // default: 60_000
296
473
  }
297
474
 
475
+ /** Only for the Node shortcut functions; a service is bound to one network. */
476
+ interface EnvTransferOptions {
477
+ network?: TronNetwork; // default: TRON_NETWORK
478
+ }
479
+
298
480
  interface TransferTrxParams extends TransferOptions {
299
481
  to: string;
300
482
  amount: Amount; // in TRX
@@ -303,14 +485,19 @@ interface TransferTrxParams extends TransferOptions {
303
485
  interface TransferTokenParams extends TransferOptions {
304
486
  to: string;
305
487
  amount: Amount; // in whole tokens
306
- tokenAddress?: string; // default: TRON_<NET>_USDT_CONTRACT
307
- feeLimitSun?: number; // default: TRON_FEE_LIMIT_SUN
488
+ tokenAddress?: string; // default: the configured USDT contract
489
+ feeLimitSun?: number; // default: the service's feeLimitSun
490
+ }
491
+
492
+ interface TokenBalanceParams {
493
+ tokenAddress?: string; // default: the configured USDT contract
494
+ owner?: string; // default: the signer's address
308
495
  }
309
496
 
310
497
  interface TransferResult {
311
498
  txid: string;
312
499
  network: TronNetwork;
313
- from: string; // sender, derived from the private key
500
+ from: string; // sender: the signer's address
314
501
  to: string;
315
502
  rawAmount: string; // smallest unit: SUN for TRX, base units for tokens
316
503
  confirmed: boolean; // false when waitForConfirmation is false
@@ -318,14 +505,25 @@ interface TransferResult {
318
505
  feeSun?: number; // total fee paid, in SUN; set when confirmed
319
506
  }
320
507
 
321
- interface NetworkConfig {
508
+ interface TronTransferServiceConfig {
509
+ network: TronNetwork;
510
+ fullHost?: string;
511
+ apiKey?: string;
512
+ usdtContract?: string;
513
+ feeLimitSun?: number;
514
+ }
515
+
516
+ interface ServiceConfig {
322
517
  network: TronNetwork;
323
518
  fullHost: string;
324
519
  apiKey: string | undefined;
325
- privateKey: string;
326
520
  usdtContract: string | undefined;
327
521
  feeLimitSun: number;
328
522
  }
523
+
524
+ interface NetworkConfig extends ServiceConfig {
525
+ privateKey: string; // from .env
526
+ }
329
527
  ```
330
528
 
331
529
  `rawAmount` is a string because token base units can exceed JavaScript's safe integer range. Use `BigInt(result.rawAmount)` to do arithmetic on it.
@@ -350,14 +548,14 @@ Example result:
350
548
  ## How a transfer works
351
549
 
352
550
  ```
353
- load config ─► validate inputs ─► check balance ─► build tx ─► sign locally ─► broadcast ─► wait for block ─► verify result
551
+ read signer address ─► validate inputs ─► check balance ─► build tx ─► signer signs ─► broadcast ─► wait for block ─► verify result
354
552
  ```
355
553
 
356
- 1. **Load config** for the network and derive the sender address from the private key.
554
+ 1. **Read the sender address** from the signer (the connected wallet, or the key's address).
357
555
  2. **Validate** the recipient, the amount, and (for tokens) the contract address and its `decimals()`.
358
556
  3. **Check balance:** TRX via the full node, tokens via `balanceOf`.
359
557
  4. **Build** the transaction: `transactionBuilder.sendTrx` for TRX, or `transactionBuilder.triggerSmartContract` calling `transfer(address,uint256)` for tokens.
360
- 5. **Sign** it locally with `trx.sign`. The private key is never sent anywhere.
558
+ 5. **Sign** it with `signer.signTransaction`. A wallet asks the user to approve; `PrivateKeySigner` signs locally. The SDK checks that the signed transaction's `txID` matches what it built.
361
559
  6. **Broadcast** it with `trx.sendRawTransaction`. If the node rejects it, the SDK throws with the decoded reason.
362
560
  7. **Wait for confirmation.** The SDK polls `getUnconfirmedTransactionInfo` every ~3 s (one TRON block) until the transaction appears in a block, up to `confirmationTimeoutMs`.
363
561
  8. **Verify.** If the transaction failed on-chain (for example a token contract revert or running out of energy), the SDK throws a `TronTransferError` with the txid.
@@ -393,33 +591,42 @@ Things to keep in mind:
393
591
  There are two error types:
394
592
 
395
593
  - **`TronTransferError`**: anything about the transfer itself (addresses, balances, broadcast, on-chain result).
396
- - **Plain `Error`**: configuration problems (from `loadConfig`) and invalid amounts (from `toBaseUnits`). These are thrown before anything touches the network.
397
-
398
- | When | Message (abridged) | Type | `txid` set? |
399
- | ------------ | ----------------------------------------------------------------- | ------------------- | ----------- |
400
- | Config | `Missing required environment variable …` | `Error` | – |
401
- | Config | `… PRIVATE_KEY must be a 64-character hex string.` | `Error` | – |
402
- | Config | `TRON_NETWORK must be "mainnet" or "testnet" …` | `Error` | – |
403
- | Config | `TRON_FEE_LIMIT_SUN must be a positive integer …` | `Error` | – |
404
- | Amount | `Invalid amount …` / `Amount must be greater than zero …` | `Error` | – |
405
- | Amount | `Amount … has more than N decimal places.` | `Error` | – |
406
- | Validation | `Invalid recipient address …` | `TronTransferError` | No |
407
- | Validation | `Recipient address is the same as the sender address.` | `TronTransferError` | No |
408
- | Validation | `Invalid token contract address …` | `TronTransferError` | No |
409
- | Validation | `No token address given and TRON_<NET>_USDT_CONTRACT is not set.` | `TronTransferError` | No |
410
- | Validation | `Call to decimals() on … failed … Is this a TRC-20 contract …?` | `TronTransferError` | No |
411
- | Balance | `Insufficient TRX balance …` / `Insufficient token balance …` | `TronTransferError` | No |
412
- | Build | `Failed to build token transfer: …` | `TronTransferError` | No |
413
- | Broadcast | `Broadcast rejected: <code> <reason>` | `TronTransferError` | Yes |
414
- | On-chain | `Transaction failed on-chain: <reason>` | `TronTransferError` | Yes |
415
- | Confirmation | `Transaction was broadcast but not confirmed within …` | `TronTransferError` | Yes |
594
+ - **Plain `Error`**: configuration problems (from `loadConfig`, `resolveServiceConfig` and the service constructor), malformed `PrivateKeySigner` keys, and invalid amounts (from `toBaseUnits`). These are thrown before anything touches the network.
595
+
596
+ | When | Message (abridged) | Type | `txid` set? |
597
+ | ------------ | ----------------------------------------------------------------------------------------- | ------------------- | ----------- |
598
+ | Config | `Missing required environment variable …` | `Error` | – |
599
+ | Config | `… PRIVATE_KEY must be a 64-character hex string.` | `Error` | – |
600
+ | Config | `TRON_NETWORK must be "mainnet" or "testnet" …` | `Error` | – |
601
+ | Config | `TRON_FEE_LIMIT_SUN must be a positive integer …` | `Error` | – |
602
+ | Config | `A TronGrid API key is required for … on mainnet …` | `Error` | – |
603
+ | Config | `network must be …` / `fullHost must be a URL …` / `feeLimitSun …` / `rateLimitRetries …` | `Error` | – |
604
+ | Amount | `Invalid amount …` / `Amount must be greater than zero …` | `Error` | – |
605
+ | Amount | `Amount … has more than N decimal places.` | `Error` | – |
606
+ | Wallet | `No wallet connected: the signer has no address.` | `TronTransferError` | No |
607
+ | Validation | `Invalid recipient address …` | `TronTransferError` | No |
608
+ | Validation | `Recipient address is the same as the sender address.` | `TronTransferError` | No |
609
+ | Validation | `Invalid token contract address …` | `TronTransferError` | No |
610
+ | Validation | `No token address given and no default USDT contract is known …` | `TronTransferError` | No |
611
+ | Validation | `Call to decimals() on … failed … Is this a TRC-20 contract …?` | `TronTransferError` | No |
612
+ | Balance | `Insufficient TRX balance …` / `Insufficient token balance …` | `TronTransferError` | No |
613
+ | Build | `Failed to build token transfer: …` | `TronTransferError` | No |
614
+ | Node | `Rate limited by … (HTTP 429) after N retries …` before broadcast (`RATE_LIMITED`) | `TronTransferError` | No |
615
+ | Node | `Request refused by … (HTTP 403) …` before broadcast (`ACCESS_DENIED`) | `TronTransferError` | No |
616
+ | Signing | `Transaction was not signed: <wallet error>` (`NOT_SIGNED`) | `TronTransferError` | No |
617
+ | Signing | `Signer returned a transaction that is unsigned or differs …` | `TronTransferError` | No |
618
+ | Broadcast | `Broadcast rejected: <code> <reason>` | `TronTransferError` | Yes |
619
+ | Broadcast | `Rate limited …` / `Request refused …` at broadcast | `TronTransferError` | Yes |
620
+ | Broadcast | `Broadcast request failed: … may still have reached the network` | `TronTransferError` | Yes |
621
+ | On-chain | `Transaction failed on-chain: <reason>` | `TronTransferError` | Yes |
622
+ | Confirmation | `Transaction was broadcast but not confirmed within …` | `TronTransferError` | Yes |
416
623
 
417
624
  Guidance for retries:
418
625
 
419
626
  - **No `txid`:** nothing was sent and no fees were charged. It's safe to fix the problem and retry.
420
- - **`txid` set:** the transaction reached the network. For a confirmation timeout, it may still succeed. **Look the txid up on an explorer (or with `trx.getTransactionInfo`) before retrying**, or you may send twice.
627
+ - **`txid` set:** the transaction may have reached the network. For a confirmation timeout, it may still succeed. **Look the txid up on an explorer (or with `trx.getTransactionInfo`) before retrying**, or you may send twice.
421
628
 
422
- Errors from TronWeb or the network itself, such as connection failures, HTTP 429 rate limiting, or an unreachable node, are passed through unchanged.
629
+ `code` is set for `RATE_LIMITED`, `ACCESS_DENIED` and `NOT_SIGNED`, so a UI can branch without matching messages. HTTP 429 is retried first; see [Rate limits](#rate-limits-http-429). Other errors from TronWeb or the network before broadcast, such as connection failures or an unreachable node, are passed through unchanged. Errors while polling for confirmation don't end the wait.
423
630
 
424
631
  ---
425
632
 
@@ -493,15 +700,16 @@ They need these variables in `.env` (see [Configuration](#configuration)):
493
700
 
494
701
  Use a recipient you control so you can reuse the funds. **On mainnet the tests spend real TRX and USDT.**
495
702
 
496
- What's covered (19 tests):
703
+ What's covered (35 tests):
497
704
 
498
- | File | Live transfers | Validation (nothing is sent) |
499
- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
500
- | [`test/transferTrx.test.ts`](test/transferTrx.test.ts) | 0.5 TRX with recipient balance check; 0.5 TRX as a number; `waitForConfirmation: false` | Bad address, self-send, zero / negative / non-numeric / over-precise amounts, insufficient balance |
501
- | [`test/transferToken.test.ts`](test/transferToken.test.ts) | 0.5 USDT via the default contract with balance check; 0.25 USDT via explicit `tokenAddress`; `waitForConfirmation: false` | Bad recipient, self-send, bad token address, non-TRC-20 address, too many decimals, insufficient balance |
705
+ | File | Live transfers | Validation (nothing is sent) |
706
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
707
+ | [`test/transferTrx.test.ts`](test/transferTrx.test.ts) | 0.5 TRX with recipient balance check; 0.5 TRX as a number; `waitForConfirmation: false` | Bad address, self-send, zero / negative / non-numeric / over-precise amounts, insufficient balance |
708
+ | [`test/transferToken.test.ts`](test/transferToken.test.ts) | 0.5 USDT via the default contract with balance check; 0.25 USDT via explicit `tokenAddress`; `waitForConfirmation: false` | Bad recipient, self-send, bad token address, non-TRC-20 address, too many decimals, insufficient balance |
709
+ | [`test/TronTransferService.test.ts`](test/TronTransferService.test.ts) | 0.000001 TRX and 0.000001 USDT through a wallet-shaped signer; balance reads | Config defaults and validation, `PrivateKeySigner`, `fromBaseUnits`, disconnected wallet, user rejection, tampered signed transaction; simulated HTTP 429/403 at each stage, including a real rebroadcast answered as a duplicate |
502
710
 
503
711
  - **Cost per run:** about 1 TRX and 0.75 USDT go to the recipient, plus network fees (up to ~0.3 TRX per TRX transfer once the sender's free bandwidth is used up; USDT transfers also burn TRX for energy unless the sender has staked energy).
504
- - **Duration:** about 25 s.
712
+ - **Duration:** about 1 minute.
505
713
  - The test files run one after the other with a generous timeout (`-t 1000000`), since each live transfer waits for a block.
506
714
 
507
715
  ---
@@ -511,16 +719,20 @@ What's covered (19 tests):
511
719
  ```
512
720
  tron-transfer-sdk/
513
721
  ├── src/
514
- │ ├── index.ts # Public exports
515
- │ ├── services.ts # transferTrx, transferToken, TronTransferError
516
- │ ├── config.ts # Env loading and validation (loadConfig)
722
+ │ ├── index.ts # Browser-safe entry (package root)
723
+ │ ├── node.ts # Node entry (/node): root + loadConfig and .env shortcut functions
724
+ │ ├── services.ts # TronTransferService, TronTransferError
725
+ │ ├── signer.ts # TronSigner interface, PrivateKeySigner
726
+ │ ├── config.ts # Service config, defaults and validation (resolveServiceConfig)
727
+ │ ├── env.ts # .env loading (loadConfig), Node only
517
728
  │ ├── client.ts # TronWeb instance factory (createTronWeb)
518
729
  │ ├── types.ts # Public parameter and result types
519
- │ └── utils.ts # Exact amount conversion (toBaseUnits)
730
+ │ └── utils.ts # Exact amount conversion (toBaseUnits, fromBaseUnits)
520
731
  ├── test/
521
732
  │ ├── helpers.ts # Shared test setup and balance readers
522
733
  │ ├── transferTrx.test.ts
523
- │ └── transferToken.test.ts
734
+ │ ├── transferToken.test.ts
735
+ │ └── TronTransferService.test.ts
524
736
  ├── examples/
525
737
  │ └── transfer.ts # Example CLI
526
738
  ├── dist/ # Build output (the only folder that gets published)
@@ -540,7 +752,7 @@ tron-transfer-sdk/
540
752
  | Package | Why |
541
753
  | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
542
754
  | [`tronweb`](https://www.npmjs.com/package/tronweb) `^6.5.1` | The official TRON JavaScript SDK: builds, signs and broadcasts transactions, and talks to full nodes. |
543
- | [`dotenv`](https://www.npmjs.com/package/dotenv) `^18` | Loads `.env` when the SDK is imported. |
755
+ | [`dotenv`](https://www.npmjs.com/package/dotenv) `^18` | Loads `.env` when the Node entry (`/node`) is imported. The browser entry never loads it. |
544
756
 
545
757
  ### Development
546
758
 
@@ -561,17 +773,20 @@ The package is ESM (`"type": "module"`). ts-mocha on its own only sets up a Comm
561
773
 
562
774
  ## Troubleshooting
563
775
 
564
- | Symptom | Likely cause and fix |
565
- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
566
- | `Missing required environment variable TRON_…` | `.env` isn't in the **current working directory** of the process, or the variable is empty. Run from the folder containing `.env`, or set the variable in the environment. |
567
- | HTTP 429 / rate-limit errors on mainnet | Set `TRON_MAINNET_API_KEY`. |
568
- | `Insufficient TRX balance` right after funding the account | The new funds haven't reached the node yet. Wait one or two blocks (~6 s) and retry. |
569
- | `Call to decimals() on … failed` | The token address isn't a TRC-20 contract **on the selected network**. A common mistake is using the mainnet USDT address on testnet, or vice versa. |
570
- | `Transaction failed on-chain: OUT_OF_ENERGY` | `feeLimitSun` was too low, or the sender ran out of TRX for energy. Raise the limit or add TRX, and check the txid. |
571
- | `Transaction was broadcast but not confirmed within …` | The network is slow or congested. Look up the txid **before** retrying; it may still succeed. |
572
- | `Broadcast rejected: … balance is not sufficient` | The balance covers the amount but not the fees. Leave some TRX for bandwidth and energy. |
573
- | TypeScript errors inside `node_modules/tronweb/...` | Enable `"skipLibCheck": true` in your tsconfig. |
574
- | `ERR_REQUIRE_ESM` or `require() of ES Module` | The package is ESM-only. Use `import`, or dynamic `import()` from CommonJS. |
776
+ | Symptom | Likely cause and fix |
777
+ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
778
+ | `Missing required environment variable TRON_…` | `.env` isn't in the **current working directory** of the process, or the variable is empty. Run from the folder containing `.env`, or set the variable in the environment. |
779
+ | `RATE_LIMITED` / HTTP 429 | The SDK already retried; wait a few seconds. On testnet, set `apiKey` (`TRON_TESTNET_API_KEY` in Node). On mainnet, check the key's quota in the TronGrid dashboard or use another `fullHost`. |
780
+ | `Insufficient TRX balance` right after funding the account | The new funds haven't reached the node yet. Wait one or two blocks (~6 s) and retry. |
781
+ | `Call to decimals() on … failed` | The token address isn't a TRC-20 contract **on the selected network**. A common mistake is using the mainnet USDT address on testnet, or vice versa. |
782
+ | `Transaction failed on-chain: OUT_OF_ENERGY` | `feeLimitSun` was too low, or the sender ran out of TRX for energy. Raise the limit or add TRX, and check the txid. |
783
+ | `Transaction was broadcast but not confirmed within …` | The network is slow or congested. Look up the txid **before** retrying; it may still succeed. |
784
+ | `Broadcast rejected: … balance is not sufficient` | The balance covers the amount but not the fees. Leave some TRX for bandwidth and energy. |
785
+ | TypeScript errors inside `node_modules/tronweb/...` | Enable `"skipLibCheck": true` in your tsconfig. |
786
+ | `ERR_REQUIRE_ESM` or `require() of ES Module` | The package is ESM-only. Use `import`, or dynamic `import()` from CommonJS. |
787
+ | `loadConfig` / `transferTrx` is not exported | Since the wallet-signer release these live in the Node entry: import from `@zebec-network/tron-transfer-sdk/node`. |
788
+ | Bundler errors about `fs`, `path` or `process` in a web app | You imported `@zebec-network/tron-transfer-sdk/node`. Web apps must import the package root. |
789
+ | `No wallet connected` in the UI | The adapter has no `address` yet. Wait for `connect()` / the `connected` state before calling transfer or balance methods. |
575
790
 
576
791
  ---
577
792
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { TronWeb } from 'tronweb';
2
- import type { NetworkConfig } from './config.js';
3
- export declare function createTronWeb(config: NetworkConfig): TronWeb;
2
+ import type { ServiceConfig } from './config.js';
3
+ export interface TronWebOptions extends Pick<ServiceConfig, 'fullHost' | 'apiKey'> {
4
+ /** Omit for a read-only client, e.g. in a browser where a wallet signs. */
5
+ privateKey?: string | undefined;
6
+ }
7
+ export declare function createTronWeb(config: TronWebOptions): TronWeb;
4
8
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAM5D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,QAAQ,CAAC;IAChF,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAM7D"}
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ import { TronWeb } from 'tronweb';
2
2
  export function createTronWeb(config) {
3
3
  return new TronWeb({
4
4
  fullHost: config.fullHost,
5
- privateKey: config.privateKey,
5
+ ...(config.privateKey ? { privateKey: config.privateKey } : {}),
6
6
  ...(config.apiKey ? { headers: { 'TRON-PRO-API-KEY': config.apiKey } } : {}),
7
7
  });
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,OAAO,IAAI,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7E,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQlC,MAAM,UAAU,aAAa,CAAC,MAAsB;IAClD,OAAO,IAAI,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7E,CAAC,CAAC;AACL,CAAC"}