import { EthExecutionAPI, Numbers, Transaction, Web3Context, Web3Eth } from "web3"; import { Contract } from "web3-eth-contract"; import { GaslessSwapRouterAbi } from "../abi/GaslessSwapRouter.js"; import { RegistryAbi } from "../abi/Registry.js"; import { ERC20Abi } from "../abi/ERC20.js"; // GaslessSwapRouterAddress registry key // https://github.com/kaiachain/kaia/blob/v2.0.0/contracts/contracts/system_contracts/multicall/MultiCallContract.sol#L140 const GASLESS_SWAP_ROUTER_NAME = "GaslessSwapRouter"; const REGISTRY_ADDRESS = "0x0000000000000000000000000000000000000401"; const MAX_UINT256 = '0x' + 'f'.repeat(64); // Using fixed constants to simplify amountRepay calculation. const GAS_LIMIT_LEND = 21000; const GAS_LIMIT_APPROVE = 100000; const GAS_LIMIT_SWAP = 500000; export type GaslessSwapRouter = Contract; export async function getGaslessSwapRouter( context: Web3Context, address?: string ): Promise { let contractAddress: string; if (!address) { // Read from the KIP-149 registry. const registry = new Contract(RegistryAbi, REGISTRY_ADDRESS, context); const address = await registry.methods.getActiveAddr(GASLESS_SWAP_ROUTER_NAME).call(); if (!address) { throw new Error("GaslessSwapRouter not found in the registry"); } contractAddress = address; } else { // Use the custom address. contractAddress = address; } return new Contract(GaslessSwapRouterAbi, contractAddress, context); } export function getAmountRepay( approveRequired: boolean, gasPrice: Numbers, ): bigint { const gasPriceBN = BigInt(gasPrice); const lendTxGas = BigInt(GAS_LIMIT_LEND); const approveTxGas = approveRequired ? BigInt(GAS_LIMIT_APPROVE) : BigInt(0); const swapTxGas = BigInt(GAS_LIMIT_SWAP); const R1 = gasPriceBN * lendTxGas; const R2 = gasPriceBN * approveTxGas; const R3 = gasPriceBN * swapTxGas; return R1 + R2 + R3; } export function getMinAmountOut( amountRepay: Numbers, appTxFee: Numbers, commissionRateBps: Numbers, ): bigint { const amountRepayBN = BigInt(amountRepay); const appTxFeeBN = BigInt(appTxFee); const commissionRateBpsBN = BigInt(commissionRateBps); const denominator = BigInt(10000); if (commissionRateBpsBN < 0 || commissionRateBpsBN >= 10000) { throw new Error("Commission rate must be between 0 and 9999 basis points"); } // minAmountOut = appTxFee / (1 - commissionRate) + amountRepay // i.e. (amountOut - amountRepay) * (1 - commissionRate) >= appTxFee // because the swap output has to be enough to repay and pay the commission. const beforeCommission = appTxFeeBN * denominator / (denominator - commissionRateBpsBN); return beforeCommission + amountRepayBN; } export async function getAmountIn( router: GaslessSwapRouter, tokenAddress: string, minAmountOut: Numbers, slippageBps: Numbers, ): Promise { const minAmountOutBN = BigInt(minAmountOut); const slippageBpsBN = BigInt(slippageBps); const denominator = BigInt(10000); // minAmountIn = DEX.getAmountIn(tokenAddress, minAmountOut * (1 + slippage)) // Have DEX calculate the required input to produce minAmountOut plus slippage. const withSlippage = minAmountOutBN * (denominator + slippageBpsBN) / denominator; const amountIn = await router.methods.getAmountIn(tokenAddress, withSlippage).call(); return BigInt(amountIn); } export async function getApproveTx( context: Web3Context, eth: Web3Eth, fromAddress: string, tokenAddress: string, routerAddress: string, gasPrice: Numbers, ): Promise { const token = new Contract(ERC20Abi, tokenAddress, context); const data = token.methods.approve(routerAddress, MAX_UINT256).encodeABI(); const nonce = await eth.getTransactionCount(fromAddress); const tx: Transaction = { type: 0, from: fromAddress, to: tokenAddress, nonce: nonce, gas: GAS_LIMIT_APPROVE, gasPrice: gasPrice, value: 0n, data: data, }; return tx; } export async function getSwapTx( eth: Web3Eth, fromAddress: string, tokenAddress: string, routerAddress: string, amountIn: Numbers, minAmountOut: Numbers, amountRepay: Numbers, gasPrice: Numbers, approveRequired: boolean, deadlineBuffer: Numbers, ): Promise { const latestBlock = await eth.getBlock("latest"); if (!latestBlock) { throw new Error("Failed to get latest block"); } const deadlineTimestamp = latestBlock.timestamp + BigInt(deadlineBuffer); let nonce = await eth.getTransactionCount(fromAddress); if (approveRequired) { nonce += 1n; } const routerInterface = new Contract(GaslessSwapRouterAbi); const data = routerInterface.methods.swapForGas( tokenAddress, amountIn, minAmountOut, amountRepay, deadlineTimestamp, ).encodeABI(); const tx: Transaction = { type: 0, from: fromAddress, to: routerAddress, nonce: nonce, gas: GAS_LIMIT_SWAP, gasPrice: gasPrice, value: 0n, data: data, }; return tx; }