You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reclaim: assign buyer as its own operator (unblocks requestClose) (#79)
requestClose/withdraw on AntseedChannels are gated on the buyer's OPERATOR (an
AntseedDeposits role), not the buyer directly — so every close reverted
NotAuthorized() even though channels(id).buyer == our wallet, the deadline had
long expired, and funds were still reserved. The normal deposit/reserve/settle
flow never assigns an operator, so it was never set.
Add the one-time self-assignment (per AntSeed: the operator is your own wallet,
you just have to assign it):
- reclaim.mjs: new `set-operator` phase — signs the EIP-712 SetOperator auth
(AntseedDeposits domain) and submits setOperator(buyer, buyer, nonce, sig).
`list` now also reports operator / operatorSet / operatorIsSelf.
- control.js: /reclaim/set-operator (on-chain, serialized with the other txs).
- shim.py + auth_proxy.py: /x/wallet/reclaim/set-operator proxies.
- Panel: when the buyer isn't its own operator, the reclaim section shows a
one-time "Enable reclaim" setup (single tx, moves no funds) and gates
Request close / Withdraw behind it.
Verified live on the deployed buyer: operator was 0x0 -> after set-operator,
operatorIsSelf=true and requestClose staticCall is authorized on all active
channels (was NotAuthorized). Grants a role only; moves no funds.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
function walletDo(op){const el=$('walletAmt');const amount=el?String(el.value).trim():'';if(!/^\\d+(\\.\\d{1,6})?$/.test(amount)||Number(amount)<=0){showErr('Enter a valid USDC amount (>0, ≤6 decimals)');return}if(op==='withdraw'&&lastWallet&&lastWallet.deposits_available!=null&&Number(amount)>Number(lastWallet.deposits_available)){showErr('You can only withdraw what is available in escrow ('+Number(lastWallet.deposits_available).toFixed(6)+' USDC). USDC reserved in channels frees up when they settle.');return}if(!confirm((op==='deposit'?'Top up':'Withdraw')+' '+amount+' USDC?\\nOn-chain tx on Base, irreversible.'))return;walletOpAmount(op,amount)}
4757
4764
async function walletOpAmount(op,amount){try{toast(op+' '+amount+' USDC…');const r=await fetch('/dashboard/api/wallet/'+op,{method:'POST',headers:{'content-type':'application/json'},credentials:'same-origin',body:JSON.stringify({amount})});if(r.status===401){showLogin();return}const t=await r.text();let d={};try{d=JSON.parse(t)}catch(_){}if(!r.ok){const raw=d.error?.message||(t&&t.trim()[0]!=='<'?t:('server error '+r.status));throw new Error(String(raw).split('\\n')[0].slice(0,240))}toast(op+' ok · '+amount+' USDC');const el=$('walletAmt');if(el)el.value='';loadMarket();loadConfig()}catch(e){showErr(e.message)}}
4758
4765
async function walletReclaimScan(){const v=$('walletReclaimView');if(v)v.innerHTML='<span class="muted">Scanning channels on-chain…</span>';try{const r=await fetch('/dashboard/api/wallet/reclaim/scan',{method:'POST',credentials:'same-origin'});if(r.status===401){showLogin();return}const t=await r.text();let d={};try{d=JSON.parse(t)}catch(_){}if(!r.ok){const raw=d.error?.message||(t&&t.trim()[0]!=='<'?t:('scan '+r.status));throw new Error(String(raw).split('\\n')[0].slice(0,240))}renderReclaim(d)}catch(e){if(v)v.innerHTML='';showErr(e.message)}}
async function walletReclaimAct(phase){const label=phase==='request-close'?'Request close on idle channels':'Withdraw closed channels';if(!confirm(label+'?\\nOn-chain tx(s) on Base — one per channel, irreversible.'+(phase==='request-close'?'\\nWithdraw becomes possible ~15 min after.':'')))return;const v=$('walletReclaimView');if(v)v.innerHTML='<span class="muted">Sending on-chain tx(s)…</span>';try{const r=await fetch('/dashboard/api/wallet/reclaim/'+phase,{method:'POST',credentials:'same-origin'});if(r.status===401){showLogin();return}const t=await r.text();let d={};try{d=JSON.parse(t)}catch(_){}if(!r.ok){const raw=d.error?.message||(t&&t.trim()[0]!=='<'?t:(phase+' '+r.status));throw new Error(String(raw).split('\\n')[0].slice(0,240))}const done=((d.channels)||[]).filter(c=>c.tx).length;toast(phase==='request-close'?('Close requested · '+done+' channel(s) · withdraw in ~15 min'):('Withdrew '+done+' channel(s)'));walletReclaimScan();loadConfig()}catch(e){showErr(e.message)}}
4766
+
function renderReclaim(d){const v=$('walletReclaimView');if(!v)return;const ch=(d&&d.channels)||[];const isSelf=!!d.operatorIsSelf;let banner='';if(!isSelf){banner='<div style="padding:8px 10px;border:1px solid rgba(230,160,30,.5);border-radius:8px;margin-bottom:8px"><div class="small"><b>One-time setup needed.</b> To reclaim channels the buyer wallet must be assigned as its own on-chain operator — requestClose is operator-gated. Single tx, moves no funds.</div><div style="margin-top:6px"><button class="btn" onclick="walletReclaimAct(\\'set-operator\\')">Enable reclaim</button></div></div>';}const pend=ch.filter(c=>Number(c.reclaimable||0)>0&&!c.closeRequested);const ready=ch.filter(c=>c.closeRequested);const rows=ch.map(c=>{const id=String(c.id||'').slice(0,10)+'…';const st=c.error?'<span class="pill bad" title="'+esc(c.error)+'">error</span>':c.closeRequested?'<span class="pill warn">closing · withdraw in ~15 min</span>':'<span class="pill">idle</span>';return '<div style="display:flex;gap:8px;align-items:center;padding:3px 0;border-top:1px solid rgba(128,128,128,.12)"><code style="font-size:11px">'+esc(id)+'</code><span class="muted" style="font-size:11px">→ '+esc(String(c.seller||'').slice(0,10))+'…</span><span style="margin-left:auto;font-variant-numeric:tabular-nums">'+Number(c.reclaimable||0).toFixed(4)+' USDC</span>'+st+'</div>';}).join('');const sk=d.skipped||{};const skN=(sk.settled||0)+(sk.timeout||0)+(sk.other||0);const skTxt=skN?' · '+skN+' already settled (not reclaimable)':'';const summary='<div class="muted" style="margin-bottom:4px">'+(ch.length?ch.length+' active channel(s) · up to '+esc(d.reclaimableTotal||'0')+' USDC reclaimable':'No active channels holding reclaimable USDC')+skTxt+'</div>';let actions='';if(isSelf){const a=[];if(pend.length)a.push('<button class="btn" onclick="walletReclaimAct(\\'request-close\\')">Request close ('+pend.length+')</button>');if(ready.length)a.push('<button class="btn" onclick="walletReclaimAct(\\'withdraw\\')">Withdraw closed ('+ready.length+')</button>');actions='<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">'+(a.join('')||'<span class="muted">Nothing to close right now.</span>')+'</div>';}v.innerHTML=banner+summary+rows+actions;}
4767
+
async function walletReclaimAct(phase){const label=phase==='set-operator'?'Enable reclaim — assign your wallet as its own channel operator':phase==='request-close'?'Request close on idle channels':'Withdraw closed channels';const note=phase==='set-operator'?'\\nOne-time on-chain tx on Base. Moves no funds.':'\\nOn-chain tx(s) on Base — one per channel, irreversible.'+(phase==='request-close'?'\\nWithdraw becomes possible ~15 min after.':'');if(!confirm(label+'?'+note))return;const v=$('walletReclaimView');if(v)v.innerHTML='<span class="muted">Sending on-chain tx…</span>';try{const r=await fetch('/dashboard/api/wallet/reclaim/'+phase,{method:'POST',credentials:'same-origin'});if(r.status===401){showLogin();return}const t=await r.text();let d={};try{d=JSON.parse(t)}catch(_){}if(!r.ok){const raw=d.error?.message||(t&&t.trim()[0]!=='<'?t:(phase+' '+r.status));throw new Error(String(raw).split('\\n')[0].slice(0,240))}if(phase==='set-operator'){toast('Reclaim enabled · operator set')}else{const done=((d.channels)||[]).filter(c=>c.tx).length;toast(phase==='request-close'?('Close requested · '+done+' channel(s) · withdraw in ~15 min'):('Withdrew '+done+' channel(s)'))}walletReclaimScan();loadConfig()}catch(e){showErr(e.message)}}
0 commit comments