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

@moon-x/react-sdk

v0.39.0

Published

React authentication hooks and components for MoonX

Readme

@moon-x/react-sdk

React SDK for MoonX — embedded wallets with passkey-protected MPC signing for Ethereum, Solana, Tron, and Bitcoin, drop-in auth UI flows, and headless wallet methods.

npm install @moon-x/react-sdk
# or
pnpm add @moon-x/react-sdk

Quick start

Wrap your app with MoonXProvider and import the CSS bundle once:

import { MoonXProvider, mainnet, sepolia } from "@moon-x/react-sdk";
import "@moon-x/react-sdk/style.css";

export default function App() {
  return (
    <MoonXProvider
      publishableKey={process.env.NEXT_PUBLIC_MOONX_PUBLISHABLE_KEY!}
      config={{
        appearance: {
          accentColor: "#6366f1",
          backgroundColor: "#ffffff",
        },
        // One unified list for EVM, Solana, Tron, and Bitcoin. A bare viem chain uses
        // its built-in RPC; non-EVM entries carry a CAIP-style id.
        chains: [
          mainnet,
          sepolia,
          { id: "solana:mainnet", rpcUrl: "https://api.mainnet-beta.solana.com" },
          { id: "tron:mainnet", rpcUrl: "https://api.trongrid.io" },
          {
            id: "bitcoin:mainnet",
            rpcUrl: process.env.NEXT_PUBLIC_BITCOIN_BLOCKBOOK_RPC!,
            network: "mainnet",
          },
        ],
        defaultChain: "eip155:1", // CAIP-2 | numeric (1) | name alias ("eth:sepolia")
      }}
    >
      <YourApp />
    </MoonXProvider>
  );
}

Open the auth modal and read user state:

import { useMoonX } from "@moon-x/react-sdk";

function LoginButton() {
  const { ready, isAuthenticated, user, start, logout } = useMoonX();

  if (!ready) return null;
  if (isAuthenticated) {
    return <button onClick={() => logout()}>Sign out ({user?.id})</button>;
  }
  return <button onClick={() => start?.()}>Sign in</button>;
}

Sign a message with one of the user's wallets:

import { useWallets } from "@moon-x/react-sdk";
import { useSignMessage } from "@moon-x/react-sdk/ethereum";

function SignDemo() {
  const { wallets } = useWallets();
  const { signMessage } = useSignMessage();

  const onSign = async () => {
    const wallet = wallets.find((w) => w.wallet_type === "ethereum");
    if (!wallet) return;
    const { signature } = await signMessage({
      message: "Hello world",
      wallet,
      options: { uiOptions: { showWalletUI: true } }, // opens the modal
    });
    console.log(signature);
  };

  return <button onClick={onSign}>Sign</button>;
}

Configuration

MoonXProvider accepts publishableKey (required) plus a config object:

| Field | Type | Purpose | |---|---|---| | iframeUrl | string | Secure iframe base URL override for local, preview, or staging deployments. Omit it to use the hosted production iframe. | | apiUrl | string | API base URL override for the same non-production deployment. Configure it together with iframeUrl so browser and iframe calls target the same stack. | | appearance | AuthAppearance | The single theming surface — branding (accentColor, backgroundColor, displayMode, logo, loginHeaderTitle, fontFamily) and all design tokens inline (colors, borderRadius, typography, card, backdrop, components, per-mode light / dark). See Theming. | | showSessionErrorAlert | boolean | Show the built-in alert when the initial session check fails. Defaults to true. Set false to render your own UI with useMoonX(). | | loginMethods | ("email" \| "sms" \| "google" \| "apple" \| "wallet")[] | Which auth methods to show in the modal. Each shows only when enabled for the app; "sms" shows only when the app config's sms_otp.enabled is true and sends only to its allowed_countries. | | walletChainType | "ethereum" \| "solana" \| "ethereum-or-solana" | Which wallet type to create at signup. | | chains | ChainConfigItem[] | Unified RPC / chain config for EVM, Solana, Tron, and Bitcoin. Bitcoin entries are { id, rpcUrl, network } and require Blockbook JSON-RPC methods for balance/broadcast. Common viem chains are re-exported by this package. See docs/rpc-configuration.md. | | defaultChain | string \| number | Which configured chain is the default. Referenced by alias, CAIP-2 id, or numeric chainId — e.g. "eth:base", "solana:mainnet", "tron:mainnet", or "bitcoin:mainnet". Defaults to the first entry in chains. | | walletConnect.projectId | string | WalletConnect v2 project ID. When set, the connect-wallet modal offers a WalletConnect (EVM) option (QR) so mobile / non-injected wallets can connect using the configured/default EVM chain. | | emailConfig, passkeyEnrollConfig, signMessageConfig, signTransactionConfig, sendTransactionConfig, exportKeyConfig | various | Per-flow UI overrides — titles, button text, etc. | | security | Record<string, never> | Reserved for future per-app security knobs. The previously-configurable assertionCacheTtlMs was removed in Phase 4 of the presence-token gating work — every sensitive op now does a fresh WebAuthn ceremony and mints scope-bound single-use JWTs via the iframe's internal orchestrator, so there is no parent-side cache left to configure. See Security below. |

