npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guardian-sdk/tron

v1.0.0

Published

Guardian SDK for Tron

Readme

@guardian-sdk/tron — Tron

Native staking support for Tron (TRX Stake 2.0), part of the Guardian SDK.

Abstracts TronWeb transaction construction and FullNode REST calls behind a clean, type-safe API so you can freeze TRX for a resource, vote for Super Representatives, unstake, and claim rewards without dealing with TronWeb internals directly.

FullNode only — no TronGrid. tron({ rpcUrl }) talks directly to a configurable FullNode HTTP endpoint. Point it at your own node or a FullNode-compatible provider.

Table of Contents


How Tron Native Staking Works

Tron Stake 2.0 splits staking into two independent, separately-signed actions: freezing TRX for a resource, and voting frozen Tron Power to a Super Representative (SR). Unlike BSC or Cardano, TRX rewards are earned only through the second step.

Freeze, Resource, and Tron Power

Freezing TRX (Delegate) locks it in exchange for one of two resources, chosen per transaction:

  • BANDWIDTH — needed for regular transactions (transfers, most contract calls)
  • ENERGY — needed for smart-contract execution (TRC20 transfers, DApp calls)

Regardless of which resource is chosen, freezing grants Tron Power 1:1 with the frozen TRX — 100 TRX frozen for either resource gives 100 votes of Tron Power. resource is a Tron-only field, present on TronDelegateTransaction/TronUndelegateTransaction but not on the shared SDK DelegateTransaction/UndelegateTransaction.

Freeze ≠ Vote — freezing alone earns nothing

The single most important thing to understand about this package: freezing TRX earns you the chosen resource (ENERGY/BANDWIDTH) and Tron Power, but zero TRX rewards until you submit a separate Vote transaction allocating that Tron Power to a Super Representative. getDelegations() reflects this with a dedicated "Frozen" status — a standing signal that the position is staked but not yet voted.

Lifecycle of a Stake

Freeze (Delegate)      stake TRX for a resource → gain resource + Tron Power   → delegation: Frozen
  │                     (earning the RESOURCE only — NO TRX rewards yet)
Vote (Vote)             allocate Tron Power to a Super Representative           → delegation: Active
  │                     (now earning TRX voting rewards)
Unfreeze (Undelegate)   begin unstaking (partial allowed); 14-day bond starts   → delegation: Pending
  │
(14 days later)                                                                → delegation: Claimable
  ├─ ClaimDelegate      WithdrawExpireUnfreeze → matured principal back to wallet
  └─ ClaimRewards       WithdrawBalance        → voting rewards (24h cooldown), independent of above

Two Independent Claims

Tron has two separate withdrawal transactions that never trigger each other:

| Transaction | Tron contract | Claims | |---|---|---| | ClaimDelegate | WithdrawExpireUnfreezeContract | Matured unfrozen principal back to the wallet | | ClaimRewards | WithdrawBalanceContract | Accrued voting rewards (24-hour cooldown) |

Partial Unstaking

Unlike Cardano — which forces a full-balance reward withdrawal — Tron's Undelegate allows partial amounts: unfreeze less than the full frozen balance for a resource. isMaxAmount: true is rejected on both Delegate and Undelegate — Tron requires an exact SUN amount; query getBalances/getDelegations to determine the max freezable/unfreezable amount before building the transaction. Each unfreeze starts its own independent 14-day unbonding clock and produces its own claimable position (up to a ~32 concurrent-pending cap enforced on-chain).

APR

Tron has no APY REST endpoint. getValidators() computes APR per Super Representative from listwitnesses + getchainparameters + getbrokerage, cached 15 minutes. The computed value is clamped to a sane, finite [0, …) range — it never returns negative, NaN, or Infinity. getchainparameters is cached separately for 10 minutes and getbrokerage per-SR for 30 minutes, with the fan-out bounded to 8 concurrent requests, to avoid rate-limiting a real FullNode on cold loads. Real per-SR brokerage/APR is fetched only for the witnesses actually returned — the requested page in getValidators(), or the distinct SRs an account voted for in getDelegations() — not for all ~439 Super Representatives on every load; stakingSummary.maxApy is an approximation computed across all witnesses using a default brokerage of 20%.

APR values are in percent (e.g. 2.48 means 2.48%). The formula accounts for the vote reward pool plus (for actual top-27 SRs) the block production reward, scaled by the SR's brokerage rate.


Installation

npm install @guardian-sdk/tron @guardian-sdk/sdk [email protected]

