Skip to main content

Developer Guide

This guide demonstrates how to integrate only swaps libraries into your apps, enabling secure, conditional cross-chain settlement powered by the dcipher network. It shows how to get started and integrate settlement logic inside smart contract functions and client-side applications.

The dcipher network provides two implementation options for integrating its cross-chain settlement primitives into your decentralized applications:

  1. onlyswaps-js - JavaScript/TypeScript library designed to simplify client-side interaction with the only swaps protocol. It allows frontend and backend applications to prepare, initiate, and manage cross-chain token swaps.
    • 🔄 Cross-chain token swaps across EVM-compatible networks
    • 💰 Dynamic fee estimation via the only swaps fee API
    • 🌐 Multi-chain support - Arbitrum, Avalanche, Base, BSC, Ethereum, and more!
  2. onlyswaps-solidity - Solidity library for integrating cross-chain token swaps logic directly into your smart contracts with upgradeability and BLS signature verification.

Choose the Implementation that Best Fits Your Use Case

  • Choose the JavaScript Library if you need to:

    • Integrate with your client applications to allow users to initiate token swaps through a UI.
    • Integrate conditional swap features into your client-side dApp interface.
  • Choose the Solidity Library if you need to:

    • Implement the full cross-chain token swap logic directly in your smart contract.
    • Build on-chain protocols that require cross-chain transfers in the smart contract.

Build with onlyswaps-js

This guide will help you integrate cross-chain token swaps into your dApp using the onlyswaps-js and dcipher network.

In this guide, we will use Base Sepolia and Avalanche Fuji testnets as examples to swap RUSD tokens. RUSD is the testing token we created for development purposes, and you can request test RUSD from the RUSD faucet.

Once you move your appliation to mainnet, only swaps also supports swapping USDT between Base, Avalanche, and more to come. You can find the support token information here. All you need to do is use the USDT_ADDRESS for different networks defined in the onlyswaps-js constants file.

Installation

Install the package using npm:

npm install onlyswaps-js

Or using yarn:

yarn add onlyswaps-js

Quickstart

1. Setting Up Clients

First, set up your viem clients and only swaps router.

import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia, avalancheFuji } from 'viem/chains';
import { RouterClient, ViemChainBackend, BASE_SEPOLIA } from 'onlyswaps-js';

// Initialize your wallet account
const account = privateKeyToAccount(process.env.PRIVATE_KEY!);

// Create public client for reading blockchain state
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http("https://sepolia.base.org") // Optional: use your own RPC
});

// Create wallet client for sending transactions
const walletClient = createWalletClient({
chain: baseSepolia,
transport: http("https://sepolia.base.org"),
account
});

// Create the only swaps backend
const backend = new ViemChainBackend(
account.address,
publicClient,
walletClient
);

// Create the router client
const router = new RouterClient(
{ routerAddress: BASE_SEPOLIA.ROUTER_ADDRESS },
backend
);

only swaps provides an API to fetch recommended fees based on current network conditions.

For more detailed explanation about the fee strcuture.

import { 
fetchRecommendedFees,
BASE_SEPOLIA,
AVAX_FUJI
} from 'onlyswaps-js';
import { baseSepolia, avalancheFuji } from 'viem/chains';

async function getOptimalFees() {
const feeRequest = {
sourceToken: BASE_SEPOLIA.RUSD_ADDRESS,
destinationToken: AVAX_FUJI.RUSD_ADDRESS,
sourceChainId: BigInt(baseSepolia.id),
destinationChainId: BigInt(avalancheFuji.id),
amount: 1000000000000000000n // 1 RUSD
};

const feeResponse = await fetchRecommendedFees(feeRequest);

console.log('Recommended fees:', {
solverFee: feeResponse.fees.solver,
networkFee: feeResponse.fees.network,
totalFee: feeResponse.fees.total,
amountToTransfer: feeResponse.transferAmount,
amountToApprove: feeResponse.approvalAmount
});

return feeResponse;
}

The fee response includes:

  • fees.solver: Fee paid to the solver who fulfills your swap (user-defined, and also can be quereid by API)
  • fees.network: Network fee charged by the dcipher network (protocol fee, deducted from swap amount)
  • fees.total: Total fee (solver + network)
  • transferAmount: Amount the user will receive on the destination chain (swapAmount - network)
  • approvalAmount: Amount to approve for the ERC20 contract (swapAmount + solverFee)
About the fee

Important: The approvalAmount is what you need to approve on the source chain, while transferAmount is what the user will receive on the destination chain. The difference accounts for the solver fee (paid separately) and network fee (deducted from swap amount).

