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

tusdt-sdk

v0.1.5

Published

TypeScript SDK for TUSDT ERC20 ink! smart contract on Substrate using Dedot

Readme

tusdt-sdk

TypeScript SDK for the TUSDT ERC20 ink! smart contract on Substrate / Bittensor.

  • Zero-reorg payment detection via finalized blocks only
  • Historical block sync from a configurable startBlock for crash-safe restarts
  • Gap recovery between live subscription events
  • Automatic reconnection with exponential backoff
  • Structured, machine-readable errors with typed codes
  • Tree-shakeable standalone functions — include only what you use
  • Full TypeScript types with JSDoc

Installation

npm install tusdt-sdk

Peer dependencies (required for backend / signer usage):

npm install dedot @polkadot/keyring @polkadot/util-crypto

Quick start

import { connect, TusdtContract, TusdtPaymentListener, formatBalance } from 'tusdt-sdk/server';

const client = await connect('wss://test.finney.opentensor.ai:443');
const tusdt  = new TusdtContract(client, CONTRACT_ADDRESS);

console.log(formatBalance(await tusdt.totalSupply()));

const listener = new TusdtPaymentListener(client, CONTRACT_ADDRESS, {
  receiverWallet: MY_WALLET,
  onPayment: async (event) => {
    console.log(`Received ${formatBalance(event.value)} TUSDT in block #${event.blockNumber}`);
  },
});

await listener.start();

Entrypoints

| Import path | Use case | |----------------------|------------------------------------------------------| | tusdt-sdk | Browser / universal — no Node.js-only deps | | tusdt-sdk/server | Node.js backend — includes connect(), TusdtPaymentListener |


API reference

connect(wsUrl) — server only

Creates a connected DedotClient (legacy JSON-RPC mode, compatible with all Substrate / Bittensor nodes).

import { connect } from 'tusdt-sdk/server';

const client = await connect('wss://test.finney.opentensor.ai:443');

createSigner(suri, type?) — server only

Convenience helper for backend/test environments. Requires @polkadot/keyring.

const signer = await createSigner('//Alice');

TusdtContract

High-level class wrapping all read (query) and write (tx) operations.

const tusdt = new TusdtContract(client, contractAddress);

// Queries (read-only, no gas)
const supply  = await tusdt.totalSupply();
const balance = await tusdt.balanceOf(address);
const allowed = await tusdt.allowance(owner, spender);

// Transactions (sign + submit)
await tusdt.transfer(signer, recipient, 1_000_000n);
await tusdt.approve(signer, spender,    500_000n);
await tusdt.transferFrom(signer, from, to, 500_000n);
await tusdt.mint(signer, recipient,  2_000_000n);  // controller only
await tusdt.burn(signer, holder,     1_000_000n);  // controller only

TusdtContractOptions

interface TusdtContractOptions {
  /** SS58 address used as caller for dry-runs. Defaults to zero address. */
  defaultCaller?: AccountId;
}

TusdtPaymentListener — server only

Production-grade listener for incoming TUSDT transfers on finalized blocks. Mirrors the architecture of tusdt-server's bt-listener service.

import { connect, TusdtPaymentListener, ErrorCode } from 'tusdt-sdk/server';

const client = await connect(WS_URL);

// Load persisted state
const lastSynced = await db.getLastSyncedBlock(); // e.g. 4_200_000

const listener = new TusdtPaymentListener(client, CONTRACT_ADDRESS, {
  receiverWallet: RECEIVER_ADDRESS,

  // Resume from last processed block (pass undefined for live-only)
  startBlock: lastSynced !== undefined ? lastSynced + 1 : undefined,

  // Called for every matching transfer on a finalized block
  onPayment: async (event) => {
    await db.recordPayment(event);
  },

  // Persist progress after every block (enables crash-safe resume)
  onBlockSynced: async (blockNumber, blockHash) => {
    await db.saveLastSyncedBlock(blockNumber, blockHash);
  },

  // Informational lifecycle events — NOT errors
  onProgress: ({ type, message, blockRange }) => {
    const range = blockRange ? ` [${blockRange.from}→${blockRange.to}]` : '';
    console.log(`[${type}]${range} ${message}`);
  },

  // Actionable errors only
  onError: (err) => {
    switch (err.code) {
      case ErrorCode.BLOCK_PROCESSING_FAILED:
        // Non-fatal — listener continues to next block
        console.warn(`[BLOCK_ERROR] ${err.message}`);
        if (err.cause) console.warn('  caused by:', err.cause.message);
        break;
      case ErrorCode.CONNECTION_FAILED:
        console.error(`[CONNECTION] ${err.message}`);
        break;
      case ErrorCode.RECONNECT_EXHAUSTED:
        console.error('[RECONNECT_EXHAUSTED] giving up');
        void listener.stop().then(() => client.disconnect()).then(() => process.exit(1));
        break;
      default:
        console.error(`[${err.code}] ${err.message}`);
        if (err.cause) console.error('  caused by:', err.cause.message);
    }
  },

  reconnect: {
    maxRetries:  Infinity,
    baseDelayMs: 1_000,
    maxDelayMs:  30_000,
  },
});

