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

@aori/mega-swap-widget

v0.1.14

Published

Embeddable cross-chain swap widget powered by Aori

Readme

@aori/mega-swap-widget

Embeddable cross-chain swap widget powered by Aori.

Install

npm install @aori/mega-swap-widget
# or
yarn add @aori/mega-swap-widget
# or
bun add @aori/mega-swap-widget

Peer Dependencies

| Package | Version | Required | | ------------------------------- | -------- | -------- | | wagmi | ^2 | Yes | | @wagmi/core | ^2 | Yes | | viem | ^2 | Yes | | @tanstack/react-query | ≥5 | Yes | | zustand | ^4 / ^5 | Yes | | abitype | ≥1 | Optional |

Quick Start

1. Configure the widget

import type { AoriSwapWidgetConfig } from '@aori/mega-swap-widget';

const config: AoriSwapWidgetConfig = {
  vtApiBaseUrl: '/api/vt',
  walletConnectProjectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  rpcOverrides: {
    1: '/api/rpc/1',
    10: '/api/rpc/10',
    8453: '/api/rpc/8453',
    42161: '/api/rpc/42161',
    // ...
  },
  theme: {
    mode: 'dark',
    dark: { /* color overrides */ },
    light: { /* color overrides */ },
  },
  // All token/chain fields are optional — omit to allow all supported tokens and chains.
  tokens: {
    defaultBase: { chainId: 1, address: '0x...' },
    defaultQuote: { chainId: 4326, address: '0x...' },
    supportedInputTokens: [
      { chainId: 1, address: '0x...' },
      // ...
    ],
    supportedOutputTokens: [
      { chainId: 4326, address: '0x...' },
      // ...
    ],
    supportedInputChains: [1, 10, 8453, 42161],
    supportedOutputChains: [4326],
    enabledChains: [1, 10, 56, 143, 4326, 8453, 42161],
    lockBase: false,
    lockQuote: false,
    disableInverting: true,
  },
  appearance: {
    widgetType: 'default',
    tokenDisplay: 'default',
    fillContainer: false,
    hideBorder: true,
    walletButtonEnabled: false,
  },
  settings: {
    defaultSlippage: 0.01,
  },
};

2. Import styles

import '@aori/mega-swap-widget/styles.css';

Add this import once in your app's entry point (e.g. _app.tsx, layout.tsx, or main.tsx). Without it, the widget will render unstyled.

3. Render

import { SwapWidget } from '@aori/mega-swap-widget';
import { ConnectButton, useConnectModal, useAccountModal } from '@rainbow-me/rainbowkit';

export default function App() {
  const { openConnectModal } = useConnectModal();
  const { openAccountModal } = useAccountModal();

  return (
    <div>
      <ConnectButton />
      <SwapWidget
        config={config}
        customWalletUI="provider"
        onRequestConnect={() => openConnectModal?.()}
        onRequestAccount={() => openAccountModal?.()}
      />
    </div>
  );
}

Wallet Connection

The widget expects your app to provide the wallet context (wagmi, RainbowKit, AppKit, etc.). A typical integration uses customWalletUI="provider" with onRequestConnect and onRequestAccount to wire the widget's connect/account buttons into your existing wallet modals — this is the recommended approach for most integrators.

Provider (recommended)

Wrap the widget inside your existing wallet provider. Pass onRequestConnect and onRequestAccount so the widget's built-in buttons open your modals. The widget inherits the wagmi context automatically.

import { useConnectModal, useAccountModal } from '@rainbow-me/rainbowkit';

function SwapPage() {
  const { openConnectModal } = useConnectModal();
  const { openAccountModal } = useAccountModal();

  return (
    <SwapWidget
      config={config}
      customWalletUI="provider"
      onRequestConnect={() => openConnectModal?.()}
      onRequestAccount={() => openAccountModal?.()}
    />
  );
}

This works the same way with AppKit or any other wagmi-compatible wallet kit — just swap the modal hooks.

Chain & RPC Configuration

Your wagmi config must include all Aori-supported chains or approvals, wrapping/unwrapping, and chain switching will fail silently.

