Catalog npm
PACKAGE EXPLORERView in registry

0xtrails

npm

SDK for Trails

sdktrailswallet
Versions collected98
Direct dependencies37From release 0.19.1
Last observed20 Oct, 17:54 UTC

README

Current package documentation

0xtrails

Trails SDK for embedding the widget and calling Trails hooks in React apps.

Install

npm install 0xtrails @tanstack/react-query viem

If you want Trails to use your existing wagmi setup, also install @0xtrails/adapter-wagmi and wagmi.

Standalone Widget

For widget-only usage, TrailsWidget does not require QueryClientProvider or TrailsProvider.

import { TrailsWidget } from '0xtrails'

const apiKey = 'YOUR_TRAILS_API_KEY'

export function App() {
  return <TrailsWidget apiKey={apiKey} renderInline />
}

When there is no parent wallet context and no adapters, the widget uses its built-in EVM runtime.

Hooks With TrailsProvider

Use TrailsProvider when you want to call Trails hooks directly.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { TrailsProvider, useAccountTotalBalanceUsd } from '0xtrails'

const apiKey = 'YOUR_TRAILS_API_KEY'
const queryClient = new QueryClient()

function BalancePreview() {
  const address = '0x0000000000000000000000000000000000000000'
  const totalBalance = useAccountTotalBalanceUsd(address)

  return <pre>{JSON.stringify(totalBalance.data, null, 2)}</pre>
}

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TrailsProvider config={{ trailsApiKey: apiKey }}>
        <BalancePreview />
      </TrailsProvider>
    </QueryClientProvider>
  )
}

Hooks

Hooks are for custom UI. Wrap your app with your existing wagmi and React Query providers, then add TrailsProvider.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { WagmiProvider } from "wagmi"
import { TrailsProvider } from "0xtrails"
import { wagmiConfig } from "./wagmi"

const queryClient = new QueryClient()

export function App() {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <TrailsProvider config={{ trailsApiKey: "<YOUR_TRAILS_API_KEY>" }}>
          <YourRoutes />
        </TrailsProvider>
      </QueryClientProvider>
    </WagmiProvider>
  )
}

Widget Modes

Import mode-specific wrappers from 0xtrails/widget.

Pay

Use Pay when your app knows the exact recipient, destination token, chain, and amount.

import { Pay } from "0xtrails/widget"

export function PayInvoice() {
  return (
    <Pay
      apiKey="<YOUR_TRAILS_API_KEY>"
      to={{
        recipient: "0xMerchantAddress",
        chain: "base",
        token: "USDC",
        amount: "49.99",
      }}
      from={{ chain: "arbitrum", token: "ETH" }}
      paymentMethod="CONNECTED_WALLET"
      payMessage="Invoice #INV-1001"
      buttonText="Pay now"
    />
  )
}

Fund

Use Fund to let a user add funds to a wallet or destination account. You can allow different payment methods, use a connected wallet, qr code transfers, exchanges, or fiat on ramp.

The user selects payment method in the widget UI

import { Fund } from "0xtrails/widget"

export function FundWallet() {
  return (
    <Fund
      apiKey="<YOUR_TRAILS_API_KEY>"
      to={{
        recipient: "0xUserWallet",
        chain: "base",
        token: "USDC",
      }}
      onFundingSuccess={(data) => console.log("Funded", data.sessionId)}
    />
  )
}

The host app pre-selects paymentMethod to credit card for funding crypto with 100 USD.

import { Fund } from "0xtrails/widget"

export function FundWallet() {
  return (
    <Fund
      apiKey="<YOUR_TRAILS_API_KEY>"
      paymentMethod="CREDIT_DEBIT_CARD"
      from={{
        currency="USD",
        amount="100"
      }}
      to={{
        recipient: "0xUserWallet",
        chain: "base",
        token: "USDC",
      }}
      onFundingSuccess={(data) => console.log("Funded", data.sessionId)}
    />
  )
}

Swap

Use Swap for same-chain and cross-chain token swaps.

import { Swap } from "0xtrails/widget"