Control the session-error UI

Requires @moon-x/react-sdk 0.25.0 or later.

If the first session check fails, MoonXProvider shows an alert before its children. The alert offers retry and sign-in actions. An origin rejection also uses this error UI.

To hide the alert, set config.showSessionErrorAlert to false:

<MoonXProvider publishableKey={publishableKey} config={{ showSessionErrorAlert: false }}>
	<YourApp />
</MoonXProvider>

Use sessionError and retrySession from useMoonX() to render recovery UI anywhere inside your app:

import { useMoonX } from "@moon-x/react-sdk";

function SessionBanner() {
	const { ready, sessionError, retrySession } = useMoonX();
	if (ready || !sessionError) return null;

	return (
		<div role="alert" className="app-session-error">
			<p>{sessionError.message}</p>
			<button type="button" onClick={retrySession}>Retry</button>
		</div>
	);
}

sessionError is null until a session check fails. It updates when checks fail, including background checks after ready becomes true. A successful check or login clears the error. Background failures preserve the last known user and authentication state.

retrySession() restarts the check and its bounded automatic retries. Failures update sessionError. Use the existing start() method for a sign-in action, and handle its rejected promise in your component. A cancellation rejects with user_cancelled.

The SDK continues automatic retries and calls the callback registered with useMoonX().onError when you hide the alert. The configuration controls presentation only. An origin must still be on the app's allowlist before the SDK can use it.

Hooks

Authentication & user state

| Hook | What it does | |---|---| | useMoonX() | The big one. { ready, sessionError, retrySession, isAuthenticated, user, start, logout, setAppearance, getSessionTokens, refreshUser, ... } + every SDK method on the same instance. | | useUser() | Just { user, refreshUser }. Re-subscribes to user changes. | | useLoginWithEmail({ onComplete?, onError? }) | Headless email-OTP. Returns { state, sendCode, loginWithCode, reset }. state is a discriminated union: idle / sending / awaiting-code / verifying / complete / error. | | useLoginWithSms({ onCodeSent?, onComplete?, onError? }) | Headless SMS OTP. Same shape as useLoginWithEmail; sendCode({ phone }) takes an international number with a leading +. | | useLinkPhone({ onCodeSent?, onComplete?, onError? }) | Attach a verified phone number to the signed-in account when the app config's phone_link.enabled is true. Returns { state, sendCode, linkPhone, reset }. A user with a passkey or email OTP steps up first. One number per account: rejects with phone already linked when the number is another account's or this account has a different one. | | useDetachPhone({ onSuccess?, onError? }) | Remove the account's phone number, with the same step-up and refusals as useDetachOAuth. Returns { state, loading, detachPhone }. To change number, remove the old one, then link the new one. | | useLoginWithOAuth() | Google + Apple flows. Returns { state, loginWithOAuth, reset }. | | useLogout() | { logout } — also clears local storage + iframe session. |

useMoonX().getCurrentSession() returns the current session or null when the iframe authoritatively reports that no session exists. Starting with @moon-x/core 0.15.0, bridge/transport failures reject instead of resolving to null. Direct polling code should catch those failures and retain its last-known auth state rather than treating an unavailable iframe as a logout.