3. Executing a Cross-Chain Swap

To execute a cross-chain swap, you need to provide:

  • Source token address: RUSD in Base Sepolia
  • Destination token address: RUSD in Aavalance Fuji testnet
  • Amount to swap
  • Solver fee: You can get from last step
  • Destination chain ID: Avalance Fuji chain Id
  • Recipient address
import { AVAX_FUJI, BASE_SEPOLIA } from 'onlyswaps-js';
import { avalancheFuji } from 'viem/chains';

async function executeSwap() {
try {
const swapRequest = {
recipient: '0x...',
srcToken: BASE_SEPOLIA.RUSD_ADDRESS,
destToken: AVAX_FUJI.RUSD_ADDRESS,
amount: 1000000000000000000n, // 1 RUSD (18 decimals)
fee: 10000000000000000n, // 0.01 RUSD solver fee
destChainId: BigInt(avalancheFuji.id) // Avalanche Fuji
};

// The swap method handles:
// 1. Token approval
// 2. Swap execution
// 3. Returns request ID for tracking
const { requestId } = await router.swap(swapRequest);

console.log('Swap request submitted:', requestId);
return requestId;
} catch (error) {
console.error('Swap failed:', error);
throw error;
}
}

4. Tracking Swap Status

After submitting a swap, you can track its status:

Fetch Request Parameters

async function checkSwapStatus(requestId: string) {
const params = await router.fetchRequestParams(requestId);

console.log('Swap parameters:', {
sender: params.sender,
recipient: params.recipient,
tokenIn: params.tokenIn,
tokenOut: params.tokenOut,
amountIn: params.amountIn,
amountOut: params.amountOut,
srcChainId: params.srcChainId,
dstChainId: params.dstChainId,
verificationFee: params.verificationFee,
solverFee: params.solverFee,
executed: params.executed, // true when verified by dcipher
requestedAt: params.requestedAt
});

return params;
}

Fetch Fulfillment Receipt

async function checkFulfillment(requestId: string) {
const receipt = await router.fetchFulfilmentReceipt(requestId);

console.log('Fulfillment status:', {
requestId: receipt.requestId,
fulfilled: receipt.fulfilled, // true when solver completes transfer
solver: receipt.solver,
recipient: receipt.recipient,
amountOut: receipt.amountOut,
fulfilledAt: receipt.fulfilledAt
});

return receipt;
}

Key Difference:

  • executed (from fetchRequestParams): true when the swap is verified by the dcipher network
  • fulfilled (from fetchFulfilmentReceipt): true when the solver completes the transfer (may not be verified yet)

Build with onlyswaps-solidity

onlyswaps-solidity enables your smart contracts to seamlessly swap tokens across different blockchains. Simply integrate the Router contract into your Solidity code, and your users can swap tokens from Ethereum to Polygon, Arbitrum to Base, or any supported chain pair—all without centralized bridges or wrapped tokens.

The Router contract is the central entry point for swap requests and contract upgrades. It manages cross-chain token swap requests, swap execution, and upgrade scheduling.

Prerequisites

Smart Contract Requirements

To integrate only swaps into your smart contract, you need:

  1. Router Interface: Import the IRouter interface
  2. Token Approval: Approve the Router to spend tokens
  3. Supported ERC20 tokens: Tokens must be supported by only swaps

Deployment Information

You'll need to know:

  • the router contract address on your source chain
  • the destination chain ID
  • the token addresses on both source and destination chains
  • the current fee structure (network fee BPS)

Quickstart

1. Import the Router Interface

First, we need to install the onlyswaps-solidity library.

forge install randa-mu/onlyswaps-solidity 

Once the library is intalled, we can import IRouter to access the interfaces for token swaps.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IRouter} from "onlyswaps-solidity/src/interfaces/IRouter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

2. Create Your Contract

contract MySwapContract {
IRouter public immutable router;

constructor(address _router) {
router = IRouter(_router);
}

// Your swap functions here
}

3: Understand the Fee Structure

The only swaps protocol uses a two-part fee structure that developers must understand when integrating swaps.

  1. Solver Fee (User-Defined)
  2. Network Fee (Protocol Fee)

For more detailed explanation about the fee strcuture.

On the Source Chain:

// User must approve and lock: swapAmount + solverFee
const totalToLock = swapAmount + solverFee;

On the Destination Chain:

// User receives: swapAmount - networkFee
// Network fee is calculated as a percentage of swapAmount
const verificationFee = (swapAmount * verificationFeeBPS) / 10000;
const amountReceived = swapAmount - verificationFee;