Import wagmiChains and buildTransports from the widget package — these provide every supported chain with built-in RPC fallback (multiple public RPCs per chain, tried in order on error or rate-limit).

import { wagmiChains, buildTransports } from '@aori/mega-swap-widget';
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { aoriConfig } from './aori.config';

const wagmiConfig = getDefaultConfig({
  appName: 'My App',
  projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
  chains: wagmiChains,
  transports: buildTransports(aoriConfig.rpcOverrides),
  ssr: false,
});

There are two separate RPC paths in the widget:

| Path | Source | Used for | | --- | --- | --- | | Widget internal | rpcOverrides in AoriSwapWidgetConfig | Balance fetching, quote pricing | | Wagmi | buildTransports() in your wagmi config | Signing, sending txs, chain switching |

rpcOverrides in your aori.config only affects the widget's internal viem clients. Wagmi transports are configured separately via buildTransports() — any URLs passed there are client-side JavaScript and will be visible in the browser. Never pass private RPC URLs directly to buildTransports.

To keep private RPCs server-side for both paths, use the same relative proxy paths (e.g. /api/rpc/1) in your rpcOverrides and pass them to buildTransports as shown above. The browser hits your API route, which forwards to the real RPC with credentials from environment variables. See Server-Side Proxying for how to set up the proxy routes.

Server-Side Proxying

In production you almost certainly want to keep your API key and private RPC URLs off the client. The widget supports this via two config fields: vtApiBaseUrl and rpcOverrides. Both accept relative paths that hit your own backend routes, which then forward requests upstream with the real credentials attached.

API Key Proxy (vtApiBaseUrl)

Instead of passing apiKey in the client config, point vtApiBaseUrl at your own API route. The widget will send all transfer/quote requests there instead of directly to the Aori API.

Widget config:

const config: AoriSwapWidgetConfig = {
  vtApiBaseUrl: '/api/vt',
  // no apiKey needed — the server injects it
  // ...
};

Next.js API route (pages/api/vt/[...path].ts):

import type { NextApiRequest, NextApiResponse } from 'next';

const VT_UPSTREAM = 'https://transfer.layerzero-api.com/v1';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { path } = req.query;
  const segments = Array.isArray(path) ? path.join('/') : path ?? '';
  const queryString = new URL(req.url!, `http://${req.headers.host}`).search;
  const upstreamUrl = `${VT_UPSTREAM}/${segments}${queryString}`;

  const apiKey = process.env.VT_API_KEY;
  if (!apiKey) {
    return res.status(500).json({ error: 'VT_API_KEY not configured' });
  }

  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
    'x-api-key': apiKey,
  };

  const upstream = await fetch(upstreamUrl, {
    method: req.method,
    headers,
    body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined,
  });

  const contentType = upstream.headers.get('content-type') ?? 'application/json';
  res.setHeader('Content-Type', contentType);
  res.status(upstream.status);

  const body = await upstream.text();
  res.send(body);
}

