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

@orbs-network/spot-react

v2.1.20

Published

Spot React Components - React components for Spot SDK

Readme

@orbs-network/spot-react

React SDK for building Swap, TWAP, Limit, Stop-Loss, and Take-Profit order interfaces on top of the Orbs Spot protocol.

spot-react owns Spot order state and exposes it through SpotProvider and focused named hooks. The host DEX remains responsible for swap-form state, wallet access, components, styling, translations, routing, and modal shells.

| I'm building… | Use… | | --- | --- | | A React order form | SpotProvider with Module.SWAP or an advanced module, then useExecution() | | A DEX router with an existing wallet flow | client.getQuote → client.prepareSwap → DEX signing → client.submitOrder → waitForOrder; no provider required | | A custom flow that needs SDK wallet orchestration | submitSwap for swaps; calculateOrder + executeOrder with client.prepareOrder for advanced orders |

Every path can import its SDK functions and types from spot-react alone. The underlying spot-ui package is framework-neutral and renders no components.

For the complete integration workflow, see the Spot React integration skill and the reference implementation.

Client and history resources are configured once per provider. History and cancellation consumers subscribe to configuration separately from live form quotes. Failed legacy-history requests remain eligible for retry, and cached legacy orders remain visible during an outage. Execution analytics belong to the provider's client.

Before You Start

The host owns partner discovery and chain selectors. Client initialization reads and validates the order-sink /config response directly and exposes failures through useClient() and clientErrorFallback. Analytics configuration also comes exclusively from this response. The demo's GitHub partner-list fetch is local to the web app and is not an SDK dependency.

Every DEX needs a member of the exported Partners enum and a server-side Orbs Spot configuration for its supported chains. The SDK fetches contract and adapter addresses from that configuration. If the DEX is not configured yet, contact @dTWAPSupportGroup before integrating.

RePermit configuration security

The /config endpoint is a security boundary. The SDK rejects configurations whose domain or order chain differs from the requested chain, and rejects zero or malformed RePermit and exchange-adapter addresses. It uses domain.verifyingContract as the ERC-20 approval spender and the v2 cancellation contract, and uses the returned adapter, reactor, and executor when constructing orders. The SDK does not independently verify deployed bytecode or contract identity, so deployments must use the trusted Orbs endpoint over TLS and contract-address changes require explicit approval from the protocol/security owner.

SpotProvider initializes the spot-ui client when a valid connected chain ID is available; the service determines partner/chain support. Client and history state are provider-scoped and require no host query provider. createClient returns a new client while sharing validated configuration templates and in-flight requests through a 60-second, 32-entry LRU cache. Each client receives a deep copy; failures are evicted. Analytics initialize after validation, without an appId or React effect. Nonce allocation is shared across clients in the loaded SDK instance.

The client owns configuration-dependent preparation, signing/approval/cancellation requests, submission, and history access. Package-level calculateOrder provides the shared form model. prepareOrder assigns fresh start, deadline, and nonce after wrapping/approval, immediately before signing. Swap execution first re-quotes: net output below the reviewed minimum rejects with INVALID_QUOTE and requires fresh review; otherwise the exact reviewed minimum remains signed.

Initialization is retried twice. After those retries, the provider keeps children mounted and renders clientErrorFallback alongside them. Supply that component to localize and style retryable initialization errors.

The authoritative shared model is available from useOrderForm().

Each mounted SpotProvider owns one stable store. Changing the module or token pair reapplies form defaults without recreating the partner/chain client, history cache, or cancellation state. If that scope changes during execution, the reset waits until the frozen execution reaches a definitive terminal outcome; unconfirmed attempts retain their snapshot until reconciliation. Changing partner or chain reconfigures only the resources whose keys actually changed.

The completed integration should:

  • keep tokens, typed amount, balances, USD prices, and quote state in the DEX's existing swap store or context;
  • provide the five required walletInteractions methods using the DEX's existing wallet stack, plus getTokenBalance for completed-wrap retries;
  • render TWAP, Limit, Stop-Loss, and Take-Profit entry points using DEX-native navigation and components;
  • use @orbs-network/swap-ui for the order review/progress flow inside the DEX's modal shell;
  • render order history and cancellation inside SpotProvider scope or through a context-preserving portal.

Installation

spot-react includes spot-ui as a dependency and re-exports all of its public functions and types. Install only spot-react for the SDK and hooks; add swap-ui when using its order progress components.

Install the latest Spot packages with the host application's package manager. Do not mix lockfiles.

