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