import { JsonRpcProvider, FetchRequest, formatUnits } from 'ethers'; import { CHAIN } from './vesting.js'; import { publicLink, readPublicSchedule } from './public-reader.js'; const $ = selector => document.querySelector(selector); const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[c]); const form = $('#lookup'), message = $('#read-status'); const query = new URLSearchParams(location.search); form.elements.workspace.value = query.get('workspace') || ''; form.elements.schedule.value = query.get('schedule') || '1'; let loadedLink = ''; const explorer = address => `${escape(address)} ↗`; const date = value => new Date(value * 1000).toLocaleString(undefined, { dateStyle:'long', timeStyle:'long' }); function render(s) { const amount = value => `${formatUnits(value, s.decimals)} ${s.symbol}`; const progress = Number(BigInt(s.vested) * 10000n / BigInt(s.total)) / 100; $('#public-detail').innerHTML = `

Schedule #${escape(s.id)}

${({ active:'VESTING', locked:'LOCKED', complete:'COMPLETED' })[s.status]}
${s.rawUnits?'

Token decimals could not be read. All amounts below are exact raw base units.

':''}
${escape(amount(s.total))}

Total allocation · ${progress.toFixed(2)}% vested

${[['Vested', amount(s.vested)], ['Claimed', amount(s.released)], ['Claimable now', amount(s.claimable)], ['Not yet vested', amount(BigInt(s.total)-BigInt(s.vested))], ['Remaining in this schedule', amount(BigInt(s.total)-BigInt(s.released))], ['Start',date(s.start)], ['Cliff',date(s.cliff)], ['End',date(s.end)]].map(([key,value])=>`
${key}
${escape(value)}
`).join('')} ${[['Token',s.token],['Creator',s.creator],['Beneficiary',s.beneficiary],['Workspace',s.workspace]].map(([key,address])=>`
${key}
${explorer(address)}
`).join('')}

Every claim pays the beneficiary above. Nothing is claimable before the cliff; at the cliff, vesting includes time elapsed since the start. Terms cannot be edited or cancelled.

Snapshot at block ${s.blockNumber} ↗ · ${escape(date(s.blockTime))}. Read again to update.

`; } async function load() { const workspace = form.elements.workspace.value.trim(), id = form.elements.schedule.value.trim(); loadedLink = ''; $('#copy-link').hidden = true; $('#public-detail').innerHTML = ''; $('#load').disabled = true; form.elements.workspace.disabled = true; form.elements.schedule.disabled = true; message.textContent = 'Reading Robinhood mainnet…'; const request = new FetchRequest(CHAIN.rpcUrls[0]); request.timeout = 12000; const provider = new JsonRpcProvider(request, undefined, { batchMaxCount: 1 }); try { const response = await fetch('/build/contracts.json', { signal: AbortSignal.timeout(12000) }); if (!response.ok) throw Error('Contract information is unavailable. Please retry.'); const artifact = (await response.json()).Veylock; const record = await readPublicSchedule(provider, artifact, workspace, id); render(record); loadedLink = publicLink(location.origin, workspace, id); history.replaceState(null, '', loadedLink); $('#copy-link').hidden = false; message.textContent = 'Contract code verified. Details were read directly from Robinhood mainnet.'; } catch(error) { const useful = error.shortMessage || error.message || 'Network request failed.'; message.textContent = `Unable to read this schedule. ${useful} Check the address and ID, then retry. No wallet is needed.`; } finally { provider.destroy(); $('#load').disabled = false; form.elements.workspace.disabled = false; form.elements.schedule.disabled = false; } } form.addEventListener('submit', event => { event.preventDefault(); void load(); }); $('#copy-link').addEventListener('click', async () => { try { await navigator.clipboard.writeText(loadedLink); message.textContent = ['localhost','127.0.0.1'].includes(location.hostname) ? 'Link copied. Host the app publicly before sharing this localhost link.' : 'Public link copied. Anyone can inspect this schedule without connecting a wallet.'; } catch { message.textContent = 'Copy this page’s URL from your address bar to share it.'; } }); if(query.get('workspace'))void load();