await listener.start();
console.log('Listener running. Press Ctrl+C to stop.');

process.on('SIGINT', async () => {
  await listener.stop();
  await client.disconnect();
});

PaymentListenerConfig

| Field | Type | Required | Description | |------------------|----------------------------------------------|----------|-------------| | receiverWallet | string | ✓ | SS58 address to watch for incoming transfers | | onPayment | (event: FinalizedTransferEvent) => void \| Promise<void> | ✓ | Called for each matching finalized transfer | | onBlockSynced | (blockNumber: number, blockHash: string) => void \| Promise<void> | — | Called after every processed block — use to persist resume position | | onProgress | (progress: ListenerProgress) => void | — | Informational lifecycle events (history sync, gap fill, reconnect attempts) | | onError | (error: TusdtSdkError) => void | — | Actionable errors only — block failures, connection drops, reconnect exhausted | | startBlock | number | — | First block to sync. If set, historical catch-up runs before live subscription | | reconnect | ReconnectConfig \| false | — | Reconnect strategy. Default: { maxRetries: Infinity, baseDelayMs: 1000, maxDelayMs: 30000 } |

onProgress vs onError

These are two distinct channels:

  • onProgress — informational only, never indicates a problem. Events: SUBSCRIPTION_STARTED, HISTORY_SYNC_START, HISTORY_SYNC_DONE, GAP_FILL_START, GAP_FILL_DONE, BLOCK_PROCESSED, RECONNECTING.
  • onError — always a TusdtSdkError requiring attention. Use err.code to branch without string-matching.

ListenerProgress

interface ListenerProgress {
  type:
    | 'SUBSCRIPTION_STARTED'   // live subscription established
    | 'HISTORY_SYNC_START'     // about to process N historical blocks
    | 'HISTORY_SYNC_DONE'      // historical sync complete
    | 'GAP_FILL_START'         // missed blocks detected, filling gap
    | 'GAP_FILL_DONE'          // gap fill complete
    | 'BLOCK_PROCESSED'        // a single block was processed
    | 'RECONNECTING';          // reconnect attempt in progress
  message:     string;                          // human-readable description
  blockNumber?: number;                         // relevant block (if applicable)
  blockRange?:  { from: number; to: number };  // relevant block range (if applicable)
}

ReconnectConfig

interface ReconnectConfig {
  maxRetries?:  number;  // Default: Infinity
  baseDelayMs?: number;  // Default: 1000
  maxDelayMs?:  number;  // Default: 30000
}

Listener properties

listener.isRunning       // boolean — true while subscribed
listener.lastBlockNumber // number  — last successfully processed finalized block

FinalizedTransferEvent

interface FinalizedTransferEvent {
  from:        string | null;  // sender SS58 address (null for mint)
  to:          string | null;  // recipient SS58 address (null for burn)
  value:       bigint;         // token amount (u64)
  blockHash:   string;         // finalized block hash (0x-prefixed hex)
  blockNumber: number;         // finalized block number
  txIndex?:    number;         // position among Transfer events in this block
}

Error handling

Every error thrown by tusdt-sdk is a TusdtSdkError. All SDK errors carry:

  • code — machine-readable TusdtErrorCode for programmatic branching (never string-match message)
  • cause — the underlying Error that triggered this one, if any
  • toJSON() — serialisable representation safe for loggers / JSON.stringify
  • toString()ErrorName [CODE]: message\n caused by: ...