MoonPay Consumer session exchange

Use loginWithMoonPayCustomerToken when the parent application already has a MoonPay Consumer customerToken. The SDK exchanges the token for a MoonX session. The iframe stores the session credentials. The method returns only userCreated.

import {
  MOONPAY_CONSUMER_SESSION_ERROR_CODES,
  type MoonPayConsumerSessionError,
  useMoonX,
} from "@moon-x/react-sdk";

const { loginWithMoonPayCustomerToken } = useMoonX();

try {
  const { userCreated } = await loginWithMoonPayCustomerToken({
    customerToken,
  });
} catch (error) {
  const code = (error as MoonPayConsumerSessionError).error_code;
  if (code === MOONPAY_CONSUMER_SESSION_ERROR_CODES.SESSION_INVALID) {
    // Get a new Consumer token and try again.
  }
}

The promise resolves after the MoonX session is stored. At that time, isAuthenticated is true. The SDK then loads the user profile in the background. Thus, user can be null for a short time. It can also stay null if the profile request fails. Use refreshUser when the application must have the profile.

The iframe uses the publishable key from its own URL for the backend exchange. It rejects the request if the key in the parent message does not match that key. A 401 response from the backend becomes the stable moonpay_consumer_session_invalid error code. Do not parse HTTP status numbers from error messages.

Passkeys

| Hook | What it does | |---|---| | usePasskeyStatus() | { status, refresh }. status.passkeys lists the user's enrolled passkeys with provider labels (e.g. "1Password on Chrome"). | | useRegisterPasskey() | First-time passkey enrollment for a user who signed in via OTP/OAuth without one. | | useAddPasskey() | Add an additional passkey to an authenticated user. | | useRemovePasskey() | Remove a passkey by its credential ID. |

Wallets

| Hook | What it does | |---|---| | useWallets() | Chain-pinned wallet list from the /ethereum, /solana, /tron, or /bitcoin subpath. | | useCreateWallet() | Mint a new MPC wallet for the imported chain subpath. | | useImportKey() | Two-mode: headless if you pass a key, modal-driven if you don't. | | useConnectWallet() | Prebuilt wallet modal. Injected EVM (EIP-6963), Solana (Wallet Standard), and EVM WalletConnect (QR, when walletConnect.projectId is set). mode defaults to "connect" (client-side connection, no session); "signIn" runs the SIWE/SIWS login ceremony and "link" attaches the wallet to the current user without replacing the session. | | useWalletLogin({ chain? }) | Headless external-wallet authentication: { wallets, signIn(walletId, chain?), link(walletId, chain?), status, error, refresh }. Both operations connect transiently and run SIWE/SIWS; sign-in establishes a session, while link requires and preserves the current session. | | useExternalWallets({ chain? }) | Headless external-wallet connection (no session): { wallets, connect, disconnect, connected, status, refresh }. Covers injected EVM + Solana Wallet Standard. For WalletConnect, use the modal or the standalone connectWalletConnect(). | | useConnectedExternalWallets() | The external wallets explicitly connected this session (shared app-wide) for a picker / account switcher; each exposes signMessage. A login wallet is not listed here — sign-in connects transiently. | | useAttachOAuth() / useDetachOAuth() | Link / unlink an OAuth provider on an existing user. |

External-wallet login (SIWE / SIWS)

Let users authenticate by signing a message with an existing wallet — SIWE for EVM, SIWS for Solana. Enable it per app with the enable_external_wallets dashboard setting; when it's off, the wallet login entry is hidden and the endpoints reject.

  • Prebuilt UI — add "wallet" to loginMethods and the login modal shows a wallet option, or open it directly:
    const sdk = useMoonX();
    await sdk.connectWallet({ mode: "signIn", walletChainType: "ethereum-and-solana" });
    // On success useMoonX().isAuthenticated === true and the session survives reload.
  • Headless — build your own picker with useWalletLogin():
    const { wallets, signIn } = useWalletLogin({ chain: "all" });
    // render `wallets`, then on click:
    await signIn(wallet.id, wallet.chain);
  • Link to the current user — use either surface while authenticated:
    await sdk.connectWallet({ mode: "link" });
    
    // Or headless:
    const { link } = useWalletLogin({ chain: "all" });
    const linkedWallet = await link(wallet.id, wallet.chain);