| Package | Version | Role | |---|---|---| | @guardian-sdk/sdk | workspace:^ | Peer — chain-agnostic core, shared types and interfaces | | tronweb | 6.1.0 | Dependency — transaction building, signing, and FullNode communication |


Quick Start

import { GuardianSDK } from "@guardian-sdk/sdk";
import { tron, chains } from "@guardian-sdk/tron";
import type { TronDelegateTransaction } from "@guardian-sdk/tron";

const sdk = new GuardianSDK([tron({ rpcUrl: "https://<your-tron-fullnode>" })]);

const ADDRESS = "TYourTronBase58Address...";
const PRIVATE_KEY = process.env.TRON_PRIVATE_KEY!;

// 1. Browse Super Representatives
const { data: validators } = await sdk.getValidators(chains.tronMainnet);
console.log(`${validators.length} SRs found`);

// 2. Check current delegations
const { delegations, stakingSummary } = await sdk.getDelegations(chains.tronMainnet, ADDRESS);
console.log(`Max APR: ${stakingSummary.maxApy.toFixed(2)}%`);

// 3. Check balances (SUN)
const balances = await sdk.getBalances(chains.tronMainnet, ADDRESS);
for (const b of balances) {
  console.log(b.type, Number(b.amount) / 1e6, "TRX");
}

// 4. Freeze 100 TRX for BANDWIDTH
const freeze: TronDelegateTransaction = {
  type: "Delegate",
  chain: chains.tronMainnet,
  amount: 100_000_000n,
  isMaxAmount: false,
  resource: "BANDWIDTH",
};
const fee = await sdk.estimateFee(freeze);
const rawTx = await sdk.sign({ transaction: freeze, fee, nonce: 0, privateKey: PRIVATE_KEY });
const txHash = await sdk.broadcast(chains.tronMainnet, rawTx);
console.log(`Frozen! https://tronscan.org/#/transaction/${txHash}`);

API Reference

tron()

Factory function. Returns a GuardianServiceContract for Tron mainnet.

function tron(config: TronConfig): GuardianServiceContract

interface TronConfig {
  rpcUrl: string;   // FullNode HTTP endpoint (no TronGrid)
  logger?: Logger;  // optional; defaults to NoopLogger
}

getValidators

Returns Super Representatives from /wallet/listwitnesses, each with a computed APR. Cached per page+pageSize for 15 minutes.

const { data, pagination } = await sdk.getValidators(chains.tronMainnet);
const page2 = await sdk.getValidators(chains.tronMainnet, { page: 2, pageSize: 50 });

Returns: Promise<ValidatorsPage>

interface Validator {
  id: string;                      // SR base58 address
  status: ValidatorStatus;         // "Active" (top-27 producing) | "Inactive"
  name: string;                    // SR url metadata, or address
  description: string;
  image: undefined;                // Not exposed by listwitnesses
  apy: number;                     // Computed APR in percent (e.g. 2.48 for 2.48%)
  delegators: undefined;           // Not exposed by listwitnesses
  operatorAddress: string;         // SR base58 address — use in transaction.validator
  creditAddress: string;           // ""
}

getDelegations

Returns resource-granular delegations — one entry per frozenV2/unfrozenV2 position, so amount is always the exact actionable unstake/claim figure.

const { delegations, stakingSummary } = await sdk.getDelegations(chains.tronMainnet, ADDRESS);

Returns: Promise<Delegations>

interface Delegation {
  id: string;
  validator: Validator;        // Real SR when Active; placeholder otherwise (never null)
  amount: bigint;               // SUN — exact frozen/unfreezing amount for this position
  status: DelegationStatus;
  delegationIndex: bigint;
  pendingUntil: number;         // ms epoch; unfreeze_expire_time for Pending/Claimable, 0 otherwise
}

type DelegationStatus = "Active" | "Pending" | "Claimable" | "Inactive" | "Frozen";

| status | Meaning | |---|---| | Active | Frozen and covered by a vote — earning both the resource and TRX rewards | | Frozen | Frozen but not (fully) voted — earning the resource only, no TRX rewards yet | | Pending | Unfreeze submitted, still inside the 14-day unbonding period | | Claimable | Unfreeze matured — withdraw the principal with ClaimDelegate |