import { TusdtSdkError, ErrorCode } from 'tusdt-sdk';
import type { InvalidAddressError } from 'tusdt-sdk';

try {
  await tusdt.transfer(signer, recipient, amount);
} catch (err) {
  if (err instanceof TusdtSdkError) {
    switch (err.code) {
      case ErrorCode.INSUFFICIENT_BALANCE:
        console.error('Not enough tokens');
        break;
      case ErrorCode.INVALID_ADDRESS:
        console.error(`Bad address: ${(err as InvalidAddressError).address}`);
        break;
      case ErrorCode.DRY_RUN_FAILED:
        console.error('Dry-run failed — tx would fail on-chain');
        break;
      default:
        console.error(`[${err.code}] ${err.message}`);
        if (err.cause) console.error('  caused by:', err.cause.message);
    }
  }
}

Error codes

| Code | Class | Thrown when | |------|-------|-------------| | INVALID_ADDRESS | InvalidAddressError | Address is not a valid SS58 or 32-byte hex | | INVALID_AMOUNT | InvalidAmountError | Amount is not a bigint, is negative, or exceeds u64 max | | BALANCE_OVERFLOW | OverflowError | Transfer would overflow recipient's u64 balance | | INVALID_BALANCE_STRING | InvalidBalanceStringError | Human-readable balance string could not be parsed | | INSUFFICIENT_BALANCE | InsufficientBalanceError | Contract: sender lacks token balance | | INSUFFICIENT_ALLOWANCE | InsufficientAllowanceError | Contract: spender lacks approved allowance | | NOT_CONTROLLER | NotControllerError | Contract: caller is not the controller (mint/burn) | | CONTRACT_REVERTED | ContractError | Contract rejected the call with an unrecognised variant | | DRY_RUN_FAILED | DryRunError | Pre-flight simulation failed (no gas consumed) | | DISPATCH_FAILED | DispatchError | On-chain extrinsic failed during execution (gas consumed) | | CONNECTION_FAILED | ConnectionError | WebSocket connection could not be established or was lost | | RECONNECT_EXHAUSTED | ReconnectExhaustedError | Listener gave up after exhausting maxRetries | | BLOCK_PROCESSING_FAILED | BlockProcessingError | A block could not be queried or decoded (non-fatal in listener) | | TIMEOUT | TimeoutError | Operation did not complete within the expected time |

Error class hierarchy

TusdtSdkError (code, message, cause)
├── ValidationError
│   ├── InvalidAddressError       (.address: string)
│   ├── InvalidAmountError        (.value: unknown)
│   ├── OverflowError             (.currentBalance: bigint, .addAmount: bigint)
│   └── InvalidBalanceStringError (.input: string)
├── ContractError                 (.contractErrorType: string)
│   ├── InsufficientBalanceError
│   ├── InsufficientAllowanceError
│   └── NotControllerError
├── DryRunError                   (.dispatchError: unknown)
├── DispatchError                 (.dispatchError: unknown)
├── ConnectionError               (.wsUrl?: string)
├── ReconnectExhaustedError       (.attempts: number)
├── BlockProcessingError          (.blockNumber: number, .blockHash?: string)
└── TimeoutError

Context fields

Each error class exposes structured context — no need to parse message strings:

import {
  InvalidAddressError,
  OverflowError,
  BlockProcessingError,
  ReconnectExhaustedError,
  ConnectionError,
} from 'tusdt-sdk';

if (err instanceof InvalidAddressError) {
  console.error(err.address);         // the bad address value
}
if (err instanceof OverflowError) {
  console.error(err.currentBalance);  // recipient's current balance
  console.error(err.addAmount);       // amount that would overflow
}
if (err instanceof BlockProcessingError) {
  console.error(err.blockNumber);     // which block failed
  console.error(err.blockHash);       // block hash if known
}
if (err instanceof ReconnectExhaustedError) {
  console.error(err.attempts);        // how many retries were made
}
if (err instanceof ConnectionError) {
  console.error(err.wsUrl);           // which WebSocket URL failed
}

Standalone tree-shakeable functions

For advanced use or frontend bundles, every operation is also available as a standalone function. Import only what you need:

