Repository Public
1import { encodeFunctionData, erc20Abi, serializeTransaction, type TransactionSerializableEIP1559 } from 'viem';2import { baseUnits } from './amounts.ts';3import { normalizeAddress } from './address.ts';4import { getAsset, getNetwork } from './networks.ts';5import type { Intent } from './intent.ts';67/** Rebuild the exact unsigned EIP-1559 transaction from an intent. Only native + allowlisted ERC-20 transfer. */8export function buildEvmTx(i: Intent): TransactionSerializableEIP1559 {9 const n = getNetwork(i.network);10 if (n.family !== 'evm' || !i.evm) throw new Error('not an EVM intent');11 if (i.swap) throw new Error('this is a swap intent, not a transfer');12 if (i.bridge) throw new Error('this is a bridge intent, not a transfer');13 if (i.xswap) throw new Error('this is a cross-chain swap intent, not a transfer');14 if (i.chainRef !== n.chainRef) throw new Error('chain id mismatch');15 const asset = getAsset(i.network, i.asset); // throws when token is not allowlisted16 if (asset.decimals !== i.decimals || asset.symbol !== i.symbol) throw new Error('asset metadata mismatch');17 const to = normalizeAddress(i.network, i.to) as `0x${string}`;18 if (to !== i.to) throw new Error('destination not normalized');19 const amount = baseUnits(i.amount);20 if (amount <= 0n) throw new Error('amount must be positive');21 const gas = baseUnits(i.evm.gasLimit), maxFeePerGas = baseUnits(i.evm.maxFeePerGas), tip = baseUnits(i.evm.maxPriorityFeePerGas);22 if (tip > maxFeePerGas) throw new Error('priority fee above max fee');23 if (gas * maxFeePerGas > baseUnits(i.maxFee)) throw new Error('fee exceeds approved maximum');24 const common = { type: 'eip1559' as const, chainId: Number(n.chainRef), nonce: i.evm.nonce, gas, maxFeePerGas, maxPriorityFeePerGas: tip };25 if (asset.id === 'native') {26 if (gas !== 21000n) throw new Error('native transfer must use 21000 gas (EOA destinations only)');27 return { ...common, to, value: amount };28 }29 if (gas > 200_000n) throw new Error('gas limit too high for ERC-20 transfer');30 if (to.toLowerCase() === asset.id.toLowerCase()) throw new Error('destination is the token contract');31 return {32 ...common, to: asset.id as `0x${string}`, value: 0n,33 data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, amount] }),34 };35}3637export const serializeEvmUnsigned = (i: Intent) => serializeTransaction(buildEvmTx(i));38