Then set VT_API_KEY in your .env (or hosting provider's environment variables):

VT_API_KEY=your_aori_api_key

Private RPC Proxy (rpcOverrides)

The same pattern works for RPC endpoints. Point each chain's override at a local route that forwards JSON-RPC requests to your private RPC, keeping the URL and any auth tokens server-side.

Widget config:

const config: AoriSwapWidgetConfig = {
  vtApiBaseUrl: '/api/vt',
  rpcOverrides: {
    1: '/api/rpc/1',
    10: '/api/rpc/10',
    56: '/api/rpc/56',
    8453: '/api/rpc/8453',
    42161: '/api/rpc/42161',
    143: '/api/rpc/143',
    4326: '/api/rpc/4326',
  },
  // ...
};

Next.js API route (pages/api/rpc/[chainId].ts):

import type { NextApiRequest, NextApiResponse } from 'next';

const RPC_URLS: Record<string, string | undefined> = {
  '1': process.env.ETHEREUM_RPC_URL,
  '10': process.env.OPTIMISM_RPC_URL,
  '56': process.env.BSC_RPC_URL,
  '8453': process.env.BASE_RPC_URL,
  '42161': process.env.ARBITRUM_RPC_URL,
  '143': process.env.MONAD_RPC_URL,
  '4326': process.env.MEGAETH_RPC_URL,
};

function jsonRpcError(res: NextApiResponse, id: unknown, code: number, message: string) {
  return res.status(200).json({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const chainId = req.query.chainId as string;
  const rpcUrl = RPC_URLS[chainId];

  if (!rpcUrl) {
    return jsonRpcError(res, null, -32603, `RPC not configured for chain ${chainId}`);
  }

  try {
    const response = await fetch(rpcUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(req.body),
    });

    res.setHeader('Cache-Control', 'no-store');
    return res.status(200).json(await response.json());
  } catch (error) {
    return jsonRpcError(res, null, -32603, error instanceof Error ? error.message : 'RPC request failed');
  }
}

.env:

ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
OPTIMISM_RPC_URL=https://opt-mainnet.g.alchemy.com/v2/YOUR_KEY
BSC_RPC_URL=https://bsc-mainnet.g.alchemy.com/v2/YOUR_KEY
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
MONAD_RPC_URL=https://your-monad-rpc.example.com
MEGAETH_RPC_URL=https://your-megaeth-rpc.example.com

Not using Next.js? The same approach works with any backend — Express, Fastify, Cloudflare Workers, etc. The proxy just needs to forward the request body upstream and return the response. The widget doesn't care what's behind the route as long as it speaks the same protocol (REST for VT, JSON-RPC for RPCs).

Props

| Prop | Type | Default | Description | | -------------------- | ---------------------------------------- | -------------- | ------------------------------------------------ | | config | AoriSwapWidgetConfig | — | Widget configuration (required) | | customWalletUI | 'none' \| 'provider' | 'provider' | 'provider' wires into your wallet modals via callbacks; 'none' hides all wallet UI | | onRequestConnect | () => void | — | Open the host app's connect modal | | onRequestAccount | () => void | — | Open the host app's account modal | | onSwapSubmitted | (quoteId: string) => void | — | Fires immediately after the user's signing/transaction steps complete and the order is submitted to the Aori backend. quoteId is the VT quote ID for tracking. | | onSwapComplete | (data: SwapCompleteData) => void | — | Fires when polling confirms the swap is settled on-chain. Returns { quoteId, aoriOrderHash, explorerUrl, details } where details is the full Aori /data/details response. | | onBaseTokenChange | (token: Asset) => void | — | Callback when the base token changes | | onQuoteTokenChange | (token: Asset) => void | — | Callback when the quote token changes | | className | string | — | Additional CSS class on the root element |

Swap Lifecycle Hooks Example

Use onSwapSubmitted to know when the order hits the Aori backend, and onSwapComplete to get the full details once it settles on-chain:

<SwapWidget
  config={aoriConfig}
  onSwapSubmitted={(quoteId) => {
    // Fires immediately after the user signs and the order is submitted.
    // quoteId is the VT quote ID — use it to track the order or just wait for onSwapComplete.
  }}
  onSwapComplete={({ aoriOrderHash, explorerUrl, details }) => {
    // Fires when polling confirms the swap is settled on-chain.
    // details is the full response from https://api.aori.io/data/details/${aoriOrderHash}

    // Redirect to a confirmation page
    router.push(`/confirmation?order=${aoriOrderHash}`);

    // Or persist to your own tx history
    const getEventTime = (type: string) =>
      details.events.find(e => e.event === type)?.timestamp ?? null;

    addToHistory({
      orderHash: aoriOrderHash,
      explorerUrl,
      inputAmount: details.inputAmount,
      inputChain: details.inputChain,
      outputAmount: details.outputAmount,
      outputChain: details.outputChain,
      srcTx: details.srcTx,
      dstTx: details.dstTx,
      // individual event timestamps — use these to show status progression in your UI
      createdAt:   getEventTime('created'),
      receivedAt:  getEventTime('received'),
      completedAt: getEventTime('completed'),
      failedAt:    getEventTime('failed'),
      canceledAt: getEventTime('cancelled'),
    });
  }}
/>

The SwapCompleteData type:

interface SwapCompleteData {
  quoteId: string;        // VT quote ID (same value as onSwapSubmitted)
  aoriOrderHash: string;  // Aori order hash, parsed from explorerUrl
  explorerUrl: string;    // https://aoriscan.io/order/0x...
  details: AoriOrderDetails;
}

interface AoriOrderDetails {
  orderHash: string;
  offerer: string;
  recipient: string;
  inputToken: string;
  inputAmount: string;
  inputChain: string;
  inputTokenValueUsd: string;
  outputToken: string;
  outputAmount: string;
  outputChain: string;
  outputTokenValueUsd: string;
  startTime: number;
  endTime: number;
  timestamp: number;
  srcTx: string | null;
  dstTx: string | null;
  events: Array<{
    event: string;   // 'created' | 'received' | 'completed' | 'failed' | 'cancelled'
    timestamp: number;
  }>;
}

What You Can Build With These Hooks

The data from onSwapComplete is generic — you decide what to do with it. Some examples:

  • Tx history component — render a live list of swaps with status, amounts, and links
  • Confirmation page — redirect to a dedicated page after settlement with order details
  • Custom notification — show a toast, modal, or banner with the swap result
  • Analytics — send swap events to your own backend or analytics pipeline
  • Order detail page — use aoriOrderHash to link to or render a full order breakdown
  • Just the data — store it in state, context, or your DB and use it however your app needs

The hooks don't dictate any UI. They hand you the data at the right moment — you wire it into whatever component or flow makes sense for your app.

Building a Live Tx History

One common pattern — use both hooks together to maintain a live list. onSwapSubmitted adds a pending entry immediately, onSwapComplete updates it in-place once settled:

const [history, setHistory] = useState([]);

const addOrUpdate = (quoteId, patch) =>
  setHistory(prev => {
    const exists = prev.find(e => e.quoteId === quoteId);
    if (exists) return prev.map(e => e.quoteId === quoteId ? { ...e, ...patch } : e);
    return [...prev, { quoteId, status: 'pending', orderHash: null, ...patch }];
  });

<SwapWidget
  config={aoriConfig}
  onSwapSubmitted={(quoteId) => {
    addOrUpdate(quoteId, { status: 'pending' });
  }}
  onSwapComplete={({ quoteId, aoriOrderHash, explorerUrl, details }) => {
    const getEventTime = (type) =>
      details.events.find(e => e.event === type)?.timestamp ?? null;

    addOrUpdate(quoteId, {
      status: getEventTime('failed') ? 'failed' : 'completed',
      orderHash: aoriOrderHash,
      explorerUrl,
      inputAmount: details.inputAmount,
      inputChain: details.inputChain,
      outputAmount: details.outputAmount,
      outputChain: details.outputChain,
      srcTx: details.srcTx,
      dstTx: details.dstTx,
      createdAt:   getEventTime('created'),
      receivedAt:  getEventTime('received'),
      completedAt: getEventTime('completed'),
      failedAt:    getEventTime('failed'),
    });
  }}
/>

// Render history however you like
{history.map(tx => <TxRow key={tx.quoteId} tx={tx} />)}

This gives you a real-time list that:

  • Shows "pending" the instant the user submits
  • Updates to "completed" or "failed" once on-chain settlement confirms
  • Has all the data needed to render amounts, chains, tx links, and a status timeline

Fetching Order History Outside the Hook

The onSwapComplete hook only captures swaps from the current session. To load a wallet's full order history — on mount, for a tx history page, etc. — use the Aori API directly with the connected wallet address.

Query all orders for a wallet (/data/query):

const res = await fetch(
  `https://api.aori.io/data/query?offerer=[connected_wallet_address]&page=0&limit=100&sortBy=createdAt_desc`
);
const { orders, pagination } = await res.json();
// orders is an array of summarised order objects with srcTx, dstTx, amounts, chains, status

Get full details for a specific order (/data/details):

const res = await fetch(`https://api.aori.io/data/details/${orderHash}`);
const details = await res.json();
// Full event history, tx hashes, block numbers, timestamps

The recommended pattern for a tx history feature:

  • On swap completion — write to your own state/DB via onSwapComplete
  • On page load — fetch historical orders from /data/query?offerer=${address} to backfill swaps from past sessions
  • On detail view — fetch /data/details/${orderHash} for the full event timeline of a specific order

Configuration

See AoriSwapWidgetConfig for the full type. Only non-default values need to be specified.

| Section | Type | Description | | --- | --- | --- | | theme | { mode, light?, dark? } | Active color mode and optional light/dark theme overrides. Each theme controls colors, border radius, fonts, and shadows. | | tokens | { defaultBase?, defaultQuote?, lockBase?, lockQuote?, enabledChains?, disableInverting?, supportedInputTokens?, supportedOutputTokens? } | Default token pair, enabled chains, lock/invert settings, and per-side token whitelists. All fields are optional — when omitted, all supported tokens and chains are available. | | appearance | { widgetType?, tokenDisplay?, assetMenuVariant?, swapButtonVariant?, ... } | Layout variant (default, compact, horizontal, split), token display style, asset menu variant, quote loader, and other UI options. | | settings | { defaultSlippage? } | Default slippage tolerance. | | integrator | { id?, feeRecipient?, feeAmount? } | Integrator ID, fee recipient address, and fee amount for revenue attribution. | | vtApiBaseUrl | string | Base URL for the Aori transfer API. Use a relative path (e.g. '/api/vt') to proxy through your server and keep the API key off the client. | | rpcOverrides | Record<number, string \| string[]> | Override RPCs for the widget's internal viem clients. Accepts direct URLs or relative paths to your own proxy routes. |

Invertible Token Lists

When disableInverting is false (or omitted), users can flip the swap direction using the invert button. When they do, supportedInputTokens and supportedOutputTokens swap sides — meaning token list A becomes the output whitelist and token list B becomes the input whitelist.

This enables bidirectional bridge configurations with a single config. For example, a USDM stablecoin bridge:

tokens: {
  supportedInputTokens: [
    { chainId: 1, address: '0x...' },     // USDC Ethereum
    { chainId: 8453, address: '0x...' },   // USDC Base
    { chainId: 42161, address: '0x...' },  // USDC Arbitrum
    // ... other stablecoins
  ],
  supportedOutputTokens: [
    { chainId: 4326, address: '0x...' },   // USDM MegaETH
  ],
  supportedInputChains: [1, 8453, 42161],
  supportedOutputChains: [4326],
  disableInverting: false,
  lockBase: false,
  lockQuote: false,
}

In the default direction, the user picks a stablecoin as input and receives USDM. After inverting, the lists flip — USDM becomes the only input option and the stablecoins become the output options.

Dynamic locking: When a side's effective token list contains exactly one token, that side's token selection is automatically disabled and the dropdown icon is hidden. No need to set lockBase or lockQuote — locking is inferred from the list length and updates dynamically on invert.

Set disableInverting: true to prevent flipping and keep the lists fixed to their configured sides.

Display Patterns

The widget is a standard React component — place it anywhere in your layout.

Inline (default)

<SwapWidget config={config} />

Modal / Dialog

const [open, setOpen] = useState(false);

return (
  <>
    <button onClick={() => setOpen(true)}>Swap</button>
    {open && (
      <div
        className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
        onClick={() => setOpen(false)}
      >
        <div onClick={(e) => e.stopPropagation()}>
          <SwapWidget config={config} />
        </div>
      </div>
    )}
  </>
);

Drawer / Slide-in

const [open, setOpen] = useState(false);

return (
  <>
    <button onClick={() => setOpen(true)}>Swap</button>
    <div
      className={`fixed top-0 right-0 z-50 h-full w-[420px] bg-background shadow-xl
        transition-transform duration-300
        ${open ? 'translate-x-0' : 'translate-x-full'}`}
    >
      <button onClick={() => setOpen(false)}>Close</button>
      <SwapWidget config={config} />
    </div>
    {open && (
      <div
        className="fixed inset-0 z-40 bg-black/30"
        onClick={() => setOpen(false)}
      />
    )}
  </>
);

These are minimal examples. In production, use your app's existing modal/drawer components (Radix, shadcn, Headless UI, etc.) for accessibility and consistent behavior.

Resources

License

MIT