0x0000…00e4

All memos sent from and to 0x0000…00e4.

<!doctype html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>zSwap</title> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400' width='400' height='400'%3E%3Crect width='400' height='400' fill='%23000'/%3E%3CclipPath id='frame'%3E%3Crect width='400' height='400'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23frame)'%3E%3Cpath d='M-60-20L460-20L460 90L80 310L460 310L460 420L-60 420L-60 310L320 90L-60 90Z' fill='white'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml"> <style> body{font-family:system-ui,sans-serif;min-height:100vh;margin:0;padding:1em;display:flex;flex-direction:column;box-sizing:border-box} .card{border-radius:1.25em;padding:1.25em;background:#fff;max-width:23em;width:100%;margin:auto;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.06)} .meta{display:flex;justify-content:space-between;align-items:center;font-size:.85em;color:#666;margin-bottom:.75em} .meta input{width:3em;font-size:.9em;padding:.15em .1em;border:0;border-bottom:1px solid #ddd;border-radius:0;background:transparent;text-align:right;outline:0;transition:border-color .15s;-moz-appearance:textfield} .meta input::-webkit-outer-spin-button,.meta input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} .meta input:hover{border-bottom-color:#888} .meta input:focus{border-bottom-color:#000} .panel{background:#f5f5f5;border-radius:.9em;padding:.85em 1em;margin:.4em 0} .panel small{color:#888;font-size:.8em} .hdr small{text-transform:uppercase;letter-spacing:.08em;font-size:.7em} .row{display:flex;gap:.4em;align-items:center;margin-top:.35em} .panel input{font-size:1.4em;border:0;background:transparent;outline:none;padding:0;flex:1;min-width:0;width:100%;font-variant-numeric:tabular-nums;transition:font-size .1s} .pill{display:inline-flex;align-items:center;gap:.4em;cursor:pointer;transition:opacity .15s} .pill:hover,.pill:focus-within{opacity:.6} .panel select{appearance:none;-webkit-appearance:none;border:0;background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 0 center;padding:0 .85em 0 0;font-size:.9em;font-weight:600;color:#222;letter-spacing:.02em;cursor:pointer} .panel select:focus{outline:0} svg{vertical-align:middle;flex-shrink:0} .flip{display:flex;align-items:center;justify-content:center;margin:-.7em auto;width:2.1em;height:2.1em;border-radius:50%;border:4px solid #fff;background:#fff;cursor:pointer;color:#888;padding:0;box-shadow:0 1px 2px rgba(0,0,0,.06);transition:background .15s,color .15s,transform .25s} .flip:hover{background:#ebebeb;color:#222;transform:rotate(180deg)} .primary{width:100%;padding:.95em;font-size:1em;border-radius:.9em;background:#000;color:#fff;border:0;cursor:pointer;margin-top:.75em;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:background .15s} .primary:hover:not(:disabled){background:#333} .primary:disabled{background:#ccc;cursor:not-allowed} #stat{font-size:.8em;color:#666;word-break:break-all;margin-top:.75em;min-height:1em;text-align:center} #stat a{color:#000;text-decoration:underline} .hdr{display:flex;justify-content:space-between;align-items:center;gap:.5em} #bal,#rate{display:block;font-size:.75em;margin-top:.5em;text-align:right} #bal:empty,#rate:empty{display:none} #bal a{color:#000;cursor:pointer;text-decoration:underline;font-weight:600} </style> <div class="card"> <div class="meta"><span id="addr">Not connected</span><label>Slippage <input id="slip" type="number" value="0.5" min="0.01" max="10" step="0.1">%</label></div> <div class="panel"><div class="hdr"><small>You pay</small><span class="pill"><span id="fromIcon"></span><select id="fromSel"></select></span></div><div class="row"><input id="amt" type="text" inputmode="decimal" placeholder="0.0"></div><small id="bal"></small></div> <button id="flip" class="flip" title="Flip"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14m-5-5 5 5 5-5"/></svg></button> <div class="panel"><div class="hdr"><small>You receive</small><span class="pill"><span id="toIcon"></span><select id="toSel"></select></span></div><div class="row"><input id="outAmt" type="text" inputmode="decimal" placeholder="0.0"></div><small id="rate"></small></div> <button id="swap" class="primary">Connect Wallet</button> <div id="stat"></div> </div> <script> const ZQUOTER="0x000000a7DfdD39f4D74c7b201501eaD119F8b86C"; const ZROUTER="0x000000000000FB114709235f1ccBFfb925F600e4"; const ZERO="0x0000000000000000000000000000000000000000"; const WETH="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; const SEL_BUILDSWAPAUTO="98d7d292"; const SEL_ALLOWANCE="dd62ed3e"; const SEL_APPROVE="095ea7b3"; const SEL_BALANCEOF="70a08231"; const C="eth_call",S="eth_sendTransaction",L="latest",I="eth_chainId"; const SOURCES=["UniV2","Sushi","zAMM","UniV3","UniV4","Curve","Lido"]; const ETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>`; const USDC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#2775CA" cx="16" cy="16" r="16"/><g fill="#FFF"><path d="M20.022 18.124c0-2.124-1.28-2.852-3.84-3.156-1.828-.243-2.193-.728-2.193-1.578 0-.85.61-1.396 1.828-1.396 1.097 0 1.707.364 2.011 1.275a.458.458 0 00.427.303h.975a.416.416 0 00.427-.425v-.06a3.04 3.04 0 00-2.743-2.489V9.142c0-.243-.183-.425-.487-.486h-.915c-.243 0-.426.182-.487.486v1.396c-1.829.242-2.986 1.456-2.986 2.974 0 2.002 1.218 2.791 3.778 3.095 1.707.303 2.255.668 2.255 1.639 0 .97-.853 1.638-2.011 1.638-1.585 0-2.133-.667-2.316-1.578-.06-.242-.244-.364-.427-.364h-1.036a.416.416 0 00-.426.425v.06c.243 1.518 1.219 2.61 3.23 2.914v1.457c0 .242.183.425.487.485h.915c.243 0 .426-.182.487-.485V21.34c1.829-.303 3.047-1.578 3.047-3.217z"/><path d="M12.892 24.497c-4.754-1.7-7.192-6.98-5.424-11.653.914-2.55 2.925-4.491 5.424-5.402.244-.121.365-.303.365-.607v-.85c0-.242-.121-.424-.365-.485-.061 0-.183 0-.244.06a10.895 10.895 0 00-7.13 13.717c1.096 3.4 3.717 6.01 7.13 7.102.244.121.488 0 .548-.243.061-.06.061-.122.061-.243v-.85c0-.182-.182-.424-.365-.546zm6.46-18.936c-.244-.122-.488 0-.548.242-.061.061-.061.122-.061.243v.85c0 .243.182.485.365.607 4.754 1.7 7.192 6.98 5.424 11.653-.914 2.55-2.925 4.491-5.424 5.402-.244.121-.365.303-.365.607v.85c0 .242.121.424.365.485.061 0 .183 0 .244-.06a10.895 10.895 0 007.13-13.717c-1.096-3.46-3.778-6.07-7.13-7.162z"/></g></g></svg>`; const USDT_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#26A17B"/><path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/></g></svg>`; const WBTC_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#F7931A"/><path fill="#FFF" fill-rule="nonzero" d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z"/></g></svg>`; const WSTETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><g fill="none"><circle fill="#00A3FF" cx="16" cy="16" r="16"/><path d="M9.437 14.864l-.181.275c-2.048 3.097-1.603 7.253 1.034 9.824 1.561 1.521 3.622 2.353 5.683 2.353 0 0 0 0-6.536-12.452z" fill="#FFF"/><path opacity=".6" d="M15.997 18.611l-6.56-3.747c6.56 12.452 6.56 12.452 6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".6" d="M22.563 14.864l.181.275c2.048 3.097 1.603 7.253-1.034 9.824-1.561 1.521-3.622 2.353-5.683 2.353 0 0 0 0 6.536-12.452z" fill="#FFF"/><path opacity=".2" d="M16.003 18.611l6.56-3.747c-6.56 12.452-6.56 12.452-6.56 12.452 0-2.683 0-5.623 0-8.705z" fill="#FFF"/><path opacity=".2" d="M16.004 10.239v6.459l5.654-3.23-5.654-3.229z" fill="#FFF"/><path opacity=".6" d="M16.005 10.239l-5.655 3.229 5.655 3.23v-6.46z" fill="#FFF"/><path d="M16.005 4.805l-5.655 8.668 5.655-3.233V4.805z" fill="#FFF"/><path opacity=".6" d="M16.004 10.238l5.658 3.23-5.658-8.674v5.444z" fill="#FFF"/></g></svg>`; const RETH_ICON=`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#ED5A37"/><path d="M16 5.75L9.89 15.85 16 19.45l6.08-3.6z" fill="#FFF"/><path d="M16 20.6l-6.11-3.59L16 25.58l6.08-8.57z" fill="#FFF"/></svg>`; const BOLD_ICON=`<svg width="20" height="20" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#63D77D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.1719 4.56641H8.58203V26.1016H15.7617V25.2422C16.8398 25.793 18.0586 26.1055 19.3555 26.1055C23.7148 26.1055 27.25 22.5703 27.25 18.207C27.25 13.8438 23.7148 10.3086 19.3555 10.3086C18.0586 10.3086 16.8398 10.6211 15.7617 11.1719V4.56641H12.1719ZM15.7617 11.1719C13.207 12.4805 11.457 15.1406 11.457 18.207C11.457 21.2734 13.207 23.9336 15.7617 25.2422V11.1719Z" fill="#1C1D4F"/></svg>`; const TOKENS=[ {sym:"ETH", addr:ZERO, dec:18, icon:ETH_ICON}, {sym:"WETH", addr:WETH, dec:18, icon:ETH_ICON}, {sym:"wstETH",addr:"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",dec:18,icon:WSTETH_ICON}, {sym:"rETH", addr:"0xae78736Cd615f374D3085123A210448E74Fc6393",dec:18,icon:RETH_ICON}, {sym:"WBTC", addr:"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",dec:8, icon:WBTC_ICON}, {sym:"USDC", addr:"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",dec:6, icon:USDC_ICON}, {sym:"USDT", addr:"0xdAC17F958D2ee523a2206206994597C13D831ec7",dec:6, icon:USDT_ICON}, {sym:"BOLD", addr:"0x6440f144b7e50D6a8439336510312d2F54beB01D",dec:18,icon:BOLD_ICON}, ]; const strip0x=h=>h.startsWith("0x")?h.slice(2):h; const pad32=h=>strip0x(h).padStart(64,"0"); const encUint=v=>pad32(BigInt(v).toString(16)); const encAddr=a=>pad32(a.toLowerCase()); const encBool=b=>pad32(b?"1":"0"); const toHex=v=>"0x"+BigInt(v).toString(16); const parseUnits=(s,dec)=>{ const t=s.trim().replace(/,/g,""); const neg=t.startsWith("-"); const body=neg?t.slice(1):t; if(!/^(?:\d+|\d*\.\d+|\d+\.)$/.test(body))throw Error("invalid amount"); const[ip,fp=""]=body.split("."); if(fp.length>dec)throw Error("too many decimals"); const v=BigInt((ip||"0")+fp.padEnd(dec,"0")); return neg?-v:v; }; const formatUnits=(v,dec)=>{ if(!dec)return BigInt(v).toString(); const s=BigInt(v).toString().padStart(dec+1,"0"); const ip=s.slice(0,-dec); const fp=s.slice(-dec).replace(/0+$/,""); return fp?`${ip}.${fp}`:ip; }; const trimAmt=(v,dec)=>{const s=formatUnits(v,dec),i=s.indexOf(".");if(i<0)return s;const r=s.slice(0,i+7).replace(/\.?0+$/,"");return r==="0"&&v?s:r}; const fitFont=el=>{ const l=el.value.length; el.style.fontSize=l>18?".9em":l>14?"1.1em":l>10?"1.25em":"1.4em"; }; const decodeBestSwap=hex=>{ const h=strip0x(hex); if(h.length<448)throw Error("bad quote"); const num=i=>BigInt("0x"+h.slice(i*64,(i+1)*64)); const off=Number(num(4))*2; if(off<448||off+64>h.length)throw Error("bad quote"); const cdLen=Number(BigInt("0x"+h.slice(off,off+64))); if(cdLen===0||off+64+cdLen*2>h.length)throw Error("bad quote"); return{ best:{source:Number(num(0)),feeBps:num(1),amountIn:num(2),amountOut:num(3)}, callData:"0x"+h.slice(off+64,off+64+cdLen*2), amountLimit:num(5), msgValue:num(6), }; }; const rpc=(method,params)=>window.ethereum.request({method,params}); const waitTx=async hash=>{ const end=Date.now()+600000; while(Date.now()<end){ const r=await rpc("eth_getTransactionReceipt",[hash]); if(r){ if(r.status!=="0x1")throw Error("tx reverted: "+hash); return r; } await new Promise(res=>setTimeout(res,1500)); } throw Error("tx not mined in time"); }; const checkWallet=async()=>{const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]);if(+c!==1||a[0]?.toLowerCase()!==account.toLowerCase())throw Error("wallet changed")}; const hexToBytes=h=>{const s=strip0x(h);const o=new Uint8Array(s.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(s.slice(i*2,i*2+2),16);return o}; const decodeString=hex=>{ const h=strip0x(hex); if(h.length<=64)return new TextDecoder().decode(hexToBytes(h)).replace(/\0+$/,""); const off=Number(BigInt("0x"+h.slice(0,64)))*2; const len=Number(BigInt("0x"+h.slice(off,off+64)))*2; return new TextDecoder().decode(hexToBytes(h.slice(off+64,off+64+len))); }; const genIcon=sym=>`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#999"/><text x="16" y="21" text-anchor="middle" fill="#fff" font-size="13" font-weight="600" font-family="system-ui">${(sym[0]||"?").toUpperCase()}</text></svg>`; const STORE="zswap:custom"; async function addCustomToken(a){ a=a.toLowerCase(); if(!/^0x[0-9a-f]{40}$/.test(a))throw Error("invalid address"); const i=TOKENS.findIndex(t=>t.addr.toLowerCase()===a); if(i>=0)return i; const[symRet,decRet]=await Promise.all([ rpc(C,[{to:a,data:"0x95d89b41"},L]), rpc(C,[{to:a,data:"0x313ce567"},L]), ]); const sym=decodeString(symRet).trim().replace(/[<>&"'`]/g,"").slice(0,16)||"?"; const dec=Number(BigInt(decRet)); if(!Number.isInteger(dec)||dec<0||dec>36)throw Error("unsupported decimals"); TOKENS.push({sym,addr:a,dec,icon:genIcon(sym)}); try{const arr=JSON.parse(localStorage.getItem(STORE)||"[]");arr.push({sym,addr:a,dec});localStorage.setItem(STORE,JSON.stringify(arr))}catch{} return TOKENS.length-1; } try{for(const t of JSON.parse(localStorage.getItem(STORE)||"[]")){ const addr=(t?.addr||"").toLowerCase(); const dec=+t?.dec; if(/^0x[0-9a-f]{40}$/.test(addr)&&Number.isInteger(dec)&&dec>=0&&dec<=36&&!TOKENS.some(x=>x.addr.toLowerCase()===addr)){ const sym=String(t.sym||"?").replace(/[<>&"'`]/g,"").slice(0,16)||"?"; TOKENS.push({sym,addr,dec,icon:genIcon(sym)}); } }}catch{} let account=null,last=null,fromBalance=0n,mode="in",bSeq=0; async function refreshBalance(){ if(!account){bal.textContent="";fromBalance=0n;render();return} const f=TOKENS[fromSel.value]; const my=++bSeq; try{ const hex=f.addr===ZERO ?await rpc("eth_getBalance",[account,L]) :await rpc(C,[{to:f.addr,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]); if(my!==bSeq)return; fromBalance=BigInt(hex); const n=+formatUnits(fromBalance,f.dec); const pretty=n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):n.toPrecision(4); bal.innerHTML=`Balance: <a href="#">${pretty}</a>`; }catch{if(my===bSeq){bal.textContent="";fromBalance=0n}} render(); } const optsHtml=()=>TOKENS.map((t,i)=>`<option value="${i}">${t.sym}</option>`).join("")+`<option value="__custom">+ Custom token…</option>`; const rebuild=()=>{for(const s of[fromSel,toSel]){const v=s.value;s.innerHTML=optsHtml();s.value=v}}; const syncDisabled=()=>{ for(const opt of fromSel.options)opt.disabled=(opt.value===toSel.value&&opt.value!=="__custom"); for(const opt of toSel.options)opt.disabled=(opt.value===fromSel.value&&opt.value!=="__custom"); }; fromSel.innerHTML=optsHtml();fromSel.value=0; toSel.innerHTML=optsHtml();toSel.value=5; syncDisabled(); const syncIcons=()=>{fromIcon.innerHTML=TOKENS[fromSel.value]?.icon||"";toIcon.innerHTML=TOKENS[toSel.value]?.icon||""}; const render=()=>{ if(!account){swap.textContent="Connect Wallet";swap.disabled=false;return} if(!last){swap.textContent="Swap";swap.disabled=true;return} if(last.amountIn>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return} swap.textContent="Swap";swap.disabled=false; }; syncIcons(); render(); const isRejection=e=>e?.code===4001||/user\s+reject|user\s+denied/i.test(e?.message||""); async function connect(){ if(!window.ethereum){stat.textContent="No wallet detected.";return} let accs; try{accs=await rpc("eth_requestAccounts",[])} catch(e){stat.textContent=isRejection(e)?"":"Error: "+(e.message||e);return} let chainId=await rpc(I,[]); if(+chainId!==1){ try{await rpc("wallet_switchEthereumChain",[{chainId:"0x1"}])} catch(e){stat.textContent=isRejection(e)?"":"Switch to Ethereum mainnet.";return} } if(!accs[0])return; account=accs[0]; addr.textContent=account.slice(0,6)+"..."+account.slice(-4); refreshBalance(); update(); } bal.addEventListener("click",async e=>{ if(e.target.tagName!=="A")return; e.preventDefault(); const f=TOKENS[fromSel.value]; let reserve=0n; if(f.addr===ZERO){ try{reserve=BigInt(await rpc("eth_gasPrice",[]))*900000n}catch{} } const max=fromBalance>reserve?fromBalance-reserve:0n; amt.value=formatUnits(max,f.dec); setMode("in");fitFont(amt); update(); }); let seq=0; const txLink=(h,label)=>`${label} <a href="https://etherscan.io/tx/${h}" target="_blank" rel="noreferrer">${h.slice(0,10)}…${h.slice(-6)}</a>`; async function update(){ const my=++seq; last=null; rate.textContent=""; render(); syncIcons(); const f=TOKENS[fromSel.value], t=TOKENS[toSel.value]; if(!f||!t)return; if(f.addr===t.addr){stat.textContent="Pick different tokens.";return} stat.textContent=""; const isIn=mode==="in"; const inEl=isIn?amt:outAmt, outEl=isIn?outAmt:amt, inTok=isIn?f:t; outEl.value=""; fitFont(outEl); if(!inEl.value.trim())return; let swapAmt; try{swapAmt=parseUnits(inEl.value.trim(),inTok.dec)}catch{return} if(swapAmt<=0n)return; if((f.addr===ZERO&&t.addr===WETH)||(f.addr===WETH&&t.addr===ZERO)){ outEl.value=inEl.value;fitFont(outEl); rate.textContent=`1 ${f.sym} = 1 ${t.sym}`; last={callData:f.addr===ZERO?"0xd0e30db0":"0x2e1a7d4d"+encUint(swapAmt),msgValue:f.addr===ZERO?swapAmt:0n,amountIn:swapAmt,from:f,to:WETH,exp:Date.now()+1500000}; render();return; } const pct=Math.min(10,Math.max(.01,+slip.value||.5)); const slipBps=BigInt(Math.round(pct*100)); const deadline=BigInt(Math.floor(Date.now()/1e3)+1800); outEl.value="...";fitFont(outEl); const acct=account; try{ const data="0x"+SEL_BUILDSWAPAUTO +encAddr(acct||ZERO)+encBool(!isIn)+encAddr(f.addr)+encAddr(t.addr) +encUint(swapAmt)+encUint(slipBps)+encUint(deadline); const ret=await rpc(C,[{to:ZQUOTER,data},L]); if(my!==seq)return; const r=decodeBestSwap(ret); if(r.best.amountIn<=0n||r.best.amountOut<=0n||(!isIn&&r.amountLimit<r.best.amountIn))throw Error("bad quote"); const finalIn=r.best.amountIn; const finalOut=r.best.amountOut; if(isIn){outAmt.value=trimAmt(finalOut,t.dec);fitFont(outAmt)} else{amt.value=trimAmt(finalIn,f.dec);fitFont(amt)} const rx=(+formatUnits(finalOut,t.dec))/(+formatUnits(finalIn,f.dec)); const s=r.best.source,raw=SOURCES[s]||"?"; const src=(s===3||s===4)&&r.best.feeBps?`${raw} ${Number(r.best.feeBps)/100}%`:raw; rate.textContent=isFinite(rx)?`1 ${f.sym} ≈ ${rx>=1?rx.toLocaleString("en-US",{maximumFractionDigits:4}):rx.toPrecision(4)} ${t.sym} · ${src}`:""; rate.textContent+=isIn?` · Min ${trimAmt(r.amountLimit,t.dec)} ${t.sym}`:` · Max ${trimAmt(r.amountLimit,f.dec)} ${f.sym}`; const inApprove=isIn?swapAmt:r.amountLimit; if(acct)last={callData:r.callData,msgValue:r.msgValue,amountIn:inApprove,from:f,exp:Date.now()+1500000}; render(); }catch(e){if(my===seq){outEl.value="";fitFont(outEl);stat.textContent="No route: "+(e.message||e)}} } let debounceT; const updateSoon=()=>{clearTimeout(debounceT);debounceT=setTimeout(update,250)}; const setMode=m=>{mode=m;const o=m==="in"?outAmt:amt;o.value="";fitFont(o);last=null;render()}; amt.addEventListener("input",()=>{setMode("in");fitFont(amt);updateSoon()}); outAmt.addEventListener("input",()=>{setMode("out");fitFont(outAmt);updateSoon()}); slip.addEventListener("input",()=>{last=null;render();updateSoon()}); slip.addEventListener("change",()=>{slip.value=Math.min(10,Math.max(.01,+slip.value||.5))}); for(const sel of [fromSel,toSel]){ sel.dataset.prev=sel.value; sel.addEventListener("change",async()=>{ if(sel.value!=="__custom"){ sel.dataset.prev=sel.value; syncDisabled(); if(sel===fromSel)refreshBalance(); update(); return; } last=null;render(); const prev=sel.dataset.prev; const a=(prompt("Paste token address:")||"").trim(); if(!a){sel.value=prev;update();return} stat.textContent="Loading token..."; try{ const idx=await addCustomToken(a); rebuild(); const otherSel=sel===fromSel?toSel:fromSel; if(String(idx)===otherSel.value){otherSel.value=prev;otherSel.dataset.prev=prev} sel.value=String(idx); sel.dataset.prev=sel.value; stat.textContent=""; syncDisabled(); refreshBalance(); update(); }catch(e){ stat.textContent="Couldn't load token: "+(e.message||e); sel.value=prev; } }); } flip.onclick=()=>{const a=fromSel.value,b=toSel.value,v=outAmt.value.replace(/,/g,"");fromSel.value=b;toSel.value=a;if(v&&!isNaN(+v))amt.value=v;setMode("in");fitFont(amt);syncDisabled();refreshBalance();update()}; window.ethereum?.on?.("chainChanged",()=>location.reload()); window.ethereum?.on?.("accountsChanged",()=>location.reload()); (async()=>{ if(!window.ethereum)return; try{ const accs=await rpc("eth_accounts",[]); const cid=await rpc(I,[]); if(accs[0]&&+cid===1){ account=accs[0]; addr.textContent=account.slice(0,6)+"..."+account.slice(-4); refreshBalance();update(); } }catch{} })(); swap.onclick=async()=>{ if(!account) return connect(); if(!last)return; if(Date.now()>last.exp){stat.textContent="Quote expired — refreshing...";updateSoon();return} swap.disabled=true; try{ const {callData,msgValue,amountIn,from,to}=last; if(msgValue!==(from.addr===ZERO?amountIn:0n))throw Error("bad value"); await checkWallet(); if(from.addr!==ZERO&&!to){ const allowData="0x"+SEL_ALLOWANCE+encAddr(account)+encAddr(ZROUTER); const aHex=await rpc(C,[{to:from.addr,data:allowData},L]); const a=BigInt(aHex); if(a<amountIn){ const n=a>0n?2:1; if(a>0n){ stat.textContent=`Approving (1/${n})...`; const zeroData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(0); const h1=await rpc(S,[{from:account,to:from.addr,data:zeroData}]); await waitTx(h1); } stat.textContent=n>1?`Approving (2/2)...`:"Approving..."; const apprData="0x"+SEL_APPROVE+encAddr(ZROUTER)+encUint(amountIn); const h2=await rpc(S,[{from:account,to:from.addr,data:apprData}]); await waitTx(h2); const nHex=await rpc(C,[{to:from.addr,data:allowData},L]); if(BigInt(nHex)<amountIn)throw Error("approval failed"); } } stat.textContent="Swapping..."; const txReq={from:account,to:to||ZROUTER,data:callData,value:toHex(msgValue)}; await rpc(C,[txReq,L]); await checkWallet(); const tx=await rpc(S,[txReq]); if(!/^0x[0-9a-f]{64}$/i.test(tx))throw Error("bad tx"); stat.innerHTML=txLink(tx,"Sent"); await waitTx(tx); stat.innerHTML=txLink(tx,"Done"); last=null; refreshBalance(); render(); }catch(e){stat.textContent=isRejection(e)?"":"Error: "+(e.message||e);render()} }; </script>