Example Calculation:

uint256 swapAmount = 100 ether; // Amount to swap
uint256 solverFee = 1 ether; // Fee for solver (user sets this)

// Get Network fee (e.g., 5% = 500 BPS)
(uint256 verificationFee, uint256 amountOut) = router.getVerificationFeeAmount(swapAmount);
// verificationFee = 5 ether (5% of 100)
// amountOut = 95 ether (amount recipient receives)

// Total tokens user must approve and lock
uint256 totalRequired = swapAmount + solverFee; // 101 ether

// What user receives on destination
// amountOut = 95 ether

// What solver receives on source (after verification)
// swapAmount + solverFee = 101 ether

Example: If swapping 100 USDT:

  • solverFee: 1 USDT (recommended)
  • networkFee: 5 USDT (5% network fee)
  • transferAmount: 95 USDT (what user receives)
  • approvalAmount: 101 USDT (what to approve: 100 + 1)

4. Basic cross-chain token swap

    
/**
* @notice Initiates a cross-chain token swap
*/
function swapTokens(
address tokenIn,
address tokenOut,
uint256 amount,
uint256 solverFee,
uint256 dstChainId,
address recipient
) external returns (bytes32 requestId) {
require(amount > 0, "Amount must be greater than 0");
require(solverFee > 0, "Solver fee must be greater than 0");
require(recipient != address(0), "Invalid recipient");

// Initiate cross-chain swap
requestId = router.requestCrossChainSwap(
tokenIn, // RUSD in source chain
tokenOut, // RUSD in destination chain
amount,
solverFee,
dstChainId,
recipient
);

emit SwapInitiated(requestId, msg.sender, amount);

// other logic here
}

This guide covers the essential aspects of integrating with the only swaps Router contract. Key takeaways:

  1. Always validate inputs and router configuration before initiating swaps
  2. Handle fees correctly - both verification and solver fees
  3. Track swap requests for your users
  4. Implement proper error handling and security measures
  5. Monitor swap status and provide cancellation mechanisms

Different Token Swap Options

To support diverse DeFi use cases while optimizing for gas efficiency and user experience, the only swaps Router contract provides multiple methods for initiating cross-chain token swaps.

  1. Basic Swap: Simple swap with traditional ERC-20 approval, requires token approval first
  2. Swap with Hooks: Swap with custom pre/post-execution logic using traditional approval
  3. Permit2 Swap: Gas-efficient swap using signature-based approval
  4. Permit2 + Hooks: Gas-efficient swap with custom execution logic

Each method balances different trade-offs:

  • Traditional approval is simpler but requires two transactions
  • Hooks allow custom logic execution for advanced scenarios like automated lending or multi-step operations.
  • Permit2 enables signature-based approval for better UX and lower gas costs.

1: Basic Swap

The simplest method using traditional ERC-20 approval. Requires two transactions: approval and swap request.

function requestCrossChainSwap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut,
uint256 solverFee,
uint256 dstChainId,
address recipient
) external returns (bytes32 requestId)

Usage Example (TypeScript/ethers.js)

...
// Step 1: Approve Router to spend tokens
const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
const amountIn = parseEther("10");
const solverFee = parseEther("1");
const totalAmount = amountIn + solverFee;

await token.approve(routerAddress, totalAmount);

// Step 2: Make swap request
const router = new ethers.Contract(routerAddress, Router_ABI, signer);
const tx = await router.requestCrossChainSwap(
tokenInAddress,
tokenOutAddress,
amountIn,
parseEther("10"), // amountOut must be in destination token units / decimals
solverFee,
84532, // dstChainId (Base Sepolia)
recipientAddress
);
...

When to use: Simple swaps, quick prototyping, when gas optimization is not critical.

2: Swap with Hooks

Enables custom logic execution before and after the swap using traditional approval.

function requestCrossChainSwapWithHooks(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut,
uint256 solverFee,
uint256 dstChainId,
address recipient,
Hook[] memory preHooks,
Hook[] memory postHooks
) external returns (bytes32 requestId)

Hook Structure

struct Hook {
address target; // Contract address to call
bytes callData; // Function call data (ABI-encoded)
uint256 gasLimit; // Maximum gas for execution
}

Example of Hook in JS/TS

const postHooks = [
{
target: tokenOutAddress,
callData: tokenOut.interface.encodeFunctionData("approve", [
aaveV3Address,
amountOut
]),
gasLimit: 100000
},
{
target: aaveV3Address,
callData: aaveV3.interface.encodeFunctionData("supply", [
tokenOutAddress,
amountOut,
recipientAddress,
0 // referralCode for Aave V3. Please refer to the Aave smart contract documentation for more information.
]),
gasLimit: 200000
}
];