Placeholder validator — every Frozen/Pending/Claimable delegation carries a non-null placeholder validator (id: "tron-frozen-{resource}", name: "Frozen — vote to earn rewards", apy: 0, status: "Inactive") so consumers never have to null-check delegation.validator. {resource} reflects the position's own resource — for Pending/Claimable it is the unfreeze's resource (ENERGY vs BANDWIDTH), so an ENERGY unstake is never labeled BANDWIDTH.

Partial-voting remainder — if the account's total frozen Tron Power exceeds its total votes, the unvoted remainder is split across the frozen resources (largest-first, each capped at that resource's frozen amount), so with no votes you get one Frozen entry per resource. Freezing and voting the full amount in lockstep produces no remainder.

Active amount = voted Tron Power, not a per-resource unstake size. For a multi-resource position, size an Undelegate from the per-resource Frozen/Pending/Claimable amounts (capped per resource) or from getBalances/frozenV2 — not from an Active amount.

Unknown SR — an Active vote to an SR that's no longer in the witness list carries a fallback validator with the SR address (status: "Inactive", apy: 0), not the Frozen placeholder.

Every shape getDelegations() can return

Amounts in TRX for readability (the SDK returns SUN — multiply by 1,000,000). "on-chain state" is the account's frozenV2 / votes / unfrozenV2; the rows are the resulting delegations[]. Two invariants always hold: Σ Active + Σ Frozen = total frozen and Σ Pending + Σ Claimable = total unfreezing.

1. Froze, never voted (earning the resource, no TRX rewards)

on-chain: frozen 100 BANDWIDTH · votes none · unfreezing none
→ [ Frozen    100  validator=tron-frozen-bandwidth ]

2. Froze + voted the full amount to one SR (the clean, common case)

on-chain: frozen 100 BANDWIDTH · votes 100→SR_A · unfreezing none
→ [ Active    100  validator=SR_A (real, apy>0) ]

3. Froze + voted across several SRs

on-chain: frozen 45,051 · votes 15,017→SR_A, 15,017→SR_B, 15,017→SR_C
→ [ Active 15,017 SR_A ] [ Active 15,017 SR_B ] [ Active 15,017 SR_C ]

4. Partial vote (voted less than frozen → Active + one Frozen remainder)

on-chain: frozen 100 ENERGY · votes 40→SR_A · unfreezing none
→ [ Active     40  SR_A ] [ Frozen     60  tron-frozen-energy ]

5. Froze two resources, no votes (one Frozen per resource — never a merged blob)

on-chain: frozen 100 BANDWIDTH + 80 ENERGY · votes none
→ [ Frozen   100  tron-frozen-bandwidth ] [ Frozen    80  tron-frozen-energy ]

6. Unfreezing in progress (one entry per unfreezeBalanceV2, each its own 14-day clock)

on-chain: frozen none · unfreezing 40 (expires in 5d) + 20 (expired)
→ [ Pending    40  pendingUntil=<future ms> ] [ Claimable  20  pendingUntil=<past ms> ]

7. Over-voted (unfroze after voting, so votes now exceed frozen → Active capped to frozen)

on-chain: frozen 109 · votes 114,657 across 3 SRs
→ [ Active 36.33 SR_A ] [ Active 36.33 SR_B ] [ Active 36.34 SR_C ]   (Σ = 109, not 114,657)

8. Fully unfrozen, votes not yet re-cast (stale votes are dropped — no 0-amount Active)

on-chain: frozen none · votes 150 (linger on-chain) · unfreezing 150
→ [ Pending   150  pendingUntil=<future ms> ]

9. Voted an SR that got delisted (Active with an address-only fallback validator)

on-chain: frozen 100 · votes 100→SR_X (not in current witness list)
→ [ Active    100  validator={ id: "SR_X", status: "Inactive", apy: 0 } ]

10. A rich real wallet (freeze + partial vote + multiple unfreezes at once)

on-chain: frozen 3,200 (1,200 BW + 2,000 EN) · votes 1,066×3 SRs (=3,198) · unfreezing 7,750+7,000+6,250+8,000
→ [ Active 1,066 SR_A ] [ Active 1,066 SR_B ] [ Active 1,066 SR_C ]
  [ Frozen     2  tron-frozen-energy ]                          (3,200 frozen − 3,198 voted)
  [ Pending 7,750 ] [ Pending 7,000 ] [ Pending 6,250 ] [ Pending 8,000 ]

An account with none of the above (never staked) returns delegations: [].

