A lightweight, customizable React widget for cross-chain token swaps powered by Optimex protocol.
npm install otx-swapA lightweight, customizable React widget for cross-chain token swaps powered by Optimex protocol.
- Cross-chain swaps between EVM, Bitcoin, and Solana networks
- Bring Your Own Wallet (BYOW) - integrate with your existing wallet infrastructure
- Fully customizable theming
- TypeScript support with full type definitions
- Lightweight bundle (~313KB gzipped: ~72KB)
- Lifecycle callbacks for tracking swap progress
``bash`
npm install otx-swapor
yarn add otx-swapor
pnpm add otx-swap
`bash`
npm install react react-dom @tanstack/react-query
`tsx
import { useState, useEffect, useMemo } from "react";
import { SwapWidget, createEvmWalletClient } from "otx-swap";
import type { ExternalWalletClient, EvmWalletAccount, Eip1193Provider } from "otx-swap";
function App() {
const [evmWallet, setEvmWallet] = useState
// Create wallet client from EIP-1193 provider (MetaMask, WalletConnect, etc.)
useEffect(() => {
const initWallet = async () => {
if (window.ethereum) {
try {
const wallet = await createEvmWalletClient({
provider: window.ethereum as Eip1193Provider,
});
setEvmWallet(wallet);
} catch (error) {
console.error("Failed to create wallet:", error);
}
}
};
initWallet();
}, []);
const walletClient = useMemo((): ExternalWalletClient => {
if (!evmWallet) return {};
return { evm: evmWallet };
}, [evmWallet]);
return (
walletClient={walletClient}
onRequestWallet={(networkId) => {
// Called when user needs to connect wallet
console.log("Connect wallet for network:", networkId);
}}
/>
);
}
`
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| apiBaseUrl | string | Yes | - | Optimex API base URL |walletClient
| | ExternalWalletClient | Yes | - | Connected wallet client object |onRequestWallet
| | (networkId?: string) => void | No | - | Callback when wallet connection is needed |theme
| | PartialThemeConfig | No | - | Custom theme configuration |defaultFromToken
| | string | No | - | Default source token ID |defaultToToken
| | string | No | - | Default destination token ID |defaultSlippage
| | number | No | 0.75 | Default slippage tolerance (%) |hideHistory
| | boolean | No | false | Hide transaction history button |affiliateFee
| | number | No | - | Affiliate fee in basis points |affiliateAddress
| | string | No | - | Affiliate fee recipient address |affiliateProvider
| | string | No | - | Affiliate provider identifier |tradeTimeout
| | number | No | - | Trade timeout in minutes |scriptTimeout
| | number | No | - | Script timeout in minutes |queryClient
| | QueryClient | No | - | Shared React Query client |renderLoading
| | ComponentType | No | - | Custom loading component |renderError
| | ComponentType | No | - | Custom error component |children
| | ReactNode | No | - | Custom content instead of default SwapCard |
Track swap progress with these callbacks:
`tsx`
walletClient={walletClient}
onQuote={(data) => {
// Called when quote is received
console.log("Quote received:", data.quote.trade_id);
}}
onWalletTx={(data) => {
// Called when wallet transaction is sent
console.log("Wallet TX:", data.tx_hash);
}}
onSubmitTx={(data) => {
// Called when transaction is submitted to network
console.log("Submit TX status:", data.status);
}}
onSwapComplete={(data) => {
// Called when swap is completed successfully
console.log("Swap complete:", data.trade.trade_id);
}}
onError={(data) => {
// Called when an error occurs
console.error("Error at stage:", data.stage, data.error);
}}
/>
#### Callback Types
`typescript
interface QuoteCallbackData {
quote: Quote;
from_token: Token;
to_token: Token;
amount_in: string;
}
interface WalletTxCallbackData {
trade_id: string;
tx_hash: string;
from_token: Token;
to_token: Token;
amount_in: string;
}
interface SubmitTxCallbackData {
trade_id: string;
tx_hash: string;
status: "success" | "error";
error?: string;
}
interface SwapCompleteCallbackData {
trade: Trade;
tx_hash: string;
}
interface SwapErrorCallbackData {
error: Error;
stage: "quote" | "approval" | "wallet_tx" | "submit_tx" | "tracking";
trade_id?: string;
}
`
The widget requires a wallet client that implements the ExternalWalletClient interface:
`typescript`
interface ExternalWalletClient {
evm?: EvmWalletAccount; // EVM wallet (Ethereum, Polygon, etc.)
btc?: WalletAccount; // Bitcoin wallet
solana?: WalletAccount; // Solana wallet
}
`typescript
interface EvmWalletAccount {
address: string;
publicKey: string;
chain: {
id: number;
name: string;
type: "EVM";
};
sendTransaction: (tx: EvmTransactionRequest) => Promise
getChainId: () => Promise
switchChain: (chainId: number) => Promise
}
interface EvmTransactionRequest {
to: 0x${string};0x${string}
value?: bigint;
data?: ;`
chainId?: number;
}
`typescript
interface WalletAccount {
address: string;
publicKey: string; // BTC public key (different from address)
chain: {
id: string; // "mainnet" or "testnet"
name: string;
type: "BTC";
};
sendTransaction: (tx: BitcoinTransactionRequest) => Promise
}
interface BitcoinTransactionRequest {
toAddress: string;
amount: bigint;
options?: {
feeRate?: number;
memo?: string;
memos?: string[];
};
}
`
`typescript`
interface WalletAccount {
address: string;
publicKey: string;
chain: {
id: string;
name: string;
type: "Solana";
};
sendTransaction: (tx: unknown) => Promise
}
We provide two helper functions to convert any EIP-1193 provider to the required wallet interface:
#### createEvmWalletClient (Recommended)
Async version that automatically fetches address and chainId from the provider if not provided:
`tsx
import { createEvmWalletClient } from "otx-swap";
import type { EvmWalletAccount, Eip1193Provider } from "otx-swap";
// Simplest usage - just pass the provider
const evmWallet = await createEvmWalletClient({
provider: window.ethereum as Eip1193Provider,
});
// Or with optional overrides
const evmWallet = await createEvmWalletClient({
provider: window.ethereum as Eip1193Provider,
chainName: "Ethereum", // Optional
});
`
#### createEvmWalletClientSync
Sync version that requires all parameters (use when you already have address and chainId):
`tsx
import { createEvmWalletClientSync } from "otx-swap";
import type { ExternalWalletClient } from "otx-swap";
const walletClient: ExternalWalletClient = {
evm: createEvmWalletClientSync({
provider: window.ethereum, // Any EIP-1193 provider
address: "0x...", // Required
chainId: 1, // Required
chainName: "Ethereum", // Optional
}),
};
`
#### With wagmi (EVM) - Recommended
Using createEvmWalletClient with wagmi's connector provider:
`tsx
import { useState, useEffect, useMemo } from "react";
import { useAccount, useChainId } from "wagmi";
import { SwapWidget, createEvmWalletClient } from "otx-swap";
import type { ExternalWalletClient, EvmWalletAccount, Eip1193Provider } from "otx-swap";
function SwapWithWagmi() {
const { isConnected, connector } = useAccount();
const chainId = useChainId();
const [evmWallet, setEvmWallet] = useState
// Create wallet client from provider (auto-fetches address & chainId)
useEffect(() => {
if (connector?.getProvider && isConnected) {
connector.getProvider().then(async (provider) => {
try {
const wallet = await createEvmWalletClient({
provider: provider as Eip1193Provider,
});
setEvmWallet(wallet);
} catch (error) {
console.error("Failed to create wallet client:", error);
setEvmWallet(null);
}
});
} else {
setEvmWallet(null);
}
}, [connector, isConnected, chainId]); // Recreate on chain change
const walletClient = useMemo((): ExternalWalletClient => {
if (!isConnected || !evmWallet) return {};
return { evm: evmWallet };
}, [isConnected, evmWallet]);
return (
walletClient={walletClient}
onRequestWallet={() => {
// Open your wallet connect modal
}}
/>
);
}
`
#### With wagmi (EVM) - Using wagmi Hooks
Alternative approach using wagmi's transaction hooks instead of the helper function:
`tsx
import { useMemo } from "react";
import { useAccount, useSendTransaction, useChainId, useSwitchChain } from "wagmi";
import { SwapWidget } from "otx-swap";
import type { ExternalWalletClient, EvmTransactionRequest } from "otx-swap";
function SwapWithWagmi() {
const { address, isConnected } = useAccount();
const chainId = useChainId();
const { sendTransactionAsync } = useSendTransaction();
const { switchChainAsync } = useSwitchChain();
const walletClient = useMemo((): ExternalWalletClient => {
if (!isConnected || !address) return {};
return {
evm: {
address,
publicKey: address,
chain: { id: chainId, name: "Ethereum", type: "EVM" },
sendTransaction: async (tx: unknown) => {
const evmTx = tx as EvmTransactionRequest;
return sendTransactionAsync({
to: evmTx.to,
value: evmTx.value,
data: evmTx.data,
});
},
getChainId: async () => chainId,
switchChain: async (id) => {
await switchChainAsync({ chainId: id });
},
},
};
}, [isConnected, address, chainId, sendTransactionAsync, switchChainAsync]);
return (
walletClient={walletClient}
/>
);
}
`
#### With Unisat (Bitcoin)
`tsx
import { SwapWidget } from "otx-swap";
import type { ExternalWalletClient, BitcoinTransactionRequest } from "otx-swap";
function SwapWithUnisat() {
const [btcAddress, setBtcAddress] = useState
const [btcPublicKey, setBtcPublicKey] = useState
const connectUnisat = async () => {
const unisat = (window as any).unisat;
const accounts = await unisat.requestAccounts();
const pubKey = await unisat.getPublicKey();
setBtcAddress(accounts[0]);
setBtcPublicKey(pubKey);
};
const walletClient: ExternalWalletClient = {
btc: btcAddress && btcPublicKey ? {
address: btcAddress,
publicKey: btcPublicKey,
chain: { id: "mainnet", name: "Bitcoin", type: "BTC" },
sendTransaction: async (tx: unknown) => {
const btcTx = tx as BitcoinTransactionRequest;
const unisat = (window as any).unisat;
return unisat.sendBitcoin(btcTx.toAddress, Number(btcTx.amount));
},
} : undefined,
};
return (
walletClient={walletClient}
onRequestWallet={() => connectUnisat()}
/>
);
}
`
Customize the widget appearance with the theme prop:
`tsx`
walletClient={walletClient}
theme={{
colors: {
primary: "#3B82F6",
primaryHover: "#2563EB",
background: "#0F172A",
surface: "#1E293B",
surfaceHover: "#334155",
text: "#F8FAFC",
textSecondary: "#94A3B8",
border: "#334155",
success: "#22C55E",
error: "#EF4444",
warning: "#F59E0B",
},
borderRadius: "md", // "none" | "sm" | "md" | "lg" | "full"
fontFamily: "Inter, sans-serif",
}}
/>
| Color | Description |
|-------|-------------|
| primary | Primary action color (buttons, links) |primaryHover
| | Primary color on hover |secondary
| | Secondary accent color |background
| | Widget background color |surface
| | Card/panel background color |surfaceHover
| | Surface color on hover |text
| | Primary text color |textSecondary
| | Secondary/muted text color |border
| | Border color |success
| | Success state color |error
| | Error state color |warning
| | Warning state color |
`tsx
import { SwapWidget } from "otx-swap";
import type { CustomLoadingProps } from "otx-swap";
function MyLoadingSpinner({ className }: CustomLoadingProps) {
return (
walletClient={walletClient}
renderLoading={MyLoadingSpinner}
/>
`
`tsx
import type { CustomErrorProps } from "otx-swap";
function MyErrorComponent({ error, onRetry, className }: CustomErrorProps) {
return (
Error: {error?.message}
walletClient={walletClient}
renderError={MyErrorComponent}
/>
`
The widget also exports standalone components for custom layouts:
`tsx
import { SwapCard, ConnectButton, useWallet } from "otx-swap";
// Use ConnectButton separately
showNetworkBadge={true}
onClick={() => {
// Custom connect logic
}}
/>
`
Full TypeScript support with exported types:
`tsx
import {
// Helper functions
createEvmWalletClient, // Async version (auto-fetches from provider) - Recommended
createEvmWalletClientSync, // Sync version (requires address & chainId)
} from "otx-swap";
import type {
// Props
SwapWidgetProps,
// Wallet types
ExternalWalletClient,
WalletAccount,
EvmWalletAccount,
ChainInfo,
Eip1193Provider,
CreateEvmWalletClientOptions, // For async version
CreateEvmWalletClientSyncOptions, // For sync version
// Transaction types
EvmTransactionRequest,
BitcoinTransactionRequest,
// Data types
Token,
Quote,
Trade,
TradeState,
NetworkType,
// Theme types
ThemeConfig,
PartialThemeConfig,
ThemeColors,
// Callback types
QuoteCallbackData,
WalletTxCallbackData,
SubmitTxCallbackData,
SwapCompleteCallbackData,
SwapErrorCallbackData,
SwapCallbacks,
} from "otx-swap";
``
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
MIT