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

@epoch-protocol/epoch-intent-widget

v0.1.3

Published

Drop-in React widget for sending cross-chain intents via Epoch Protocol

Readme

@epoch-protocol/epoch-intent-widget

A drop-in React widget for sending cross-chain intents through the Epoch Protocol. Render one component, get a full payment / swap / yield UX: wallet connect, token picker, live quoting, signing, on-chain settlement polling, and progress UI — all themeable to match your app.

The widget covers three flows out of the box:

| Mode | What the user does | Typical use | | ------ | ---------------------------------------------------------------------------------- | ---------------------------------------- | | pay | Pays a fixed amount you specify, in any token they hold on any supported chain | Checkout, top-ups, "buy this", donations | | swap | Picks both the source and destination token — classic exchange UX | In-app swaps / bridging | | earn | Deposits into (or withdraws from) a lending market, sourced from 1delta | Yield / lending integrations |

Under the hood the widget consumes the headless @epoch-protocol/epoch-flows-sdk. If you want the business logic without the React UI, use that package directly — see Headless escape hatch.

Deeper docs:


Contents


Install

pnpm add @epoch-protocol/epoch-intent-widget

This package ships with peer dependencies you must already have (or install):

pnpm add react react-dom wagmi viem @tanstack/react-query lucide-react

| Peer | Range | Why | | ----------------------- | ------- | ------------------------------------------- | | react, react-dom | ^18 | The widget is a React component | | wagmi | ^2 | Wallet account + wallet client + connectors | | viem | ^2 | Signing, RPC reads, unit math | | @tanstack/react-query | ^5 | Required by wagmi v2 | | lucide-react | ^1.14 | Icons used inside the widget |


Prerequisites: providers & CSS

The widget assumes your app is already a wagmi app. You need three things in place before rendering it:

  1. A WagmiProvider with at least one connector configured (the widget shows its own connector picker when no wallet is connected — it reads whatever connectors you registered).
  2. A QueryClientProvider (wagmi v2 requires it).
  3. The widget stylesheet imported once at your app root.
// main.tsx
import ReactDOM from "react-dom/client";
import { WagmiProvider, http, createConfig } from "wagmi";
import { base, optimism, arbitrum, polygon, mainnet } from "wagmi/chains";
import { injected } from "wagmi/connectors";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

// 👇 Import the widget styles once. Without this the widget renders unstyled.
import "@epoch-protocol/epoch-intent-widget/styles.css";

import App from "./App";

const config = createConfig({
  chains: [mainnet, base, optimism, arbitrum, polygon],
  connectors: [injected()], // or RainbowKit / Web3Modal connectors
  transports: {
    [mainnet.id]: http(),
    [base.id]: http("https://mainnet.base.org"),
    [optimism.id]: http("https://mainnet.optimism.io"),
    [arbitrum.id]: http("https://arb1.arbitrum.io/rpc"),
    [polygon.id]: http("https://polygon.lava.build"),
  },
});

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </WagmiProvider>,
);

Wallet connect is built in. When the widget opens and no account is connected, it renders a connector list from your wagmi config. You don't need RainbowKit — but if you already use it, the widget will list those connectors too.

Theming portals: the widget renders its modal into a portal. If you theme via the theme prop you're covered automatically. If you build the surrounding UI against the same design tokens, project them onto :root with themeToCssVars.


Quick start

The simplest integration — send a fixed amount of USDC to an address. The user pays in whatever token they hold; Epoch routes it to the destination.

import { useState } from "react";
import { EpochIntentWidget } from "@epoch-protocol/epoch-intent-widget";

export default function PayButton() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Pay 0.15 USDC</button>

      <EpochIntentWidget
        isOpen={open}
        onClose={() => setOpen(false)}
        api={{ baseUrl: "https://your-allocator.example.com" }}
        mode="pay"
        title="Send USDC"
        submitButtonText="Send"
        toAddress="0x4235215114484bACDfF0071dB54Dc9faaD3489a9"
        toAmount="0.15"
        toChainId={8453}
        toToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        onSuccess={({ nonce }) => console.log("settled", nonce)}
      />
    </>
  );
}

That's the whole integration: render the component, control isOpen/onClose, point api.baseUrl at your Epoch allocator. The widget owns connect → balance → quote → sign → settle.


The api prop

api is the only required prop besides isOpen/onClose. It tells the widget where to talk to the network.