export function SwapToBaseUsdc() {
  return (
    <Swap
      apiKey="<YOUR_TRAILS_API_KEY>"
      onSwapSuccess={(data) => console.log("Swap complete", data.sessionId)}
    />
  )
}

Earn

Use Earn for a built-in UI cross-chain experience for earning yield.

import { Earn } from "0xtrails/widget"

export function EarnUsdc() {
  return (
    <Earn
      apiKey="<YOUR_TRAILS_API_KEY>"
      buttonText="Deposit into a protocol"
      onEarnSuccess={(data) => console.log("Earn deposit", data.sessionId)}
    />
  )
}

Withdraw

Use Withdraw for crypto withdrawal flows. Let users withdraw and swap their assets to any token on any chain.

import { Withdraw } from "0xtrails/widget"

export function WithdrawToBase() {
  return (
    <Withdraw
      apiKey="<YOUR_TRAILS_API_KEY>"
      onWithdrawSuccess={(data) => console.log("Withdrawn", data.sessionId)}
    />
  )
}

Custom Labels

Every mode accepts a labels object that renames the widget's own copy. Each field is optional and falls back to the built-in string, so you can override one word or all four. A blank string counts as no override too — binding a field straight to a text input does the right thing when the box is cleared. A donation flow, for instance, is fund mode with the funding vocabulary swapped out:

import { Fund } from "0xtrails/widget"

export function Donate() {
  return (
    <Fund
      apiKey="<YOUR_TRAILS_API_KEY>"
      to={{ recipient: "0xReliefFund", chain: "polygon", token: "USDC" }}
      labels={{
        confirmButton: "Donate",
        formTitle: "Donate",
        methodsTitle: "Choose a donation method",
        tokensTitle: "Donate with any token",
      }}
    />
  )
}
FieldRenamesDefault
confirmButtonThe primary form button"Fund", "Pay", "Swap", "Deposit", "Withdraw", by mode
formTitleThe amount screen's title, and the crypto-transfer screens that continue it"Fund", "Pay", "Swap", "Withdraw", "Earn" by mode; "Deposit" on the transfer screens
methodsTitleThe payment method picker's title"Choose a funding method", "Choose a payment method" in pay and earn, "Payment methods" on the pay flow's own picker
tokensTitleThe title of every "pay with this" token listVaries by mode and screen — "Fund with any token", "Select From Token", "Select Token to Withdraw", …

formTitle deliberately follows the user into the crypto-transfer screens (the QR code and deposit-address steps), so a donation flow keeps saying "Donate" instead of switching to "Deposit" halfway through. The card on-ramp screens are left alone — they hand off to a third party and read that way by design.

tokensTitle covers the origin pickers only. Destination pickers ("Receive", "Select destination token") keep their own titles, since the token the user sends and the token the recipient gets are different questions.

Two things the overrides deliberately leave alone:

  • Transient button states. "Enter an amount", "Getting quote...", "Connect your wallet" and "Insufficient Balance" report what the widget is doing, not what the button does, so they keep their own copy.
  • Screens borrowed from another mode. Overrides apply only to the configured mode. Open the swap tab from a fund widget's header and the button still says "Swap" — a "Donate" label there would be wrong.

For the modal trigger button specifically, buttonText still wins over labels.confirmButton.

consentCheckbox puts a consent line directly above the primary button, with a link the user can open before they commit. Setting the prop is what makes the tick mandatory — there is no separate flag, because an optional consent checkbox is not a thing anyone wants.

import { Fund } from "0xtrails/widget"

export function DonateWithTerms() {
  return (
    <Fund
      apiKey="<YOUR_TRAILS_API_KEY>"
      to={{ recipient: "0xReliefFund", chain: "polygon", token: "USDC" }}
      labels={{ confirmButton: "Donate" }}
      consentCheckbox={{
        label:
          "I've read and agree to the " +
          '<a href="https://example.org/terms">Terms &amp; Conditions</a>',
        requiredMessage: "Please accept the Terms & Conditions to donate.",
      }}
    />
  )
}
FieldPurposeDefault
labelThe consent line, as inline HTML. Required.—
requiredMessageStanding notice raised by a press on an unticked boxnone — see below

