npm.io
0.4.2 • Published 19h ago

@topazdex/id-connect

Licence
MIT
Version
0.4.2
Deps
0
Size
261 kB
Vulns
0
Weekly
0

@topazdex/id-connect

Add Topaz ID — a BNB Chain smart-wallet global account — to your dapp as a one-click login. Users sign in with their existing Topaz ID account (id.topazdex.com) — email, Google, or an external wallet — and connect with their Topaz ID smart contract wallet (Kernel/ZeroDev). No seed phrase, no extension, and no Privy app of your own.

Topaz ID is built on Privy's global wallets. Your app is the requester and references Topaz ID's public app id — that's the whole integration. You don't need a Privy account, and your domain does not need to be allowlisted by Topaz ID.

Demo

See it live: topaz-id-demo.vercel.app — a Next.js + RainbowKit app demonstrating connect, profile display, smart-wallet sends, and a batched approve + swap. Source: topazdex/topaz-id-connect-demo.

New in 0.4

0.4 adds the smart-wallet action client — the recommended way to send transactions: useTopazIdClient on /react, and createTopazIdClient on the new /actions entry for non-React apps. See Using the wallet. Purely additive: existing integrations keep working unchanged.

If value-bearing transactions were failing for you on plain wagmi (the consent popup can't estimate the cost, and submitting errors), this client is the fix — upgrade to ^0.4.1 and route sends through it. As with 0.3, ^0.3 consumers don't automatically cross the minor — upgrade deliberately.

Upgrading to 0.3.0

0.3.0 makes Topaz ID smart-account-first: the connected account is now the user's smart contract wallet (Kernel/ZeroDev) — their identity on id.topazdex.com — instead of the embedded signer EOA. Most apps need no code change (you already read useAccount().address), but note:

  • The address changes. useAccount().address is now the smart wallet, so anything keyed on the old EOA (allowlists, prior balances) won't carry over.
  • Sends are gas-sponsored UserOperations, and signatures are ERC-1271/6492, not ECDSA — see Using the wallet. Update SIWE/ecrecover backends to an ERC-1271-aware check.
  • Opt out: pass { smartWalletMode: false } to topazIdConnector(), topazIdWallet(), or TopazIdProvider to keep the Legacy signer-EOA mode — see Smart vs Legacy wallets.

^0.2 consumers don't automatically cross the minor — you upgrade deliberately.

Install

yarn add @topazdex/id-connect @privy-io/cross-app-connect wagmi viem \
  @tanstack/react-query

Add @rainbow-me/rainbowkit if you use the RainbowKit picker, or @privy-io/react-auth if your app is itself a Privy app. All peer dependencies are optional and only pulled in by the entrypoints that need them — see Peer dependencies.

@privy-io/cross-app-connect pins viem@2.52.0. Match it to avoid peer warnings.

Quick start

The fastest path: wrap your app in TopazIdProvider (it sets up wagmi for BNB Chain, the Topaz ID connector, and React Query for you), then connect with useTopazIdLogin. No createConfig, no RainbowKit.

// app/providers.tsx
"use client";
import { TopazIdProvider } from "@topazdex/id-connect/react";

export function Providers({
  children,
  cookie,
}: {
  children: React.ReactNode;
  cookie?: string | null;
}) {
  return <TopazIdProvider cookie={cookie}>{children}</TopazIdProvider>;
}
// app/layout.tsx (Next.js App Router) — pass the cookie for clean SSR hydration
import { headers } from "next/headers";
import { Providers } from "./providers";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const cookie = (await headers()).get("cookie");
  return (
    <html lang="en">
      <body>
        <Providers cookie={cookie}>{children}</Providers>
      </body>
    </html>
  );
}
// any client component
import { useTopazIdLogin } from "@topazdex/id-connect/react";
import { useAccount } from "wagmi";

export function SignIn() {
  const { login, logout } = useTopazIdLogin();
  const { address, isConnected } = useAccount();

  return isConnected ? (
    <button onClick={logout}>{address}</button>
  ) : (
    <button onClick={login}>Sign in with Topaz ID</button>
  );
}

TopazIdProvider accepts appId (target a staging app), smartWalletMode (defaults to true; pass false for the legacy signer-EOA), transport (custom RPC), queryClient (bring your own), ssr (defaults to true, enabling wagmi cookie storage), and cookie (the request cookie header, so a connected wallet survives SSR without a flash). Draw the "use client" boundary in your app — the library stays framework-agnostic.

