@swapper-finance/deposit-sdk
v0.5.3
Published
Easy iframe embedding for Swapper deposit widget with full TypeScript support
Maintainers
Readme
@swapper-finance/deposit-sdk
Easy iframe embedding SDK for the Swapper Finance deposit widget with full TypeScript support.
The SDK provides two integration options: iframe embedding for seamless integration into your application, and modal popup for a focused user experience. Both options support full customization and TypeScript types.
Building your own flow instead of embedding the widget? The SDK also ships a typed client for the Swapper API v1 — see API client (server-side).
Installation
npm install @swapper-finance/deposit-sdkQuick Start
Option 1: Embed in Container
import { SwapperIframe } from '@swapper-finance/deposit-sdk';
// Create and mount the iframe
const swapper = new SwapperIframe({
container: '#swapper-container', // or pass an HTMLElement
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
});Option 2: Open in Modal Popup
import { openSwapperModal } from '@swapper-finance/deposit-sdk';
// Open in a centered modal with backdrop
const modal = openSwapperModal({
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
});
// Close programmatically if needed
// modal.close();Configuration Options
Required Parameters
integratorId- Your integrator identifierdstChainId- Destination chain IDdstTokenAddr- Destination token addressdepositWalletAddress- Wallet address for deposits
Optional Parameters
Styling
The SDK supports a two-tier styling system:
const swapper = new SwapperIframe({
// ... required params
styles: {
// Level 1: Theme mode (light or dark)
themeMode: 'dark',
// Level 2: Component-specific overrides (highest priority)
componentStyles: {
primaryColor: '#FF6B35',
backgroundColor: '#1A1A1A',
textColor: '#E8E8E8',
borderRadius: '12px',
width: '100%',
primaryButtonTextColor: '#FFFFFF',
// ... more style options
},
},
});Custom Contract Calls
import { ContractCallType } from '@swapper-finance/deposit-sdk';
const swapper = new SwapperIframe({
// ... required params
customContractCalls: [
{
callType: ContractCallType.CALL,
target: '0x...',
value: '0',
callData: '0x...',
payload: '0x...'
},
],
});Custom Data
Integrator-scoped data forwarded verbatim to the widget. Use it for values only your app can supply — for example a balance the user holds on an external venue, which the widget has no way to read itself.
The widget interprets these keys per integrator: a partner with a custom home screen reads the keys it knows about and ignores everything else. Unknown or malformed values are dropped without breaking the deposit flow, so you can start sending a field before widget support ships. Values must be JSON-serializable.
const swapper = new SwapperIframe({
// ... required params
customData: {
dex: 'hyperliquid',
dexBalanceUsd: 1234.56,
instructionsUrl: 'https://docs.example.com/deposits',
},
});Keep it fresh with updateCustomData() —
typically on the transaction_success event, once a deposit has changed the
value you are displaying.
Connecting Your Wallet (wallet)
Pass a wallet and the widget skips its own connect step and uses yours. Two modes:
Signer mode — hand over an ethers v5/v6 Signer or a viem WalletClient; the
SDK normalizes it and reads the address, chain and wallet name itself:
const swapper = new SwapperIframe({
// ... required params
wallet: { signer },
});
// Keep it in sync as the user switches account or chain (pass null to disconnect):
swapper.updateSigner(nextSigner);Handler mode — bridge the requests yourself:
const swapper = new SwapperIframe({
// ... required params
wallet: {
onTransactionRequest: (tx) => wallet.sendTransaction(tx), // → { hash }
onChainSwitchRequest: (chainId) => wallet.switchChain(chainId),
// Optional, see below
onSignRequest: async (request) =>
request.method === 'eth_signTypedData_v4'
? wallet.signTypedData(request.typedData)
: wallet.signMessage(request.message, request.isHex),
autoConnect: { address, chainId },
},
});Signing (onSignRequest)
Most flows only need transactions. Deposit from Polymarket and Deposit from
Perps are different: they authorize with a signature — an EIP-712 payload or a
personal_sign — and never send a transaction from the user's wallet.
In signer mode this is automatic: whatever your signer implements
(signMessage / _signTypedData / signTypedData) is bridged, and the widget is
told on connect what it can rely on. In handler mode it's opt-in via
onSignRequest.
If the wallet can't sign, nothing breaks — those two flows just fall back to asking the user to connect a wallet inside the widget, exactly as they did before this bridge existed. Either way the widget's top-right wallet button offers Switch Wallet, so the user can always step off the wallet you passed and pick their own.
Two details worth knowing when implementing onSignRequest by hand:
request.isHexmarks a raw 32-byte digest (e.g. a Gnosis Safe transaction hash). Sign it as bytes — signing the text"0x…"recovers a different address and the Safe rejects it. With ethers,signMessage(getBytes(message)); with viem,signMessage({ message: { raw: message } }).request.typedDatais the fulleth_signTypedData_v4payload, so itstypesincludeEIP712Domain. viem takes it as-is; ethers derives that entry itself and throws if it's also passed, so strip it first.
Narrowing what we advertise (capabilities)
What the widget is told on connect is inferred, and the inference errs on the generous side in both modes:
- signer mode — we check which methods your signer implements. On any ethers or viem signer both are always present (prototype methods, injected actions), so both capabilities are advertised regardless of what the wallet behind the transport would accept.
- handler mode — supplying
onSignRequestadvertises both, since we can't tell what your handler does with eachrequest.method.
If that's wrong for your wallet — a hardware or embedded wallet that refuses
EIP-712, or a handler that only implements personal_sign — say so, and the
widget keeps the flows needing the missing piece on its own connect-wallet step
instead of offering one it can't finish:
wallet: {
signer, // or the handler trio
capabilities: { signMessage: true, signTypedData: false },
}It only ever narrows: declaring a capability your signer doesn't implement won't make it available.
API Reference
Constructor
new SwapperIframe(options: SwapperIframeOptions)Methods
mount(container: HTMLElement | string): void
Mounts the iframe to a container element.
swapper.mount('#my-container');
// or
swapper.mount(document.getElementById('my-container'));updateConfig(config: Partial<SwapperConfig>): void
Updates the configuration dynamically via postMessage.
swapper.updateConfig({
depositWalletAddress: '0xNewAddress...',
dstChainId: '1',
});updateStyles(styles: SwapperStyles): void
Updates only the styles.
swapper.updateStyles({
themeMode: 'light',
brandTheme: {
primaryColor: '#836FFF',
},
});updateCustomContractCalls(calls: ContractCall[]): void
Updates custom contract calls.
swapper.updateCustomContractCalls([
{
callType: ContractCallType.CALL,
target: '0x...',
value: '0',
callData: '0x...',
payload: '0x...',
},
]);updateCustomData(customData: Record<string, unknown>): void
Merges into the integrator-scoped customData payload — pass
only the keys that changed, the rest are preserved. Set a key to undefined to
drop it. Also available on SwapperModal, where it is safe to call before the
modal is built.
swapper.on('transaction_success', async () => {
const balanceUsd = await fetchVenueBalance();
swapper.updateCustomData({ dexBalanceUsd: balanceUsd });
});getConfig(): SwapperConfig
Returns the current configuration.
const config = swapper.getConfig();
console.log(config.depositWalletAddress);navigateTo(tab: SwapperWidgetTab): void
Navigates the widget to a tab from outside the iframe. A tab is a deposit
option id ("transferCrypto", "depositWithCash", "walletDeposit",
"depositFromPolymarket", "depositFromPerps") or "home". Requests for
tabs not enabled for your integration are ignored by the widget.
swapper.navigateTo('transferCrypto');goHome(): void
Shorthand for navigateTo('home').
goBack(): void
Triggers the same action as the widget's internal back button. No-op when no
back action is currently available (see navigation_state.canGoBack).
getNavigationState(): NavigationStatePayload | null
Returns the last navigation state reported by the widget (current tab, title,
step, back availability), or null before the first navigation_state event.
const nav = swapper.getNavigationState();
if (nav?.canGoBack) swapper.goBack();destroy(): void
Removes the iframe and cleans up event listeners.
swapper.destroy();Widget Events
The widget emits structured events via postMessage that you can listen to through the SDK. This lets you react to user actions like completed transactions.
Event Envelope
Every event posted from the iframe follows this shape:
interface SwapperWidgetEvent {
type: "SWAPPER_EVENT"; // discriminator
version: "1.0"; // protocol version
name: WidgetEventName; // e.g. "transaction_success"
timestamp: string; // ISO 8601
payload: WidgetEventPayload; // event-specific data
}Listening with onEvent (recommended)
The simplest way to listen for widget events is to pass an onEvent callback in the config. It receives every event (equivalent to .on("*", ...)):
import { SwapperIframe, WidgetEventName } from '@swapper-finance/deposit-sdk';
import type { TransactionSuccessPayload } from '@swapper-finance/deposit-sdk';
const swapper = new SwapperIframe({
container: '#swapper-container',
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
onEvent: (event) => {
if (event.type === WidgetEventName.TRANSACTION_SUCCESS) {
const payload = event.data as TransactionSuccessPayload;
console.log('Transaction succeeded:', payload.txHash);
}
},
});This also works with openSwapperModal:
import { openSwapperModal, WidgetEventName } from '@swapper-finance/deposit-sdk';
openSwapperModal({
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
onEvent: (event) => {
console.log('Widget event:', event.type, event.data);
},
});Listening with on() / off()
For more granular control, you can register handlers on the iframe instance directly:
import { SwapperIframe, WidgetEventName } from '@swapper-finance/deposit-sdk';
import type { TransactionSuccessPayload } from '@swapper-finance/deposit-sdk';
const swapper = new SwapperIframe({
container: '#swapper-container',
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
});
// Listen for a specific event
swapper.on(WidgetEventName.TRANSACTION_SUCCESS, (event) => {
const payload = event.data as TransactionSuccessPayload;
console.log('Transaction succeeded:', payload.txHash);
console.log('Deposit option:', payload.depositOption); // "walletDeposit" | "transferCrypto" | "depositWithCash"
});
// Listen for all events with wildcard
swapper.on('*', (event) => {
console.log('Widget event:', event.type, event.data);
});
// Remove a specific handler
const handler = (event) => { /* ... */ };
swapper.on(WidgetEventName.TRANSACTION_SUCCESS, handler);
swapper.off(WidgetEventName.TRANSACTION_SUCCESS, handler);Listening with window.addEventListener
You can also listen for events directly on the window. Each event is a MessageEvent containing a SwapperWidgetEvent envelope:
import { WIDGET_EVENT_PROTOCOL_VERSION, WidgetEventName } from '@swapper-finance/deposit-sdk';
import type { SwapperWidgetEvent, TransactionSuccessPayload } from '@swapper-finance/deposit-sdk';
window.addEventListener('message', (event: MessageEvent) => {
if (event.data?.type !== 'SWAPPER_EVENT') return;
const message = event.data as SwapperWidgetEvent;
if (message.version !== WIDGET_EVENT_PROTOCOL_VERSION) return;
if (message.name === WidgetEventName.TRANSACTION_SUCCESS) {
const payload = message.payload as TransactionSuccessPayload;
console.log('Tx hash:', payload.txHash);
console.log('Explorer:', payload.explorerUrl);
}
});TransactionSuccessPayload
| Field | Type | Required | Description |
|------------------|-------------------|----------|---------------------------------------|
| depositOption | DepositFlowType | Yes | "walletDeposit", "transferCrypto", or "depositWithCash" |
| txHash | string | No | Transaction hash |
| explorerUrl | string | No | Link to block explorer |
| tokenSymbol | string | No | Token symbol (e.g. "USDC") |
| tokenAddress | string | No | Token contract address |
| chainId | string | No | Chain ID of the transaction |
| amountReceived | string | No | Amount received |
NavigationStatePayload
Emitted as a navigation_state event whenever the widget's navigation
changes (tab switch, in-flow step change, back-button availability).
| Field | Type | Required | Description |
|---------------|--------------------|----------|----------------------------------------------------------------|
| tab | SwapperWidgetTab | Yes | Current tab: a deposit option id or "home" |
| route | string | Yes | Raw internal route path (e.g. "/transfer-crypto") |
| title | string | Yes | Full display title, e.g. "Buy with QR / Verification" |
| flowName | string | Yes | Flow name part of the title ("Buy Assets" on home) |
| step | string | No | Current in-flow step (e.g. "Verification") |
| canGoBack | boolean | Yes | Whether goBack() currently has an effect |
| hasHomeView | boolean | Yes | Whether the integration shows a home screen |
External Navigation (Custom Nav Outside the Iframe)
You can hide the widget's internal navigation header and drive navigation from your own page — useful when the widget should blend into your app's chrome (your own tabs, title bar and back button).
Three pieces work together:
styles.hideNavigation: truehides the widget's internal header (breadcrumbs, back button, home title/close and wallet button).navigation_stateevents report the current tab, title, step and back-button availability, so your UI always mirrors the widget.navigateTo()/goBack()/goHome()execute navigation inside the widget from your buttons.
import { SwapperIframe, WidgetEventName } from '@swapper-finance/deposit-sdk';
import type { NavigationStatePayload } from '@swapper-finance/deposit-sdk';
const swapper = new SwapperIframe({
container: '#swapper-container',
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
styles: {
themeMode: 'dark',
hideNavigation: true, // hide the nav inside the iframe
},
});
// Mirror the widget's navigation in your own UI
swapper.on(WidgetEventName.NAVIGATION_STATE, (event) => {
const nav = event.data as NavigationStatePayload;
document.querySelector('#widget-title')!.textContent = nav.title;
document.querySelector<HTMLButtonElement>('#back-button')!.hidden =
!nav.canGoBack;
highlightActiveTab(nav.tab); // your own tab bar
});
// Drive the widget from your buttons
document.querySelector('#tab-qr')!.addEventListener('click', () =>
swapper.navigateTo('transferCrypto'),
);
document.querySelector('#tab-fiat')!.addEventListener('click', () =>
swapper.navigateTo('depositWithCash'),
);
document.querySelector('#back-button')!.addEventListener('click', () =>
swapper.goBack(),
);Notes:
goBack()mirrors the internal back button exactly: when no back button would be shown inside the widget (canGoBack: false), the command is ignored.navigateTo()requests for deposit options not enabled viasupportedDepositOptionsare ignored by the widget.- The same methods exist on
SwapperEmbed; forSwapperModal, usemodal.getIframe()?.navigateTo(...). - With
hideNavigation: truethe wallet button (wallet deposit flow) and the home close button are hidden too — render your own equivalents if needed. - If the deployed widget doesn't support external navigation yet, events simply never fire and commands are ignored — safe to ship ahead of the widget deploy.
Flexible Widget Height (Auto-Resize)
The widget's home page can show a variable number of deposit options. With a fixed height this can leave an empty gap below the options. In flexible height mode the home page auto-scales to its content instead; all other pages keep the standard fixed height (560px).
SwapperModal / openSwapperModal enable this automatically — nothing to
configure. The modal container smoothly animates to the widget's requested
height (clamped to 90% of the viewport):
import { openSwapperModal } from '@swapper-finance/deposit-sdk';
openSwapperModal({
integratorId: 'your-id',
dstChainId: '8453',
dstTokenAddr: '0x...',
depositWalletAddress: '0x...',
// Home page auto-sizes; other pages stay at the fixed height.
});Notes:
- Users with
prefers-reduced-motionget an instant snap instead of the animation. - If the deployed widget doesn't support flexible height yet, it simply keeps the fixed height — safe to use ahead of the widget deploy.
Modal API Reference
Opening a Modal
Quick Function: openSwapperModal(options)
import { openSwapperModal } from '@swapper-finance/deposit-sdk';
const modal = openSwapperModal({
// Required configuration
integratorId: 'your-id',
dstChainId: '8453',
dstTokenAddr: '0x...',
depositWalletAddress: '0x...',
// Optional: Widget styling
styles: {
themeMode: 'light',
},
// Optional: Modal styling
modalStyle: {
overlayColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: '16px'
},
// Optional: Callback when closed
onClose: () => {
console.log('Modal was closed');
},
});
// Close programmatically
modal.close();Modal Options
modalStyle Configuration
interface ModalStyle {
/**
* Modal width (default: '450px')
*/
width?: string;
/**
* Modal height (default: '560px')
*/
height?: string;
/**
* Background overlay color (default: 'rgba(0, 0, 0, 0.7)')
*/
overlayColor?: string;
/**
* Modal border radius (default: '16px')
*/
borderRadius?: string;
/**
* Z-index for modal (default: 10000)
*/
zIndex?: number;
/**
* Show close button (default: false)
* Set to true to show a close button
*/
showCloseButton?: boolean;
}Modal Class Methods
SwapperModal Class
For more control, use the SwapperModal class directly:
import { SwapperModal } from '@swapper-finance/deposit-sdk';
// Create modal instance
const modal = new SwapperModal({
integratorId: 'your-id',
dstChainId: '8453',
dstTokenAddr: '0x...',
depositWalletAddress: '0x...',
modalStyle: {
showCloseButton: true, // Optional: show close button
},
onClose: () => {
console.log('Modal closed');
},
});
// Open modal
modal.open();
// Close modal — hides the modal (the widget stays loaded for an instant
// reopen) and tells the widget to return to its home screen, so the next
// open() starts fresh instead of resuming on the previously selected option
modal.close();
// Check if open
if (modal.isModalOpen()) {
console.log('Modal is open');
}
// Get iframe instance
const iframe = modal.getIframe();
// Destroy completely
modal.destroy();Preloading the Modal
Build the modal hidden and load the widget in the background so the first
open() is instant:
import { preloadSwapperModal } from '@swapper-finance/deposit-sdk';
// e.g. when your page mounts
const modal = preloadSwapperModal({
integratorId: 'your-id',
dstChainId: '8453',
dstTokenAddr: '0x...',
depositWalletAddress: '0x...',
});
// later, on user action — opens instantly
button.addEventListener('click', () => modal.open());The equivalent with the class API is new SwapperModal(options) followed by
modal.preload(), or in one step with the preload option:
const modal = new SwapperModal({
// ...config
preload: true, // build hidden + load the widget immediately
});If something changed between preload and open (e.g. the deposit address), pass
a config patch to open() — it is applied to the live widget right before it
becomes visible:
modal.open({ depositWalletAddress: '0x...newAddress' });The modal also exposes updateConfig(patch) and updateSigner(signer)
directly (no need to go through getIframe()); both are safe to call before
the modal has been built.
During preload the widget loads everything (bundle, chains, tokens, UI) but
does not create a smart wallet authorization — otherwise every visitor
would get one, including those who never open the widget. The authorization
process starts on the first open(), which signals the widget that it is now
visible.
If you embed SwapperIframe directly in a hidden/off-screen container, you
can get the same behavior manually:
const iframe = new SwapperIframe({
// ...config
deferSmartWalletAuth: true,
});
// when your UI actually reveals the widget:
iframe.notifyWidgetOpened();Preloading an Inline Embed
For widgets embedded in the page layout (not a modal), use SwapperEmbed.
The iframe loads immediately on a hidden position: fixed host; calling
showIn(target) moves the host over the target element's bounding box, so
the already-loaded widget appears there instantly. (The iframe is never
reparented in the DOM — that would force a reload.) The host mirrors the
target's drawable box — bounding rect, corner radii, border widths and
stacking position (computed from the target's outermost stacking-context
ancestor, or the zIndex option if you pass one) — and follows it across
scrolling, window resizes and target size changes.
import { preloadSwapperEmbed } from '@swapper-finance/deposit-sdk';
// e.g. when your page mounts — starts loading in the background
const embed = preloadSwapperEmbed({
integratorId: 'your-id',
dstChainId: '8453',
dstTokenAddr: '0x...',
depositWalletAddress: '0x...',
});
// later, on user action — the widget appears over the placeholder instantly
button.addEventListener('click', () => embed.showIn('#widget-placeholder'));
// hide (widget stays loaded and routes back to its home screen,
// so the next showIn() starts fresh) / full teardown
embed.hide();
embed.destroy();The target element is a placeholder: size it to the box the widget should
occupy (e.g. 450px × 560px). Like the modal preload, smart wallet
authorization is deferred until the first showIn().
Pass flexibleHeight: true to let the widget's home page auto-scale: the
embed animates the placeholder's height to the widget's requested value, so
the surrounding page layout reflows with it.
Available Style Properties
ComponentStyles
interface ComponentStyles {
// Layout
width?: string; // Widget width (default: 450px)
border?: string; // Border style (e.g. "1px solid #ccc")
borderRadius?: string; // Border radius (default: 24px)
padding?: string; // Outer padding of every page (default: 24px), e.g. "16px"
// Brand colors
primaryColor?: string; // Main brand color (buttons, links, primary accents)
accentColor?: string; // Secondary accent color for highlights
sphereColor?: string; // Color of the decorative sphere
// Backgrounds & surfaces
backgroundColor?: string; // Widget background color
surfaceColor?: string; // Surface color (cards, inputs)
surfaceAltColor?: string; // Alternative surface color
// Status colors
successColor?: string; // Success state color
successAltColor600?: string; // Alternative success color shade
errorColor?: string; // Error state color
warningColor?: string; // Warning state color
// Typography
textColor?: string; // Main text color
primaryButtonTextColor?: string; // Text color on primary buttons
}API client (server-side)
SwapperApiClient is a typed client for the Swapper API v1
(https://swapper.finance/api/v1): list chains and tokens, get a quote with a
ready-to-sign transaction, track the transfer until it lands, and create
deposit addresses users fund from any wallet or exchange. Every request and
response is typed (Step, Estimate, FeeCost, StatusResponse, …), and
chains use Swapper's own ids ("8453", "mainnet-beta"). Zero dependencies:
it uses the global fetch (Node.js 18+).
Full reference: docs/api-client.md. Runnable example: examples/api-node.
Quick start
import {
SwapperApiClient,
approvalNeeded,
feeFromBps,
NATIVE_TOKEN_ADDRESS,
} from '@swapper-finance/deposit-sdk';
const swapper = new SwapperApiClient({
apiKey: process.env.SWAPPER_API_KEY, // secret: keep it on your server
});
// 1. Quote: 10 USDC on Base → ETH on Arbitrum, with a 0.3% integrator fee
const quote = await swapper.getQuote({
fromChain: 8453,
toChain: 42161,
fromToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
toToken: NATIVE_TOKEN_ADDRESS,
fromAmount: '10000000', // smallest unit: 10 USDC
fromAddress: userAddress,
fee: feeFromBps(30), // 0.003, needs an API key
});
// 2. ERC-20 source: approve estimate.approvalAddress for action.fromAmount first
if (approvalNeeded(quote)) {
// allowance < quote.action.fromAmount → approve(quote.estimate.approvalAddress, quote.action.fromAmount)
}
// 3. Send the raw transaction with any wallet library (viem, ethers, a custodial API…)
const { to, data, value, gasLimit, chainId } = quote.transactionRequest;
const txHash = await sendWithYourWallet({ chainId, to, data, value, gasLimit });
// 4. Track it until DONE or FAILED. Polling is also what records the transfer and accrues your fee.
const status = await swapper.waitForStatus({
txHash,
fromChain: quote.action.fromChainId,
toChain: quote.action.toChainId,
});
console.log(status.status, status.substatus); // "DONE" "COMPLETED"transactionRequest holds plain strings (to, data, hex value, optional
hex gasLimit, chainId), so it works with any library. On Solana, data is a base64-encoded
VersionedTransaction and no approval is needed. With viem:
import { createPublicClient, createWalletClient, erc20Abi, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });
const publicClient = createPublicClient({ chain: base, transport: http() });
if (approvalNeeded(quote)) {
const token = quote.action.fromToken.address as `0x${string}`;
const spender = quote.estimate.approvalAddress as `0x${string}`;
const amount = BigInt(quote.action.fromAmount);
const allowance = await publicClient.readContract({
address: token, abi: erc20Abi, functionName: 'allowance', args: [account.address, spender],
});
if (allowance < amount) {
const hash = await wallet.writeContract({
address: token, abi: erc20Abi, functionName: 'approve', args: [spender, amount],
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
const tx = quote.transactionRequest;
const txHash = await wallet.sendTransaction({
to: tx.to as `0x${string}`,
data: tx.data as `0x${string}`,
value: BigInt(tx.value ?? 0),
gas: tx.gasLimit ? BigInt(tx.gasLimit) : undefined,
});viem is not a dependency of this package; install it yourself if you use it.
Keys and integrator
- With an API key (
x-swapper-api-key, created in the Integrator Dashboard): higher rate limits, integrator fees (fee), and your fee breakdown ongetStatus. Keys are secret, so use them server-side only: the client logs a one-time warning if it gets one in a browser. With a key,integratoris not sent unless you pass it togetQuoteexplicitly; a value that doesn't match the key's integrator is rejected. - Without a key (free tier): pass
integrator: 'your-integrator-id'. It is sent with every request (it's required on quotes).
A typical setup quotes on your backend, hands quote.transactionRequest to
your frontend for the user's wallet to sign, then tracks the returned hash on
the backend with waitForStatus.
Errors and rate limits
Every failure is a SwapperApiError with a stable code, the HTTP status
(0 when there was no response) and a requestId to quote to support:
import { SwapperApiError } from '@swapper-finance/deposit-sdk';
try {
await swapper.getQuote(params);
} catch (err) {
if (err instanceof SwapperApiError) {
// API codes: VALIDATION_ERROR, INVALID_INTEGRATOR, INVALID_FEE, NO_ROUTE_FOUND, RATE_LIMITED, …
// client codes: NETWORK_ERROR, TIMEOUT, ABORTED, INVALID_RESPONSE, TRANSFER_NOT_FOUND
console.error(err.code, err.status, err.message, err.requestId);
console.error(err.retryAfter, err.rateLimit); // on 429
}
}
swapper.lastRateLimit; // { limit: 600, remaining: 598, reset: 1791125700 } from the last response- Retries:
429,5xx, network errors and timeouts are retried up tomaxRetriestimes (default2) with exponential backoff, waitingRetry-Afteron a429. Other4xxare never retried. - Timeouts: each attempt is bounded by
timeoutMs(default 30 s). - Rate limits:
lastRateLimitholds the latestX-RateLimit-*headers (resetin unix seconds), and errors carry their own inerr.rateLimit. waitForStatuskeeps polling throughNOT_FOUND(for up tonotFoundTimeoutMs, 10 min by default),429and transient errors, and rejects withTIMEOUTaftertimeoutMs(30 min by default).
Examples
Modal Popup Usage
<!DOCTYPE html>
<html>
<head>
<title>Swapper Modal</title>
</head>
<body>
<button id="open-modal">Open Swapper</button>
<script type="module">
import { openSwapperModal } from '@swapper-finance/deposit-sdk';
document.getElementById('open-modal').onclick = () => {
openSwapperModal({
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
modalStyle: {
width: '600px',
height: '800px',
},
onClose: () => {
console.log('Modal closed');
},
});
};
</script>
</body>
</html>React Example (Embedded)
import { useEffect, useRef } from 'react';
import { SwapperIframe } from '@swapper-finance/deposit-sdk';
function SwapperWidget() {
const containerRef = useRef<HTMLDivElement>(null);
const swapperRef = useRef<SwapperIframe | null>(null);
useEffect(() => {
if (containerRef.current && !swapperRef.current) {
swapperRef.current = new SwapperIframe({
container: containerRef.current,
integratorId: 'your-integrator-id',
dstChainId: '8453',
dstTokenAddr: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
depositWalletAddress: '0x2A018F2506acaEEE2C10632514Fc5DCa9eE2c28A',
});
}
return () => {
swapperRef.current?.destroy();
swapperRef.current = null;
};
}, []);
return <div ref={containerRef} />;
}