Important Notes

  • Recipient Address: When using post-hooks, set recipient to the HookExecutor address so tokens are sent there and hooks can execute
  • Hook Execution: Pre-hooks execute on the source chain before token transfer; post-hooks execute on the destination chain after token transfer
  • Gas Limits: Set appropriate gas limits for each hook to prevent failures
  • Hook Failures: If any hook fails, the entire transaction reverts

When to Use: Need custom logic (e.g., auto-supply tokens received from a swap on the destination chain into lending protocols), Complex DeFi workflows(e.g.,Unstaking tokens just-in-time for swapping, claiming airdrop, lending, etc. )

3: Swap with Permit2

Gas-efficient swap using signature-based approval. Reduces transactions from 2 to 1.

function requestCrossChainSwapPermit2(
RequestCrossChainSwapPermit2Params calldata params
) external returns (bytes32 requestId)

Usage Example

import { ethers, AbiCoder, MaxUint256 } from "ethers";

// Step 1: One-time setup - Approve Permit2 contract
await token.approve(permit2Address, MaxUint256);

// Step 2: Prepare Permit2 signature (off-chain, free!)
const srcChainId = await provider.getNetwork().then(n => n.chainId);
const permitNonce = 0;
const permitDeadline = MaxUint256;

const permit2Domain = {
name: "Permit2",
chainId: srcChainId,
verifyingContract: permit2Address
};

const permit2Types = {
PermitWitnessTransferFrom: [
{ name: "permitted", type: "TokenPermissions" },
{ name: "spender", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "witness", type: "SwapRequestWitness" }
],
TokenPermissions: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" }
],
SwapRequestWitness: [
{ name: "router", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
{ name: "amountIn", type: "uint256" },
{ name: "amountOut", type: "uint256" },
{ name: "solverFee", type: "uint256" },
{ name: "dstChainId", type: "uint256" },
{ name: "recipient", type: "address" },
{ name: "additionalData", type: "bytes" }
]
};

// Encode empty pre/post hooks payload for `additionalData`. Add hook parameters here if your swap uses hooks as per the above `Swap with Hooks` example.
const emptyHooks: Hook[] = [];
const additionalData = AbiCoder.defaultAbiCoder().encode(
["bytes32", "bytes32"],
[
ethers.keccak256(AbiCoder.defaultAbiCoder().encode(
["tuple(address,bytes,uint256)[]"],
[emptyHooks]
)),
ethers.keccak256(AbiCoder.defaultAbiCoder().encode(
["tuple(address,bytes,uint256)[]"],
[emptyHooks]
))
]
);

const permit2Message = {
permitted: {
token: tokenInAddress,
amount: (amountIn + solverFee).toString()
},
spender: permit2RelayerAddress,
nonce: permitNonce,
deadline: permitDeadline,
witness: {
router: routerAddress,
tokenIn: tokenInAddress,
tokenOut: tokenOutAddress,
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
solverFee: solverFee.toString(),
dstChainId: 137, // Polygon
recipient: recipientAddress,
additionalData: additionalData
}
};

// Sign the message (off-chain, no gas cost!)
const signature = await signer.signTypedData(
permit2Domain,
permit2Types,
permit2Message
);

// Step 3: Make swap request (single transaction!)
const params = {
requester: signer.address,
tokenIn: tokenInAddress,
tokenOut: tokenOutAddress,
amountIn: amountIn,
amountOut: amountOut,
solverFee: solverFee,
dstChainId: 137,
recipient: recipientAddress,
permitNonce: permitNonce,
permitDeadline: permitDeadline,
signature: signature,
preHooks: [], // Empty - no hooks
postHooks: [] // Empty - no hooks
};

const tx = await router.requestCrossChainSwapPermit2(params);

When to use: Want to reduce gas costs, better UX (users sign off-chain), meta-transactions, and Time-limited approvals

Quick Comparison

MethodTransactionsGas CostHooksBest For
Basic Swap2HigherSimple swaps
Hooks2HigherCustom logic
Permit21LowGas-efficient swaps

Need Help?

Stuck on something? We're here to help!

  • 💬 Join Telegram - Get real-time support in our Telegram channel
  • 🐛 Report Issues - Found a bug? Open an issue on GitHub
  • 📚 Browse Docs - Check out our full documentation for more examples
  • 📧 Contact Us - Email [email protected] for direct support

Visit our Community & Support page for more resources and ways to connect.