A blank label removes the gate. Setting the prop is what makes the tick mandatory, so a whitespace-only label means no checkbox renders and the confirm button is not gated — the flow keeps working with the consent step silently gone. That is deliberate (there is nothing to render), but it is the failure mode to watch if you bind label to a CMS field or an i18n key that might not resolve. The SDK logs a warning when it happens.

The button stays live. While the box is unticked the confirm button keeps its normal enabled styling, so a ready quote still reads as ready. Pressing it does not submit: the row shakes, tints, and focus moves to the checkbox.

requiredMessage decides what a failed press leaves behind. Set it and the message stands under the checkbox until the box is ticked, so a user who missed the shake still has the reason on screen. Omit it and the shake and tint are the whole response — lighter, and it never changes the resting layout, but a second press teaches nothing the first did not. Either way the failed press moves focus to the checkbox, so assistive technology announces the unticked control.

label is sanitised. It is the one prop rendered as HTML, against a fixed allowlist:

  • Kept: a, b, strong, i, em, span, br. Every other tag is dropped rather than escaped, so an unsupported tag renders as clean text.
  • Every attribute is stripped. An anchor keeps its href if that href resolves to http(s) or mailto — which includes relative (/terms) and protocol-relative (//host/path) forms, emitted exactly as written. Anything else (javascript:, data:) is unwrapped to plain text so nothing inert looks clickable.
  • Surviving anchors are rewritten to target="_blank" rel="noopener noreferrer", so reading the terms never costs the user a filled-in quote.

This allowlist keeps a label from breaking the widget or injecting an element. It is not a validator for a URL you interpolate from somewhere else — if the href comes from a CMS, a query string or user input, check it yourself before it gets here.

Opening the link never ticks the box — the two are deliberately separate acts. If you would rather the link opened your own in-page modal, add a delegated click listener and call preventDefault(); the widget renders inside a shadow root, but its events are composed, so composedPath() sees the anchor.

Consent is per session and never persisted. A reload asks again, which is what makes the tick evidence of assent rather than a remembered preference.

Like labels, the checkbox belongs to the configured mode's screens. Open the swap tab from a fund widget's header and it does not follow.

useQuote

useQuote gives you a quote and an executable send function for a fully custom UI. Exactly one amount should be provided: from.amount or from.amountRaw for exact-input, or to.amount or to.amountRaw for exact-output. You can use "feeOptions" to allow paying gas in stablecoins.

import { useQuote, QuoteError } from "0xtrails"
import { useAccount, useWalletClient } from "wagmi"

export function CustomSwap() {
  const { address } = useAccount()
  const { data: walletClient } = useWalletClient()

  const {
    quote,
    send,
    isLoadingQuote,
    quoteError,
    quoteErrorPrettified,
    feeOptions,
    refetchQuote,
    abort,
  } = useQuote({
    from: {
      chain: "base",
      token: "USDC",
      amount: "10",
    },
    to: {
      chain: "arbitrum",
      token: "ETH",
      recipient: address,
    },
    onStatusUpdate: (states) => console.log("status", states),
  })

  if (quoteError instanceof QuoteError) {
    console.log("Quote failed with code", quoteError.code)
  }

  return (
    <section>
      {isLoadingQuote && <p>Loading quote...</p>}
      {quoteErrorPrettified && <p>{quoteErrorPrettified}</p>}

      {quote && (
        <>
          <p>
            Spend {quote.originAmountDisplay} for{" "}
            {quote.destinationAmountDisplay}
          </p>
          <p>Estimated time: {quote.completionEstimateDisplay}</p>
          <p>Fee options: {feeOptions.length}</p>
        </>
      )}

      <button disabled={!send} onClick={() => send?.()}>
        Send
      </button>
      <button onClick={() => refetchQuote()}>Refresh</button>
      <button onClick={() => abort()}>Abort</button>
    </section>
  )
}

For previewing a quote, without needing a connected wallet, pass walletAddress.

useTrailsSendTransaction

useTrailsSendTransaction is a destination-first transaction hook with Trails routing. It mirrors useQuote's from/to shape and can open the Trails widget modal when the user needs to choose the origin chain, token, or amount so you don't have to build this step.

Render TrailsHookModal once inside TrailsProvider so modal-driven sends have somewhere to mount.

Describe what the recipient should receive via to and let the user pick how to pay. Set to.amount for an exact-output send (the modal only needs origin token selection).

import { useTrailsSendTransaction } from "0xtrails"

export function RequestUsdcPayment() {
  const { sendTransaction, isPending } = useTrailsSendTransaction({
    receiptActionButtonText: "Back to app",
  })

  return (
    <>
      <button
        disabled={isPending}
        onClick={() =>
          sendTransaction({
            to: {
              chain: "polygon",
              token: "USDC", // or a contract address
              recipient: "0xMerchantAddress",
              amount: "25",
            },
          })
        }
      >
        Pay 25 USDC
      </button>
    </>
  )
}

Provide from ({ chain, token, amount }) when your app already knows the source asset to skip the modal. Use from.amountRaw/to.amountRaw when you already have smallest-unit amounts.

Composable Actions

Composable actions describe what should happen after funds arrive on the destination chain. They are resolved into destination calls by the SDK and can be used from both useQuote and useTrailsSendTransaction.

Available action builders include:

  • swap for destination-chain swaps.
  • lend and deposit for supported earn markets.
  • custom for bulding your own action.
  • assertCondition for runtime checks.
  • dynamic() to spend the intent wallet's runtime balance when value amounts are unknown.

Quote And Execute Actions

import {
  assertCondition,
  dynamic,
  lend,
  swap,
  useQuote,
} from "0xtrails"
import { useAccount, useWalletClient } from "wagmi"

export function QuoteActions() {
  const { address } = useAccount()
  const { data: walletClient } = useWalletClient()

  const actions = [
    // Check if we received enough USDC on the destination chain
    assertCondition({
      erc20Balance: { token: "USDC", gte: "100" },
    }),
    // Swap 100 USDC in WETH (defaults to Uniswap V3)
    swap({
      tokenIn: "USDC",
      tokenOut: "WETH",
      amountIn: "100",
      fee: "0.05"
    }),
    // Lend all WETH received from the swap 
    lend({
      marketId: "base-weth-aave-v3-lending",
      amount: dynamic(),
      receiverAddress: address,
    }),
  ]

  const { quote, send, isLoadingQuote } = useQuote({
    from: { chain: "base", token: "USDC" },
    to: { chain: "base", token: "USDC", amount: "100" },
    actions,
  })

  return (
    <button disabled={!send || isLoadingQuote} onClick={() => send?.()}>
      {quote ? "Execute recipe" : "Get quote"}
    </button>
  )
}

actions is mutually exclusive with to.calls and deprecated to.calldata.

Execute Actions With The Widget Modal

import { dynamic, erc20Utils, lend, swap, useTrailsSendTransaction } from "0xtrails"

export function WidgetActionRecipe({ userAddress }: { userAddress: `0x${string}` }) {
  const actions = [
    swap({
      tokenIn: "USDC",
      tokenOut: "USDT",
      amountIn: dynamic(),
      fee: "0.05",
    }),
    lend({
      marketId: "polygon-usdt-fluid-lending",
      amount: dynamic(),
      receiverAddress: userAddress,
    }),
  ]

  const { sendTransaction, isPending } = useTrailsSendTransaction({
    actions,
    receiptActionButtonText: "Done",
  })

  return (
    <>
      <button
        disabled={isPending}
        onClick={() =>
          sendTransaction({
            to: {
              chain: "polygon",
              token: erc20Utils.USDC.addressOn("polygon"),
              recipient: userAddress,
              amount: "50"
            },
          })
        }
      >
        Choose payment token
      </button>
    </>
  )
}

Raw Destination Calls

Use to.calls when you need low-level ABI-encoded calls instead of action builders.

import { buildApproveAndCall, buildCall, erc20Utils, useQuote } from "0xtrails"
import { useAccount } from "wagmi"
import { stakingAbi } from "./stakingAbi"

export function RawCallsQuote() {
  const { address } = useAccount()

  const stakingCall = buildCall({
    to: "0xStakingContract",
    data: {
      abi: stakingAbi,
      functionName: "stake",
      args: [1_000_000n],
    },
  })

  const calls = buildApproveAndCall({
    token: erc20Utils.USDC.addressOn("base"),
    call: stakingCall,
  })

  const { quote, send } = useQuote({
    from: { chain: "base", token: "USDC" },
    to: {
      chain: "base",
      token: "USDC",
      amount: "1",
      calls,
    },
  })

  return (
    <button disabled={!send} onClick={() => send?.()}>
      {quote ? "Stake USDC" : "Loading quote"}
    </button>
  )
}

Utilities

Common exports from 0xtrails:

  • getSupportedChains, useSupportedChains, and resolveChainId.
  • getChainInfo, getAllChains, and attemptSwitchChain.
  • useTokenList, useSupportedTokens, useTokenBalances, useTokenBalancesForMultipleAccounts, useAccountTotalBalanceUsd, useHasSufficientBalanceToken, and useHasSufficientBalanceUsd.
  • getTokenPrice, getTokenPrices, useTokenPrice, useTokenPrices
  • erc20Utils, buildCall, buildApproveAndCall, buildErc20Approve, and getAmountWithSlippage for token metadata, ERC-20 calls, approvals, and custom calldata.
  • uniswapV3 and sushiswapV3 for custom Uniswap V3 / SushiSwap V3 pool lookup, quote, and swap call construction.
  • useEarnProviders, useEarnMarkets, useEarnBalances, getEarnProviders, getEarnMarkets, and getEarnBalances.
  • useResolveActions, encodeDestinationForIntentProtocol, createHydratePayload, and encodeMulticallHydrateExecute for advanced destination-call and hydrate flows.
  • prepareSend, commitIntent, quoteIntent, sendOriginTransaction, getIntent, useGetIntent, pollIntentReceipt, and useIntentReceiptMonitor.
  • getAccountTransactionHistory, useAccountTransactionHistory, useIntentTransactionHistory, and getTxTimeDiff.
  • useFiatCurrencyList, useExchangeRate, getCurrencyInfo
  • getTrailsClient, useTrailsClient, TrailsClient
  • useIntentRecover, useIntentRecoverWithAddress, buildRefundTransactionWithSignature, and signPayload.

TrailsProvider mounts the built-in wallet runtime by default. Add config.adapters when Trails should use explicit wallet runtime adapters instead.

Use Trails with wagmi

If your app already uses wagmi, add @0xtrails/adapter-wagmi so Trails can share your wallet setup. Wagmi 3 is the primary integration target; wagmi 2 remains supported through the same adapter peer range.

For more options and wagmi 2/3 examples, see packages/wallet/adapters/wagmi/README.md, apps/examples/wagmi3, and apps/examples/wagmi2.

import { TrailsProvider, TrailsWidget } from '0xtrails'
import { wagmiAdapter } from '@0xtrails/adapter-wagmi'
import { WagmiProvider } from 'wagmi'

const apiKey = 'YOUR_TRAILS_API_KEY'
// Create wagmiConfig with your chains, transports, and connectors.
const adapters = [wagmiAdapter({ wagmiConfig })]

export function App() {
  return (
    <WagmiProvider config={wagmiConfig}>
      <TrailsProvider config={{ trailsApiKey: apiKey, adapters }}>
        <TrailsWidget renderInline />
      </TrailsProvider>
    </WagmiProvider>
  )
}

Use the same wagmiConfig instance for both WagmiProvider and wagmiAdapter(...) when Trails should bridge into the host-owned wagmi runtime. If there is no matching surrounding WagmiProvider, the adapter mounts its own runtime with the provided config.

Runtime ownership

Adapter runtime selection and lifecycle ownership are separate:

  • wagmi runtime selection is based on wagmiConfig identity
    • same config instance as the surrounding WagmiProvider → bridge to the host runtime
    • no matching surrounding provider → mount an adapter-owned runtime
  • managedBy is for composite adapter lists and controls lifecycle ownership in Trails
    • managedBy: 'host' means Trails can use the wallet but should not own host lifecycle actions like disconnect
    • managedBy: 'trails' means Trails owns that adapter's wallet lifecycle
const adapters = [
  { adapter: wagmiAdapter({ wagmiConfig: hostWagmiConfig }), managedBy: 'host' },
  { adapter: wagmiAdapter({ wagmiConfig: trailsWagmiConfig }), managedBy: 'trails' },
]

Development

Regenerate widget SVG components from a folder of raw SVGs with pnpm --dir packages/0xtrails icons:generate <source-dir>.

Rules

  • wagmi is optional and only needed for adapter-based integrations; wagmi 3 is the primary target and wagmi 2 remains supported for compatibility
  • hooks require both QueryClientProvider and TrailsProvider
  • adapters can live on TrailsProvider config.adapters or TrailsWidget adapters
  • do not provide adapters in both places at the same time
  • TrailsWidget adapters cannot be used inside an existing parent wallet context, including the default runtime mounted by TrailsProvider

Use Trails with Solana

Install @0xtrails/svm to enable Solana edge payments — users fund from a Solana wallet and the payment lands on any EVM chain.

npm install @0xtrails/svm

Pass svmAdapter alongside your EVM adapter. With no wallet option, it auto-detects Wallet Standard Solana wallets such as Phantom and Solflare:

import { TrailsWidget, evmAdapter } from '0xtrails'
import { svmAdapter } from '@0xtrails/svm'

const adapters = [
  evmAdapter({ wallets: [{ id: 'injected', name: 'MetaMask' }] }),
  svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }),
]

