memoscan
← all memos

Memo 0x751923f7…d72a9c on Ethereum

0x68575b07…67c7·#25,913,808·view on Etherscan
name){ const sym = escHTML(symbol), nm = escHTML(clipForDisplay(name||'', 28)); const label = formatDelay(delay); return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">' + `<title>SLOW ${sym} · ${label}</title>` + `<path d="${PLATE_D}" fill="#0A0A0A" stroke="#fff" stroke-width="4"/>` + '<line x1="20" y1="60" x2="280" y2="60" stroke="#fff" stroke-width="2"/>' + '<text x="20" y="44" font-family="Helvetica,Arial,sans-serif" font-size="26" fill="#fff">SLOW</text>' + '<g font-family="monospace" text-anchor="middle" fill="#fff">' + `<text x="150" y="150" font-size="${fit(blen(sym),240,44,14)}">${sym}</text>` + `<text x="150" y="185" font-size="${fit(blen(nm),250,14,8)}" fill="#8A8A8A">${nm}</text>` + `<text x="150" y="240" font-size="${fit(blen(label),250,22,11)}">${label}</text>` + '</g></svg>'; } function paintPreview(){ el.fPreview.hidden = false; const meta = S.meta[(S.token||'').toLowerCase()]; const name = S.token===ZERO ? 'Ether' : (meta&&meta.name) || (S.symbol||''); const blank = !S.symbol && !S.delay; el.nftBox.classList.toggle('waiting', blank); el.nftBox.innerHTML = renderNFT(S.token, S.delay||0, S.symbol||'\u2014', S.symbol?name:''); el.previewHint.textContent = blank ? 'updates as you choose' : 'drawn on chain'; } function ready(){ return !!(S.resolved && S.token && S.symbol && S.amount && S.delay); } function missing(){ if(S.resolving) return 'Looking up that name…'; if(!S.resolved) return 'Enter who it goes to'; if(!S.token || !S.symbol) return 'Pick an asset'; if(!S.amount) return 'Enter an amount'; if(!S.delay) return 'Choose how long it stays locked'; return ''; } function paintFoot(){ const gap = S.sending ? '' : missing(); el.confirmBtn.disabled = S.sending || !!gap; el.footHint.textContent = gap || (!S.sending && !S.account ? 'Connects a wallet first' : ''); if(gap) el.confirmBtn.replaceChildren(document.createTextNode('Send')); } function renderReview(){ const show = ready(); paintFoot(); el.fReview.hidden = !show; if(!show) return; const who = S.resolvedName || shortAddr(S.resolved); el.summary.textContent = `${S.amount} ${S.symbol} → ${who}`; el.recipFull.replaceChildren(); if(S.resolved){ const full = document.createElement('b'); full.textContent = S.resolved; if(S.resolvedName){ el.recipFull.append(document.createTextNode(`${S.resolvedName} resolves to `), full); } else { el.recipFull.append(full); } } const t0=nw(), unlock=t0+S.delay, claw=unlock+GRACE; const dest = S.dest ? cfg(S.dest) : null; const rows= dest ? [ {cls:'now', when:'Now', what:`Leaves Ethereum. The timelock starts when it arrives on ${dest.name}, usually within a few minutes.`}, {cls:'', when:`+ ${fmtTime(S.delay)} after arrival`, what:`${S.resolvedName||shortAddr(S.resolved)} can claim it on ${dest.name}. Until then you can reverse it there.`}, {cls:'warn', when:'+ 30 days', what:`If it was never claimed, you can recover it on ${dest.name}.`}, ] : [ {cls:'now', when:'Now', what:`Locked. You can reverse it and take it back at any point until it unlocks.`}, {cls:'', when:fmtWhen(unlock), what:`Unlocks after ${fmtTime(S.delay)}. ${who} can claim it${S.autoClaim?', or a keeper will settle it for them':''}. You can no longer reverse it.`}, {cls:'warn', when:fmtWhen(claw), what:`If it was never claimed, you can recover it from here.`}, ]; el.timeline.replaceChildren(); for(const r of rows){ const li=document.createElement('li'); if(r.cls) li.className=r.cls; const w=document.createElement('span'); w.className='when'; w.textContent=r.when; const t=document.createElement('span'); t.className='what'; t.textContent=r.what; const box=document.createElement('div'); box.append(w,t); li.appendChild(box); el.timeline.appendChild(li); } el.confirmBtn.replaceChildren( document.createTextNode(`Send ${S.amount} `), Object.assign(document.createElement('span'),{className:'cs',textContent:S.symbol}), document.createTextNode(S.dest ? ` to ${cfg(S.dest).short}` : '')); previewPlan(); paintPreview(); } async function updateTipPreview(){ if(!S.autoClaim){ el.tipPre.textContent=''; return; } el.tipPre.textContent='Estimating…'; const tip=await estimateTip(S.token); if(!S.autoClaim) return; S.tip=tip; el.tipPre.textContent=`Keeper tip ≈ ${fmtAmt(tip,18)} ETH, refunded if ${S.resolvedName||'they'} settle it directly.`; } async function previewPlan(){ el.planNote.textContent=''; if(!S.account||!S.token||!S.amount) return; const c=cfg(); const prefix = S.chainId!=null && S.chainId!==c.id ? `On ${c.name} — your wallet will be asked to switch. ` : ''; try { const meta = S.token===ZERO?{decimals:18}:await tokenMeta(S.token); const wei = parseUnits(S.amount,meta.decimals); const plan = await planDeposit(wei); el.planNote.textContent = prefix + planLabel(plan); } catch(e){ el.planNote.textContent = prefix.trim(); } } async function maxSpendable(){ if(S.balance===null) return null; if(S.token!==ZERO) return S.balance; const units = S.dest ? 600_000n : 220_000n; let reserve=3_000_000_000_000_000n; try { const fh=await rpc('eth_feeHistory',['0x5','latest',[50]]); const base=BigInt(fh.baseFeePerGas.at(-1)); let prio=BigInt(fh.reward?.at(-1)?.[0]||0); if(prio<1_000_000_000n) prio=1_000_000_000n; reserve=units*(base*2n+prio); } catch(e){} if(S.autoClaim){ try { if(!S.tip) S.tip = await estimateTip(S.token); reserve += S.tip; } catch(e){} } if(S.dest && BRIDGES[S.dest]){ try { reserve += (await bridgeCosts(S.dest, S.resolved || S.account, S.delay || 0)).extra; } catch(e){} } return S.balance>reserve ? S.balance-reserve : 0n; } function setAmount(v){ S.amount = v || null; if(el.amountInput.value!==v) el.amountInput.value=v||''; if(v) S.step=Math.max(S.step,2); render(); renderReview(); } function validateAmount(){ const raw=el.amountInput.value.trim(); if(!raw){ S.amount=null; setNote(el.amountNote,''); render(); renderReview(); return; } try { const wei=parseUnits(raw,S.decimals); if(wei<=0n){ S.amount=null; setNote(el.amountNote,'Enter an amount above zero','err'); render(); renderReview(); return; } if(S.balance!==null && wei>S.balance){ S.amount=null; setNote(el.amountNote,`You have ${fmtAmt(S.balance,S.decimals)} ${S.symbol}`,'err'); render(); renderReview(); return; } setNote(el.amountNote,''); S.amount=raw; S.step=Math.max(S.step,2); } catch(e){ S.amount=null; setNote(el.amountNote,e.message,'err'); } render(); renderReview(); } function txVisible(on){ el.txbar.hidden=!on; document.body.classList.toggle('txlive', on); } function txShow(msg, step, hash){ txVisible(true); el.txSpin.hidden=false; el.txMsg.textContent=msg; el.txStep.textContent=step||''; el.txDismiss.hidden=true; if(hash){ el.txLink.hidden=false; el.txLink.href=`${cfg().explorer}/tx/${hash}`; el.txLink.textContent=hash.slice(0,10)+'…'; } else el.txLink.hidden=true; } function txDone(msg, hash){ txVisible(true); el.txSpin.hidden=true; el.txMsg.textContent=msg; el.txStep.textContent=''; el.txDismiss.hidden=false; if(hash){ el.txLink.hidden=false; el.txLink.href=`${cfg().explorer}/tx/${hash}`; el.txLink.textContent=hash.slice(0,10)+'…'; } clearTimeout(txDone.t); txDone.t=setTimeout(()=>txVisible(false), 12000); } const txHide = () => { txVisible(false); clearTimeout(txDone.t); }; async function handleConfirm(){ if(!ready()) return; if(!S.account && !await connect(true)) return; if(!await ensureChain()) return; S.sending=true; paintFoot(); try { if(S.slowDeployed[S.chain]===false) throw new Error(`SLOW is not deployed on ${cfg().name}`); const meta = S.token===ZERO?{decimals:18}:await tokenMeta(S.token); if(S.token!==ZERO && !meta.ok) throw new Error('That token did not answer symbol() or decimals()'); const wei=parseUnits(S.amount,meta.decimals); if(S.balance!==null && wei>S.balance) throw new Error(`You have ${fmtAmt(S.balance,meta.decimals)} ${S.symbol}`); await deposit(); const who=S.resolvedName||shortAddr(S.resolved); txDone(`Sent to ${who}. Locked for ${fmtTime(S.delay)}.`, S.lastHash); setView('send'); resetForm(); loadTransfers(true); } catch(e){ txHide(); fail(e); } finally { S.sending=false; paintFoot(); } } function linkBase(){ return location.origin + location.pathname; } function payLink(){ const q=new URLSearchParams(); if(S.resolvedName||S.resolved) q.set('to', S.resolvedName||S.resolved); if(S.symbol) q.set('asset', S.token===ZERO?'ETH':S.token); if(S.amount) q.set('amount', S.amount); if(S.delay) q.set('delay', String(S.delay)); q.set('chain', String(S.chain)); return `${linkBase()}#/pay?${q}`; } const txLink = id => `${linkBase()}#/tx/${id}?chain=${S.chain}`; async function copyLink(url, label){ try { if(navigator.clipboard?.writeText) await navigator.clipboard.writeText(url); else { const t=document.createElement('textarea'); t.value=url; t.style.position='fixed'; t.style.opacity='0'; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove(); } toast(`${label} copied`); } catch(e){ toast(url, 9000); } } function parseRoute(){ const h=location.hash||''; const m=/^#\/(pay|tx)(?:\/([^?]*))?(?:\?(.*))?$/.exec(h); if(!m) return null; return {kind:m[1], arg:m[2]||'', q:new URLSearchParams(m[3]||'')}; } async function applyRoute(){ const r=parseRoute(); if(!r) return false; const want=Number(r.q.get('chain')); if(want && CHAINS[want] && want!==S.chain) switchChain(want); if(r.kind==='pay'){ setView('send'); const asset=(r.q.get('asset')||'').trim(); if(asset){ const known=cfg().tokens.find(t=> t.symbol.toLowerCase()===asset.toLowerCase() || t.address.toLowerCase()===asset.toLowerCase()); if(known){ S.token=known.address.toLowerCase(); S.symbol=known.symbol; S.decimals=known.decimals; S.step=Math.max(S.step,1); } else if(isAddr(asset)){ const m=await tokenMeta(asset); if(m.ok){ S.token=asset.toLowerCase(); S.symbol=m.symbol; S.decimals=m.decimals; S.step=Math.max(S.step,1); } } } const delay=parseInt(r.q.get('delay')||'',10); if(delay>0 && delay<=MAX_DELAY){ S.delay=delay; const preset=qs(`#timeRow [data-time="${delay}"]`); S.delayDisplay = preset ? preset.textContent : fmtTime(delay).toUpperCase(); } const amount=(r.q.get('amount')||'').trim(); if(amount){ el.amountInput.value=amount; } const to=(r.q.get('to')||'').trim(); if(to){ el.recipientInput.value=to; } paintPresets(); refreshBalance(); if(amount) validateAmount(); if(to) await resolveRecipient(to); render(); renderReview(); return true; } if(r.kind==='tx' && /^\d+$/.test(r.arg)){ await openTransferById(r.arg); return true; } return false; } async function openTransferById(id){ try { const [ph]=await mc([{to:SLOW,data:cd(SEL.pendingTransfers,['uint256'],[BigInt(id)])}]); if(!ph) throw new Error('Could not read that transfer'); const [ts,from,to,tid,amt]=decode(['uint256','address','address','uint256','uint256'],ph); if(ts===0n){ toast('That transfer has already been settled, reversed or recovered.',6000); setView('take'); return; } const {token,delay}=decodeId(tid); await tokenMeta(token).catch(()=>{}); const m=S.meta[token.toLowerCase()]||{symbol:'???',decimals:18}; const t={id:String(id), from, to, tokenId:tid.toString(), token, symbol:m.symbol, decimals:m.decimals, amountRaw:amt, amount:fmtAmt(amt,m.decimals), timestamp:Number(ts), delay, unlockTime:Number(ts)+delay}; const me=(S.account||'').toLowerCase(); S.tab = me===to.toLowerCase() ? 'inbound' : 'outbound'; setView('take'); openDetail(t, m.symbol==='???'?shortAddr(token):m.symbol); } catch(e){ fail(e); } } const THEMES=[ {key:'system', label:'System', sub:'follow the OS', glyph:'\u25d0'}, {key:'light', label:'Light', sub:'', glyph:'\u2600'}, {key:'dark', label:'Dark', sub:'', glyph:'\u263e'}, ]; const sysDark = () => matchMedia('(prefers-color-scheme: dark)').matches; function applyTheme(){ const light = S.theme==='light' || (S.theme==='system' && !sysDark()); document.body.classList.toggle('light',light); el.themeBtn.textContent = (THEMES.find(t=>t.key===S.theme)||THEMES[0]).glyph; el.themeBtn.title = `Appearance: ${S.theme}`; } function setTheme(k){ S.theme=k; try { k==='system' ? localStorage.removeItem('slow.theme') : localStorage.setItem('slow.theme',k); } catch(e){} applyTheme(); } function openPicker(modal, list, items, current){ return new Promise(resolve=>{ let done=false; const finish=v=>{ if(done) return; done=true; modal.__cancel=null; hideModal(modal); resolve(v); }; list.replaceChildren(); for(const it of items){ const b=document.createElement('button'); b.type='button'; b.className='pick'; b.setAttribute('aria-current',String(it.key===current)); const ico=document.createElement('span'); ico.className='ico'; if(it.icon){ const t=document.createElement('span'); t.innerHTML=it.icon; if(t.firstElementChild) ico.appendChild(t.firstElementChild); } else if(it.img){ const i=document.createElement('img'); i.src=it.img; i.alt=''; i.loading='lazy'; i.onerror=()=>{ i.remove(); ico.textContent=(it.label||'?').slice(0,1); }; ico.appendChild(i); } else if(it.glyph){ ico.textContent=it.glyph; } b.appendChild(ico); const lab=document.createElement('span'); lab.className='lbl2'; const nm=document.createElement('b'); nm.textContent=it.label; lab.appendChild(nm); if(it.sub){ const sm=document.createElement('small'); sm.textContent=it.sub; lab.appendChild(sm); } b.appendChild(lab); const tick=document.createElement('span'); tick.className='tick'; tick.textContent = it.key===current ? '\u2713' : ''; b.appendChild(tick); b.onclick=()=>finish(it.key); list.appendChild(b); } modal.__cancel=()=>finish(null); showModal(modal); }); } /* ══════════════════════════════════════════════════════════════════ Wiring ══════════════════════════════════════════════════════════════════ */ try { S.theme = localStorage.getItem('slow.theme') || 'system'; } catch(e){ S.theme='system'; } if(!THEMES.some(t=>t.key===S.theme)) S.theme='system'; applyTheme(); matchMedia('(prefers-color-scheme: dark)').addEventListener?.('change',()=>{ if(S.theme==='system') applyTheme(); }); el.themeBtn.onclick=async()=>{ const k=await openPicker(el.themeModal, el.themeList, THEMES.map(t=>({key:t.key,label:t.label,sub:t.sub,glyph:t.glyph})), S.theme); if(k) setTheme(k); }; el.themeCancel.onclick=()=>el.themeModal.__cancel?.(); el.chainBtn.onclick=async()=>{ const id=await openPicker(el.chainModal, el.chainList, CHAIN_IDS.map(id=>({ key:id, label:CHAINS[id].name, icon:ICONS[id], sub:CHAINS[id].tokens.map(t=>t.symbol).join(' · '), })), S.chain); if(id) switchChain(id); }; el.chainCancel.onclick=()=>el.chainModal.__cancel?.(); el.walletBtn.onclick=()=>S.account?disconnect():connect(true); el.walletCancel.onclick=()=>el.walletModal.__cancel?.(); el.homeBtn.onclick=()=>setView('home'); el.sendBox.onclick=()=>setView('send'); el.takeBox.onclick=async()=>{ setView('take'); if(!S.account) await connect(true); }; el.segSend.onclick=()=>setView('send'); el.segTake.onclick=async()=>{ setView('take'); if(!S.account) await connect(true); }; el.segGuard.onclick=async()=>{ setView('guard'); if(!S.account) await connect(true); }; el.gSet.onclick=()=>openGuardModal('set'); el.gRemove.onclick=()=>openGuardModal('remove'); el.gAddWard.onclick=()=>openGuardModal('ward'); el.gScan.onclick=scanWards; el.gCommit.onclick=commitGuardianTx; el.gCancel.onclick=cancelGuardianTx; el.guardApply.onclick=applyGuardModal; el.gaApply.onclick=()=>{ const w=S.guard.wards.find(x=>x.addr===el.gaModal.dataset.ward); if(w) guardApprove(w); }; el.gaCancel.onclick=()=>hideModal(el.gaModal); el.guardCancel.onclick=()=>hideModal(el.guardModal); el.guardInput.addEventListener('keydown',e=>{ if(e.key==='Enter') el.guardApply.click(); }); el.destRow.addEventListener('click',e=>{ const b=e.target.closest('.opt'); if(!b||b.disabled) return; const id=Number(b.dataset.dest); S.dest = id===MAINNET ? null : id; buildDestRow(); render(); renderReview(); }); el.cryptoGrid.addEventListener('click',e=>{ const b=e.target.closest('.crypto'); if(!b) return; S.token=b.dataset.token.toLowerCase(); S.symbol=b.dataset.symbol; S.decimals=Number(b.dataset.decimals)||18; S.step=Math.max(S.step,1); setNote(el.amountNote,''); tokenMeta(S.token).then(m=>{ if(S.token===b.dataset.token.toLowerCase()){ S.decimals=m.decimals; paintPresets(); } }); paintPresets(); refreshBalance(); buildDestRow(); if(S.amount) validateAmount(); }); el.amountRow.addEventListener('click',e=>{ const o=e.target.closest('.opt'); if(!o||o.disabled||!o.dataset.amount) return; setAmount(o.dataset.amount); setNote(el.amountNote,''); }); let amtTimer; el.amountInput.addEventListener('input',()=>{ clearTimeout(amtTimer); amtTimer=setTimeout(validateAmount,220); }); el.amountInput.addEventListener('blur',validateAmount); el.maxBtn.onclick=async()=>{ const max=await maxSpendable(); if(max===null){ toast('Connect a wallet to see your balance'); return; } if(max<=0n){ setNote(el.amountNote,'Not enough ETH left over to pay for the transaction','err'); return; } setAmount(formatUnits(max,S.decimals)); setNote(el.amountNote, S.token===ZERO ? 'Leaves enough ETH to pay for this transaction' : ''); }; el.timeRow.addEventListener('click',e=>{ const o=e.target.closest('.opt'); if(!o) return; if(o===el.customTimeOpt){ showModal(el.timeModal); return; } S.delay=parseInt(o.dataset.time); S.delayDisplay=o.textContent; render(); renderReview(); }); el.timeApply.onclick=()=>{ const r=fmtCustomTime({d:el.dInput.value,h:el.hInput.value,m:el.mInput.value,s:el.sInput.value}); if(!r.seconds){ toast('Enter a delay longer than zero'); return; } if(r.seconds>MAX_DELAY){ toast('The longest delay is 100 years'); return; } S.delay=r.seconds; S.delayDisplay=r.display; hideModal(el.timeModal); render(); renderReview(); }; el.customTokenOpt.onclick=()=>{ el.tokenInput.value=''; setNote(el.tokenStatus,''); showModal(el.tokenModal); }; el.tokenApply.onclick=async()=>{ const addr=el.tokenInput.value.trim(); if(!isAddr(addr)){ setNote(el.tokenStatus,'That is not a contract address','err'); return; } setNote(el.tokenStatus,'Reading the token…'); const meta=await tokenMeta(addr); if(!meta.ok){ setNote(el.tokenStatus,'That address did not answer symbol() or decimals()','err'); return; } S.token=addr.toLowerCase(); S.symbol=meta.symbol; S.decimals=meta.decimals; S.step=Math.max(S.step,1); hideModal(el.tokenModal); paintPresets(); refreshBalance(); buildDestRow(); if(S.amount) validateAmount(); }; el.tokenInput.addEventListener('keydown',e=>{ if(e.key==='Enter') el.tokenApply.click(); }); [el.dInput,el.hInput,el.mInput,el.sInput].forEach(i=>i.addEventListener('keydown',e=>{ if(e.key==='Enter') el.timeApply.click(); })); el.autoClaimChk.onchange=()=>{ S.autoClaim=el.autoClaimChk.checked; if(!S.autoClaim) S.tip=0n; updateTipPreview(); renderReview(); }; let ensTimer; el.recipientInput.addEventListener('input',()=>{ const v=el.recipientInput.value.trim(); S.resolved=null; S.resolvedName=null; S.resolveSeq++; renderReview(); clearTimeout(ensTimer); if(!v){ setNote(el.ens,''); return; } ensTimer=setTimeout(()=>resolveRecipient(v),450); }); el.recipientInput.addEventListener('keydown',e=>{ if(e.key!=='Enter') return; if(ready()) el.confirmBtn.click(); else el.amountInput.focus(); }); el.amountInput.addEventListener('keydown',e=>{ if(e.key==='Enter' && ready()) el.confirmBtn.click(); }); el.confirmBtn.onclick=handleConfirm; el.detailClose.onclick=()=>hideModal(el.detailModal); el.exitClose.onclick=()=>hideModal(el.exitModal); el.exitTo.oninput=el.exitData.oninput=updateExit; el.exitSend.onclick=doExit; el.exitCopy.onclick=()=>{ if(S.exitPlan&&!S.exitPlan.error) copyLink(exitText(S.exitPlan,S.exit),'Calls'); }; el.detailShare.onclick=()=>{ if(S.detail) copyLink(txLink(S.detail.t.id),'Link'); }; el.shareBtn.onclick=()=>copyLink(payLink(),'Request link'); el.recipeBtn.onclick=()=>{ const r=depositRecipe(); if(!r){ toast('Fill in the transfer first'); return; } copyLink(recipeText(r), 'Call'); }; el.txDismiss.onclick=txHide; el.helpBtn.onclick=()=>showModal(el.helpModal); el.helpClose.onclick=()=>hideModal(el.helpModal); qsa('.tab[data-tab]').forEach(t=>t.addEventListener('click',()=>{ S.tab=t.dataset.tab; paintTabs(); render(); renderList(); })); addEventListener('focus',()=>{ if(S.view==='take'&&S.account) loadTransfers(false); }); (()=>{ const parts=(location.hostname||'').split('.'); const fromHost = /^0x[0-9a-fA-F]{40}$/.test(parts[0]||'') && /^\d+$/.test(parts[1]||'') ? Number(parts[1]) : null; let saved=null; try { saved=Number(localStorage.getItem('slow.chain'))||null; } catch(e){} S.chain = (fromHost&&CHAINS[fromHost]) ? fromHost : (saved&&CHAINS[saved]) ? saved : MAINNET; })(); buildTokenGrid(); buildDestRow(); paintChain(); paintNet(); paintWallet(); setView('home'); checkDeployed(); addEventListener('hashchange', applyRoute); eagerConnect().then(applyRoute); })(); </script> </body> </html>