Wallet users have no email — the compliance gate is sanctions/AML screening of the connecting address for sign-in and link (both chains). A screened-out wallet fails with a clear, non-retryable message ("This wallet can't be used.") and no identity or session change is made. The identity token's sub is the internal MoonX user ID; it does not expose the wallet provider or address.

The wallet used to sign in is transient — it is not added to useConnectedExternalWallets(). Adding an external wallet for on-going use is a separate, explicit connect action. Connected external wallets are cleared on logout().

Linking is idempotent for a wallet already attached to the same user. A wallet owned by another MoonX user rejects with wallet_already_linked.

Find or create a wallet

useMoonX().findOrCreateWallet(chain) returns { wallet } with the first existing wallet for that chain. If no wallet exists, it runs the normal creation flow. For bitcoin, it matches the first wallet with the requested address type (p2tr by default) and creates one of that type if none exists:

const { wallet } = await findOrCreateWallet("bitcoin", { addressType: "p2wpkh" });

It rejects with wallets_unavailable rather than creating a wallet when the existing list can't be read.

const { findOrCreateWallet } = useMoonX();
const { wallet } = await findOrCreateWallet("solana");

createWallet() keeps its existing behavior: every call creates a new wallet, including concurrent calls. Existing callers need no new option.

Only findOrCreateWallet() shares concurrent requests. Sharing requires the same transport, publishable key, login, and chain (and address type for Bitcoin). Each completed or failed operation releases its shared request. Separate tabs and devices can still race. A routine access-token refresh does not interrupt the operation; the helper rejects with session_changed only if a different login replaces the session while it runs. If a different login replaces the session during creation, the wallet can already exist on the earlier login when the call rejects. After session_changed, confirm the intended account before you retry with findOrCreateWallet(), not createWallet(). The retry searches the current account before it creates a wallet. If the new login is the same account, the retry finds the wallet that the first call created.

Per-chain wallet methods — /ethereum, /solana, /tron, and /bitcoin

Chain-aware hooks live under subpaths so they don't pull in the other chain's adapters if you only use one:

import { useSignMessage, useSignTransaction, useSendTransaction } from "@moon-x/react-sdk/ethereum";
import { useSignMessage as useSignSolanaMessage } from "@moon-x/react-sdk/solana";
import { useSignMessage as useSignTronMessage } from "@moon-x/react-sdk/tron";
import {
  useSignMessage as useSignBitcoinMessage,
  useSignTransaction as useSignBitcoinTransaction,
} from "@moon-x/react-sdk/bitcoin";

Ethereum hooks:

  • useSignMessage — EIP-191 personal_sign. Returns { signature: "0x..." }.
  • useSignTransaction — Signs an EIP-1559 tx. Returns { signature, serializedSigned, hash }.
  • useSignTypedData — EIP-712. Returns { signature: "0x..." }.
  • useSignHash — Raw ECDSA digest sign (Privy parity — secp256k1_sign).
  • useSign7702Authorization — EIP-7702 delegation auth.
  • useSendTransaction — Sign + broadcast. Accepts a per-call chain selector and an inline rpcUrl override (both fall back to provider config).
  • useGetBalance — Native token balance. Same per-call chain / rpcUrl options.

Solana hooks:

  • useSignMessage — Ed25519 signature.
  • useSignTransaction — Signs a serialized base58 tx. Returns { signedTransaction: Uint8Array }.
  • useSendTransaction — Sign + broadcast. Accepts a per-call chain selector and an inline rpcUrl override.
  • useGetBalance — Lamport balance. Same per-call chain / rpcUrl options.
  • useGetTokenAccounts — SPL token accounts by owner. Same per-call chain / rpcUrl options.

Tron hooks:

  • useSignMessage — TIP-191 message signature.
  • useSignTransaction — Signs a Tron transaction JSON object.
  • useSendTransaction — Sign + broadcast through the configured Tron fullnode.
  • useGetBalance — Native TRX balance using the configured Tron fullnode.