RainbowKit

Prefer RainbowKit's wallet picker? Configure wagmi yourself and add the Topaz ID wallet. Connector helpers live at @topazdex/id-connect/connectors.

import { topazIdWallet, TOPAZ_ID_CHAIN } from "@topazdex/id-connect/connectors";
import { connectorsForWallets } from "@rainbow-me/rainbowkit";
import { createConfig, http } from "wagmi";

const connectors = connectorsForWallets(
  [{ groupName: "Sign in", wallets: [topazIdWallet()] }],
  { appName: "Your App", projectId: "<walletconnect-project-id>" },
);

export const wagmiConfig = createConfig({
  chains: [TOPAZ_ID_CHAIN], // BNB Chain (56)
  transports: { [TOPAZ_ID_CHAIN.id]: http() },
  connectors,
  ssr: true,
});

"Topaz ID" now appears in the RainbowKit picker. Selecting it opens a Topaz ID consent window where the user signs in — no new wallet is created.

The @topazdex/id-connect/rainbow-kit subpath still works as a deprecated alias of /connectors, so existing imports keep compiling. New code should use /connectors.

Plain wagmi (no RainbowKit)

import { topazIdConnector, TOPAZ_ID_CHAIN } from "@topazdex/id-connect/connectors";
import { createConfig, http } from "wagmi";

export const wagmiConfig = createConfig({
  chains: [TOPAZ_ID_CHAIN],
  transports: { [TOPAZ_ID_CHAIN.id]: http() },
  connectors: [topazIdConnector()],
  ssr: true,
});

Using the wallet

For the smoothest smart-wallet UX, use the high-level Topaz ID client instead of hand-rolling provider RPC calls. It exposes sendTransaction, sendCalls, and writeContract, and hides Topaz smart-wallet details such as privy_sendSmartWalletTx, native BNB value formatting, and approval+action batching.

import { useTopazIdClient } from "@topazdex/id-connect/react";
import { erc20Abi, parseEther, parseUnits } from "viem";

const { data: topazClient } = useTopazIdClient();

await topazClient?.sendTransaction({
  to,
  value: parseEther("0.01"),
});

await topazClient?.sendCalls({
  calls: [
    {
      address: TOKEN_ADDRESS,
      abi: erc20Abi,
      functionName: "approve",
      args: [ROUTER_ADDRESS, parseUnits("100", 18)],
    },
    {
      to: ROUTER_ADDRESS,
      data: swapCalldata,
    },
  ],
});

useTopazIdClient also returns isTopazId, and data stays undefined when the connected wallet isn't Topaz ID — so in a multi-wallet dapp the drop-in pattern is a single branch, no connector sniffing:

const { data: topazClient } = useTopazIdClient();

const hash = topazClient
  ? await topazClient.sendTransaction({ to, value }) // Topaz ID smart wallet
  : await sendTransactionAsync({ to, value, chainId: 56 }); // any other wallet

Pass useTopazIdClient({ appId }) when your connector was configured with a custom app id.

Framework-agnostic apps can use the action client directly with any EIP-1193-ish provider:

import { createTopazIdClient } from "@topazdex/id-connect/actions";

const topazClient = await createTopazIdClient({
  provider,
  account,
  chainId: 56,
});

await topazClient.sendCalls({ calls: [approvalCall, swapCall] });

Plain object literals work for every call; the optional txCall(...) / contractCall(...) builders do the same thing but validate the target address eagerly, so a typo fails before a consent popup ever opens.

Plain wagmi (useSendTransaction / useWriteContract) still works for calls that carry no native BNB — approvals and most contract writes. Anything with a native value must go through this client: wagmi hex-encodes value, and the Topaz ID popup rejects hex quantity strings. That mismatch is why value-bearing transactions fail with a "can't estimate cost" popup on raw connector integrations while approvals sail through.