interface ApiConfig {
  /** Epoch allocator (smallocator) base URL. Required. */
  baseUrl: string;
  /** Per-chain RPC overrides for on-chain reads (balances, etc). Optional. */
  rpcUrls?: Record<number, string>;
  /** 1delta pools/positions proxy base URL. Required for live Earn data. */
  positionsBaseUrl?: string;
}
  • baseUrl — your Epoch allocator endpoint. All quote / solve / status calls go here.
  • rpcUrls — optional. By default the widget uses built-in public RPCs to read token balances; override per chain if you have your own nodes.
  • positionsBaseUrl — only needed for Earn. Points at the proxy that serves 1delta /pools and user-positions data. Omit it and Earn falls back to the bundled static market configs (good for demos).
api={{
  baseUrl: 'https://allocator.example.com',
  rpcUrls: { 8453: 'https://base-mainnet.my-node.com' },
  positionsBaseUrl: 'https://positions.example.com', // earn only
}}

Mode: Pay

mode="pay" (the default). You fix what is received and where; the user pays from any token/chain they hold. There are two ways to describe the payment.

Flat props (simplest)

Good for plain "send X token to an address" flows. The widget looks up token metadata from its built-in registry; pass toTokenDecimals / toTokenSymbol for tokens outside it.

<EpochIntentWidget
  isOpen={open}
  onClose={close}
  api={{ baseUrl }}
  mode="pay"
  toAddress="0x4235…89a9"
  toAmount="0.15" // decimal string (human units)
  toChainId={8453}
  toToken="0x8335…2913" // destination token address
  // toTokenSymbol="USDC"  // needed only for unknown tokens
  // toTokenDecimals={6}
/>

Nested intent (full control)

Use this when you need a custom protocol interaction (buy a raffle ticket, mint, deposit to a contract, etc.) rather than a plain transfer. requiredAmount is a bigint in atomic units.

<EpochIntentWidget
  isOpen={open}
  onClose={close}
  api={{ baseUrl }}
  title="Buy raffle ticket"
  submitButtonText="Buy ticket"
  intent={{
    requiredToken: {
      address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      symbol: "USDC",
      decimals: 6,
    },
    requiredAmount: 5_000_000n, // 5 USDC, atomic units
    destinationChainName: "Base",
    positionLabel: "1 Raffle Ticket", // shown in the summary
    config: {
      protocol: "raffles",
      action: "buyTicket",
      fixedOutput: true, // user pays whatever it costs to deliver exactly this
      destinationChainId: 8453,
      extraDataTypestring: "address raffleAddress,uint256 numberOfTickets",
      extraData: {
        raffleAddress: "0x0000000000000000000000000000000000000001",
        numberOfTickets: "1",
      },
    },
  }}
/>

See IntentConfig for every field. The demo's pay/scenarios.ts has copy-paste examples for both shapes.

Pinning vs. picking the destination

By default the destination token is pinned to what you passed. Set lockDestinationToken={false} to let the user re-pick the destination token/chain (the widget then overrides requiredToken + destinationChainId on submit).

Scoping the source side

sourceChainIds={[8453, 42161]}                     // restrict the source chain picker
defaultSourceChainId={8453}                         // pre-select a chain
defaultSourceTokenAddress="0x8335…2913"             // pre-select a token (needs defaultSourceChainId)
sourceTokenFilter={(t) => t.symbol !== 'DAI'}       // hide candidates by predicate

Mode: Swap

mode="swap" is a classic exchange UX — the user picks both sides. You provide an initial destination intent; lockDestinationToken is forced off internally so the user can always change what they receive.

<EpochIntentWidget
  isOpen={open}
  onClose={close}
  api={{ baseUrl }}
  mode="swap"
  title="Swap"
  usdPriceFor={({ symbol }) => (symbol === "USDC" ? 1 : null)} // optional "≈ $…" line
  intent={{
    requiredToken: {
      address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      symbol: "USDC",
      decimals: 6,
    },
    requiredAmount: 1_000_000n,
    destinationChainName: "Base",
    config: {
      protocol: "swap", // routes through DEX/bridge solvers, not a protocol interaction
      action: "swap",
      fixedOutput: true,
      destinationChainId: 8453,
    },
  }}
/>

Setting protocol: 'swap' (or 'transfer' / 'bridge') makes the solver treat it as a pure token-in → token-out route. Any other protocol string is treated as a custom protocol interaction.


Mode: Earn

mode="earn" renders a deposit/withdraw surface over lending markets. Markets come from 1delta. You have two data sources:

A. Static / bundled configs (zero backend). Forward the bundled 1delta configs and the picker works immediately — ideal for demos and getting started.

import {
  EpochIntentWidget,
  HARDCODED_ONEDELTA_CONFIGS,
} from "@epoch-protocol/epoch-intent-widget";