Bitcoin hooks:

  • useCreateWallet / useImportKey — Accept addressType: "p2tr" | "p2wpkh" | "p2pkh".
  • Bitcoin import accepts compressed mainnet WIF or 32-byte hex; export returns compressed mainnet WIF. WIF does not encode the address type, so re-import with the same addressType to reproduce the original address. Taproot (bc1p) is the backwards-compatible default; Native SegWit (bc1q) and Legacy P2PKH (1) use separate ECDSA wallets with BIP84- and BIP44-shaped derivation paths respectively.
  • useSignMessage — Produces an smp-prefixed BIP322 Simple proof for P2TR and P2WPKH, or a ful-prefixed BIP322 Full proof for P2PKH, and returns { signature, messageHash, address, scheme }.
  • useSignTransaction — Validates, batch-signs, and finalizes a base64/hex PSBT v0. Every input must match the selected single-key wallet. P2TR supports BIP86 key-path DEFAULT/ALL with witnessUtxo; P2WPKH supports BIP143 ALL with witnessUtxo; P2PKH supports legacy ALL with the complete previous transaction in nonWitnessUtxo. Taproot script paths, multisig, and unsafe sighash modes are rejected. The iframe derives and verifies the wallet key before signing, so callers do not need to include MoonX's internal Taproot key.
  • useSendTransaction — Signs and broadcasts through the configured Blockbook-compatible RPC, refusing a returned txid mismatch.
  • useGetBalance — Confirmed native BTC balance through bb_getAddress.
  • useGetUtxos — Confirmed spendable outputs through bb_getUTXOs, for PSBT construction.

Each chain subpath also exports useCreateWallet, useWallets, useImportKey, useExportKey, and useFundWallet. Tron funding supports native TRX and USDT-TRC20; Bitcoin funding supports native BTC for all three address types through MoonPay.

Every signing hook accepts options.uiOptions.showWalletUI to toggle modal vs headless mode. With UI, the user sees the message/transaction in a modal and the biometric prompt fires only on the Sign tap. Headless skips the modal entirely.

Passkey-based operations use a fresh WebAuthn ceremony per call. See Security below.

Identity-backed ephemeral signers

useEphemeralSigner().provision() and .revoke() accept session-based presence when the app enables identity-backed encryption and the user has a usable KMS wrap. Users without passkeys can create, re-provision, and revoke signers without an email OTP or a verified email address.

The SDK prefers an enrolled passkey when one is available. If the passkey cannot assert, an eligible identity-backed session can authorize the operation. Passkey-only users still need a passkey assertion.

Provisioning and revocation each require a separate, scoped, single-use presence token. The SDK obtains these tokens internally. Key export and passkey management keep their existing authentication requirements.

Deploy the matching platform and wallets backend changes before releasing this SDK. Both services must accept session presence for ephemeral_signer_provision and ephemeral_signer_revoke.

Ephemeral signer list status

useEphemeralSigner().signers includes non-revoked signers with status active, needs_reauth, or expired. Expired signers remain visible and can be revoked. Provision a fresh signer for needs_reauth or expired.

Older backends can omit status. The SDK preserves their responses. Inspect expires_at and handle signing errors instead of treating a missing status as proof that a signer is usable.

Before the backend starts returning expired, update exhaustive status handling and runtime validators in your application. Updating the SDK alone does not change application logic. This status support can be released before backend PR #241.

Security

For passkey-only users, sensitive operations drive this server-verified ceremony per call:

  1. Server-issued challenge. SDK posts to /auth/passkey/presence/begin, server inserts a row in app_user_passkey_challenges and returns the WebAuthn options.
  2. Fresh WebAuthn assertion. The parent runs navigator.credentials.get; the user does a biometric. The assertor strips response.userHandle from the server-bound payload (DEK hygiene) and surfaces the userHandle separately for the iframe's local AES-GCM unwrap.
  3. Scope-bound, single-use JWT mint. SDK posts to /auth/passkey/presence/verify with purpose: "internal" and the scope set this op needs (e.g. ["keyshare_read", "sign"]). Server consumes the challenge, verifies the signature against the credential's stored public key, mints one short-lived JWT per scope with a unique jti.
  4. Per-endpoint enforcement. Each scoped JWT carries the X-MoonX-Presence header on its matching gated endpoint. MoonX middleware pins the JWT's op claim to the endpoint, then burns the jti in platform.app_presence_jti_used via INSERT ... ON CONFLICT DO NOTHING. Replay of the same token is rejected as presence token already consumed. Tokens have a 30-second TTL.