A few rules keep transactions routing through the smart wallet reliably:

  • Sign and send with this client (or plain wagmi for zero-value calls) — never @privy-io/react-auth signing hooks. Those are embedded-wallet-only and execute from the underlying Privy EOA instead of the user's Topaz ID smart wallet.
  • Every action opens a Topaz ID consent window; the user approves each one. Trigger sends from a direct user interaction (a button click) so browsers don't block the popup — a send fired after a long await chain can be popup-blocked. Prefer batching an approval + action into one sendCalls bundle: one popup, one approval, atomic execution.
  • Pass native value as a bigint and let the SDK format it. The popup expects a plain JSON number and rejects hex. Above 2^53−1 wei (~0.009 BNB) the conversion can round by sub-1000-wei dust — economically negligible, and round amounts (0.1 / 1 / 10 BNB) are exactly representable. Don't pre-encode value yourself.
  • sendCalls degrades gracefully. It submits one atomic bundle; if the wallet rejects the bundle, the SDK retries the calls sequentially (one consent popup per call) and returns the last call's hash. Pass atomicRequired: true to get the batch error instead of the fallback.
  • Treat receipt lookups as best-effort. The returned hash is usually a transaction hash, but some smart-wallet flows return an id that eth_getTransactionReceipt cannot resolve. Poll for the receipt with a timeout and fall back to re-reading your app state (balances, allowances) rather than blocking the UI on the receipt alone.