export function App() {
  return (
    <TrailsWidget
      apiKey="YOUR_TRAILS_API_KEY"
      adapters={adapters}
      fundOptions={{ fundMethodsList: ['connected-wallet'] }}
      renderInline
    />
  )
}

Solana funding is available through the connected-wallet funding path when a Solana adapter is configured. Supported origin tokens are SOL, USDC, USDT, and PYUSD.

With wagmi

Combine wagmiAdapter and svmAdapter when your app uses both EVM and Solana wallets:

import { wagmiAdapter } from '@0xtrails/adapter-wagmi'
import { svmAdapter } from '@0xtrails/svm'

const adapters = [
  wagmiAdapter({ wagmiConfig }),
  svmAdapter({ rpcUrls: ['https://api.mainnet-beta.solana.com'] }),
]

For more options and examples, see packages/0xtrails-svm/README.md.

License

Apache-2.0

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 98

Collected releases
VersionPublishedDirect dependenciesOpen version
0.19.1Registry latest7 Oct 202637
0.19.07 Oct 202637
0.18.616 Sept 202637
0.18.510 Sept 202637
0.18.41 Sept 202637
0.18.330 Aug 202637
0.18.219 Aug 202637
0.18.118 Aug 202637
0.18.018 Aug 202637
0.17.127 Jul 202636
0.17.021 Jul 202636
0.16.29 Jun 202641
0.16.14 Jun 202641
0.16.021 May 202641
0.0.0-canary-f34333b4e54352fe3e761ba2f3ffd4117d041fe620 May 202641
0.0.0-canary-3a59770274bcb6f3bebd5d1b93a2c92d1fc4edbd8 May 202642
0.15.17 May 202643
0.15.04 May 202643
0.0.0-canary-d5b980adf75ce5a288cc968588580fcad1540b7c29 Apr 202643
0.0.0-canary-5e6e9be4ef5eafc6cbf711872e5a63f41346be2529 Apr 202643

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 17:54 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:b2736a63eaf2223c4c9759233b8731760261e76791a5024c6194b94581f16412