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.3.5

Published

TUSDT (PSP22) payment SDK for Substrate — React provider, hooks and dialog on top of a dedot engine

Readme

tusdt-sdk

v0.3.4 — fixes raw injected signer address loss when using LunoKit / dedot / polkadot-extension wallets (payment no longer throws normalizeSigner: received a raw injected signer with no address).

React payment SDK for the TUSDT PSP22 ink! smart contract on Bittensor / Substrate.

Drop a <PaymentButton> into your app, let a user sign a TUSDT transfer to your platform wallet, and receive a finalized receipt ({ blockHash, extrinsicHash, blockNumber }) you can POST to your backend.

  • React 18 provider + hooks built on @tanstack/react-query
  • Accessible payment dialog (Radix UI) with light/dark themes and CSS-variable theming
  • BYO signer — pass your wallet account + signer as props (works with any dedot-compatible injector, e.g. LunoKit, Talisman, SubWallet)
  • Tree-shakeable subpath exports — frontends never pull engine code they don't use
  • Ships chain presets for Bittensor mainnet + testnet
  • Full TypeScript types, SSR-safe (Next.js app-router compatible)

Install

npm install tusdt-sdk dedot @tanstack/react-query react react-dom

@tanstack/react-query, react, and react-dom are peer dependencies — the SDK reuses your app's existing instances. If you don't already have a QueryClient, the provider creates one for you.


Quick start (3 steps)

1. Define a config

// app/tusdt-config.ts
import { createConfig } from 'tusdt-sdk/config';
import { bittensorTestnet } from 'tusdt-sdk/chains';

export const tusdtConfig = createConfig({
  appName: 'My Storefront',
  chains: [bittensorTestnet],
  defaultRecipient: '5GGq...platformWallet',
});

2. Mount the provider

// app/providers.tsx
'use client';
import { TusdtKitProvider } from 'tusdt-sdk/ui';
import 'tusdt-sdk/ui/styles.css';
import { tusdtConfig } from './tusdt-config';

export function Providers({ children, account, signer }) {
  return (
    <TusdtKitProvider
      config={tusdtConfig}
      account={account}     // { address, meta? } from your wallet connector
      signer={signer}       // dedot-compatible injected signer
      theme={{ mode: 'light' }}
    >
      {children}
    </TusdtKitProvider>
  );
}

3. Render a payment button

import { PaymentButton } from 'tusdt-sdk/ui';

<PaymentButton
  recipient="5GGq...platformWallet"
  amount={1_000_000_000n}                 // 1.000000000 TUSDT (9 decimals)
  onSuccess={async (receipt) => {
    await fetch('/api/payments/confirm', {
      method: 'POST',
      body: JSON.stringify(receipt),       // { blockHash, extrinsicHash, blockNumber }
    });
  }}
/>

That's it. The dialog handles balance display, address truncation, fee preview, signing, broadcast, and the in-block → finalized status machine.


Package layout

The SDK ships as a single package with subpath exports so frontends only bundle what they import.

| Import | Contents | |-------------------------------|---------------------------------------------------------------------| | tusdt-sdk | Engine: TusdtContract, executeTransfer, errors, formatters | | tusdt-sdk/config | createConfig, config types | | tusdt-sdk/chains | defineChain, bittensorMainnet, bittensorTestnet | | tusdt-sdk/react | TusdtProvider, hooks (useBalance, useTransferTusdt, …) | | tusdt-sdk/ui | TusdtKitProvider, PaymentDialog, PaymentButton | | tusdt-sdk/ui/styles.css | Dialog stylesheet |


Wallet connection (BYO)

The SDK accepts any of the common signer shapes via an internal normalizeSigner helper — no manual wrapping needed:

  1. @luno-kit/reactuseSigner() returns a { data?, isLoading } wrapper which the SDK auto-unwraps. Pass it directly:

    import { useAccount, useSigner } from '@luno-kit/react';
    import { TusdtKitProvider } from 'tusdt-sdk/ui';
    
    function AppShell({ children }) {
      const { account } = useAccount();
      const signer = useSigner();   // wrapper { data?, isLoading } — auto-unwrapped
      return (
        <TusdtKitProvider config={tusdtConfig} account={account} signer={signer}>
          {children}
        </TusdtKitProvider>
      );
    }

    Equivalent: signer={useSigner().data}. While useSigner().data is undefined, the dialog renders read-only.

  2. dedot directconnector.getSigner() returns a raw InjectedSigner (no address). Wrap it with { address, signer }:

    import { TusdtKitProvider } from 'tusdt-sdk/ui';
    
    function AppShell({ children }) {
      const [account, setAccount] = useState(null);
      const [rawSigner, setRawSigner] = useState(null);
    
      // after connecting via dedot connector:
      // setAccount(accounts[0]); setRawSigner(await connector.getSigner());
    
      const signer = account && rawSigner
        ? { address: account.address, signer: rawSigner }
        : undefined;
    
      return (
        <TusdtKitProvider config={tusdtConfig} account={account} signer={signer}>
          {children}
        </TusdtKitProvider>
      );
    }
  3. @polkadot/extension-dappweb3FromAddress(addr) gives { signer, ... }. Wrap with address, same as dedot:

    const injected = await web3FromAddress(account.address);
    <TusdtKitProvider
      config={tusdtConfig}
      account={{ address: account.address }}
      signer={{ address: account.address, signer: injected.signer }}
    />
  4. @polkadot/keyring (Node / tests)IKeyringPair has its own address:

    import { Keyring } from '@polkadot/keyring';
    const pair = new Keyring({ type: 'sr25519' }).addFromUri('//Alice');
    <TusdtKitProvider config={tusdtConfig} account={{ address: pair.address }} signer={pair} />
  5. Already-wrapped { address, signer: { signPayload } } shapes also work as-is.