npm install @orbs-network/spot-react@latest @orbs-network/swap-ui@latest
# or: pnpm add @orbs-network/spot-react@latest @orbs-network/swap-ui@latest
# or: yarn add @orbs-network/spot-react@latest @orbs-network/swap-ui@latest

If migrating from @orbs-network/twap-ui, remove it before installing the packages above.

Migrating from 1.x

Version 2 is a deliberately breaking, headless API. Replace the broad useSpot and useSwapExecution interfaces with focused hooks such as useOrderForm, useExecution, useSubmitButton, and useOrders. Provider values now use input/output terminology (inputToken, outputToken, inputBalanceRaw, and minTradeSizeUsd), and the host supplies wallet operations through walletInteractions. Debug query parameters and the isDev option are no longer supported.

@orbs-network/swap-ui is a recommended companion for the documented order progress modal, but it is not imported by spot-react and is not required for a headless or custom-progress integration.

Peer Dependency

The host application must provide React. Zustand is an internal runtime dependency of spot-react; integrators do not install or configure it.

| Package | Version | | --- | --- | | react | ^18 \|\| ^19 |

viem is not a dependency. Adapt the wallet library already used by the DEX through walletInteractions.

The published entry includes a "use client" directive, so it can be imported directly from a Next.js App Router client component.

Integration Model

Keep the DEX swap form as the source of truth. Pass the following adapted values directly to SpotProvider:

| Value | Expected shape | | --- | --- | | Selected tokens | Token objects with address, symbol, decimals, and optional logoUrl | | Wrapped native token | The connected chain's wrapped-native Token, supplied by the DEX | | Typed source amount | User-facing decimal string, for example "1.25" | | Quote output | Raw destination-token amount for the current typed input; swaps map quote.minimumAmountOut unchanged to minimumOutputAmountRaw | | Input balance | Raw integer string | | USD prices | USD value of exactly one whole source/destination token | | Chain and account | Values from the connected wallet/account state |

If several Spot components need the same DEX-owned values, expose a small DEX adapter context. Do not copy the swap state into a parallel Spot store, prop-drill long value lists, or create a hook whose only purpose is forwarding props to SpotProvider.

Memoize objects with useMemo and functions with useCallback. In particular, keep stable identities for selected tokens, wrappedNativeToken, marketQuote, walletInteractions, and callbacks.

Provider Setup

import { useMemo } from "react";
import {
  type ClientErrorFallbackProps,
  Module,
  Partners,
  SpotProvider,
  type Callbacks,
  type MarketQuote,
  type Token,
  type WalletInteractions,
} from "@orbs-network/spot-react";

function ClientErrorFallback({
  error,
  retry,
  isRetrying,
}: ClientErrorFallbackProps) {
  return (
    <div role="alert">
      <p>{getLocalizedErrorMessage(error)}</p>
      <button type="button" disabled={isRetrying} onClick={retry}>
        {isRetrying ? t("retrying") : t("retry")}
      </button>
    </div>
  );
}