interface StakingSummary {
  totalProtocolStake: number;   // Σ SR voteCount
  maxApy: number;               // Max computed APR across SRs
  minAmountToStake: bigint;     // 1_000_000n (1 TRX freeze minimum)
  unboundPeriodInMillis: number; // unfreezeDelayDays × 86_400_000 (14 days)
  redelegateFeeRate: 0;         // Tron has no redelegation
  activeValidators: number;     // Top-27 producing SRs
  totalValidators: number;      // Full witness list length
}

getBalances

Returns balance information for a Tron address. All amounts in SUN (1 TRX = 1,000,000 SUN).

const balances = await sdk.getBalances(chains.tronMainnet, ADDRESS);

Returns: Promise<Balance[]>

| BalanceType | What it represents on Tron | |---|---| | Available | Liquid TRX (getAccount.balance) | | Staked | Σ frozen principal across both resources (frozenV2) | | Pending | Σ unfreezing entries still inside the 14-day bond | | Claimable | Σ unfreezing entries that have matured — withdrawable via ClaimDelegate | | Rewards | Unclaimed voting rewards (getReward) — withdrawable via ClaimRewards |

No double-counting: an amount that has started unfreezing is no longer counted in Staked, and rewards come only from votes (freezing without voting keeps Rewards at 0 for that stake).

Only Stake 2.0 is supported — legacy Stake 1.0 (freezeBalance/frozen) is not reflected in getBalances/getDelegations.


getNonce

Tron uses ref-block + expiration, not an account nonce. This always returns 0.

const nonce = await sdk.getNonce(chains.tronMainnet, ADDRESS); // always 0

estimateFee

Tron fees are resource-based, not gas: a transaction consumes bandwidth (∝ serialized size) and, for contract calls, energy (≈0 for pure staking ops). When the account's free + staked bandwidth already covers the estimated transaction size, the operation is genuinely free (total: 0n); otherwise the shortfall is burned as TRX at the chain's per-point price (getTransactionFee), floored at 1 SUN/point so a misreported 0 from the chain parameters never produces a free fee.

const fee = await sdk.estimateFee(transaction);

Returns: Promise<ResourceFee>

interface ResourceFee {
  type: "ResourceFee";
  bandwidth: bigint;   // Bandwidth points consumed
  energy: bigint;       // Energy consumed (≈0 for pure staking ops)
  total: bigint;        // TRX burned (SUN) when free/available resources don't cover it
}

sign

Signs a Tron transaction and returns the serialized signed transaction (JSON string) ready to broadcast.

const rawTx = await sdk.sign({
  transaction,
  fee,
  nonce: 0,          // unused; Tron has no account nonce
  privateKey,         // raw secp256k1 private key hex (with or without 0x prefix — same as BSC)
});

Internally: buildUnsignedTx builds the unsigned transaction via TronWeb's transactionBuilder against the configured FullNode, TronWeb signs the txID (SHA256(raw_data), secp256k1) with the private key, and the fully signed transaction is returned as JSON.


prehash and compile

For MPC wallets, hardware signers, or custody setups where private keys are not available in the application process.

const { serializedTransaction, signArgs } = await sdk.prehash({ transaction, fee, nonce: 0 });
// serializedTransaction === the txID (SHA256 of raw_data) — the exact value an external
// secp256k1 signer must sign. The unbuilt raw tx is carried in signArgs._rawTx for compile().

const rawTx = await sdk.compile({
  signArgs,
  signature: externalSignatureHex, // secp256k1 signature over serializedTransaction
});

const txHash = await sdk.broadcast(chains.tronMainnet, rawTx);

signArgs from prehash() must be passed through unchanged to compile().


Transaction Flows

import { GuardianSDK, chains } from "@guardian-sdk/tron";
import { tron, type TronDelegateTransaction, type TronUndelegateTransaction } from "@guardian-sdk/tron";
import type { VoteTransaction, ClaimDelegateTransaction, ClaimRewardsTransaction } from "@guardian-sdk/sdk";

const sdk = new GuardianSDK([tron({ rpcUrl: "https://<your-tron-fullnode>" })]);
const ADDRESS = "TYourTronBase58Address..."; // owner address
const privateKey = process.env.TRON_PRIVATE_KEY!;

// FREEZE — stake 100 TRX for BANDWIDTH. Gains resource + Tron Power. Earns NOTHING yet.
const freeze: TronDelegateTransaction = {
  type: "Delegate", chain: chains.tronMainnet, amount: 100_000_000n, isMaxAmount: false, resource: "BANDWIDTH", account: ADDRESS,
};
const freezeFee = await sdk.estimateFee(freeze);
const freezeSigned = await sdk.sign({ transaction: freeze, fee: freezeFee, nonce: 0, privateKey }); // Tron has no account nonce
await sdk.broadcast(chains.tronMainnet, freezeSigned);

