@sky-mavis/ronin-dex
v0.0.2
Published
Ronin DEX SDK
Readme
Ronin DEX SDK
Multi-chain swap SDK for integrating token swap flows across EVM and Solana networks.
Why this SDK
- Aggregates routes across supported providers.
- Supports same-chain and cross-chain swap flows.
- Provides quote, build, approve/sign, submit, and receipt lifecycle handling.
- Emits step-by-step execution events for UI status tracking.
- Designed for app-level control over providers, accounts, and signer behavior.
Installation
npm install @sky-mavis/ronin-dexThis package relies on peer dependencies (for example ethers, @solana/web3.js, and @uniswap/sdk-core). Install peer dependencies required by your runtime.
Integration model
The SDK is split into two layers:
CoreDex: infrastructure/dependency wiring (apiUrl, provider manager, account resolver, signer resolver).Dex: application-facing API (getDexTokens,getQuotes,handle,subscribe, currency helpers).
Typical app lifecycle:
- Initialize
CoreDexonce when wallet/provider context is ready. - Create a
Dexinstance from thatCoreDex. - Resolve token metadata and create/select currencies.
- Request quotes with amount, direction, account, and slippage.
- Subscribe to quote execution events for UI states.
- Execute with
dex.handle(...), optionally passing custom network fee settings.
Example-driven integration (same pattern as examples/web)
This section mirrors the web example architecture so teams can adopt it with minimal adaptation.
1) Provider manager (examples/web/src/adapters/provider-manager.ts)
import { JsonRpcProvider } from 'ethers'
import { Connection } from '@solana/web3.js'
const providerCache = new Map<number, JsonRpcProvider | Connection>()
function resolveRpcUrl(rpcUrl: string): string {
return rpcUrl.startsWith('/') ? `${window.location.origin}${rpcUrl}` : rpcUrl
}
export function createProviderManager() {
return {
getProvider: (chainId: number) => {
const cached = providerCache.get(chainId)
if (cached) return cached
const chain = SUPPORTED_CHAINS.find((c) => c.chainId === chainId)
if (!chain) throw new Error(`Unsupported chain: ${chainId}`)
const provider =
chain.blockchain === 'solana'
? new Connection(resolveRpcUrl(chain.rpcUrl), 'confirmed')
: new JsonRpcProvider(resolveRpcUrl(chain.rpcUrl), chainId)
providerCache.set(chainId, provider)
return provider
},
getChainConfigs: () => SUPPORTED_CHAINS,
}
}2) Wallet signers for EVM + Solana
import { BrowserProvider } from 'ethers'
import { WalletTypeEnum, BlockchainEnum } from '@sky-mavis/ronin-dex'
import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js'
// EVM signer using a standard EIP-1193 provider (MetaMask, Ronin Wallet, etc.)
export function createSigner(eip1193Provider: any, address: string) {
const browserProvider = new BrowserProvider(eip1193Provider)
return {
signMessage: async (message: string) =>
(await browserProvider.getSigner(address)).signMessage(message),
signTransaction: async (txRequest: any) =>
(await browserProvider.getSigner(address)).signTransaction(txRequest),
sendTransaction: async (txRequest: any) =>
(await browserProvider.getSigner(address)).sendTransaction(txRequest),
signTypedDataV4: async (message: string) => {
const parsed = JSON.parse(message)
return (await browserProvider.getSigner(address)).signTypedData(
parsed.domain,
parsed.types,
parsed.message,
)
},
getAccount: () => ({
index: 0,
address,
type: WalletTypeEnum.OTHER,
walletId: 'browser-wallet',
blockchain: BlockchainEnum.EVM,
}),
connect: () => createSigner(eip1193Provider, address),
getTag: () => 'browser-wallet',
}
}
// Solana signer using a Ronin-style unified provider
function toBase64(tx: Transaction | VersionedTransaction): string {
const bytes =
tx instanceof Transaction
? tx.serialize({ requireAllSignatures: false, verifySignatures: false })
: tx.serialize()
return Buffer.from(bytes).toString('base64')
}
function parseSignature(payload: unknown): string {
if (typeof payload === 'string') return payload
if (payload && typeof payload === 'object') {
const data = payload as Record<string, unknown>
if (typeof data.signature === 'string') return data.signature
if (typeof data.result === 'string') return data.result
}
throw new Error('Unable to resolve transaction signature from provider response')
}
export function createSolanaSigner(
provider: any,
address: string,
connection?: Connection,
) {
return {
address,
connection,
connect: (nextConnection: Connection) =>
createSolanaSigner(provider, address, nextConnection),
getAccount: () => ({
index: 0,
address,
type: WalletTypeEnum.OTHER,
walletId: 'browser-wallet',
blockchain: BlockchainEnum.SOLANA,
}),
getTag: () => 'browser-wallet-solana',
signTypedDataV4: async () => {
throw new Error('signTypedDataV4 is not supported for Solana')
},
signMessage: async (message: string) => {
const encoded = Buffer.from(message, 'utf8').toString('base64')
const response = await provider.request({
method: 'sol_signMessage',
params: [{ message: encoded }],
})
return parseSignature(response)
},
signTransaction: async (txRequest: Transaction | VersionedTransaction) => {
const response = await provider.request({
method: 'sol_signTransaction',
params: [toBase64(txRequest)],
})
return parseSignature(response)
},
sendTransaction: async (txRequest: Transaction | VersionedTransaction) => {
const response = await provider.request({
method: 'sol_signAndSendTransaction',
params: [toBase64(txRequest)],
})
return parseSignature(response)
},
}
}3) DexProvider setup (examples/web/src/context/DexContext.tsx)
import { useEffect, useState } from 'react'
import { CoreDex, Dex } from '@sky-mavis/ronin-dex'
import { createProviderManager } from '../adapters/provider-manager'
import { createSigner, createAccountsGetter, isEvmAddress } from '../adapters/wallet-signer'
import { createSolanaSigner } from '../adapters/solana-signer'
const API_URL = import.meta.env.VITE_API_URL || '/proxy/v3/public'
const RESOLVED_API_URL =
API_URL && API_URL.startsWith('/') ? `${window.location.origin}${API_URL}` : API_URL
// Inside your provider component, create CoreDex + Dex when wallet context is ready:
function useDexInstance(evmAddress, solanaAddress, provider, solanaProvider) {
const [dex, setDex] = useState(null)
useEffect(() => {
if (!evmAddress || !provider) return
const core = new CoreDex({
debug: true,
apiUrl: RESOLVED_API_URL,
providerManager: createProviderManager(),
getAccounts: createAccountsGetter(evmAddress, solanaAddress),
getSignerForAddress: async (baseAddress) => {
if (!isEvmAddress(baseAddress) && solanaAddress && solanaProvider) {
return createSolanaSigner(solanaProvider, solanaAddress)
}
return createSigner(provider, evmAddress)
},
})
setDex(new Dex(core))
}, [evmAddress, solanaAddress, provider, solanaProvider])
return dex
}4) Quote fetch flow (examples/web/src/hooks/useQuotes.ts)
import { DEFAULT_SLIPPAGE } from '@sky-mavis/ronin-dex'
import { parseUnits } from 'ethers'
const currencyIn = dex.createDexCurrency({
chainId: tokenIn.chainId,
address: tokenIn.address,
decimals: tokenIn.decimals,
symbol: tokenIn.symbol,
name: tokenIn.name,
standard: tokenIn.isNative ? 'native' : 'erc20',
})
const currencyOut = dex.createDexCurrency({
chainId: tokenOut.chainId,
address: tokenOut.address,
decimals: tokenOut.decimals,
symbol: tokenOut.symbol,
name: tokenOut.name,
standard: tokenOut.isNative ? 'native' : 'erc20',
})
const result = await dex.getQuotes({
currencyIn,
currencyOut,
account: <USER_ACCOUNT>, // 0x...
amount: parseUnits(<USER_INPUT_AMOUNT>, tokenIn.decimals).toString(), // raw amount
direction: 'exactIn',
slippageToleranceBps: DEFAULT_SLIPPAGE, // 0.5%
})
const firstQuote = result[0]5) Execute + track state (examples/web/src/hooks/useSwap.ts)
const unsubscribe = dex.subscribe(quote.quoteId, (event) => {
switch (event.state) {
case 'computing':
setStatus('building')
break
case 'authorizing':
case 'approving-token':
case 'signing-permit':
setStatus('approving')
break
case 'signing-transaction':
case 'populating-transaction':
setStatus('signing')
break
case 'transaction-submitted':
setStatus('submitted')
break
case 'waiting-for-receipt':
setStatus('waiting')
break
case 'success':
setStatus('success')
break
case 'failed':
case 'authorize-failed':
case 'approve-token-failed':
case 'sign-permit-failed':
setStatus('failed')
break
}
})
try {
await dex.handle(quote, { waitForReceipt: true })
} finally {
unsubscribe()
}6) Token search flow (examples/web/src/hooks/useTokens.ts)
const response = query.trim()
? await dex.getDexTokens(chainId, { query: query.trim() })
: await dex.getDexTokens(chainId)
const tokenList = response.ids.map((id) => response.data[id])Event-driven execution states
For product and support stakeholders, these are the states that are usually mapped into UX:
- Preparation:
computing,populating-transaction - Approval/authorization:
authorizing,approving-token,signing-permit - Signing/submission:
signing-transaction,transaction-submitted - Confirmation:
waiting-for-receipt,success - Failure paths:
failed,authorize-failed,approve-token-failed,sign-permit-failed
Subscribe by quoteId and always unsubscribe after completion or timeout.
Provider-specific approval behavior (important)
Approval/permit handling differs by route provider today:
- KyberSwap:
/buildcurrently returns swap calldata only, so the SDK performs approval/permit handling internally before swap submission. - Relay / Li.fi:
/buildalready returns the full execution sequence (for example approve + swap), so the SDK does not add an extra local approval step.
What this means for integrators:
- Do not assume a fixed transaction count per quote; it can vary by provider and token allowance state.
- Drive UX from SDK execution events (
authorizing,approving-token,signing-permit,transaction-submitted,waiting-for-receipt) instead of hardcoding provider behavior. - If you call
build(...)directly, execute transactions in the returned order as-is.
Operational guidance for production teams
- Keep
apiUrlexplicit per environment (dev/staging/prod). - Set sensible quote/handle timeouts and surface clear retry messaging.
- Persist
quoteId, provider, selected route, and tx hash for support traceability. - Log SDK events through your app logger for observability and incident triage.
- Validate account-chain compatibility before calling
handle(...). - Use simulation/preflight checks in wallet UX when available.
Example web app
A reference integration exists in examples/web.
Run locally
# from repository root
npm install
cd examples/web
npm install
npm run devEnvironment
examples/web/.env uses:
VITE_PROXY_TARGET: upstream host for API and RPC proxying.VITE_API_URL: API path consumed byCoreDex(/proxy/v3/publicin dev).VITE_SOLANA_RPC_URL: proxied Solana RPC path (/solana-rpcin dev).
examples/web/vite.config.ts configures proxy routes for /proxy and /solana-rpc.
License
ISC