If you need the normalizer in your own code (e.g. for the engine API outside React):

import { normalizeSigner } from 'tusdt-sdk';
const n = normalizeSigner(signer, account.address); // -> { address, kind, raw, signer?/pair? }

If account/signer are undefined, the dialog renders in a read-only state — users still see the recipient/amount, but the Pay button is disabled until a wallet is connected.


Default recipient

recipient is optional on <PaymentButton> and <PaymentDialog> when you set defaultRecipient in createConfig({ defaultRecipient }). The dialog falls back to it automatically:

// config sets defaultRecipient
<PaymentButton amount={1_000_000_000n} onSuccess={...} />

Imperative payment modal

Use usePaymentModal() to open the payment dialog from anywhere inside <TusdtKitProvider> without rendering <PaymentDialog> yourself:

import { usePaymentModal } from 'tusdt-sdk/ui';

function CheckoutButton() {
  const { openPaymentModal } = usePaymentModal();
  return (
    <button
      onClick={() =>
        openPaymentModal({
          recipient: '5GGq...platformWallet',
          amount: 2_500_000_000n,
          title: 'Order #1234',
          onSuccess: (receipt) => postToBackend(receipt),
        })
      }
    >
      Checkout
    </button>
  );
}

<TusdtKitProvider> mounts a single <PaymentDialog> driven by this state.


API reference

createConfig(params)

import { createConfig } from 'tusdt-sdk/config';

createConfig({
  appName?: string;
  chains: readonly TusdtChain[];          // required, non-empty
  defaultRecipient?: string;              // platform wallet for payments
  autoConnect?: boolean;
  queryClient?: QueryClient;              // optional, host-owned
});

Returns a frozen, validated TusdtConfig. Throws if chains is empty.

defineChain(chain)

import { defineChain } from 'tusdt-sdk/chains';

const myChain = defineChain({
  genesisHash: '0x...',
  name: 'My Substrate Chain',
  nativeCurrency: { name: 'Unit', symbol: 'UNIT', decimals: 12 },
  rpcUrls: { webSocket: ['wss://rpc.example.com'] },
  ss58Format: 42,
  testnet: false,
  token: {
    name: 'TUSDT',
    symbol: 'TUSDT',
    contractAddress: '5GGq...',
    decimals: 9,
  },
});

Built-in presets: bittensorMainnet, bittensorTestnet.

Hooks

All hooks must be called inside a <TusdtProvider> (or <TusdtKitProvider>).

import {
  useTusdtConfig, useChain, useApi, useAccount,
  useTusdtContract, useTusdtMetadata,
  useBalance, useTransferTusdt, useDryRunTransfer,
  usePaymentModal,
} from 'tusdt-sdk/react';

| Hook | Returns | |-------------------------------|---------| | useTusdtConfig() | The frozen config object | | useChain() | { chain, chainId, chains, switchChain(genesisHash) } | | useApi() | { client, isApiReady, error, reconnect() } (dedot client) | | useAccount() | { account?, signer?, isConnected } | | useTusdtContract() | Memoized TusdtContract for the active chain, or null | | useTusdtMetadata() | { symbol, decimals, contractAddress } from the active chain | | useBalance({ address? }) | UseQueryResult<{ raw, formatted, symbol, decimals }> — 12s refetch | | useDryRunTransfer({ recipient, amount }) | UseQueryResult<{ refTime, proofSize }> | | useTransferTusdt() | See below | | usePaymentModal() | { isOpen, open(props?), close() } — imperative dialog control |

useTransferTusdt()

const {
  transfer,            // (args) => void
  transferAsync,       // (args) => Promise<receipt>
  isPending,
  status,              // react-query status
  detailedStatus,      // 'idle' | 'signing' | 'broadcasting' | 'inBlock' | 'finalized' | 'success' | 'failed'
  data,                // { blockHash, extrinsicHash, blockNumber } on success
  error,
  reset,
} = useTransferTusdt({ onSuccess, onError });

transfer({ recipient: '5GGq...', amount: 1_000_000_000n });

On success the hook invalidates ['tusdt','balance', chainId, contractAddress] so any mounted useBalance refetches automatically.

<PaymentDialog />