// VOTE — allocate 100 votes (100 TRX of Tron Power) to a Super Representative. NOW earning rewards.
const vote: VoteTransaction = { type: "Vote", chain: chains.tronMainnet, validator: "T<SR-address>", amount: 100_000_000n, account: ADDRESS };
const voteFee = await sdk.estimateFee(vote);
const voteSigned = await sdk.sign({ transaction: vote, fee: voteFee, nonce: 0, privateKey });
await sdk.broadcast(chains.tronMainnet, voteSigned);

// UNFREEZE — partial unstake of 40 TRX. Starts the 14-day unbonding clock.
const unfreeze: TronUndelegateTransaction = {
  type: "Undelegate", chain: chains.tronMainnet, amount: 40_000_000n, isMaxAmount: false, resource: "BANDWIDTH", account: ADDRESS,
};
const unfreezeFee = await sdk.estimateFee(unfreeze);
const unfreezeSigned = await sdk.sign({ transaction: unfreeze, fee: unfreezeFee, nonce: 0, privateKey });
await sdk.broadcast(chains.tronMainnet, unfreezeSigned);

// CLAIM PRINCIPAL — after 14 days, withdraw the matured unfrozen TRX.
// validator/index are optional on ClaimDelegateTransaction and IGNORED by Tron
// (withdrawExpireUnfreeze withdraws whatever has matured for this account) — omit them.
const claimPrincipal: ClaimDelegateTransaction = { type: "ClaimDelegate", chain: chains.tronMainnet, amount: 0n, account: ADDRESS };
const claimPrincipalFee = await sdk.estimateFee(claimPrincipal);
const claimPrincipalSigned = await sdk.sign({ transaction: claimPrincipal, fee: claimPrincipalFee, nonce: 0, privateKey });
await sdk.broadcast(chains.tronMainnet, claimPrincipalSigned);

// CLAIM REWARDS — independent, anytime rewards accrued (24h cooldown).
// validator is optional on ClaimRewardsTransaction and IGNORED by Tron
// (withdrawBlockRewards withdraws the whole account reward balance) — omit it.
const claimRewards: ClaimRewardsTransaction = { type: "ClaimRewards", chain: chains.tronMainnet, amount: 0n, account: ADDRESS };
const claimRewardsFee = await sdk.estimateFee(claimRewards);
const claimRewardsSigned = await sdk.sign({ transaction: claimRewards, fee: claimRewardsFee, nonce: 0, privateKey });
await sdk.broadcast(chains.tronMainnet, claimRewardsSigned);

See the full runnable flow with logging and delegation-state assertions in examples/tron-native-staking-sample.ts.


Signing Flows

See the main README signing flows diagram for a visual reference of the direct and MPC signing paths.

Tron signs the txIDSHA256(raw_data) — with a single secp256k1 key (not Ed25519 like Cardano, and not the same curve BSC keys are typically stored on but the same key format as EVM chains).


Logging

Logging is opt-in — pass a logger to tron() to enable it:

import { ConsoleLogger } from "@guardian-sdk/sdk";
import { tron } from "@guardian-sdk/tron";

const sdk = new GuardianSDK([
  tron({
    rpcUrl: "https://<your-tron-fullnode>",
    logger: new ConsoleLogger("debug"),
  }),
]);

See the main README Logging section for full details.

Private keys and signatures are never logged at any level.


Error Handling

Every error thrown by the SDK extends GuardianError. See the main README Error Handling section for the catch pattern and base class reference.

| Code | Thrown when | |---|---| | INVALID_AMOUNT | Freeze below 1 TRX, isMaxAmount: true on Delegate/Undelegate (unsupported — pass an exact amount), vote not a whole number of TRX, over-voting, or unfreeze exceeding frozen balance | | INVALID_RESOURCE | Undelegate/fee estimation with a missing or invalid resource (must be "BANDWIDTH" or "ENERGY") | | UNSUPPORTED_OPERATION | Voting for an unknown Super Representative | | INVALID_SIGNING_ARGS | Missing privateKey/account, or compile() called without signArgs._rawTx from prehash() |


Supported Chains

import { chains } from "@guardian-sdk/tron";

| Chain | Symbol | Explorer | |---|---|---| | Tron Mainnet | TRX | https://tronscan.org |


← Back to Guardian SDK