<EpochIntentWidget
  isOpen={open}
  onClose={close}
  api={{ baseUrl }}
  mode="earn"
  earnDefaultTab="deposit"
  earnMarketsSource={HARDCODED_ONEDELTA_CONFIGS}
  title="Earn"
/>;

B. Live data. Set api.positionsBaseUrl to your 1delta pools/positions proxy. The widget fetches /pools (one request per chain, in parallel) and the user's open positions for withdraw.

<EpochIntentWidget
  isOpen={open}
  onClose={close}
  api={{ baseUrl, positionsBaseUrl: "https://positions.example.com" }}
  mode="earn"
  earnChainIds={[1, 8453, 42161]} // chains to fan /pools over
  earnLenderFilter="AAVE_V3,MORPHO" // CSV of 1delta lender keys
  earnPoolsSortBy="totalDepositsUsd" // default
  earnPoolsSortDir="DESC" // default
  earnPoolsPerChain={100} // max rows per chain
/>

Useful Earn props: earnDefaultTab ('deposit'|'withdraw'), earnHideTabs, earnDepositDefaults / earnWithdrawDefaults (override the intent protocol/action/extraDataTypestring), earnSolverUrl.

Earn is mainnet-only — the 1delta upstream doesn't index testnet pools. Hide the Earn entry point when your app is in testnet mode.


Amounts, tokens & chains

Two amount conventions — don't mix them up:

| Prop | Type | Units | Example | | ----------------------- | -------- | ---------------------- | --------------------- | | toAmount (flat pay) | string | Human / decimal | "0.15" | | intent.requiredAmount | bigint | Atomic (smallest unit) | 5_000_000n (5 USDC) |

Built-in registry. The widget bundles common stablecoins + WETH across Ethereum, Base, Optimism, Polygon, Arbitrum (and Sepolia testnets). For flat-pay with a registry token you can omit toTokenSymbol/toTokenDecimals. For anything else, pass them.

You can read the registries yourself:

import {
  EPOCH_SUPPORTED_CHAINS,
  EPOCH_SUPPORTED_TOKENS,
  getEpochTokensByChainEnv,
  getChainName,
} from "@epoch-protocol/epoch-intent-widget";

IntentConfig

interface IntentConfig {
  protocol: string; // 'transfer' | 'swap' | 'bridge' → simple route; else protocol interaction
  action: string; // e.g. 'pay', 'swap', 'buyTicket', 'deposit'
  protocolHashIdentifier?: string; // override the keccak256(protocol) hash
  extraDataTypestring?: string; // ABI-style typestring for extraData fields
  extraData?: Record<string, string | boolean | number | bigint>;
  fixedOutput?: boolean; // true → "deliver exactly requiredAmount", user pays the quoted input
  destinationChainId?: number; // mainnet destination
  destinationTestnetChainId?: number; // testnet destination (used when network === 'testnet')
  slippageBps?: number; // output slippage tolerance, default 100 (1%); 0 = strict
}

Theming

Three layers, smallest-to-largest effort:

1. Preset

theme = "light"; // or "dark"

2. Token overrides (EpochTheme)

Override only the tokens you care about; the rest fall back to the light defaults. Tokens are projected onto --epoch-* CSS variables.

theme={{
  colorPrimary: '#16a34a',
  colorPrimaryHover: '#15803d',
  radiusLg: '16px',
  fontFamily: "'Inter', sans-serif",
}}

Common tokens: colorPrimary, colorPrimaryHover, colorBackground, colorSurface, colorBorder, colorTextPrimary, colorTextSecondary, colorTextMuted, colorSuccess, colorError, radiusSm/radiusMd/radiusLg, shadowMd, fontFamily. (See EpochTheme for the full list.)

3. Per-slot class names (classNames)

Take full CSS control of individual elements. Providing a className for a slot skips the widget's default inline styles for it — works with vanilla CSS, Tailwind, or CSS modules.

classNames={{
  container: 'bg-white shadow-2xl rounded-2xl max-w-md',
  button: 'bg-blue-600 hover:bg-blue-700 text-white rounded-xl py-3 font-semibold',
  overlay: 'backdrop-blur-sm',
}}

Slots: overlay, container, header, body, footer, receiveCard, receiveAmount, receiveLabel, payCard, payAmount, payLabel, button, chainSelector, tokenSelector, banner, progress.

Advanced: CSS variables & portals

If you build surrounding UI against the same tokens and need them on :root (so portalled modal children resolve them), project the theme yourself:

import {
  themeToCssVars,
  LIGHT_THEME,
} from "@epoch-protocol/epoch-intent-widget";