The connected account is a smart contract wallet (Kernel/ZeroDev on BNB Chain), which differs from a plain EOA in two ways worth knowing:

  • Sends are UserOperations relayed through Topaz ID. sendTransaction / writeContract are submitted via Topaz ID's bundler + paymaster — gas is sponsored by Topaz ID's paymaster policy (so the user typically needs no BNB for gas), and multiple calls (e.g. approve + swap) can batch into a single atomic action.
  • Signatures are ERC-1271 / ERC-6492, not ECDSA. personal_sign and eth_signTypedData_v4 (wagmi's useSignMessage / useSignTypedData) return a contract signature. If your backend verifies signatures (e.g. SIWE), use an ERC-1271/6492-aware check — viem's verifyMessage / verifyTypedData with a BNB Chain public client — not ecrecover.

Need the Legacy signer EOA instead of the Smart wallet? Pass { smartWalletMode: false } to topazIdConnector(), topazIdWallet(), or TopazIdProvider. That address is signer-only — not where the user holds funds. See Smart vs Legacy wallets.

Smart vs Legacy wallets

Topaz ID has two wallet modes, labelled here the same way as on id.topazdex.com:

  • Smart — the user's smart contract wallet (Kernel/ZeroDev). The default, and the right choice for every new integration.
  • Legacy — the underlying Privy signer EOA. Exposed only for backward compatibility with existing dapps whose users transacted with that EOA directly before the smart-wallet cutover.

New integrations need to do nothing — the connector defaults to Smart, and you shouldn't surface Legacy at all. Only an existing dapp with users who hold funds on the signer EOA should offer both. When you show both modes or let the user switch, label them "Smart" and "Legacy" — the canonical strings, descriptions, and a TopazIdWalletMode type are exported so your toggle matches ours:

import {
  TOPAZ_ID_WALLET_MODES,
  topazIdWalletMode,
  type TopazIdWalletMode,
} from "@topazdex/id-connect";

TOPAZ_ID_WALLET_MODES.smart;
// → { mode: "smart",  label: "Smart",  description: "Gas-free smart wallet (recommended)" }
TOPAZ_ID_WALLET_MODES.legacy;
// → { mode: "legacy", label: "Legacy", description: "Your original Privy signing wallet" }

// Map the connector flag to a mode (undefined/true → "smart", false → "legacy"):
topazIdWalletMode(false); // "legacy"

To offer both in a RainbowKit picker, add a second, Legacy-labelled connector alongside the default:

import { topazIdWallet } from "@topazdex/id-connect/connectors";
import { TOPAZ_ID_NAME, TOPAZ_ID_LEGACY_WALLET_LABEL } from "@topazdex/id-connect";

const wallets = [
  topazIdWallet(), // Smart (default)
  topazIdWallet({
    smartWalletMode: false,
    name: `${TOPAZ_ID_NAME} (${TOPAZ_ID_LEGACY_WALLET_LABEL})`, // "Topaz ID (Legacy)"
  }),
];

Show the user's Topaz ID profile

Topaz ID owns each wallet's name, handle, and avatar. Render real identity instead of a bare address. Framework-agnostic helpers live at the root entry; a React Query hook lives at /react.

import { displayNameForWallet, avatarForWallet } from "@topazdex/id-connect";
import { useTopazIdProfile } from "@topazdex/id-connect/react";

const { data: profile } = useTopazIdProfile(address);
const label = displayNameForWallet(profile ?? null, address);
const avatar = avatarForWallet(profile ?? null, "/default-avatar.png");

Reads are public and CORS-open. found: false → fall back to the address; never block your UI on the fetch. fetchTopazIdProfile returns null on a network or HTTP failure (aborts re-throw so React Query can tell a cancellation from an empty result).

Already using Privy?

If your app is itself a Privy app, skip the connector and add Topaz ID as a cross-app login method. The /privy entry gives you the login-method constant, a login/link hook, and a thin provider — all using your own Privy app id.

import {
  TopazIdPrivyProvider,
  topazIdLoginMethod,
  useTopazIdCrossAppLogin,
} from "@topazdex/id-connect/privy";

// 1. Wrap your app. Topaz ID is prepended to your login methods.
<TopazIdPrivyProvider
  appId={MY_PRIVY_APP_ID}
  config={{ loginMethodsAndOrder: { primary: ["email", "wallet"] } }}
>
  <App />
</TopazIdPrivyProvider>;

// 2. Or wire it into a plain <PrivyProvider> yourself:
//    config={{ loginMethodsAndOrder: { primary: ["email", topazIdLoginMethod] } }}

// 3. Trigger the cross-app login from a button.
const { login } = useTopazIdCrossAppLogin();
<button onClick={login}>Continue with Topaz ID</button>;

To read the linked Topaz ID smart wallet address from the Privy user, use useTopazIdAccount — it returns the smart wallet as address (the identity to display and look up) and the embedded signer EOA separately:

import { useTopazIdAccount } from "@topazdex/id-connect/privy";

const { address, signerAddress } = useTopazIdAccount();
// address       → the user's Topaz ID smart contract wallet (their identity)
// signerAddress → the embedded EOA that signs for it (signer-only)

address is undefined until the user's smart wallet is provisioned and linked, so guard on it with a loading state before rendering or transacting. If you display both, label them Smart (address) and Legacy (signerAddress) — see Smart vs Legacy wallets.

Exports

Entry Contents
@topazdex/id-connect TOPAZ_ID_APP_ID, TOPAZ_ID_CONNECTOR_ID, TOPAZ_ID_CHAIN_ID, TOPAZ_ID_NAME, TOPAZ_ID_ICON_URL, TOPAZ_ID_BASE_URL, TOPAZ_ID_SMART_WALLET_LABEL, TOPAZ_ID_LEGACY_WALLET_LABEL, TOPAZ_ID_WALLET_MODES, topazIdWalletMode, TopazIdWalletMode, TopazIdWalletModeInfo, fetchTopazIdProfile, displayNameForWallet, avatarForWallet, shortenAddress, TopazIdProfile
@topazdex/id-connect/connectors topazIdWallet, topazIdConnector, TOPAZ_ID_CHAIN, TopazIdConnectorOptions
@topazdex/id-connect/actions createTopazIdClient, txCall, contractCall, isTopazIdConnectorId, TopazIdClient, TopazIdClientOptions, TopazIdCall, TopazIdContractCall, TopazIdSendCallsParameters, TopazIdCapabilities, TopazIdProviderLike
@topazdex/id-connect/rainbow-kit Deprecated alias of /connectors
@topazdex/id-connect/react TopazIdProvider, useTopazIdLogin, useTopazIdClient, useTopazIdProfile
@topazdex/id-connect/privy TopazIdPrivyProvider, useTopazIdCrossAppLogin, useTopazIdAccount, topazIdLoginMethod

Peer dependencies

All peers are optional; install only what your entrypoints use.

You use Install
Profile helpers only (@topazdex/id-connect) nothing extra
TopazIdProvider / useTopazIdLogin / useTopazIdClient (/react) wagmi, viem, @tanstack/react-query, react, @privy-io/cross-app-connect
Connectors (/connectors) @privy-io/cross-app-connect, viem, wagmi (+ @rainbow-me/rainbowkit for topazIdWallet)
Action client (/actions) viem
useTopazIdProfile only (/react) @tanstack/react-query, react
Privy cross-app (/privy) @privy-io/react-auth, react

Releasing

Publishing is automated: .github/workflows/publish.yml runs on a published GitHub Release and npm publishes via OIDC trusted publishing (no token, provenance included). It does not publish on a push to main or a bare tag push — creating the Release is the trigger.

  1. Land changes on main (green CI).
  2. Bump version in package.json (semver).
  3. Commit Release vX.Y.Z and push.
  4. Create the Release — its tag (minus v) must equal package.json#version:
    gh release create vX.Y.Z --title vX.Y.Z --generate-notes
  5. Watch the Actions tab, then confirm: npm view @topazdex/id-connect version.

See CLAUDE.md for the full process and gotchas.

License

MIT

Keywords