What this closes: captured userHandle + session JWT no longer unlocks DEK material offline. Even with both, an attacker can't fetch wraps / keyshares / drive the co-signer without producing a fresh WebAuthn signature for each op — which requires the credential's private key in the user's authenticator. See apps/platform/docs/notes/passkeys/presence-tokens.md (in the backend repo) for the full threat model and scope matrix.

The previously-configurable security.assertionCacheTtlMs and per-call requireFreshAssertion were removed entirely when presence-token gating shipped — they no longer exist on the public TypeScript surface. Every op is always-fresh by construction.

Theming

All theming flows through a single appearance object. Set a few high-level branding fields (accentColor, backgroundColor, displayMode, logo, loginHeaderTitle, fontFamily) — the SDK derives a full palette from accentColor / backgroundColor — and reach for the inline design tokens (colors, borderRadius, typography, card, backdrop, components, per-mode light / dark) when you want finer control.

<MoonXProvider
  config={{
    appearance: {
      // Branding
      accentColor: "#6366f1",
      backgroundColor: "#0f172a",
      displayMode: "dark",            // "light" | "dark" | "auto"
      fontFamily: "Inter, sans-serif",
      logo: { src: "/logo.svg", srcDark: "/logo-dark.svg", height: 32 },

      // Design tokens (all inline on appearance)
      colors: { warning: "#f59e0b", info: "#3b82f6", link: "#6366f1", ring: "#6366f1" },
      borderRadius: { button: "12px", card: "20px", input: "12px" }, // or "none" | "sm" | "md" | "lg"
      typography: {
        fontFamilyHeading: "Poppins, sans-serif",
        fontUrl: "https://fonts.googleapis.com/css2?family=Poppins&display=swap",
        letterSpacing: "0.01em",
        fontSize: { sm: "13px", lg: "17px", xl: "22px" },
      },
      card: { shadow: "0 10px 25px -5px rgb(0 0 0 / 0.2)", padding: "24px", maxWidth: "420px" },
      backdrop: { color: "rgb(0 0 0 / 0.5)", blur: "8px" },

      // Per-mode overrides (merged on top of the base tokens for the resolved mode)
      dark: { colors: { background: "#0b0b0f" } },
    },
  }}
>

Update appearance at runtime — handy for a light/dark toggle:

const { setAppearance } = useMoonX();
setAppearance({ displayMode: colorScheme });

Merge order (low → high): base light/dark theme → accentColor / backgroundColor derivations → appearance tokens (colors, borderRadius, …) → per-mode appearance.light / appearance.dark. So an explicit appearance token always wins over the high-level derivations.

Defaults

With no appearance the modal uses MoonPay's look: ink (#010101) pill buttons on white, #f4f3f9 panels, #e6e4ef hairlines, a 400px card with a 24px radius, 24px gutters, 48px buttons and inputs, 56px option rows, and the system font stack (SYSTEM_FONT_STACK, exported). Dark mode follows the same roles. The SDK ships no web fonts: set fontFamily (and typography.fontUrl to load one).

Text on the accent colour is chosen for contrast, so any accentColor keeps readable button labels; hover shades lighten dark accents and darken light ones.

Token reference

All theming tokens emit --moonx-* CSS variables. The base palette/radius/font emit the core tokens (e.g. --moonx-color-*, --moonx-border-radius-*); the granular tokens fall back to them (e.g. --moonx-radius-input → --moonx-border-radius-md).

