memoscan
← all memos

Memo 0xce85d9b7…3b8250 on Ethereum

0x68575b07…67c7·#25,913,805·view on Etherscan
onst symbol = sh ? decodeStringLoose(sh) : ''; const name = nh ? decodeStringLoose(nh) : ''; let decimals = 18, ok=false; if(dh){ try { decimals=Number(decode(['uint256'],dh)[0]); ok=true; } catch(e){} } if(!(decimals>=0&&decimals<=36)) { decimals=18; ok=false; } return S.meta[k]={symbol:symbol||'???', name:name||symbol||'???', decimals, ok:ok&&!!symbol}; } async function refreshBalance(){ if(!S.account||!S.token){ el.balHint.textContent=''; return; } const tok=S.token, acct=S.account; try { let raw; if(tok===ZERO) raw=BigInt(await rpc('eth_getBalance',[acct,'latest'])); else { const [h]=await mc([{to:tok,data:cd(SEL.balanceOf,['address'],[acct])}]); raw = h ? decode(['uint256'],h)[0] : 0n; } if(S.token!==tok||S.account!==acct) return; S.balance=raw; paintPresets(); } catch(e){ } } function hasAtomic(caps){ if(!caps||typeof caps!=='object') return false; const pick = id => { for(const k of Object.keys(caps)){ try { if(BigInt(k)===id) return caps[k]; } catch(e){} } return undefined; }; const c = pick(BigInt(S.chain)) ?? pick(0n); const st = c?.atomic?.status; return st==='supported'||st==='ready'; } async function canBatch(){ if(S.batchCap!==null) return S.batchCap; if(!S.account) return (S.batchCap=false); try { const caps=await rpc('wallet_getCapabilities',[S.account]); return (S.batchCap=hasAtomic(caps)); } catch(e){ return (S.batchCap=false); } } async function sendBatch(calls){ const res = await rpc('wallet_sendCalls',[{ version:'2.0.0', chainId:cfg().hex, from:S.account, atomicRequired:true, calls: calls.map(c=>({to:c.to, data:c.data, value:c.value||'0x0'})), }]); const id = typeof res==='string' ? res : res?.id; if(!id) throw new Error('Wallet returned no batch id'); const end=Date.now()+600000; let wait=1200; while(Date.now()<end){ try { const st=await rpc('wallet_getCallsStatus',[id]); const rs=(st&&st.receipts)||[]; if(rs.some(r=>r&&r.status==='0x0')) throw new Error('BATCH: the batch reverted on chain'); const last=rs[rs.length-1], h=last&&(last.transactionHash||last.hash); if(h) return h; const q=String(st&&st.status); if(q==='400'||q==='500'||/fail|revert|reject/i.test(q)) throw new Error('BATCH: the wallet reported the batch failed'); } catch(e){ if(/^BATCH: /.test(e.message)) throw new Error(e.message.slice(7)); } await sleep(wait); wait=Math.min(wait+400,4000); } throw new Error('Batch not confirmed in time — check your wallet activity before retrying'); } const EIP712_DOMAIN_TH = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'); function domainSeparator(name, version, verifying){ return keccak256('0x'+strip(EIP712_DOMAIN_TH) + strip(keccak256(name)) + strip(keccak256(version)) + word(S.chain) + word(verifying)); } async function permitInfo(token, owner){ const k=token.toLowerCase()+':'+owner.toLowerCase(); const cached = S.permitCache[k]; if(cached === null) return null; if(cached){ const fresh = await permitNonce(token, owner); return fresh===null ? null : {...cached, nonce:fresh}; } let out=null; try { const [ds,th,nc,nm] = await mc([ {to:token,data:SEL.domainSeparator}, {to:token,data:SEL.permitTypehash}, {to:token,data:cd(SEL.nonces,['address'],[owner])}, {to:token,data:SEL.name}, ]); if(ds && ds!=='0x' && nc && nm){ const typehashOk = !th || strip(th).length<64 || strip(th).slice(0,64).toLowerCase()===TYPEHASH_2612; if(typehashOk){ const name=decodeStringLoose(nm); const nonce=decode(['uint256'],nc)[0]; if(name){ for(const ver of ['1','2','1.0','1.1','V1','']){ if(domainSeparator(name,ver,token).toLowerCase()===ds.toLowerCase()){ out={name,version:ver,nonce}; break; } } } } } } catch(e){} S.permitCache[k]=out; return out; } async function permitNonce(token, owner){ try { const [r]=await mc([{to:token,data:cd(SEL.nonces,['address'],[owner])}]); return r ? decode(['uint256'],r)[0] : null; } catch(e){ return null; } } async function signPermit(token, info, owner, spender, value, deadline){ const td = { types:{ EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'}, {name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}], Permit:[{name:'owner',type:'address'},{name:'spender',type:'address'}, {name:'value',type:'uint256'},{name:'nonce',type:'uint256'},{name:'deadline',type:'uint256'}], }, primaryType:'Permit', domain:{name:info.name,version:info.version,chainId:S.chain,verifyingContract:token}, message:{owner,spender,value:value.toString(),nonce:info.nonce.toString(),deadline:deadline.toString()}, }; const sig = strip(await rpc('eth_signTypedData_v4',[owner,JSON.stringify(td)])); if(sig.length!==130) throw new Error('Wallet returned a malformed signature'); let v=parseInt(sig.slice(128,130),16); if(v<27) v+=27; return {v:BigInt(v), r:'0x'+sig.slice(0,64), s:'0x'+sig.slice(64,128)}; } async function slowHasPermit(){ if(S.slowPermit!==null) return S.slowPermit; try { const code = await rpc('eth_getCode',[SLOW,'latest']); S.slowPermit = typeof code==='string' && code.includes(SEL.depositToWithPermit.slice(2)); } catch(e){ S.slowPermit=false; } return S.slowPermit; } async function sendTx(tx){ if(!await ensureChain()) throw new Error(`Switch to ${cfg().name}`); return rpc('eth_sendTransaction',[tx]); } async function waitTx(hash){ const end=Date.now()+600000; let wait=1500, misses=0; while(Date.now()<end){ try { const r=await rpc('eth_getTransactionReceipt',[hash]); misses=0; if(r){ if(r.status!=='0x1') throw new Error('REVERT'); return r; } wait=Math.min(wait+500,6000); } catch(e){ if(e.message==='REVERT') throw new Error('The transaction reverted on chain'); if(++misses>2) wait=Math.min(wait*2,15000); } await sleep(wait); } throw new Error('Not confirmed within 10 minutes — check your wallet before retrying'); } async function slowTx(data, msg, step, value){ txShow(msg, step); const tx={from:S.account,to:SLOW,data}; if(value&&value!=='0x0') tx.value=value; const h=await sendTx(tx); S.lastHash=h; txShow(msg,'Waiting for confirmation',h); await waitTx(h); return h; } const c1 = (sel,id) => cd(sel,['uint256'],[BigInt(id)]); const wfCall = t => cd(SEL.withdrawFrom,['address','address','uint256','uint256'],[t.from,S.account,BigInt(t.tokenId),t.amountRaw]); function depositCalldata(wei, permitSig, deadline){ const isETH = S.token===ZERO; const tokenArg = isETH?ZERO:S.token; const amountArg = isETH?0n:wei; if(permitSig){ return S.autoClaim ? cd(SEL.depositToWithTipAndPermit, ['address','address','uint256','uint96','uint256','bytes','uint256','uint256','uint256','uint256'], [tokenArg,S.resolved,wei,BigInt(S.delay),S.tip,'0x',deadline,permitSig.v,BigInt(permitSig.r),BigInt(permitSig.s)]) : cd(SEL.depositToWithPermit, ['address','address','uint256','uint96','bytes','uint256','uint256','uint256','uint256'], [tokenArg,S.resolved,wei,BigInt(S.delay),'0x',deadline,permitSig.v,BigInt(permitSig.r),BigInt(permitSig.s)]); } return S.autoClaim ? cd(SEL.depositToWithTip,['address','address','uint256','uint96','uint256','bytes'], [tokenArg,S.resolved,wei,BigInt(S.delay),S.tip,'0x']) : cd(SEL.depositTo,['address','address','uint256','uint96','bytes'], [tokenArg,S.resolved,amountArg,BigInt(S.delay),'0x']); } async function planDeposit(wei){ const isETH=S.token===ZERO; if(isETH) return {kind:'direct', calls:1}; let allowance=0n; try { const [h]=await mc([{to:S.token,data:cd(SEL.allowance,['address','address'],[S.account,SLOW])}]); if(h) allowance=decode(['uint256'],h)[0]; } catch(e){} if(allowance>=wei) return {kind:'direct', calls:1, allowance}; if(await canBatch()) return {kind:'batch', calls:allowance>0n?3:2, allowance}; if(await slowHasPermit() && await permitInfo(S.token,S.account)) return {kind:'permit', calls:1, allowance}; return {kind:'approve', calls:allowance>0n?3:2, allowance}; } function planLabel(p){ if(!p) return ''; switch(p.kind){ case 'direct': return 'One transaction.'; case 'batch': return 'One confirmation — your wallet batches the approval and the deposit atomically.'; case 'permit': return 'One transaction — you sign the approval instead of paying for it.'; case 'approve': return `Two transactions: an exact-amount approval${p.allowance>0n?' (reset first, this token requires it)':''}, then the deposit.`; } return ''; } async function deposit(){ const isETH=S.token===ZERO; const meta = isETH?{decimals:18}:await tokenMeta(S.token); const wei = parseUnits(S.amount, meta.decimals); if(!S.autoClaim) S.tip = 0n; else if(!S.tip) S.tip = await estimateTip(S.token); const value = isETH ? wei + S.tip : S.tip; const valueHex = value>0n ? '0x'+value.toString(16) : '0x0'; const shortfall = need => S.balance!==null && need>S.balance ? `This needs ${fmtAmt(need,18)} ETH and you have ${fmtAmt(S.balance,18)}. ` + `Lower the amount — Max leaves room for the fees.` : null; if(S.dest){ if(S.token!==ZERO) throw new Error(`${S.symbol} cannot be bridged — send it on ${cfg().name}`); if(!canBridge(S.chain, S.dest)) throw new Error(`A transfer to ${cfg(S.dest).short} could not be reversed or recovered from this account`); const b = await bridgePlan(S.dest, S.resolved, S.delay, wei); const short = shortfall(b.value); if(short) throw new Error(short); txShow(`Sending to ${cfg(S.dest).short}`, 'Confirm in your wallet'); const h = await sendTx({from:S.account, to:b.to, data:b.data, value:'0x'+b.value.toString(16)}); S.lastHash=h; txShow(`Sending to ${cfg(S.dest).short}`, 'Waiting for the L1 confirmation', h); await waitTx(h); return; } if(isETH){ const short = shortfall(value); if(short) throw new Error(short); } const plan = await planDeposit(wei); if(plan.kind==='direct'){ await slowTx(depositCalldata(wei),'Sending','Confirm in your wallet',valueHex); return; } if(plan.kind==='batch'){ txShow('Sending','Approve the batch in your wallet'); const calls=[]; if(plan.allowance>0n) calls.push({to:S.token,data:cd(SEL.approve,['address','uint256'],[SLOW,0n])}); calls.push({to:S.token,data:cd(SEL.approve,['address','uint256'],[SLOW,wei])}); calls.push({to:SLOW,data:depositCalldata(wei),value:valueHex}); if(!await ensureChain()) throw new Error(`Switch to ${cfg().name}`); const h=await sendBatch(calls); S.lastHash=h; txShow('Sending','Waiting for confirmation',h); await waitTx(h); return; } if(plan.kind==='permit'){ txShow('Sending','Sign the approval in your wallet'); const info=await permitInfo(S.token,S.account); const deadline=BigInt(nw()+1800); const sig=await signPermit(S.token,info,S.account,SLOW,wei,deadline); txShow('Sending','Confirm the transfer in your wallet'); const h=await sendTx({from:S.account,to:SLOW,data:depositCalldata(wei,sig,deadline),value:valueHex}); S.lastHash=h; txShow('Sending','Waiting for confirmation',h); await waitTx(h); return; } const steps = plan.allowance>0n ? [0n,wei] : [wei]; for(let i=0;i<steps.length;i++){ txShow(`Approving ${S.symbol}`, `Step ${i+1} of ${steps.length+1}`); const h=await sendTx({from:S.account,to:S.token,data:cd(SEL.approve,['address','uint256'],[SLOW,steps[i]])}); txShow(`Approving ${S.symbol}`, `Step ${i+1} of ${steps.length+1}`, h); await waitTx(h); } const [fresh]=await mc([{to:S.token,data:cd(SEL.allowance,['address','address'],[S.account,SLOW])}]); if(!fresh||decode(['uint256'],fresh)[0]<wei) throw new Error('The approval did not take effect'); txShow('Sending',`Step ${steps.length+1} of ${steps.length+1}`); const h=await sendTx({from:S.account,to:SLOW,data:depositCalldata(wei),value:valueHex}); S.lastHash=h; txShow('Sending','Waiting for confirmation',h); await waitTx(h); } const KEEPER_GAS = {eth:150_000n, erc20:220_000n}; async function estimateTip(token){ const c=cfg(); const units = token===ZERO ? KEEPER_GAS.eth : KEEPER_GAS.erc20; let price; try { price = BigInt(await rpc('eth_gasPrice',[],c.id)); } catch(e){ price = c.fallbackGasPrice; } if(!price || price < c.minGasPrice) price = c.minGasPrice; let tip = units * price * 3n / 2n; if(tip < c.minTip) tip = c.minTip; return tip; } async function getGate(){ if(S.gate!==undefined) return S.gate; const [h]=await mc([{to:SLOW,data:SEL.gateAddr}]); S.gate = h ? '0x'+h.slice(-40) : null; if(S.gate===ZERO) S.gate=null; return S.gate; } const LIST_PAGE = 100; const LIST_CAP = 600; const LIST_KIND = { out: {all:SEL.getOut, count:SEL.outCount, at:SEL.outAt}, in: {all:SEL.getIn, count:SEL.inCount, at:SEL.inAt}, }; async function readIdList(kind, who){ const k = LIST_KIND[kind]; const [whole] = await mc([{to:SLOW, data:cd(k.all,['address'],[who])}]); if(whole){ try { return decode(['uint256[]'],whole)[0]; } catch(e){} } const [ch] = await mc([{to:SLOW, data:cd(k.count,['address'],[who])}]); if(!ch) return null; let n; try { n = Number(decode(['uint256'],ch)[0]); } catch(e){ return null; } if(!Number.isFinite(n) || n < 0) return null; if(n === 0) return []; if(n > LIST_CAP) return {overflow:n}; const out=[]; for(let i=0;i<n;i+=LIST_PAGE){ const calls=[]; for(let j=i;j<Math.min(i+LIST_PAGE,n);j++) calls.push({to:SLOW, data:cd(k.at,['address','uint256'],[who,BigInt(j)])}); const res=await mc(calls); for(const r of res){ if(!r) return null; try { out.push(decode(['uint256'],r)[0]); } catch(e){ return null; } } } return out; } const LOAD_COOLDOWN = 12000; async function loadTransfers(force){ if(!S.account) return; if(!force && Date.now()-S.lastLoad < LOAD_COOLDOWN) return; S.lastLoad=Date.now(); const seq=++S.loadSeq, account=S.account; const stale = () => S.loadSeq!==seq || S.account!==account; S.loading=true; renderList(); try { const alias = S.chain!==MAINNET ? aliasOf(account) : null; const [gRes, own, iRaw, viaBridge] = await Promise.all([ mc([{to:SLOW,data:cd(SEL.guardians,['address'],[account])}]), readIdList('out', account), readIdList('in', account), alias ? readIdList('out', alias) : Promise.resolve([]), ]); if(stale()) return; const gh = gRes[0]; S.listError = null; const lists = alias ? [own, iRaw, viaBridge] : [own, iRaw]; const overflow = lists.find(x => x && x.overflow); if(overflow){ S.loading=false; S.listError = `This account has ${group(String(overflow.overflow))} pending entries — more than ` + `this page can list. Anyone can add one with a dust deposit, so a very large count is ` + `usually somebody stuffing the list rather than transfers you are owed. ` + `Open a transfer by its link and dismiss it to take it off this list; dismissing removes ` + `the listing only, never the transfer.`; renderList(); return; } if(lists.some(x => x === null)){ S.loading=false; S.listError = `Could not read your transfers on ${cfg().name}. This is a failed read, not an ` + `empty list — do not treat it as proof that nothing is pending.`; renderList(); return; } S.aliasHasTransfers = viaBridge.length>0; const oIds = [...own, ...viaBridge]; const iIds = iRaw; if(gh==null){ S.loading=false; S.guardUnknown=true; renderList(); toast('Could not read your guardian setting — reload before sending'); return; } S.guardUnknown=false; const gAddr = strip(gh).length>=40 ? '0x'+gh.slice(-40) : ZERO; const hasGuardian = isAddr(gAddr) && gAddr.toLowerCase()!==ZERO; const all=[...new Map([...oIds,...iIds].map(id=>[id.toString(),id])).values()]; const pts = await mc(all.map(id=>({to:SLOW,data:cd(SEL.pendingTransfers,['uint256'],[id])}))); if(stale()) return; const map=new Map(); all.forEach((id,i)=>{ if(!pts[i]) return; try { const [ts,from,to,tid,amt]=decode(['uint256','address','address','uint256','uint256'],pts[i]); if(ts!==0n) map.set(id.toString(),{ts,from,to,tid,amt}); } catch(e){} }); const tokens=[...new Set([...map.values()].map(v=>decodeId(v.tid).token))]; await Promise.all(tokens.map(t=>tokenMeta(t).catch(()=>{}))); if(stale()) return; const build = id => { const k=id.toString(), pt=map.get(k); if(!pt) return null; const {token,delay}=decodeId(pt.tid); const m=S.meta[token.toLowerCase()]||{symbol:'???',decimals:18}; const timestamp=Number(pt.ts); return { id:k, from:pt.from, to:pt.to, tokenId:pt.tid.toString(), token, symbol:m.symbol, decimals:m.decimals, amountRaw:pt.amt, amount:fmtAmt(pt.amt,m.decimals), timestamp, delay, unlockTime:timestamp+delay, }; }; const outArr=oIds.map(build).filter(Boolean).sort((a,b)=>a.unlockTime-b.unlockTime); const inArr =iIds.map(build).filter(Boolean).sort((a,b)=>a.unlockTime-b.unlockTime); const extra=[]; if(hasGuardian) for(const t of outArr) extra.push({kind:'g',t,call:{to:SLOW, data:cd(SEL.isWithdrawalApprovalNeeded,['address','address','uint256','uint256'], [account,account,BigInt(t.tokenId),t.amountRaw])}}); const gate=await getGate(); if(gate) for(const t of [...inArr,...outArr]) extra.push({kind:'t',t,call:{to:gate,data:c1(SEL.tips,t.id)}}); if(extra.length){ const res=await mc(extra.map(x=>x.call)); if(stale()) return; res.forEach((r,i)=>{ if(!r) return; const {kind,t}=extra[i]; try { const v=decode(['uint256'],r)[0]; if(kind==='g') t.guardianPending = v!==0n; else if(v>0n) t.tipped=true; } catch(e){} }); } if(stale()) return; S.out=outArr; S.inb=inArr; S.hasGuardian=hasGuardian; S.listError=null; await loadUnlocked(); if(stale()) return; } catch(e){ if(!stale()) toast('Could not load transfers — '+errText(e), 5000); } finally { if(!stale()){ S.loading=false; renderList(); } } } const unlockedKey = () => `slow.unlocked.${S.chain}.${(S.account||'').toLowerCase()}`; function rememberUnlocked(t, key){ try { const k=key||unlockedKey(); const cur=JSON.parse(localStorage.getItem(k)||'[]'); if(!cur.some(x=>x.id===t.tokenId)){ cur.push({id:t.tokenId, token:t.token, symbol:t.symbol, decimals:t.decimals}); localStorage.setItem(k, JSON.stringify(cur)); } } catch(e){} } function forgetUnlocked(id){ try { const k=unlockedKey(); const cur=JSON.parse(localStorage.getItem(k)||'[]').filter(x=>x.id!==id); localStorage.setItem(k, JSON.stringify(cur)); } catch(e){} } async function loadUnlocked(){ if(!S.account){ S.unlocked=[]; return; } let notes=[]; try { notes=JSON.parse(localStorage.getItem(unlockedKey())||'[]'); } catch(e){} if(!notes.length){ S.unlocked=[]; return; } const acct=S.account; const res=await mc(notes.map(n=>({to:SLOW, data:cd(SEL.unlockedBalances,['address','uint256'],[acct,BigInt(n.id)])}))); if(S.account!==acct) return; const live=[]; notes.forEach((n,i)=>{ if(!res[i]) return; let bal=0n; try { bal=decode(['uint256'],res[i])[0]; } catch(e){ return; } if(bal>0n){ live.push({...n, raw:bal, amount:fmtAmt(bal,n.decimals), zero:0}); return; } const seen=(n.zero||0)+1; if(seen<3) live.push({...n, zero:seen, raw:0n, amount:'0', pending:true}); else forgetUnlocked(n.id); }); try { localStorage.setItem(unlockedKey(), JSON.stringify( live.map(({id,token,symbol,decimals,zero})=>({id,token,symbol,decimals,zero})))); } catch(e){} S.unlocked=live.filter(u=>!u.pending); } async function doWithdraw(u, to){ if(!guardWritable()) return; to = to || S.account; try { if(S.hasGuardian){ const [r]=await mc([{to:SLOW, data:cd(SEL.isWithdrawalApprovalNeeded,['address','address','uint256','uint256'], [S.account,to,BigInt(u.id),u.raw])}]); if(r && decode(['uint256'],r)[0]!==0n){ showApprovalNeeded(u, await withdrawOpId(u, to), to); return; } } await slowTx(cd(SEL.withdrawFrom,['address','address','uint256','uint256'], [S.account,to,BigInt(u.id),u.raw]), `Withdrawing ${u.amount} ${u.symbol}`, 'Confirm in your wallet'); toast(`Withdrew ${u.amount} ${u.symbol}.`); await loadUnlocked(); renderList(); } catch(e){ fail(e); } } async function withdrawOpId(u, to){ try { const [p]=await mc([{to:SLOW, data:cd(SEL.predictWithdrawalId,['address','address','uint256','uint256'], [S.account,to,BigInt(u.id),u.raw])}]); return p ? decode(['uint256'],p)[0].toString() : '(could not derive)'; } catch(e){ return '(could not derive)'; } } function showApprovalNeeded(u, opId, to){ to = to || S.account; S.detail=null; el.detailShare.hidden=true; el.detailForget.hidden=true; el.detailForget.onclick=null; el.detailTitle.textContent='Guardian approval needed'; el.detailRows.replaceChildren(); const rows=[ ['Amount', `${u.amount} ${u.symbol}`], ['To', to], ['Position id', u.id], ['Operation id', opId], ['Guardian', S.guard.guardian || '(unknown)'], ]; for(const [k,v] of rows){ const row=document.createElement('div'); row.className='kv'; const dt=document.createElement('dt'), dd=document.createElement('dd'); dt.textContent=k; dd.textContent=v; row.append(dt,dd); el.detailRows.appendChild(row); } const btn=el.detailAction; btn.hidden=false; btn.disabled=false; btn.className='btn alt'; btn.textContent='Copy the details for your guardian'; btn.onclick=()=>copyLink( `SLOW withdrawal awaiting your approval\nchain: ${cfg().name} (${S.chain})\n`+ `account: ${S.account}\nto: ${to}\nposition id: ${u.id}\namount: ${u.raw}\n`+ `operation id: ${opId}\napprove with: approveTransfer(${S.account}, ${opId})`, 'Details'); showModal(el.detailModal); } const HEXDATA = /^0x([0-9a-fA-F]{2})*$/; function exitRecipe(u, toRaw, dataRaw){ const raw=(dataRaw||'').trim(); const data = raw==='0x' ? '' : raw; const to=((toRaw||'').trim())||S.account; if(!isAddr(to)) return {error:'Enter a destination address'}; if(to.toLowerCase()===SLOW.toLowerCase()) return {error:'SLOW itself cannot be the destination'}; if(to===ZERO) return {error:'The zero address cannot be the destination'}; if(S.gate && to.toLowerCase()===String(S.gate).toLowerCase()) return {error:'The keeper gate cannot be the destination'}; if(data && !HEXDATA.test(data)) return {error:'Call data must be 0x followed by whole bytes'}; const isETH=u.token===ZERO; const wf = dest => ({to:SLOW, value:0n, data:cd(SEL.withdrawFrom,['address','address','uint256','uint256'],[S.account,dest,BigInt(u.id),u.raw])}); const ethCall = !!data && isETH; const settleTo = ethCall ? S.account : to; const calls=[wf(settleTo)]; if(data) calls.push({to, data, value: ethCall ? u.raw : 0n}); return {chainId:S.chain, chainName:cfg().name, calls, settleTo, target:to, needsAtomic:calls.length>1, gap: !!data && !isETH}; } function exitText(r, u){ const lines=[`# SLOW withdrawal — ${r.chainName} (chain ${r.chainId})`, `# ${u.amount} ${u.symbol} · position ${u.id}`, '']; r.calls.forEach((c,i)=>lines.push(`call ${i+1}`, ` to: ${c.to}`, ` value: ${c.value} wei`, ` data: ${c.data}`, '')); if(r.needsAtomic) lines.push('note: send these as one atomic batch. Run apart, the funds sit at the', ' destination between them and anyone who can call it may take them.'); return lines.join('\n'); } function showExit(u){ S.exit=u; el.exitTitle.textContent='Withdraw'; el.exitAmt.textContent=`${u.amount} ${u.symbol==='???'?shortAddr(u.token):u.symbol} · position ${u.id}`; el.exitTo.value=''; el.exitData.value=''; updateExit(); showModal(el.exitModal); } async function updateExit(){ const u=S.exit; if(!u) return; const r=exitRecipe(u, el.exitTo.value, el.exitData.value); S.exitPlan=r; if(r.error){ setNote(el.exitNote, r.error, 'err'); el.exitSend.disabled=true; el.exitCopy.disabled=true; return; } el.exitCopy.disabled=false; const dest = r.target.toLowerCase()===(S.account||'').toLowerCase() ? 'yourself' : shortAddr(r.target); if(!r.needsAtomic){ setNote(el.exitNote, `One transaction: the underlying goes to ${dest}.`,'ok'); el.exitSend.disabled=false; el.exitSend.textContent='Withdraw'; return; } const batch=await canBatch(); if(S.exitPlan!==r) return; el.exitSend.disabled=!batch; el.exitSend.textContent = batch?'Withdraw and call':'Withdraw'; if(batch){ setNote(el.exitNote, r.gap ? `One confirmation: the token lands at ${dest} and the call runs in the same batch.` : `One confirmation: you receive the ETH and it is forwarded to ${dest} with your call.`,'ok'); } else { setNote(el.exitNote, 'Your wallet cannot batch, so these two calls would run apart — ' +'the funds would sit at the destination unattributed in between. Copy them and send them atomically.','err'); } } async function doExit(){ const u=S.exit, r=S.exitPlan; if(!u||!r||r.error) return; if(!guardWritable()) return; if(!r.needsAtomic){ hideModal(el.exitModal); await doWithdraw(u, r.settleTo); return; } try { if(S.hasGuardian){ const [q]=await mc([{to:SLOW, data:cd(SEL.isWithdrawalApprovalNeeded,['address','address','uint256','uint256'], [S.account,r.settleTo,BigInt(u.id),u.raw])}]); if(q && decode(['uint256'],q)[0]!==0n){ hideModal(el.exitModal); showApprovalNeeded(u, await withdrawOpId(u, r.settleTo), r.settleTo); return; } } if(!await ensureChain()) throw new Error(`Switch to ${cfg().name}`); hideModal(el.exitModal); txShow(`Withdrawing ${u.amount} ${u.symbol}`,'Confirm in your wallet'); const h=await sendBatch(r.calls.map(c=>({to:c.to,data:c.data,value:'0x'+c.value.toString(16)}))); S.lastHash=h; txShow(`Withdrawing ${u.amount} ${u.symbol}`,'Waiting for confirmation',h); await