tronlink-signer
GitHub: https://github.com/TronLink/mcp-tronlink-signer/tree/main/packages/tronlink-signer
Standalone SDK for signing TRON transactions via the TronLink browser wallet. Private keys never leave TronLink — all signing happens in the browser through a local approval page.
Installation
npm install tronlink-signer
Quick Start
import { TronSigner } from "tronlink-signer";
const signer = new TronSigner();
await signer.start();
const { address, network } = await signer.connectWallet();
const { txId, status } = await signer.sendTrx("TXxx...", 1); // status: "success" | "pending" | "failed"
const { txId: txId2, status: s2 } = await signer.signTransaction(tx, "nile", true); // broadcast + auto-confirm
const { balance } = await signer.getBalance("TXxx...");
await signer.stop();
API
new TronSigner(config?: Partial<AppConfig>)
Creates a new signer instance. If no config is provided, it reads from environment variables via loadConfig().
signer.start(): Promise<void>
Starts the local HTTP server for browser approval.
signer.stop(): Promise<void>
Stops the server and clears all pending requests.
signer.getConfig(): AppConfig
Returns the current configuration.
signer.connectWallet(network?, options?): Promise<{ address: string; network: string }>
Opens the browser to connect TronLink and retrieve the wallet address and current network. If the wallet is already connected (same browser tab still open), auto-completes without user interaction.
signer.sendTrx(to, amount, network?, options?): Promise<BroadcastResult>
Sends TRX to a recipient address. Opens a browser approval page for the user to confirm. Returns { txId, status, error? } where status is "success", "pending", or "failed" (see Broadcast Result).
| Parameter | Type | Description |
|---|---|---|
to | string | Recipient Tron address (base58) |
amount | string \| number | Amount of TRX to send |
network | TronNetwork | Optional network override |
options | SignerOptions | Optional — pass { signal } to enable cancellation |
signer.sendTrc20(contractAddress, to, amount, decimals?, network?, options?): Promise<BroadcastResult>
Sends TRC20 tokens. Opens a browser approval page. Returns { txId, status, error? } — see Broadcast Result.
| Parameter | Type | Description |
|---|---|---|
contractAddress | string | TRC20 token contract address |
to | string | Recipient Tron address (base58) |
amount | string | Amount in human-readable units (e.g., "1.5" for 1.5 USDT). Decimals conversion is automatic. |
decimals | number | Optional. Token decimals. Omit to auto-detect via the contract's decimals() view (recommended — avoids 10^N magnitude errors on non-6dp tokens like USDD/SUN/JST). |
network | TronNetwork | Optional network override |
options | SignerOptions | Optional — pass { signal } to enable cancellation |
signer.signMessage(message, network?, options?): Promise<{ signature: string }>
Signs a plain text message.
signer.signTypedData(typedData, network?, options?): Promise<{ signature: string }>
Signs EIP-712 typed data.
const { signature } = await signer.signTypedData({
domain: { name: "MyDApp", version: "1", chainId: 728126428 },
types: {
Greeting: [{ name: "contents", type: "string" }],
},
primaryType: "Greeting",
message: { contents: "Hello Tron!" },
});
signer.signTransaction(transaction, network?, broadcast?, options?): Promise<{ signedTransaction; txId?; status?; error? }>
Signs a raw transaction. When broadcast is true, the signed transaction is broadcast on-chain via TronLink and the SDK automatically polls for on-chain confirmation — see Broadcast Result for the possible status values.
| Parameter | Type | Description |
|---|---|---|
transaction | Record<string, unknown> | Raw transaction object to sign |
network | TronNetwork | Optional network override |
broadcast | boolean | Whether to broadcast after signing (default: false) |
options | SignerOptions | Optional — cancellation, confirmation control, broadcast callback |
// Sign only
const { signedTransaction } = await signer.signTransaction(tx);
// Sign, broadcast, and wait for confirmation (default)
const { signedTransaction, txId, status, error } = await signer.signTransaction(tx, "nile", true);
// status === "success" — confirmed on-chain
// status === "pending" — broadcast succeeded but not confirmed within timeout
// status === "failed" — on-chain execution failed; `error` carries the reason
// Broadcast without waiting for confirmation
const result = await signer.signTransaction(tx, "nile", true, { confirm: false });
// Get notified as soon as the tx enters the mempool
const result = await signer.signTransaction(tx, "nile", true, {
onBroadcasted: ({ txId }) => console.log("Broadcast:", txId),
});
Broadcast Result
Broadcasting methods (sendTrx, sendTrc20, and signTransaction with broadcast: true) return a BroadcastResult:
interface BroadcastResult {
txId: string;
status: "success" | "pending" | "failed";
error?: string; // set when status === "failed"
}
"failed" means the transaction reached the chain but execution failed (e.g. OUT_OF_ENERGY, Solidity revert). It is not thrown — inspect status instead. The SDK only throws when broadcast itself never happened (signature error, user rejection, network unreachable, etc.).
signer.waitForTransaction(txId, network?, options?): Promise<{ status; error? }>
Polls the network for transaction confirmation. Returns { status: "success" } when confirmed, { status: "pending" } on timeout, or { status: "failed", error } when the transaction reverted / ran out of energy.
| Parameter | Type | Description |
|---|---|---|
txId | string | Transaction ID to monitor |
network | TronNetwork | Optional network override |
options | WaitForTransactionOptions | Optional — timeoutMs (default: 30000), signal |
const { status, error } = await signer.waitForTransaction(txId, "nile");
if (status === "success") console.log("Confirmed on-chain");
else if (status === "failed") console.warn("Execution failed:", error);
Note: When using
signTransactionwithbroadcast: true, confirmation polling is automatic — you don't need to callwaitForTransactionseparately unless you setconfirm: false.
signer.getBalance(address, network?): Promise<{ balance: string; balanceSun: number }>
Gets TRX balance for an address. No browser approval needed.
signer.onBrowserDisconnect
Setter for a callback that fires when the approval page is closed or loses connection (heartbeat timeout). Useful for cleanup or re-prompting the user.
signer.onBrowserDisconnect = () => {
console.log("Browser approval page was closed");
};
Cancellation & Options
All signing methods accept an optional SignerOptions:
| Option | Type | Description |
|---|---|---|
signal | AbortSignal | Cancels the pending request. If already aborted, the browser page is not opened. |
confirm | boolean | Wait for on-chain confirmation after broadcast (default: true). Only applies when broadcast: true. |
confirmTimeoutMs | number | Max time (ms) to wait for confirmation (default: 30000). |
onBroadcasted | (info) => void | Fires after the tx enters the mempool (before confirmation). Callback errors are swallowed. |
const controller = new AbortController();
// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30_000);
try {
const { txId } = await signer.sendTrx("TXxx...", 1, undefined, {
signal: controller.signal,
});
} catch (e) {
// e.message === "CANCELLED_BY_CALLER"
}
How It Works
- Your code calls a signing method (e.g.,
signMessage) - A local HTTP server starts on port 3386 and a single browser tab opens the approval page
- The approval page discovers the wallet via TIP-6963 protocol (fallback to
window.tron) - Auto-unlocks wallet and switches network if needed
- For
connectWallet, if the wallet is already connected, it auto-completes without user interaction - For signing/sending, the user reviews the transaction details and clicks Approve / Reject
- The approval page parses transaction types (TRX transfer, TRC20, TRC721 NFT, stake, delegate, vote, etc.) into human-readable display
- TronLink handles signing in the browser
- The result is returned to your code — the page stays open and polls for the next request
All subsequent operations reuse the same browser tab. Each server session has a unique ID — old browser tabs from previous sessions are automatically invalidated. The page detects server disconnection via heartbeat and shows a session expired message. The local server binds to 127.0.0.1 only. If port 3386 is in use, it auto-increments. Requests timeout after 5 minutes. The server gracefully shuts down on process exit (SIGINT/SIGTERM).
Networks
All signing methods accept an optional network parameter:
| Network | Full Host | Explorer |
|---|---|---|
mainnet (default) | https://api.trongrid.io | https://tronscan.org |
nile | https://nile.trongrid.io | https://nile.tronscan.org |
shasta | https://api.shasta.trongrid.io | https://shasta.tronscan.org |
Environment Variables
| Variable | Description | Default |
|---|---|---|
TRON_NETWORK | Default network | mainnet |
TRON_HTTP_PORT | Local HTTP server port | 3386 |
TRON_API_KEY | TronGrid API key (optional) | - |
Types
type TronNetwork = "mainnet" | "nile" | "shasta";
interface AppConfig {
network: TronNetwork;
httpPort: number;
apiKey?: string;
}
interface NetworkConfig {
name: string;
fullHost: string;
explorerUrl: string;
}
interface SignerOptions {
signal?: AbortSignal;
confirm?: boolean;
confirmTimeoutMs?: number;
onBroadcasted?: (info: { txId: string; signedTransaction: Record<string, unknown> }) => void;
}
interface WaitForTransactionOptions {
timeoutMs?: number;
signal?: AbortSignal;
}
type BroadcastStatus = "success" | "pending" | "failed";
interface BroadcastResult {
txId: string;
status: BroadcastStatus;
error?: string;
}
Exports
// Class
export { TronSigner } from "./tron-signer.js";
// Config
export { NETWORKS, DEFAULT_HTTP_PORT, REQUEST_TIMEOUT_MS, loadConfig } from "./config.js";
// Types
export type {
TronNetwork, NetworkConfig, AppConfig,
PendingRequestType, PendingRequest,
SendTrxData, SendTrc20Data,
SignMessageData, SignTypedDataData, SignTransactionData,
SignerOptions, WaitForTransactionOptions,
BroadcastStatus, BroadcastResult,
} from "./types.js";