import { queryBalanceOf, queryTotalSupply } from 'tusdt-sdk';
import { executeTransfer }                  from 'tusdt-sdk';
import { watchFinalizedTransfers }          from 'tusdt-sdk';
import { subscribeTransfer }                from 'tusdt-sdk';
import { safeApprove }                      from 'tusdt-sdk';
import { validateAddress, formatBalance }   from 'tusdt-sdk';

Query functions

queryController(client, contract): Promise<AccountId>
queryTotalSupply(client, contract): Promise<Balance>
queryBalanceOf(client, contract, address): Promise<Balance>
queryAllowance(client, contract, owner, spender): Promise<Balance>

Transaction functions

executeTransfer(client, contract, signer, to, amount, opts?): Promise<TxResult>
executeApprove(client, contract, signer, spender, amount, opts?): Promise<TxResult>
executeTransferFrom(client, contract, signer, from, to, amount, opts?): Promise<TxResult>
executeMint(client, contract, signer, to, amount, opts?): Promise<TxResult>
executeBurn(client, contract, signer, from, amount, opts?): Promise<TxResult>
executeIncreaseAllowance(client, contract, signer, spender, delta, opts?): Promise<TxResult>
executeDecreaseAllowance(client, contract, signer, spender, delta, opts?): Promise<TxResult>

Finalized event watchers

watchFinalizedTransfers(client, contract, callback, opts?): Promise<Unsubscribe>
watchFinalizedTransfersTo(client, contract, account, callback, opts?): Promise<Unsubscribe>

Best-chain event subscriptions

subscribeTransfer(client, contract, callback, opts?): Promise<Unsubscribe>
subscribeApproval(client, contract, callback, opts?): Promise<Unsubscribe>
subscribeTransferFrom(client, contract, account, callback, opts?): Promise<Unsubscribe>
subscribeTransferTo(client, contract, account, callback, opts?): Promise<Unsubscribe>

Helper

// Safe approve: sets allowance to 0 then to target amount (avoids double-spend race)
safeApprove(client, contract, signer, spender, amount, opts?): Promise<[TxResult, TxResult]>

Token utilities

import { formatBalance, parseBalance, TUSDT_DECIMALS } from 'tusdt-sdk';

TUSDT_DECIMALS        // 6

formatBalance(1_000_000n)  // "1.000000"
formatBalance(1_500_000n)  // "1.500000"

parseBalance("1.5")        // 1_500_000n
parseBalance("1500000", false)  // 1_500_000n (raw integer, no decimal shift)

formatBalance throws InvalidAmountError if the input is not a non-negative bigint.
parseBalance throws InvalidBalanceStringError for malformed strings and InvalidAmountError for negative results.


Validation utilities

import { validateBalance, validateAddress, validateOverflowSafe } from 'tusdt-sdk';

// Throws InvalidAmountError if value is not a valid non-negative bigint ≤ u64 max
validateBalance(amount);

// Throws InvalidAddressError if address is not valid SS58 / hex
validateAddress(address);

// Throws OverflowError if currentBalance + addAmount > U64_MAX
validateOverflowSafe(currentBalance, addAmount);

SubstrateClient interface

The SDK accepts any object matching this interface:

interface SubstrateClient {
  rpc: {
    chain_getFinalizedHead():              Promise<unknown>;
    chain_getHeader(hash: string):         Promise<{ number: unknown }>;
    chain_getBlockHash(n: number):         Promise<unknown>;
    chain_subscribeFinalizedHeads(cb: (header: any) => void): Promise<unknown>;
    chain_subscribeNewHeads(cb: (header: any) => void):       Promise<unknown>;
  };
  query: any;
  /** Returns a client snapshot bound to blockHash — used for historical queries. */
  at(blockHash: string): Promise<{ query: any }>;
  disconnect(): Promise<void>;
}

The connect() helper returns a DedotClient.legacy() instance that satisfies this interface.


Publishing

cd tusdt-sdk
npm run build
npm publish --access public

Build output (CommonJS + ESM + types) is emitted to dist/ via tsup.


Testing with the consumer script

cd test-consumer
npm install
node index.mjs

Environment variables:

| Variable | Default | |-----------------|---------| | WS_URL | wss://test.finney.opentensor.ai:443 | | CONTRACT_ADDR | Testnet TUSDT contract address | | RECEIVER_ADDR | Wallet address to watch | | START_BLOCK | Block number to start historical sync from |