function SpotOrderForm({ module }: { module: Module }) {
  // Read these from the DEX's existing swap and wallet state.
  const {
    account,
    chainId,
    inputCurrency,
    outputCurrency,
    wrappedNativeCurrency,
    inputBalanceRaw,
    inputAmountUi,
    quotedInputAmountUi,
    quotedOutputAmountRaw,
    isQuoteLoading,
    inputTokenUsdPrice,
    outputTokenUsdPrice,
    refetchBalances,
    dexWallet,
  } = useDexSpotAdapter();

  const inputToken = useMemo<Token | undefined>(() => {
    if (!inputCurrency) return undefined;
    return {
      address: inputCurrency.address,
      symbol: inputCurrency.symbol,
      decimals: inputCurrency.decimals,
      logoUrl: inputCurrency.logoUrl,
    };
  }, [inputCurrency]);

  const outputToken = useMemo<Token | undefined>(() => {
    if (!outputCurrency) return undefined;
    return {
      address: outputCurrency.address,
      symbol: outputCurrency.symbol,
      decimals: outputCurrency.decimals,
      logoUrl: outputCurrency.logoUrl,
    };
  }, [outputCurrency]);

  const wrappedNativeToken = useMemo<Token | undefined>(() => {
    if (!wrappedNativeCurrency) return undefined;
    return {
      address: wrappedNativeCurrency.address,
      symbol: wrappedNativeCurrency.symbol,
      decimals: wrappedNativeCurrency.decimals,
      logoUrl: wrappedNativeCurrency.logoUrl,
    };
  }, [wrappedNativeCurrency]);

  const marketQuote = useMemo<MarketQuote>(() => {
    const shouldQuote = Boolean(
      inputAmountUi && inputCurrency && outputCurrency,
    );
    const isStale = shouldQuote && inputAmountUi !== quotedInputAmountUi;
    const currentQuotedOutputAmountRaw =
      !shouldQuote || isStale ? undefined : quotedOutputAmountRaw;
    const isLoading = shouldQuote && (isStale || isQuoteLoading);

    return {
      quotedOutputAmountRaw: currentQuotedOutputAmountRaw,
      isLoading,
      noLiquidity:
        shouldQuote && !isLoading && !currentQuotedOutputAmountRaw,
    };
  }, [
    inputCurrency,
    isQuoteLoading,
    outputCurrency,
    quotedOutputAmountRaw,
    quotedInputAmountUi,
    inputAmountUi,
  ]);

  const walletInteractions = useMemo<WalletInteractions>(
    () => createWalletInteractions(dexWallet),
    [dexWallet],
  );

  const callbacks = useMemo<Callbacks>(
    () => ({
      onWrapSuccess: () => refetchBalances(),
      onOrderCreated: () => refetchBalances(),
      onOrderFilled: () => refetchBalances(),
      onOrdersProgressUpdate: () => refetchBalances(),
      onCancelOrderSuccess: () => refetchBalances(),
    }),
    [refetchBalances],
  );

  return (
    <SpotProvider
      partner={Partners.Quick}
      module={module}
      inputAmountUi={inputAmountUi}
      priceProtectionPercent={3}
      minTradeSizeUsd={5}
      marketQuote={marketQuote}
      walletInteractions={walletInteractions}
      chainId={chainId}
      account={account}
      inputToken={inputToken}
      outputToken={outputToken}
      wrappedNativeToken={wrappedNativeToken}
      inputBalanceRaw={inputBalanceRaw}
      inputTokenUsdPrice={inputTokenUsdPrice}
      outputTokenUsdPrice={outputTokenUsdPrice}
      callbacks={callbacks}
      clientErrorFallback={ClientErrorFallback}
      displayFeePercent={0.25}
    >
      <SpotFormContent />
    </SpotProvider>
  );
}

marketQuote.quotedOutputAmountRaw is the quote's raw output amount for the current inputAmountUi, not a standalone per-token price. For swaps, pass quote.netAmountOut and minimumOutputAmountRaw: quote.minimumAmountOut. The returned minimum is authoritative; do not apply slippage again. If the quote belongs to older inputs, omit both amounts and report isLoading: true until a current quote arrives.

spot-react has no network registry. Always pass the wrappedNativeToken prop from the DEX's own chain configuration; its value may be undefined only until a chain is known. It is used for native/wrapped pair detection, allowance checks, approval, and order preparation. Explorer links are also host-owned; callbacks and history values expose transaction hashes so the DEX can format links with its existing chain metadata.

Use the connected wallet chain as the source of truth. When it is absent or unsupported, the provider does not initialize a client and submission stays disabled. The DEX submit area should show its connect-wallet or switch-network control.

SpotProvider Props

| Prop | Type | Required | Description | | --- | --- | --- | --- | | partner | Partners | Yes | DEX partner enum | | module | Module | Yes | SWAP, TWAP, LIMIT, STOP_LOSS, or TAKE_PROFIT | | inputAmountUi | string | Yes | User-entered input-token amount | | priceProtectionPercent | number | Yes | Price Protection percentage for advanced orders; ignored for SWAP, which signs 500 bps oracle protection | | minTradeSizeUsd | number | Yes | Minimum individual trade size in USD | | marketQuote | MarketQuote | Yes | { quotedOutputAmountRaw?, minimumOutputAmountRaw?, isLoading?, noLiquidity? }; swaps use quote.netAmountOut and quote.minimumAmountOut unchanged | | walletInteractions | WalletInteractions | Yes | Five wallet methods implemented by the DEX | | chainId | number | No | Connected wallet chain ID | | account | Address | No | Connected wallet address | | inputToken | Token | No | Input token metadata | | outputToken | Token | No | Output token metadata | | wrappedNativeToken | Token \| undefined | Yes | Host-provided wrapped-native token; pass undefined only before a chain is known | | inputBalanceRaw | string \| undefined | Yes | Raw input-token balance; pass undefined while disconnected or loading | | inputTokenUsdPrice | string \| undefined | Yes | USD value of one whole input token; pass undefined while loading | | outputTokenUsdPrice | string | No | USD value of one whole output token | | callbacks | Callbacks | No | Lifecycle and field-change callbacks | | displayFeePercent | number | No | Display-only fee estimate percentage; does not collect or subtract fees | | supportLegacyOrders | boolean | No | Include supported legacy v1 orders in history | | clientErrorFallback | ComponentType<ClientErrorFallbackProps> | No | Host-rendered client initialization error UI with error, retry, and isRetrying | | errorFallback | ComponentType<SpotErrorFallbackProps> | No | Host-rendered fallback for unexpected calculation or rendering errors |