const vars = themeToCssVars(LIGHT_THEME) as Record<string, string>;
for (const [k, v] of Object.entries(vars)) {
  document.documentElement.style.setProperty(k, v);
}

CTA copy

Override button labels per state without touching styles:

ctaLabels={{
  submit: 'Confirm swap',
  quoting: 'Pricing…',
  signing: 'Approve in wallet',
  submitting: 'Sending intent…',
  polling: 'Settling on-chain…',
  complete: 'All done',
}}

Callbacks & lifecycle

The widget drives the whole flow and reports progress through callbacks.

<EpochIntentWidget
  // …
  onOpen={() => {}}
  onStart={({ sessionId, mode }) => {}} // user hit submit
  onSign={({ sessionId }) => {}} // signature requested
  onSuccess={({ sessionId, nonce, status }) => {}} // settled on-chain
  onError={({ sessionId, error }) => {}}
  onStatus={({ status, progress, activeStep }) => {}} // every transition
  onIntentSent={({ nonce }) => {}} // intent submitted, before settle
  onIntentComplete={({ nonce, status }) => {}} // settle finished
  onSourceTokenChange={({ chainId, tokenAddress }) => {}}
  onQuote={({ payAmount, payAmountRaw, error }) => {}} // fires on each quote (fixedOutput intents)
/>

Lifecycle status (onStatus.status):

idle → submitting → sent → polling → complete
                                  ↘ error

progress is 0–100; activeStep is the index into the widget's progress stepper. A successful flow auto-closes the modal after a short delay (onSuccess fires first).


Testnet

network = "testnet"; // default 'mainnet'
allowNetworkToggle; // optionally let the user flip in-widget

In testnet mode the widget uses the Sepolia chain/token registries (Base Sepolia 84532, Ethereum Sepolia 11155111, Optimism Sepolia 11155420) and reads intent.config.destinationTestnetChainId. Test USDC addresses: Base Sepolia 0x036CbD53842c5426634e7929541eC2318f3dCF7e, Optimism Sepolia 0x2BB4FfD7E2c6D432b697554Efd77fA13bdbefd69. Dummy-lending Earn markets are on Base Sepolia and Ethereum Sepolia; Optimism Sepolia is supported as a deposit source only (cross-chain into those markets). See Mode: Earn.


Full props reference

EpochIntentWidgetProps — only isOpen, onClose, and api are required.

| Prop | Type | Default | Notes | | ---------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------- | ------------------------------------------------------------------------ | | isOpen | boolean | — | Required. Controls visibility. | | onClose | () => void | — | Required. Dismiss handler. | | api | ApiConfig | — | Required. See The api prop. | | mode | 'pay' \| 'swap' \| 'earn' | 'pay' | Flow selector. | | flow | same | — | Legacy alias for mode. | | intent | IntentProps | — | Nested pay/swap intent. | | toAddress / toAmount / toChainId / toToken / toTokenDecimals / toTokenSymbol | flat | — | Flat-pay shorthand (alternative to intent). | | sourceChainIds | number[] | all | Restrict source chain picker. | | sourceTokenFilter | (t) => boolean | — | Hide source (chain, token) candidates. | | defaultSourceChainId | number | — | Pre-select a source chain. | | defaultSourceTokenAddress | `0x${string}` | — | Pre-select a source token (needs defaultSourceChainId). | | lockDestinationToken | boolean | true | Pay-only; false lets the user re-pick destination. Forced off in Swap. | | usdPriceFor | (t) => number \| null \| Promise<…> | — | Resolver for the "≈ $…" line. | | ctaLabels | Partial<{…}> | — | Per-state button copy. | | earnDefaultTab | 'deposit' \| 'withdraw' | 'deposit' | Earn starting tab. | | earnHideTabs | boolean | false | Hide deposit/withdraw tabs. | | earnMarketsSource | OneDeltaConfig[] | — | Bundled/static market configs. | | earnDepositDefaults / earnWithdrawDefaults | defaults | — | Override earn intent protocol/action/typestring. | | earnChainIds | number[] | [1,8453,42161,10,137] | Chains to fetch pools for (live mode). | | earnLenderFilter | string | — | CSV of 1delta lender keys. | | earnPoolsPerChain | number | 100 | Max rows per chain. | | earnPoolsSortBy | enum | totalDepositsUsd | Pool sort field. | | earnPoolsSortDir | 'ASC' \| 'DESC' | DESC | Pool sort direction. | | earnSolverUrl | string | — | Earn solver override. | | network | 'mainnet' \| 'testnet' | 'mainnet' | Active network env. | | allowNetworkToggle | boolean | false | Show in-widget network toggle. | | renderInline | boolean | false | Render inline instead of a modal overlay. | | theme | 'light' \| 'dark' \| EpochTheme | 'light' | See Theming. | | classNames | EpochClassNames | — | Per-slot class overrides. | | title / submitButtonText | string | — | Header + CTA copy. | | onOpen/onStart/onSign/onSuccess/onError/onStatus | callbacks | — | See Callbacks & lifecycle. | | onIntentSent/onIntentComplete | callbacks | — | Submit / settle payloads. | | onSourceTokenChange/onQuote | callbacks | — | Source selection + quote results. |

