import { encodeFunctionData, getAddress, parseAbi, zeroAddress } from 'viem';
const chainId = 4663;
const abi = parseAbi([
'struct Leg { address adapter; address tokenOut; uint24 fee; int24 tickSpacing; address hooks; bool nativePool; }',
'struct Branch { uint256 amountIn; Leg[] legs; }',
'function swap(address tokenIn,address tokenOut,uint256 amountIn,uint256 minOut,address recipient,uint256 deadline,Branch[] branches) payable returns(uint256 amountOut)',
]);
const same = (a, b) => getAddress(a) === getAddress(b);
async function json(base, path, options = {}) {
const response = await fetch(new URL(path, base), {
...options,
signal: AbortSignal.timeout(20000),
redirect: 'error',
});
const body = await response.json();
if (!response.ok) {
const error = new Error(body.error?.message ?? `HTTP ${response.status}`);
error.code = body.error?.code;
error.retryAfter = response.headers.get('Retry-After');
error.requestId = body.error?.requestId;
throw error;
}
return body;
}
export async function prepareNativeSwap({
base = 'https://api.route.fun',
recipient,
tokenOut,
amountIn,
slippageBps = 50,
}) {
recipient = getAddress(recipient);
tokenOut = getAddress(tokenOut);
if (recipient === zeroAddress || tokenOut === zeroAddress)
throw new Error('Use a nonzero recipient and ERC-20 output');
if (!/^[1-9][0-9]*$/.test(amountIn) || BigInt(amountIn) > (1n << 127n) - 1n)
throw new Error('Input must be a positive base-unit integer within the API limit');
if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > 500)
throw new Error('Slippage must be between 0 and 500 bps');
const config = await json(base, '/api/v2/config');
if (config.chainId !== chainId || config.settlementMode !== 'fee-free' ||
config.protocolFeeBps !== 0 || !config.enabledProviders?.route)
throw new Error('This recipe requires the enabled fee-free direct V2 release');
const executor = getAddress(config.tokenOutputExecutors?.route ?? config.executors.route);
if (executor === zeroAddress) throw new Error('Direct settlement is unavailable');
const query = new URLSearchParams({
chainId: String(chainId), tokenIn: 'ETH', tokenOut, amountIn, provider: 'route',
});
const quote = await json(base, `/api/v2/quote?${query}`);
if (quote.chainId !== chainId || quote.provider !== 'route' ||
!same(quote.tokenIn, zeroAddress) || !same(quote.tokenOut, tokenOut) ||
quote.amountIn !== amountIn || quote.execution?.status !== 'simulated' ||
quote.protocolFeeBps !== 0 || quote.feeAmount !== '0' ||
quote.amountOut !== quote.grossAmountOut)
throw new Error('Quote does not match the requested fee-free swap');
const now = () => Math.floor(Date.now() / 1000);
if (!Number.isSafeInteger(quote.expiresAt) || quote.expiresAt <= now())
throw new Error('Quote expired; request and review another');
const minimum = BigInt(quote.execution.amountOut) * BigInt(10000 - slippageBps) / 10000n;
if (minimum <= 0n) throw new Error('Minimum is too small');
const deadline = now() + 300;
const request = {
chainId, provider: 'route', tokenIn: zeroAddress, tokenOut, amountIn,
amountOutMinimum: String(minimum), recipient, deadline,
expiresAt: quote.expiresAt, branches: quote.branches,
};
const prepared = await json(base, '/api/v2/swap', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
const expectedData = encodeFunctionData({
abi, functionName: 'swap',
args: [zeroAddress, tokenOut, BigInt(amountIn), minimum, recipient, BigInt(deadline),
quote.branches.map((b) => ({ ...b, amountIn: BigInt(b.amountIn) }))],
});
if (prepared.chainId !== chainId || prepared.approval !== null ||
prepared.simulationRequired !== true || prepared.protocolFeeBps !== 0 ||
prepared.feeAmount !== '0' || prepared.amountOut !== prepared.grossAmountOut ||
BigInt(prepared.amountOut) < minimum ||
!same(prepared.transaction.to, executor) || prepared.transaction.value !== amountIn ||
prepared.transaction.data.toLowerCase() !== expectedData.toLowerCase() ||
quote.expiresAt <= now())
throw new Error('Prepared transaction changed reviewed terms or expired');
return {
transaction: prepared.transaction,
simulationRequired: true,
review: { chainId, executor, tokenIn: zeroAddress, tokenOut, amountIn,
recipient, amountOutMinimum: String(minimum), deadline, expiresAt: quote.expiresAt },
preparedAmountOut: prepared.amountOut,
};
}