inputBalanceRaw and inputTokenUsdPrice are required provider props; pass undefined only while their values are genuinely unavailable. The output-token USD price remains optional because it is used for display details rather than submission readiness. These required keys make host ownership explicit while allowing disconnected or loading states; use the same pattern for wrappedNativeToken before a chain is known.

minTradeSizeUsd must be a positive USD threshold approved for the partner; there is intentionally no SDK default. priceProtectionPercent uses percentage units, so 3 means 3% (300 bps); swaps ignore it and sign 500 bps (5%) oracle protection independently of the UI minimum. displayFeePercent only populates form.fees for the review UI. Protocol fee collection is configured separately by the partner and backend.

WalletInteractions

WalletInteractions extends the SDK's OrderWalletInteractions with cancelOrder; one wallet adapter can support both SDK orchestration and React history. The ApproveTokenProps, GetAllowanceProps, and CancelOrderProps aliases remain available for existing adapters.

The DEX must implement the five required methods. Add getTokenBalance when retrying an attempt with completed native-wrap metadata. Write methods must wait for the transaction receipt, throw when the transaction reverts, and then return the transaction hash.

const walletInteractions: WalletInteractions = {
  wrapNativeToken: async (amountWei) => {
    const txHash = await dexWallet.wrapNativeToken(amountWei);
    await dexWallet.waitForReceipt(txHash);
    return txHash;
  },

  approveToken: async ({ tokenAddress, amount, spenderAddress }) => {
    const txHash = await dexWallet.approveToken({
      tokenAddress,
      amount,
      spenderAddress,
    });
    await dexWallet.waitForReceipt(txHash);
    return txHash;
  },

  cancelOrder: async ({ order, contractAddress, args, abi }) => {
    const txHash = await dexWallet.writeContract({
      address: contractAddress,
      abi,
      functionName: "cancel",
      args,
    });
    await dexWallet.waitForReceipt(txHash);
    return txHash;
  },

  signOrder: ({ signerAddress, typedData }) =>
    dexWallet.signTypedData({ ...typedData, account: signerAddress }),

  getAllowance: ({ tokenAddress, spenderAddress }) =>
    dexWallet.getAllowance({ tokenAddress, spenderAddress }),

  getTokenBalance: ({ tokenAddress }) =>
    dexWallet.getTokenBalance({ tokenAddress }),
};

| Method | Contract | | --- | --- | | wrapNativeToken(amountWei) | Wrap native currency, wait for confirmation, return tx hash | | approveToken({ tokenAddress, amount, spenderAddress }) | Approve the requested spender, wait for confirmation, return tx hash | | cancelOrder({ order, contractAddress, args, abi }) | Call cancel with the supplied ABI and args, wait for confirmation, return tx hash | | signOrder({ signerAddress, typedData }) | Adapt the framework-neutral EIP-712 request to the host wallet and return its original 0x-prefixed signature | | getAllowance({ tokenAddress, spenderAddress }) | Return the connected account's raw allowance as a string | | getTokenBalance({ tokenAddress }) | Optional; return the captured account's current raw ERC-20 balance as a string. Required when reusing completed wrap metadata |

The signature returned by signOrder is submitted unchanged. Do not split it into { v, r, s }, rewrite its recovery byte, or otherwise normalize the wallet's byte representation. On a native-input retry, a previous wrap receipt alone does not prove the tokens are still available. The executor checks the current wrapped-token balance and wraps only a shortfall. onWrapSuccess.amount reports the amount actually wrapped in that step.

Building with focused hooks

Import only the hooks each component needs. Leaf components should call the smallest hook that provides what they render:

import {
  useExecution,
  useLimitPrice,
  useOrderForm,
  useOrders,
  useOutputAmount,
  usePriceDisplay,
  useTrades,
} from "@orbs-network/spot-react";

const calculatedForm = useOrderForm();
const tradesPanel = useTrades();
const limitPricePanel = useLimitPrice();
const outputAmount = useOutputAmount();

const execution = useExecution();
const history = useOrders();

useOrderForm() reads the calculated form shared by OrderFormProvider; it does not rerun the calculation. The other hooks expose smaller panel contracts for clearer component code. They currently share the same calculated-form context, so a form update can rerender every mounted form-panel hook.