Deprecated: earnMarkets (use earnMarketsSource), earnUseMockData (no-op).


Exports

Beyond the widget itself, the package re-exports config, helpers, and design-system primitives:

import {
  EpochIntentWidget, // the component

  // Theme
  DEFAULT_THEME,
  LIGHT_THEME,
  DARK_THEME,
  resolveTheme,
  themeToCssVars,

  // Registries + helpers
  EPOCH_SUPPORTED_CHAINS,
  EPOCH_TESTNET_CHAINS,
  EPOCH_SUPPORTED_TOKENS,
  EPOCH_TESTNET_TOKENS,
  getEpochChains,
  getEpochChainById,
  getChainName,
  getEpochTokensByChainEnv,
  getEpochTokensBySymbol,

  // Earn data
  useEarnMarkets,
  useUserPositions,
  useEarnConfigs,
  useLendingPools,
  HARDCODED_ONEDELTA_CONFIGS,
  chainLabelFor,
  toEpochEarnMarket,
  flattenConfigsToMarkets,

  // Pure intent builders
  buildPayIntentFromFlatProps,
  buildEarnDepositIntent,
  buildEarnWithdrawIntent,

  // Utils
  formatAmount,
  truncateAddress,
  cn,

  // UI primitives (same design system)
  Card,
  Pill,
  TokenAvatar,
  Skeleton,
  Stat,
  SegmentedTabs,
  RowAccordion,
  SearchInput,
  FilterDropdown,
  TokenAmountCard,
} from "@epoch-protocol/epoch-intent-widget";

It also pass-through re-exports the headless SDK — see below.


Headless escape hatch

The widget is a UI layer over @epoch-protocol/epoch-flows-sdk. If you want to build your own UI but keep Epoch's intent-building, quoting, and settlement-polling logic, you don't need a separate install — the SDK is re-exported here:

import {
  EpochFlowsSDK,
  PaySession,
  EarnSession,
} from "@epoch-protocol/epoch-intent-widget";

…or depend on @epoch-protocol/epoch-flows-sdk directly. See that package's README for the full headless API.


Demo app

A runnable reference integration lives in demo/. It wires the widget into a Vite + wagmi + RainbowKit app with Pay / Swap / Earn surfaces and editable scenarios.

cd demo
pnpm install
pnpm dev

Best files to copy from:

| Path | Shows | | -------------------------------------------------------------- | ---------------------------------------------- | | demo/src/main.tsx | Provider setup + CSS import + theme projection | | demo/src/app/App.tsx | Rendering the widget + wiring api per mode | | demo/src/pay/scenarios.ts | Flat-pay & nested-intent prop examples | | demo/src/earn/earnMarkets.ts | Earn props with bundled configs |

Demo env vars: VITE_EPOCH_API_BASE_URL (allocator), VITE_POSITIONS_API_BASE_URL (1delta proxy), VITE_EARN_SOLVER_URL.


Troubleshooting

| Symptom | Cause / fix | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Widget renders unstyled | You didn't import '@epoch-protocol/epoch-intent-widget/styles.css' at your app root. | | "must wrap … in WagmiProvider" / no connectors listed | Missing WagmiProvider, or no connectors configured in your wagmi config. | | React Query errors on mount | Missing QueryClientProvider (wagmi v2 requires it). | | "Unknown destination token" on flat pay | Token isn't in the built-in registry — pass toTokenDecimals and toTokenSymbol. | | Earn picker is empty / shows static rows | Set api.positionsBaseUrl for live data; without it Earn uses bundled configs only. | | Quote comes back below the displayed amount | Expected for cross-chain/cross-token routes — tune intent.config.slippageBps (default 100 = 1%; 0 = strict). | | Modal theme tokens missing in portal | Project tokens onto :root with themeToCssVars (see Advanced). | | Wrong network / testnet tokens | Set network="testnet" and use destinationTestnetChainId in your intent config. |


License

MIT