<PaymentDialog
  open={open}
  onOpenChange={setOpen}
  recipient="5GGq..."
  amount={1_000_000_000n}    // omit to let the user enter an amount
  amountFixed={true}         // hide AmountInput
  title="Pay with TUSDT"
  description="Complete your order"
  onSuccess={(receipt) => {}}
  onError={(err) => {}}
  portalContainer={document.body}
/>

<PaymentButton />

Convenience wrapper — renders a button that opens PaymentDialog. Same payment props as the dialog, plus standard <button> props.

createWebhookPayload(receipt, { recipient, amount })

Helper to build the exact JSON body to POST to your platform backend:

import { createWebhookPayload } from 'tusdt-sdk/react';

const body = createWebhookPayload(receipt, { recipient, amount });
// { blockHash, extrinsicHash, blockNumber, recipient, amount: "1000000" }

Theming

Pass a theme prop to the provider:

<TusdtKitProvider
  config={tusdtConfig}
  theme={{
    mode: 'dark',
    colors: {
      accent: '#8b5cf6',
      accentForeground: '#ffffff',
      surface: '#0b0b0f',
    },
  }}
>

Theme values are applied as CSS variables (--tusdt-accent, --tusdt-surface, …) on the provider's wrapper div. You can also override them directly in your own CSS:

.my-app {
  --tusdt-accent: #f43f5e;
  --tusdt-radius: 12px;
}

Make sure to import the stylesheet once at your app root:

// Next.js (app router)
// app/layout.tsx
import 'tusdt-sdk/ui/styles.css';

// Vite / CRA
// src/main.tsx
import 'tusdt-sdk/ui/styles.css';

Backend webhook flow

 ┌────────┐  PaymentButton  ┌───────────┐  sign+broadcast   ┌──────────┐
 │ User   │ ─────────────▶ │  Wallet   │ ─────────────────▶ │ Bittensor│
 └────────┘                 └───────────┘                    └────┬─────┘
      ▲                                                           │ finalized
      │   onSuccess(receipt)                                      ▼
      │ ◀─────────────────────────────────  PaymentDialog ◀───  TxResult
      │                                          │
      │                                          ▼
      │                              POST /api/payments/confirm
      │                              { blockHash, extrinsicHash, blockNumber }
      │                                          │
      │                                          ▼
      │                                   Your backend
      │                              (verify on-chain, credit user)

Your backend should independently verify the transaction by querying the chain at blockHash — never trust the client receipt alone.


Engine API (lower-level)

If you only need the contract engine (e.g. for a Node.js backend or non-React frontend), import directly from the root:

import { TusdtContract, executeTransfer, formatBalance, parseBalance } from 'tusdt-sdk';
import { DedotClient, WsProvider } from 'dedot';

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

const balance = await tusdt.balanceOf(address);
console.log(formatBalance(balance));

await tusdt.transfer(signer, recipient, parseBalance('1.5'));

Available functions: queryController, queryTotalSupply, queryBalanceOf, queryAllowance, executeTransfer, executeApprove, executeTransferFrom, executeMint, executeBurn, executeIncreaseAllowance, executeDecreaseAllowance, safeApprove.

Validation helpers: validateAddress, validateBalance, validateOverflowSafe. Formatting: formatBalance, parseBalance, TUSDT_DECIMALS.


Errors

All SDK errors extend TusdtSdkError with a typed code field. Branch on code, never on message:

import { TusdtSdkError, ErrorCode } from 'tusdt-sdk';

try {
  await transferAsync({ recipient, amount });
} catch (err) {
  if (err instanceof TusdtSdkError) {
    switch (err.code) {
      case ErrorCode.INSUFFICIENT_BALANCE: /* … */ break;
      case ErrorCode.INVALID_ADDRESS:      /* … */ break;
      case ErrorCode.DRY_RUN_FAILED:       /* … */ break;
      case ErrorCode.DISPATCH_FAILED:      /* … */ break;
      case ErrorCode.CONNECTION_FAILED:    /* … */ break;
    }
  }
}

| Code | Meaning | |------|---------| | INVALID_ADDRESS / INVALID_AMOUNT / BALANCE_OVERFLOW | Validation failed before sending | | INSUFFICIENT_BALANCE / INSUFFICIENT_ALLOWANCE | Contract rejection | | NOT_CONTROLLER | Caller not authorized for mint/burn | | DRY_RUN_FAILED | Pre-flight simulation rejected the tx | | DISPATCH_FAILED | Tx included on-chain but execution failed | | CONNECTION_FAILED | WebSocket connection lost |


SSR / Next.js

The provider is fully SSR-safe — WebSocket connections are deferred to useEffect. Mount <TusdtKitProvider> inside a 'use client' boundary:

// app/providers.tsx
'use client';
import { TusdtKitProvider } from 'tusdt-sdk/ui';
// …

useBalance, useTransferTusdt, etc. return isApiReady: false until the client is connected — the dialog handles this state.


License

MIT


Development

npm install
npm run build       # tsup + tsc
npm run check       # biome lint + format check
npm run format      # biome format --write
npm run test:unit   # vitest