| Value | Key returns | | --- | --- | | useOutputAmount() | amount: { raw, ui, usd }, isLoading | | useTrades() | totalTrades, maxTrades, onChange, error, structured input/min-output/trigger-output amounts, input/output tokens | | useDuration() | duration, input/unit callbacks, milliseconds, error | | useFillDelay() | fillDelay, input/unit callbacks, milliseconds, error | | useLimitPrice() | price: { raw, ui, usd }, canonical raw price, percentage, enable/toggle actions, tokens, loading/error state | | useTriggerPrice() | price: { raw, ui, usd }, canonical raw price, percentage, structured output-per-trade amount, tokens, loading/error state | | usePriceDisplay() | Inversion state/callback, actual and display-direction tokens, market-order state | | useDisclaimer() | Disclaimer translation key or undefined | | useInputErrors() | { type, args } or undefined | | useSubmitButton() | disabled and loading | | useExecution() | Submission/recovery, status, steps, errors, retained order/account, resets, resolved tokens, and tx hashes | | useOrders() | Provider-scoped categorized order lists and polling state | | useHistoryOrder() | Display-ready values for one history order | | useCancelOrder() | Per-order cancellation state and action | | useClient() | Provider-scoped initialized client state | | useAmountUi() | Raw-token amount formatting |

Child components should call the relevant focused hook themselves instead of receiving hook-returned panels through intermediate props. useOrders() activates order-history fetching while an orders consumer is mounted; form-only integrations do not fetch or poll history.

Keep the submit button disabled while loading is true. If client initialization fails, SpotProvider keeps child components mounted and renders the retryable clientErrorFallback alongside them.

For Module.SWAP, a missing or invalid minimum reports InputErrors.INVALID_SWAP_MINIMUM through useInputErrors() once the other required inputs are ready. Render and translate this error; useSubmitButton() then disables submission without showing an indefinite quote-loading spinner.

Trade-count validation behavior

An explicitly selected TWAP trade count persists when inputAmountUi changes. If lowering the amount makes that count greater than the newly calculated maxTrades, Spot does not clamp or reset it: useTrades().error and useInputErrors() report InputErrors.MAX_TRADES, and submission remains disabled until the user selects a valid count. This is an intentional behavior change from integrations that silently reset the trade count on every amount edit. Hosts should render the returned validation error so the user can correct the value.

Panel Visibility

| Panel | TWAP | LIMIT | STOP_LOSS | TAKE_PROFIT | | --- | --- | --- | --- | --- | | Trades amount | Yes | — | — | — | | Fill delay | Yes | — | — | — | | Duration | — | Yes | Yes | Yes | | Limit price | Yes, optional | Always on | Optional | Optional | | Trigger price | — | — | Yes | Yes |

Use TimeUnit.Minutes, TimeUnit.Hours, and TimeUnit.Days for duration and fill-delay controls. Resolve error and disclaimer keys through the DEX's i18n system.

Display Amounts

Every calculated amount uses the same { raw, ui, usd } shape. Convert raw into the DEX's native amount type before display when possible:

const amount = CurrencyAmount.fromRawAmount(
  inputCurrency,
  tradesPanel.inputAmountPerTrade.raw,
);

return `${amount.toSignificant()} ${inputCurrency.symbol}`;

Use .ui for editable text inputs or when the DEX has no amount object. Do not format .raw integer strings directly for user display.

Submit and Progress Modal

Use useExecution() for execution state, useOrderForm() for review details, and @orbs-network/swap-ui's SwapFlow for the creation/progress UI.

import { SwapFlow, SwapStatus as SwapUiStatus } from "@orbs-network/swap-ui";
import {
  ExecutionStatus,
  Module,
  useExecution,
  useOrderForm,
} from "@orbs-network/spot-react";