| Group | Tokens | CSS variables | |---|---|---| | colors | accent, accentForeground, background, background2/3, foreground, foreground2–4, border, success, error, … | --moonx-color-* | | colors | warning, info, link, ring (focus) | --moonx-color-* | | borderRadius | sm (icon buttons, chips) · md (inputs, rows, banners) · lg (panels) · full | --moonx-border-radius-* | | borderRadius | button, card, input | --moonx-radius-* | | typography | fontFamilyHeading, fontSizeHeading, fontWeightHeading, colorHeading, letterSpacing(Heading), fontSize.{sm,lg,xl}, fontWeight.{medium,bold} | --moonx-font-*, --moonx-letter-spacing* | | typography | fontUrl | injected <link rel="stylesheet"> | | card | padding, shadow, borderWidth, maxWidth | --moonx-card-*, --moonx-gutter | | backdrop | color, blur | --moonx-backdrop-* |

borderRadius presets scale every radius together: "lg" is the default (8 / 12 / 20px, 24px card, pill buttons), "md" and "sm" are progressively squarer (with matching button radii), "none" is square. A single-length card.padding (e.g. "28px") sets the gutter that the header, content and sticky buttons all inset by; a multi-value shorthand is applied to the content as given.

Sizes are px, so a host stylesheet that changes the root font size doesn't rescale the modal, and the modal resets the element styles it uses (button, input, p, headings, label, …), so global host styles don't leak in.

Logos and other images

  • appearance.logo is a URL or { src, srcDark, height, align }. It shows on the login screen and on request screens (sign, send). A per-call uiOptions.logoUrl overrides it for that screen; the logo set in the dashboard is the fallback.
  • Any shape works. Wordmarks keep their proportions up to the card's width, and square marks stay square. height is clamped to 16–96px (default 48; request screens use 40).
  • Set srcDark if your logo is dark. Without it, a logo shown in dark mode gets a hairline outline in the text colour so a black mark doesn't vanish. Turn that off with the CSS variable --moonx-logo-on-dark-filter: none.
  • Broken images degrade quietly. If a logo fails to load, or isn't an http(s), relative, data:image/ or blob: URL, the screen renders without it (request screens fall back to an icon). dApp and wallet icons fall back to the dApp's initial or the chain logo, and they're fetched without a referrer.

Behaviour you get for free

  • One modal for every flow: switching screens keeps the same dialog, tweens its height and fades the new screen in; it animates out on close.
  • role="dialog" with focus kept inside, focus returned on close, and Esc / backdrop click doing what the close button does (except on code-entry and in-flight signing screens, where a stray press would lose work).
  • A bottom sheet on phones (≤480px) that respects the home-indicator inset.
  • prefers-reduced-motion is honoured.

Per-component overrides

For finer control, appearance.components styles individual surfaces — card, button, input — independently of the global tokens. button and input accept background, text, border (color), borderWidth, and radius; card accepts the same minus background (the card surface is the global backgroundColor). Anything you omit falls through to the global tokens / --moonx-* defaults. The header is styled via the typography tokens instead (appearance.typography.colorHeading, fontFamilyHeading, …).

<MoonXProvider
  config={{
    appearance: {
      components: {
        card:   { border: "#23233a", radius: "1rem" },
        button: { background: "#a78bfa", text: "#0b0b14", radius: "9999px" },
        input:  { background: "#15151f", border: "#23233a", borderWidth: "1px" },
      },
    },
  }}
>

Each field maps to a --moonx-<component>-* CSS variable (e.g. --moonx-card-bg, --moonx-button-border-color); radius reuses --moonx-radius-<component>. Overrides layer on top of the global tokens, so you can restyle just the card while everything else follows the theme.

EVM chain helpers

The package re-exports gas / RPC utilities that work with any viem Chain:

import {
  getEvmGasPrice,
  getEvmMaxPriorityFeePerGas,
  getEvmNonce,
  estimateEvmGas,
  estimateEvmGasReserve,
  getRpcUrl,
  getChainById,
  isChainSupported,
  validateChainConfig,
  // Unified-config resolution (also used internally by the hooks):
  resolveChainEntry,
  resolveRpcUrl,
  resolveWsUrl,
  normalizeChainKey,
  buildChainIndex,
  isEvmEntry,
} from "@moon-x/react-sdk";

Useful when you need a pre-flight gas reserve estimate, want to resolve a configured chain's RPC yourself, or are building a custom chain-picker.

React Native

For React Native apps, use @moon-x/react-native-sdk — same hook shape with a WebView-backed transport.

License

UNLICENSED. All rights reserved.