function SpotOrderFlow() {
  const {
    status,
    error,
    inputToken,
    outputToken,
    currentStepIndex,
    totalSteps,
    isUnconfirmed,
    resumeTracking,
    orderId,
  } = useExecution();
  const form = useOrderForm();
  if (isUnconfirmed) {
    return (
      <div role="status">
        <p>{error?.message || "Order status needs confirmation."}</p>
        {orderId && <p>Order: {orderId}</p>}
        <button type="button" onClick={resumeTracking}>Check order status</button>
      </div>
    );
  }
  const swapStatus =
    status === ExecutionStatus.SUCCESS
      ? SwapUiStatus.SUCCESS
      : status === ExecutionStatus.FAILED
        ? SwapUiStatus.FAILED
        : status === ExecutionStatus.LOADING
          ? SwapUiStatus.LOADING
          : undefined;

  return (
    <SwapFlow
      inAmount={form.inputAmount.ui}
      outAmount={form.outputAmount.ui}
      inToken={{ symbol: inputToken?.symbol, logoUrl: inputToken?.logoUrl }}
      outToken={{ symbol: outputToken?.symbol, logoUrl: outputToken?.logoUrl }}
      swapStatus={swapStatus}
      currentStepIndex={currentStepIndex}
      totalSteps={totalSteps}
      components={{
        Main: <SwapFlow.Main inUsd={form.inputAmount.usd} outUsd={form.outputAmount.usd} />,
        Success: <SwapFlow.Success title={form.module === Module.SWAP ? "Swap completed" : "Order created"} />,
        Failed: <SwapFlow.Failed error={error?.message} />,
      }}
    />
  );
}

Wrap SwapFlow in the DEX's modal shell and skin it with DEX colors, surfaces, typography, token logos, and loaders. Once useExecution().status is set, hide the review details, confirm button, duplicate title, and secondary footer actions so the progress/success/failure content owns the modal.

Execution State and Reset

useExecution() returns:

  • submitOrder, resumeTracking, phase, status, isExecuting, isUnconfirmed, isSuccess, isFailed, isRejected, isPreparingOrder, and canDismiss;
  • currentStep, currentStepIndex, totalSteps, and executionSteps;
  • error, order, orderId, account, inputToken, outputToken, chainId, wrapTxHash, and approvalTxHash;
  • returnToOrderForm() and startNewOrder().

spot-react does not clear the DEX input. Do that only when a successful modal closes:

const { status, isSuccess, isUnconfirmed, canDismiss, returnToOrderForm, startNewOrder } =
  useExecution();

const onClose = useCallback(() => {
  if (!canDismiss) return;
  setIsModalOpen(false);
  if (isUnconfirmed) return;

  if (isSuccess) {
    setInputAmount("");
    setTimeout(startNewOrder, 500);
  } else if (status) {
    setTimeout(returnToOrderForm, 500);
  }
}, [canDismiss, isSuccess, isUnconfirmed, returnToOrderForm, setInputAmount, startNewOrder, status]);

isExecuting covers preparation, wrapping, approval, signing, submission, and active swap settlement tracking. isSuccess becomes true only after a swap fills; other modules report success after submission. Use phase === ExecutionPhase.SETTLING for settlement-specific messages.

Ambiguous submission, stopped tracking, and unconfirmed settlement use ExecutionPhase.UNCONFIRMED. In that phase, isUnconfirmed and canDismiss are true and isExecuting is false. The order snapshot, captured account/chain, and available order ID remain in execution state. Closing the modal preserves them. Fresh submissions and both reset actions remain blocked until reconciliation. Render a recovery action before mapping the coarse status to generic failure UI.

resumeTracking() checks the existing order without signing again. When submission acceptance was ambiguous, it can discover the exact previously prepared payload in the original account and chain's history. For swaps, a confirmed fill becomes SUCCESS; a confirmed cancellation or expiry becomes FAILED and permits a new attempt. Advanced order creation succeeds once acceptance is confirmed. Missing/indexing results and network failures remain UNCONFIRMED.

Submission uses one POST with an optional lifecycle signal and a fixed 30-second timeout covering fetch and body reads. Pre-abort prevents the POST; abort/timeout after sending leaves acceptance uncertain and enters recovery. No POST is retried automatically. Settlement normally polls every two seconds; errors/missing snapshots back off at 2, 4, 8, 16, then 30 seconds and reset after a matching snapshot. Unknown statuses stay open even past the deadline. Service statuses expired, failed, and partially_completed become expired only after the signed deadline has passed, with completion and cancellation taking precedence.

Both reset actions return false during active or unconfirmed execution. Keep the modal open while isExecuting. returnToOrderForm() dismisses a definitive failure or rejection while preserving the form and completed-wrap metadata; reusing that metadata requires getTokenBalance to verify funds again. startNewOrder() resets Spot's internal form and retry metadata after a confirmed terminal outcome. The host still clears its own input only after success. The short delay lets the close animation finish before state resets.

phase is the precise execution state: idle → preparing → wrapping → approving → signing → submitting, followed by settling → success for swaps or immediate success for advanced order creation. Wrapping and approval are skipped when unnecessary. Definitive failures and wallet rejections use failed and rejected; uncertain submission or settlement uses unconfirmed. status is the coarser ExecutionStatus value. Map it explicitly to the separately versioned @orbs-network/swap-ui SwapStatus enum as shown above. Each attempt has an internal identity, duplicate starts are rejected atomically, and stale async writes cannot replace a newer attempt. Host callbacks are observational: synchronous throws and rejected callback promises never change a wallet or API result.

Once an attempt begins, useOrderForm() exposes its frozen form and useExecution() exposes its captured tokens, account, and chain. Preparation runs after wrapping and approval; the resulting payload is retained through signing, submission, and recovery. Host quote, token, chain, or time updates cannot replace an unresolved order.

error is a ParsedError: code retains string SDK classifications or numeric wallet codes, with 0 as the fallback. Optional reason, orderId, HTTP status, and serviceCode preserve settlement and service details for host error UI.

Callbacks

Callbacks cover wallet notifications, analytics, field synchronization, and balance refresh:

const callbacks: Callbacks = {
  onWrapRequest: () => {},
  onWrapSuccess: ({ txHash, amount }) => {},
  onApproveRequest: () => {},
  onApproveSuccess: ({ txHash, token, amount }) => {},
  onSignOrderRequest: () => {},
  onSignOrderSuccess: (signature) => {},
  onSignOrderError: (error) => {},
  onOrderCreated: (order) => {},
  onOrderFilled: (order) => {},
  onOrdersProgressUpdate: (orders) => {},
  onSubmitOrderFailed: ({ code, message }) => {},
  onSubmitOrderRejected: () => {},
  onCancelOrderRequest: (order) => {},
  onCancelOrderSuccess: ({ order, txHash }) => {},
  onCancelOrderFailed: (error) => {},
  onCopy: () => {},
  onLimitPriceChange: (price) => {},
  onLimitPricePercentChange: (percent) => {},
  onTriggerPriceChange: (price) => {},
  onTriggerPricePercentChange: (percent) => {},
  onOrderDurationChange: (orderDuration) => {},
  onTradeIntervalChange: (tradeInterval) => {},
  onTradeCountChange: (tradeCount) => {},
};

Refetch balances in onWrapSuccess, onOrderCreated, onOrderFilled, onOrdersProgressUpdate, and onCancelOrderSuccess. Avoid an onOrderCreated toast unless the host DEX specifically wants one; the submit modal already shows creation success.

Cancellation and Order History

Use the exported cancellation hook inside SpotProvider:

import { OrderStatus, useCancelOrder } from "@orbs-network/spot-react";

function CancelButton({ order }) {
  const { cancelOrder, disabled, isLoading, isSuccess, isError, error, txHash } =
    useCancelOrder(order);

  if (order.status !== OrderStatus.Open) return null;

  return (
    <button onClick={cancelOrder} disabled={disabled || isLoading}>
      {isLoading ? "Cancelling..." : "Cancel"}
    </button>
  );
}

Cancellation uses the initialized client for both v1 and v2 orders, keeping request construction on one path; disabled remains true while that client is loading or unavailable. History starts after client initialization because v2 requests need the configured exchange adapter. When supportLegacyOrders is enabled, v1 history is loaded once and retained while v2 history continues polling.

Order history is available directly from useOrders(). The query is enabled only while a component using this hook is mounted:

const { data: orders, isLoading, isRefetching, refetch } = useOrders();

// orders?.all, orders?.open, orders?.completed, orders?.cancelled, orders?.expired

Like useClient(), data is undefined before the first result. refetch() resolves to the latest categorized orders or undefined when no request can be made. Default the category you render to an empty array, for example const allOrders = orders?.all ?? [].

Use useHistoryOrder(order, inputToken?, outputToken?) for display fields. For large histories, use the virtualization library already present in the DEX for both the orders and fills lists. Store the selected order.historyKey, then look up the current order from orders?.all ?? [] by that key; do not store a stale copy of the order object. The key avoids collisions between numeric v1 IDs from different TWAP contracts; order.id remains the protocol ID used for display and cancellation.

Amounts returned by useHistoryOrder and its fills use { raw, ui }, matching the form model without fabricating unavailable historical USD values.

OrderFilter contains ALL, OPEN, COMPLETED, CANCELLED, and EXPIRED. OrderType identifies the calculated module/mode combination: limit, TWAP market/limit, stop-loss market/limit, or take-profit market/limit. Normalized Order and OrderFill retain src/dst and in/out names because those are protocol/history response models; form-facing APIs use input/output.

Keep history, details, and modal portals under SpotProvider context. Details should include execution summary, order info, fills, explorer/copy actions, and cancellation for open orders.

Translations

The SDK returns keys rather than final user-facing strings.

Disclaimer keys are:

  • limitOrderDisclaimer
  • marketOrderDisclaimer
  • triggerMarketPriceDisclaimer

Input errors have the shape { type, args }. Resolve type through the DEX's i18n system and interpolate args, including values such as maxTrades, minTrades, minTradeSize, duration, and fillDelay. Current duration and fill-delay arguments are human-readable; custom or older integrations should convert raw milliseconds before display.

React helper APIs

import {
  useAmountUi,
  useClient,
} from "@orbs-network/spot-react";

useAmountUi(decimals, rawAmount);
useClient();

Submit through useExecution(). Internal mutations and execution-store hooks are deliberately not exported, keeping one supported workflow and one error contract. useClient() must be called under SpotProvider; every caller shares the same partner/chain query, and RePermit configuration is available as client?.rePermitData. Initialization recovery belongs to clientErrorFallback, which receives the query error and retry action.

Public utilities and constants include:

import {
  getOrderExecutionRate,
  getOrderFillDelayMillis,
  getOrderLimitPriceRate,
  getTwapConfig,
  calculateOrder,
  toAmountRaw,
  toAmountUI,
  invertPriceInput,
  getTriggerPriceRate,
  eqIgnoreCase,
  isNativeAddress,
  DISCLAIMER_URL,
  ORBS_TWAP_FAQ_URL,
  ORBS_SLTP_FAQ_URL,
  ORBS_LOGO,
  ORBS_WEBSITE_URL,
} from "@orbs-network/spot-react";

React integrations can import the entire public Spot SDK API, including calculateOrder, its deprecated calculateOrderForm alias, swap helpers, and their types, from @orbs-network/spot-react. It includes spot-ui as a runtime dependency; no separate install is needed. Non-React integrations should import them from @orbs-network/spot-ui. Lower-level calculators remain internal entry points; do not import from dist/* or package-internal source paths.

Integration Checklist

  • Use the DEX's existing tokens, inputs, selectors, components, styling, wallet controls, and routing.
  • Keep chain switching out of the Spot token selector; use the DEX network control.
  • Keep the form visible while disconnected; replace only the submit area with connect/switch controls.
  • Persist Price Protection separately from swap slippage. Show DEX slippage for Module.SWAP; show Price Protection for advanced orders.
  • Use @orbs-network/swap-ui for order creation/progress content.
  • Convert raw amounts into the DEX's amount type before display.
  • Use callbacks for balance refetch and reset the input only after success.
  • Put Spot tabs beside the Swap tab using the DEX's navigation pattern.
  • Split production integrations into focused components, hooks, context, and utils files.
  • Do not add another error boundary around Spot; SpotProvider already includes one.

License

MIT

DEX route competition without a provider

When a DEX already owns the swap form and selects among liquidity sources, import the framework-neutral SDK helpers from @orbs-network/spot-react:

import {
  createClient, isSwapSupported,
  waitForOrder, QuoteError, type SwapQuote,
} from "@orbs-network/spot-react";

Use client.getQuote({ inToken, outToken, inAmount, user, slippage, signal }) to inherit the initialized client's partner and chain. The standalone getQuote export remains available for quoting without a client. Slippage is a percentage: 0.5 means 0.5%. Before a wallet connects, a preview account such as 0x0000000000000000000000000000000000000001 can satisfy user; include it in the quote key. This is quote-request metadata; pass the connected account as userAddress for preparation and execution. A refetch solely to bind the signer is unnecessary; the host owns its quote cache policy and live wallet context checks.

Keep wrapping, approval, and EIP-712 signing in the DEX's existing wallet flow. After confirmed approval to client.spenderAddress, call client.prepareSwap(quote, account), sign its signingRequest, then call client.submitOrder(prepared.order, signature, { signal }). The manual router flow fetches a current quote after wrapping/approval and checks its net output against the original quote.minimumAmountOut before pure preparation. A lower quote requires fresh review; keep the reviewed minimum when it is still covered. All swaps have a fixed 3-minute expiration from preparation, immediately before signing. Call waitForOrder({ order }); it derives the ID, chain, and deadline from the returned order. Tracking needs no initialized client. No walletInteractions object, React provider, or calculated form is required for this path.

No separate spot-ui installation is needed. See the router example. The optional submitSwap convenience function remains available from spot-react when SDK wallet orchestration through callbacks is preferred. The 3-minute window starts at final preparation after wrapping and approval. Calculated form swaps in SpotProvider use the same fixed expiration. spot-react reuses the SDK wallet executor and watchOrder stream underlying waitForOrder; both tracking helpers take only order and optional signal. Its provider adds React state, frozen review data, history updates, and modal lifecycle.