// Server-rendered first paint. A reader without JavaScript, or a tool that judges a// page by its HTML, sees what Duly is, the mainnet evidence and how to try it. When// the app's script runs it keeps the landing as it is and replaces invoice pages with// the interactive version. Everything here is pure, so it is tested without a network.// Every value that is not a literal goes through esc(), including data from the chain.import { MAINNET as E, NEARBLOCKS_TX, NEARBLOCKS_API_TX, BASESCAN_TX, BLOCKSCOUT_TX } from '../../evidence/mainnet.js';import { formatAmount } from '../../web/lib/money.js';export const REPO = 'https://code.swiftbotlab.workers.dev/duly';const ID = /^\d{1,10}-\d{1,10}$/;const HASH = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/;const EVM_HASH = /^0x[0-9a-fA-F]{64}$/;/** The same headers web/_headers gives static files; Worker responses do not get those. */export const SECURITY_HEADERS = { 'content-security-policy': "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://1click.chaindefuser.com https://free.rpc.fastnear.com https://rpc.mainnet.fastnear.com https://rpc.mainnet.near.org https://test.rpc.fastnear.com https://rpc.testnet.fastnear.com https://rpc.testnet.near.org; base-uri 'none'; form-action 'none'; frame-ancestors 'none'", 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff', 'permissions-policy': 'publickey-credentials-get=(self), publickey-credentials-create=(self)',};export function esc(v) { return String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);}const usdc = (u) => { try { return formatAmount(BigInt(u)); } catch { return '?'; }};const shortHash = (h) => `${h.slice(0, 8)}…${h.slice(-6)}`;const utc = (ms) => { const n = Number(ms); return n ? new Date(n).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : '';};export const STATUS_WORDS = { open: 'Not paid yet', overdue: 'Overdue', partial: 'Part paid', paid: 'Paid', partially_refunded: 'Part refunded', refunded: 'Refunded', cancelled: 'Cancelled',};const STATUS_KIND = { open: 'pending', overdue: 'bad', partial: 'pending', paid: 'good', partially_refunded: 'neutral', refunded: 'neutral', cancelled: 'neutral' };const nearTx = (key) => E.near.find((t) => t.key === key).hash;const baseTx = (key) => E.base.find((t) => t.key === key).hash;// One icon set: 16 px grid, 1.6 stroke, decorative only.// One icon set: 16 px grid, 1.6 stroke, decorative only.const ICONS = { copy: '<rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5V4A1.5 1.5 0 0 0 9 2.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5"/>', ext: '<path d="M7 3.5H3.5v9h9V9M9.5 3.5h3v3M12.5 3.5L7.5 8.5"/>', check: '<path d="M3 8.5l3 3 7-7"/>', lock: '<rect x="3.5" y="7" width="9" height="6.5" rx="1.5"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/>', term: '<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M5 6.5l2 1.5-2 1.5M8.5 10H11"/>', file: '<path d="M4 2.5h5l3 3v8H4zM9 2.5v3h3"/>', replay: '<path d="M3 8a5 5 0 1 0 1.5-3.6M3 2.5v2.5h2.5"/>', sun: '<circle cx="8" cy="8" r="2.8"/><path d="M8 1.5v1.6M8 12.9v1.6M1.5 8h1.6M12.9 8h1.6M3.4 3.4l1.1 1.1M11.5 11.5l1.1 1.1M3.4 12.6l1.1-1.1M11.5 4.5l1.1-1.1"/>', moon: '<path d="M13 9.6A5.5 5.5 0 0 1 6.4 3 5.5 5.5 0 1 0 13 9.6z"/>', key: '<circle cx="5.5" cy="10.5" r="3"/><path d="M7.6 8.4L13.5 2.5M11 5l1.6 1.6M12.4 3.6L14 5.2"/>', link: '<path d="M6.5 9.5l3-3M7 4.8l1-1a2.8 2.8 0 0 1 4 4l-1 1M9 11.2l-1 1a2.8 2.8 0 0 1-4-4l1-1"/>',};const icon = (name) => `<svg class="ico" viewBox="0 0 16 16" aria-hidden="true">${ICONS[name]}</svg>`;const copyBtn = (value, what) => `<button type="button" class="copy" data-copy="${esc(value)}" aria-label="Copy ${esc(what)}" title="Copy ${esc(what)}">${icon('copy')}</button>`;/** A transaction as a receipt row: network, short hash linked to the explorer, what it did, copy. */function txRow(net, hash, what, explorer = 'https://nearblocks.io') { const near = net === 'near'; const url = near ? `${explorer}/txns/${hash}` : BASESCAN_TX(hash); const alt = near ? '' : `<a class="alt" href="${esc(BLOCKSCOUT_TX(hash))}">Blockscout</a>`; return `<li class="tx"><span class="net ${near ? 'near' : 'base'}">${near ? 'NEAR' : 'Base'}</span><span class="h"><a href="${esc(url)}" class="mono" title="${esc(hash)}">${esc(shortHash(hash))}</a>${what ? `<span class="what">${esc(what)}</span>` : ''}</span><span class="acts">${alt}${copyBtn(hash, 'transaction hash')}<a class="ext" href="${esc(url)}" aria-label="Open in explorer">${icon('ext')}</a></span></li>`;}const i3 = () => E.invoices.find((i) => i.id === '1-3');const keptOf = (inv) => { try { return usdc(BigInt(inv.paid) - BigInt(inv.setAside)); } catch { return '?'; }};const sentOf = (key) => { const t = E.base.find((b) => b.key === key); try { return formatAmount(BigInt(t.amount), { min: 6 }); } catch { return '?'; }};// Chains the pay page offers in the picker, named in type (no permitted logo set carries all of them).const CHAINS = ['Base', 'Arbitrum', 'Optimism', 'Polygon', 'Gnosis', 'NEAR'];/** Hero: the client's pay window for invoice 1-3, exactly as the contract recorded it. Served finished * (Paid). With motion allowed, app.js plays the payment once: Base picked, the route pulses Base -> * NEAR Intents -> dulypay.near while the clock runs to the real 31.8 s, the stamp lands, the packets fill. */export function heroCardHtml() { const inv = i3(); const secs = E.secondsToClose['1-3']; const settle = nearTx('settle3'); return `<figure class="hero-art frame" data-visual data-ui="pay-window" aria-label="Invoice 1-3 on NEAR mainnet, paid from Base"><div class="pay-win" id="proof-inv" data-step="done"><div class="win-bar" aria-hidden="true"><span class="win-dots"><i></i><i></i><i></i></span><span class="win-url">duly.swiftbotlab.workers.dev/i/1-3</span></div><div class="sheet"><div class="sheet-head"><div><p class="sheet-title">Invoice 1-3</p><p class="sub">Number 0003, NEAR mainnet</p></div><span class="chip good proof-chip" id="proof-chip">${esc(STATUS_WORDS[inv.status] || inv.status)}</span></div><p class="sheet-amt"><b class="num">${esc(usdc(inv.amount))}</b><span>USDC</span></p><dl class="sheet-kv"><div><dt>Billed to</dt><dd>${icon('lock')}Private, encrypted on device</dd></div><div><dt>Merchant account</dt><dd class="mono">${esc(shortHash(E.merchant))}</dd></div></dl><div class="pay-with"><p class="lbl">Paid with USDC on</p><ul class="chain-pick" aria-label="Chains the pay page offers">${CHAINS.map((c) => `<li${c === 'Base' ? ' class="on"' : ''}>${esc(c)}</li>`).join('')}</ul><p class="quote"><b class="num">${esc(sentOf('pay3'))} USDC</b> sent from Base</p><p class="quote-wait" aria-hidden="true">Waiting for the client to pay</p></div><div class="route"><ol class="route-line" aria-label="Route of the payment"><li class="r-node">Base</li><li class="r-wire" aria-hidden="true"><i></i></li><li class="r-node">NEAR Intents</li><li class="r-wire" aria-hidden="true"><i></i></li><li class="r-node end">${esc(E.contract)}</li></ol><p class="clock"><b class="num" id="proof-clock">${esc(String(secs))} s</b><span>from the Base block to invoice 1-3 closed on NEAR</span></p></div><div class="packets"><div class="packet you"><span class="pk-l">Merchant</span> <b class="num" data-final="${esc(keptOf(inv))} USDC">${esc(keptOf(inv))} USDC</b><i class="pk-fill" aria-hidden="true"></i></div><div class="packet tax"><span class="pk-l">Tax set-aside</span> <b class="num" data-final="${esc(usdc(inv.setAside))} USDC">${esc(usdc(inv.setAside))} USDC</b><i class="pk-fill" aria-hidden="true"></i></div></div><svg class="paid-stamp" viewBox="0 0 132 48" aria-hidden="true"><rect x="2" y="2" width="128" height="44" rx="6"/><text x="66" y="25">PAID</text></svg></div><div class="settle-card"><p>Settled on NEAR by <span class="mono">ft_on_transfer</span>. Both shares split in the same receipt, no server in the path.</p><span class="hash"><a href="${esc(NEARBLOCKS_TX(settle))}" class="mono" title="${esc(settle)}">${esc(shortHash(settle))}</a>${copyBtn(settle, 'settlement hash')}</span></div></div><button type="button" class="replay" id="replay">${icon('replay')}Replay the payment</button></figure>`;}/** The scroll story: three scenes on one stage. Without JavaScript all three stand in a column. */const HOW_STAGE = `<div class="how-stage" data-ui="how-stage" aria-hidden="true"><div class="stage-in"><div class="scene s1" data-scene="1"><div class="mini-sheet"><p class="ms-title"><span>New invoice</span><span class="ms-no">Number 0003</span></p><div class="ms-lines"><p><span class="redact"></span><span class="redact amt"></span></p><p><span class="redact short"></span><span class="redact amt"></span></p></div><p class="ms-row"><span class="ms-k">${icon('key')}Signed in with a passkey</span><span class="ms-v">no wallet, no NEAR</span></p><p class="ms-row"><span class="ms-k">${icon('lock')}Client and line items</span><span class="ms-v">encrypted on this device</span></p><p class="ms-row total"><span class="ms-k">Invoice 1-3</span><span class="ms-v num">1.00 USDC</span></p><div class="ms-link"><span class="ms-url">${icon('link')}duly.swiftbotlab.workers.dev/i/1-3#…</span><span class="ms-btn">Copy link</span></div></div></div><div class="scene s2" data-scene="2"><svg class="flow flow-h" viewBox="0 0 560 400" role="presentation"><g class="flow-paths"><path id="fp0" class="fp hot" d="M112 40 C 220 40, 200 200, 300 200"/><path id="fp1" class="fp" d="M112 104 C 220 104, 210 200, 300 200"/><path id="fp2" class="fp" d="M112 168 C 210 168, 230 200, 300 200"/><path id="fp3" class="fp" d="M112 232 C 210 232, 230 200, 300 200"/><path id="fp4" class="fp" d="M112 296 C 220 296, 210 200, 300 200"/><path id="fp5" class="fp" d="M112 360 C 220 360, 200 200, 300 200"/><path class="fp hot out" d="M372 200 L 424 200"/></g><g class="flow-pulses"><path class="pz p0" d="M112 40 C 220 40, 200 200, 300 200"/><path class="pz p1" d="M112 104 C 220 104, 210 200, 300 200"/><path class="pz p2" d="M112 168 C 210 168, 230 200, 300 200"/><path class="pz p3" d="M112 232 C 210 232, 230 200, 300 200"/><path class="pz p4" d="M112 296 C 220 296, 210 200, 300 200"/><path class="pz p5" d="M112 360 C 220 360, 200 200, 300 200"/><path class="pz pout" d="M372 200 L 424 200"/></g><g class="flow-nodes">${CHAINS.map((c, i) => `<g class="fn${i === 0 ? ' hot' : ''}"><rect x="8" y="${22 + i * 64}" width="104" height="36" rx="6"/><text x="60" y="${45 + i * 64}">${esc(c)}</text></g>`).join('')}<g class="fn hub"><rect x="300" y="168" width="72" height="64" rx="32"/><text x="336" y="196">NEAR</text><text x="336" y="214">Intents</text></g><g class="fn dest"><rect x="424" y="164" width="128" height="72" rx="6"/><text x="488" y="192">dulypay.near</text><text class="t2" x="488" y="214">ft_on_transfer</text></g></g></svg><svg class="flow flow-v" viewBox="0 0 300 250" role="presentation"><g class="flow-paths"><path class="fp hot" d="M90 27 C 120 27, 110 76, 118 76"/><path class="fp" d="M90 75 C 120 75, 110 76, 118 76"/><path class="fp" d="M90 123 C 120 123, 110 76, 118 76"/><path class="fp" d="M210 27 C 180 27, 190 76, 182 76"/><path class="fp" d="M210 75 C 180 75, 190 76, 182 76"/><path class="fp" d="M210 123 C 180 123, 190 76, 182 76"/><path class="fp hot out" d="M150 108 L 150 186"/></g><g class="flow-pulses"><path class="pz s p0" d="M90 27 C 120 27, 110 76, 118 76"/><path class="pz s p1" d="M90 75 C 120 75, 110 76, 118 76"/><path class="pz s p2" d="M90 123 C 120 123, 110 76, 118 76"/><path class="pz s p3" d="M210 27 C 180 27, 190 76, 182 76"/><path class="pz s p4" d="M210 75 C 180 75, 190 76, 182 76"/><path class="pz s p5" d="M210 123 C 180 123, 190 76, 182 76"/><path class="pz s pout" d="M150 108 L 150 186"/></g><g class="flow-nodes"><g class="fn hot"><rect x="4" y="10" width="86" height="34" rx="6"/><text x="47" y="28">Base</text></g><g class="fn"><rect x="4" y="58" width="86" height="34" rx="6"/><text x="47" y="76">Arbitrum</text></g><g class="fn"><rect x="4" y="106" width="86" height="34" rx="6"/><text x="47" y="124">Optimism</text></g><g class="fn"><rect x="210" y="10" width="86" height="34" rx="6"/><text x="253" y="28">Polygon</text></g><g class="fn"><rect x="210" y="58" width="86" height="34" rx="6"/><text x="253" y="76">Gnosis</text></g><g class="fn"><rect x="210" y="106" width="86" height="34" rx="6"/><text x="253" y="124">NEAR</text></g><g class="fn hub"><circle cx="150" cy="76" r="32"/><text x="150" y="70">NEAR</text><text x="150" y="86">Intents</text></g><g class="fn dest"><rect x="66" y="186" width="168" height="56" rx="6"/><text x="150" y="206">dulypay.near</text><text class="t2" x="150" y="226">ft_on_transfer</text></g></g></svg></div><div class="scene s3" data-scene="3"><div class="pk-row"><div class="packet big you"><span class="pk-l">You 80%</span><b>0.80</b><i class="pk-fill" aria-hidden="true"></i></div><div class="packet big tax"><span class="pk-l">Tax 20%</span><b>0.20</b><i class="pk-fill" aria-hidden="true"></i></div><div class="packet big empty"><span class="pk-l">Subcontractor</span><b>optional</b></div></div><div class="csv"><p class="csv-name">${icon('file')}invoices.csv</p><table><thead><tr><th>Invoice number</th><th>Status</th><th>Received</th><th>Set aside</th></tr></thead><tbody><tr><td>INV-0003</td><td>Paid</td><td>1.00</td><td>0.20</td></tr></tbody></table></div></div></div></div>`;const HOW = `<section class="section how" id="how" data-moment="scroll-story"><div class="wrap"><h2>One link out, one payment in, books sorted</h2><p class="intro">Your client never needs an account. You never install a wallet or buy gas.</p><div class="how-grid"><ol class="how-steps"><li data-step="1"><h3>Send a link</h3><p>Sign in with your fingerprint or face, add line items and share the link. Client names and line items are encrypted on your device; only the amount, dates and a fingerprint of the document go on-chain.</p></li><li data-step="2"><h3>Your client pays from any chain</h3><p>Any major token on any major chain. NEAR Intents converts it to USDC and delivers it with the invoice number attached, so the payment closes the invoice on-chain.</p></li><li data-step="3"><h3>Shares land in their own balances</h3><p>Tax and subcontractor shares move into their own balances in the same step. Export a CSV your accountant can open.</p></li></ol>${HOW_STAGE}</div></div></section>`;/** The mainnet evidence: the one event (invoice 1-3) held large, every other movement as a ledger row. */const MOVES = [ { tag: 'Payout', amt: '0.10 USDC', what: 'Merchant withdraws 0.10 USDC to Base; gas paid by the relayer, payout started by the contract', txs: [['near', 'withdraw', 'withdraw'], ['base', 'payout', 'lands on Base']] }, { tag: 'Refund', amt: '0.05 USDC', what: 'Merchant refunds 0.05 USDC of invoice 1-1 to the address recorded at payment', txs: [['near', 'refund', 'refund'], ['base', 'refundOut', 'back with the payer']] }, { tag: 'Payment', amt: '0.25 USDC', what: 'Invoice 1-1, 0.25 USDC, paid from Base by script; 0.05 set aside', txs: [['near', 'settle1', 'ft_on_transfer'], ['base', 'pay1', 'from Base']] }, { tag: 'Deploy', what: 'Contract deployed and initialised', txs: [['near', 'deploy', 'deploy and init']] }, { tag: 'Setup', what: 'Merchant account created by the relayer. The merchant then registers, sets the 20% tax share and issues invoices 1-1 to 1-4 by signing; the relayer pays every fee. The account holds 1 yoctoNEAR and never bought gas', txs: [['near', 'account', 'account created'], ['near', 'register', 'register'], ['near', 'shares', 'set_shares, 20% tax'], ['near', 'issue1', 'issue invoice 1-1'], ['near', 'issue2', 'issue invoice 1-2'], ['near', 'issue3', 'issue invoice 1-3'], ['near', 'issue4', 'issue invoice 1-4']], },];const txOf = ([net, key, what]) => txRow(net, net === 'near' ? nearTx(key) : baseTx(key), what);export function evidenceHtml(live) { const total = E.near.length + E.base.length; const liveLine = live ? `<p class="live" id="live-totals"><span>Read from the chain at ${esc(utc(live.at))}: ${esc(live.invoices)} invoices, ${esc(usdc(live.received))} USDC received in total. The contract holds ${esc(usdc(live.held))} USDC and owes ${esc(usdc(live.liabilities))}.</span></p>` : ''; const moves = MOVES.map( (r) => `<li class="move"><div class="move-head"><span class="tag">${esc(r.tag)}</span>${r.amt ? `<span class="move-amt num">${esc(r.amt)}</span>` : ''}</div><p>${esc(r.what)}</p><ul class="txs">${r.txs.map(txOf).join('')}</ul></li>`, ).join('\n'); const cmd = `git clone ${REPO} && cd duly && node scripts/verify-mainnet.mjs`; return `<section class="section proof" id="evidence"><div class="wrap"><h2>Proof on NEAR mainnet</h2><p class="intro">On ${esc(E.date)} a client paid 1.00 USDC from Base. The payment itself closed invoice 1-3 on NEAR 31.8 seconds later, with 0.20 USDC already moved to the tax set-aside. No server marked it paid. Amounts were kept tiny and all of the money was ours.</p><dl class="contract"><div><dt>Contract</dt><dd><a href="https://nearblocks.io/address/${esc(E.contract)}" class="mono">${esc(E.contract)}</a>${copyBtn(E.contract, 'contract name')}</dd></div><div><dt>Code hash</dt><dd><code class="mono" title="${esc(E.codeHash)}">${esc(E.codeHash)}</code>${copyBtn(E.codeHash, 'code hash')}</dd></div><div><dt>Transactions</dt><dd>${esc(total)} on NEAR and Base</dd></div></dl><div class="ledger"><div class="ledger-main"><article class="event" data-ui="receipt" aria-label="Invoice 1-3, the payment and its settlement"><div class="event-head"><h3>Invoice 1-3</h3><span class="event-amt num">1.00 USDC</span><span class="chip good">Paid</span></div><ol class="event-steps"><li><p class="es-what"><span class="es-k">Payment</span>Client pays 1.00 USDC for invoice 1-3 from Base, through the pay page</p><ul class="txs">${txOf(['base', 'pay3', 'to a 1Click deposit address'])}</ul></li><li class="es-gap" aria-hidden="true"><span>31.8 s</span></li><li class="key-tx"><p class="es-what"><span class="es-k">Settlement</span>NEAR Intents delivers 1.00 USDC by ft_transfer_call. ft_on_transfer closes invoice 1-3 and moves 0.20 USDC to the tax set-aside in the same receipt, 31.8 s after the Base block</p><ul class="txs">${txOf(['near', 'settle3', 'ft_on_transfer'])}</ul></li></ol><div class="event-split" aria-hidden="true"><span class="you">Merchant 0.80</span><span class="tax">Tax 0.20</span></div></article>${liveLine}<div class="verify" data-ui="terminal"><div class="verify-bar">${icon('term')}<span>Check it without trusting this page</span>${copyBtn(cmd, 'command')}</div><pre><span class="p">$ </span>${esc(cmd).replace(' && ', ' && \\\n ').replace(' && node', ' && \\\n node')}</pre><p>Re-checks all ${esc(total)} transactions, the code hash, the agent's key and the invoice state against public NEAR and Base RPCs, with no keys. Explorer pages block some automated readers; the same data is in JSON at <a href="${esc(NEARBLOCKS_API_TX(nearTx('settle3')))}">api.nearblocks.io</a> and on <a href="${esc(BLOCKSCOUT_TX(baseTx('pay3')))}">Blockscout</a>.</p></div></div><ul class="moves">${moves}</ul></div></div></section>`;}function tryItHtml() { return `<section class="section try-sec" id="try"><div class="wrap"><h2>Four ways to check Duly yourself</h2><p class="intro">Start with test money in the browser, pay a real invoice for about 10 cents, or re-run everything from a terminal.</p><div class="tries"><article class="try primary-try" data-ui="steps"><div class="try-top"><p class="try-net">NEAR testnet</p><h3>No wallet, no passkey, no money</h3></div><ol class="try-steps"><li>Open <a href="/test/app">/test/app</a> and press <strong>Start</strong>.</li><li>Press <strong>New invoice</strong>, fill it in and press <strong>Issue invoice and get link</strong>.</li><li>In <strong>Settings</strong> keep the Tax 20% row and press <strong>Save</strong>.</li><li>Open the invoice, press <strong>Open as client</strong>, then <strong>Simulate the client paying</strong>: you see a PAID stamp and the settlement transaction.</li><li><strong>Money</strong> shows your balance and the tax jar, split in that transaction. <strong>Download CSV</strong> gives one row per invoice.</li></ol><p class="small muted">This is the real contract on testnet with worthless test USDC; only the cross-chain conversion is simulated, because NEAR Intents has no testnet.</p><div class="go"><a class="btn primary" href="/test/app">Try it with test money</a></div></article><article class="try"><p class="try-net">NEAR mainnet, optional</p><h3>Real money, about 10 cents</h3><p>Invoice <a href="/i/1-4">1-4</a> is open for 0.10 USDC until 2026-11-30. Open it in a browser, pick USDC on Arbitrum, Optimism, Polygon, Gnosis or NEAR (on Base and Ethereum 0.10 is below the route minimum), enter your own address for refunds, press <strong>See the price</strong>, then <strong>Get payment address</strong>. The page checks the NEAR Intents quote signature before it shows an address. When the money lands the invoice closes on-chain by itself.</p><p class="small muted">The first payment closes it; anything paid after that is recorded as extra and stays refundable.</p><div class="go"><a class="btn" href="/i/1-4">Open invoice 1-4</a></div></article><article class="try"><p class="try-net">Read only</p><h3>A paid invoice</h3><p><a href="/i/1-3">Invoice 1-3</a> shows the 1.00 USDC payment from Base and its settlement transaction.</p><div class="go"><a class="btn" href="/i/1-3">Open invoice 1-3</a></div></article><article class="try"><p class="try-net">Terminal</p><h3>From a terminal</h3><p><code>node scripts/demo-testnet.mjs</code> runs the whole loop on testnet in about a minute and prints every transaction; <code>node scripts/verify-mainnet.mjs</code> re-checks the mainnet run.</p><div class="go"><a class="btn" href="${esc(REPO)}">Get the source</a></div></article></div></div></section>`;}const REAL = `<section class="section real" id="real"><div class="wrap"><h2>What is real and what is simulated</h2><p class="intro">Mainnet runs the whole path with real USDC. Test mode runs the same contract on testnet; only the cross-chain leg is simulated.</p><div class="table-card" data-ui="table"><div class="scroll"><table class="facts-table"><thead><tr><th>Piece</th><th>Live (mainnet)</th><th>Test mode (testnet)</th></tr></thead><tbody><tr><td data-label="Piece">Contract: invoices, set-asides, refunds</td><td data-label="Live (mainnet)"><span class="ok">Real: <span class="mono">dulypay.near</span>, unaudited; new invoices 0.50 to 25 USDC</span></td><td data-label="Test mode (testnet)"><span class="ok">Real: <span class="mono">dulypay.testnet</span></span></td></tr><tr><td data-label="Piece">Gas sponsorship (NEP-366 meta transactions)</td><td data-label="Live (mainnet)"><span class="ok">Real</span></td><td data-label="Test mode (testnet)"><span class="ok">Real</span></td></tr><tr><td data-label="Piece">Client payment from another chain</td><td data-label="Live (mainnet)"><span class="ok">Real, through the NEAR Intents 1Click API</span></td><td data-label="Test mode (testnet)"><span class="ok sim">Simulated: the Simulate button sends test USDC</span></td></tr><tr><td data-label="Piece">Subcontractor share at settlement</td><td data-label="Live (mainnet)">Same code; the mainnet run used a tax share only</td><td data-label="Test mode (testnet)"><span class="ok">Real: 20% tax and 10% subcontractor in one transaction</span></td></tr><tr><td data-label="Piece">Collections agent</td><td data-label="Live and test mode" colspan="2">Plain rules on a timer with a function-call key; no language model</td></tr></tbody></table></div></div></div></section>`;// QR code of the test app's address, drawn once with the app's own generator (web/vendor/qrcode.js),// so a judge on a desktop can open the test app on a phone. Regenerate if the host changes.const TEST_APP_URL = 'https://duly.swiftbotlab.workers.dev/test/app';const TEST_APP_QR = { n: 33, d: 'M0 0h7v1h-7zM8 0h1v1h-1zM10 0h2v1h-2zM13 0h4v1h-4zM18 0h1v1h-1zM22 0h2v1h-2zM26 0h7v1h-7zM0 1h1v1h-1zM6 1h1v1h-1zM9 1h2v1h-2zM12 1h3v1h-3zM16 1h1v1h-1zM18 1h3v1h-3zM24 1h1v1h-1zM26 1h1v1h-1zM32 1h1v1h-1zM0 2h1v1h-1zM2 2h3v1h-3zM6 2h1v1h-1zM9 2h1v1h-1zM12 2h1v1h-1zM14 2h1v1h-1zM16 2h3v1h-3zM20 2h3v1h-3zM24 2h1v1h-1zM26 2h1v1h-1zM28 2h3v1h-3zM32 2h1v1h-1zM0 3h1v1h-1zM2 3h3v1h-3zM6 3h1v1h-1zM8 3h5v1h-5zM16 3h2v1h-2zM21 3h2v1h-2zM24 3h1v1h-1zM26 3h1v1h-1zM28 3h3v1h-3zM32 3h1v1h-1zM0 4h1v1h-1zM2 4h3v1h-3zM6 4h1v1h-1zM8 4h2v1h-2zM11 4h1v1h-1zM13 4h1v1h-1zM16 4h2v1h-2zM19 4h1v1h-1zM23 4h1v1h-1zM26 4h1v1h-1zM28 4h3v1h-3zM32 4h1v1h-1zM0 5h1v1h-1zM6 5h1v1h-1zM8 5h2v1h-2zM13 5h1v1h-1zM15 5h1v1h-1zM20 5h2v1h-2zM24 5h1v1h-1zM26 5h1v1h-1zM32 5h1v1h-1zM0 6h7v1h-7zM8 6h1v1h-1zM10 6h1v1h-1zM12 6h1v1h-1zM14 6h1v1h-1zM16 6h1v1h-1zM18 6h1v1h-1zM20 6h1v1h-1zM22 6h1v1h-1zM24 6h1v1h-1zM26 6h7v1h-7zM8 7h1v1h-1zM10 7h1v1h-1zM12 7h1v1h-1zM14 7h1v1h-1zM17 7h1v1h-1zM19 7h3v1h-3zM23 7h1v1h-1zM0 8h1v1h-1zM4 8h1v1h-1zM6 8h3v1h-3zM10 8h2v1h-2zM15 8h1v1h-1zM17 8h1v1h-1zM21 8h2v1h-2zM24 8h6v1h-6zM32 8h1v1h-1zM1 9h1v1h-1zM3 9h3v1h-3zM8 9h1v1h-1zM10 9h1v1h-1zM12 9h1v1h-1zM15 9h4v1h-4zM21 9h5v1h-5zM29 9h3v1h-3zM2 10h1v1h-1zM6 10h1v1h-1zM9 10h2v1h-2zM12 10h5v1h-5zM18 10h1v1h-1zM20 10h1v1h-1zM23 10h3v1h-3zM29 10h1v1h-1zM31 10h1v1h-1zM1 11h1v1h-1zM3 11h1v1h-1zM5 11h1v1h-1zM8 11h1v1h-1zM10 11h4v1h-4zM16 11h1v1h-1zM19 11h2v1h-2zM22 11h2v1h-2zM25 11h1v1h-1zM32 11h1v1h-1zM1 12h1v1h-1zM3 12h4v1h-4zM10 12h1v1h-1zM12 12h2v1h-2zM16 12h1v1h-1zM22 12h2v1h-2zM26 12h1v1h-1zM28 12h2v1h-2zM1 13h4v1h-4zM7 13h2v1h-2zM14 13h1v1h-1zM18 13h3v1h-3zM23 13h1v1h-1zM27 13h1v1h-1zM29 13h2v1h-2zM0 14h2v1h-2zM5 14h7v1h-7zM13 14h3v1h-3zM17 14h1v1h-1zM20 14h1v1h-1zM23 14h7v1h-7zM31 14h1v1h-1zM1 15h1v1h-1zM3 15h1v1h-1zM9 15h1v1h-1zM12 15h7v1h-7zM21 15h1v1h-1zM23 15h1v1h-1zM25 15h2v1h-2zM28 15h1v1h-1zM1 16h1v1h-1zM3 16h1v1h-1zM5 16h4v1h-4zM12 16h3v1h-3zM16 16h2v1h-2zM19 16h4v1h-4zM26 16h1v1h-1zM28 16h1v1h-1zM31 16h1v1h-1zM0 17h3v1h-3zM7 17h2v1h-2zM13 17h4v1h-4zM18 17h1v1h-1zM23 17h1v1h-1zM25 17h1v1h-1zM29 17h3v1h-3zM1 18h2v1h-2zM5 18h2v1h-2zM11 18h1v1h-1zM13 18h1v1h-1zM15 18h1v1h-1zM17 18h1v1h-1zM21 18h3v1h-3zM27 18h1v1h-1zM29 18h1v1h-1zM0 19h3v1h-3zM4 19h1v1h-1zM7 19h2v1h-2zM11 19h1v1h-1zM13 19h2v1h-2zM17 19h3v1h-3zM21 19h1v1h-1zM23 19h5v1h-5zM31 19h2v1h-2zM0 20h1v1h-1zM3 20h1v1h-1zM6 20h2v1h-2zM10 20h1v1h-1zM14 20h2v1h-2zM18 20h3v1h-3zM23 20h2v1h-2zM26 20h4v1h-4zM31 20h2v1h-2zM0 21h2v1h-2zM4 21h2v1h-2zM7 21h2v1h-2zM11 21h2v1h-2zM16 21h1v1h-1zM19 21h1v1h-1zM21 21h3v1h-3zM27 21h1v1h-1zM29 21h3v1h-3zM2 22h1v1h-1zM5 22h4v1h-4zM11 22h2v1h-2zM14 22h1v1h-1zM16 22h1v1h-1zM19 22h1v1h-1zM21 22h5v1h-5zM31 22h1v1h-1zM2 23h2v1h-2zM5 23h1v1h-1zM7 23h3v1h-3zM13 23h1v1h-1zM17 23h1v1h-1zM19 23h3v1h-3zM24 23h1v1h-1zM31 23h1v1h-1zM0 24h3v1h-3zM4 24h1v1h-1zM6 24h3v1h-3zM10 24h1v1h-1zM12 24h2v1h-2zM15 24h1v1h-1zM17 24h2v1h-2zM20 24h10v1h-10zM8 25h1v1h-1zM10 25h1v1h-1zM14 25h5v1h-5zM21 25h1v1h-1zM24 25h1v1h-1zM28 25h1v1h-1zM30 25h1v1h-1zM0 26h7v1h-7zM8 26h3v1h-3zM12 26h4v1h-4zM18 26h1v1h-1zM24 26h1v1h-1zM26 26h1v1h-1zM28 26h2v1h-2zM31 26h1v1h-1zM0 27h1v1h-1zM6 27h1v1h-1zM10 27h1v1h-1zM13 27h1v1h-1zM20 27h1v1h-1zM22 27h1v1h-1zM24 27h1v1h-1zM28 27h1v1h-1zM32 27h1v1h-1zM0 28h1v1h-1zM2 28h3v1h-3zM6 28h1v1h-1zM8 28h5v1h-5zM16 28h1v1h-1zM19 28h1v1h-1zM22 28h1v1h-1zM24 28h6v1h-6zM31 28h2v1h-2zM0 29h1v1h-1zM2 29h3v1h-3zM6 29h1v1h-1zM9 29h1v1h-1zM17 29h4v1h-4zM23 29h2v1h-2zM26 29h6v1h-6zM0 30h1v1h-1zM2 30h3v1h-3zM6 30h1v1h-1zM10 30h1v1h-1zM12 30h4v1h-4zM18 30h1v1h-1zM20 30h1v1h-1zM25 30h6v1h-6zM0 31h1v1h-1zM6 31h1v1h-1zM10 31h1v1h-1zM15 31h3v1h-3zM21 31h5v1h-5zM29 31h1v1h-1zM0 32h7v1h-7zM8 32h1v1h-1zM10 32h3v1h-3zM16 32h2v1h-2zM19 32h1v1h-1zM21 32h2v1h-2zM24 32h3v1h-3zM32 32h1v1h-1z' };export function landingHtml({ test = false, live = null } = {}) { const base = test ? '/test' : ''; const total = E.near.length + E.base.length; return `<div data-ssr="landing" class="landing"><header class="nav" id="nav"><div class="wrap nav-in"><a class="skip" href="#main">Skip to content</a><a class="brand" href="${base}/">Duly</a>${test ? '<span class="chip pending nav-test">Test mode</span>' : ''}<nav class="nav-links" aria-label="Sections"><a href="#how">How it works</a><a href="#evidence">Proof</a><a href="#try">Check it</a><a href="#real">Limits</a></nav><div class="nav-cta"><button type="button" class="theme-toggle" id="theme-toggle" aria-label="Switch to dark mode">${icon('moon')}${icon('sun')}</button><a class="nav-src" href="${esc(REPO)}">Source</a><a class="btn primary" href="/test/app">Try it with test money</a></div></div></header><main id="main"><section class="hero" id="top"><div class="wrap hero-grid"><div class="hero-text">${test ? '<p class="notice test">Test mode. No real money.</p>' : `<p class="notice"><a href="#evidence"><span>Live on NEAR mainnet<span class="n-more">, ${esc(total)} transactions you can re-check</span></span></a></p>`}<h1>Invoicing for freelancers, paid from any chain</h1><p class="lead">Your client pays with any major token on any major chain. You receive USDC on NEAR, and the payment itself closes the invoice on-chain and sets the tax and subcontractor shares aside in the same step.</p><div class="btn-row"><a class="btn primary" href="/test/app">Try it with test money</a><a class="btn" href="/i/1-4">Pay a real 0.10 USDC invoice</a></div><p class="fact">No wallet to install, no gas to buy. Shares split on-chain.</p></div>${heroCardHtml()}</div></section>${HOW}${evidenceHtml(live)}${tryItHtml()}${REAL}</main><footer class="closing" id="contact"><div class="wrap"><div class="close-grid"><div class="close-row"><h2>Send your first invoice with test money</h2><p>The real contract on NEAR testnet, in your browser. No wallet, no passkey, no money.</p><a class="btn primary btn--xl" href="/test/app">Try it with test money</a></div><figure class="close-qr" data-visual aria-label="QR code that opens the test app"><p class="cq-title">Try it on your phone</p><svg viewBox="-2 -2 ${TEST_APP_QR.n + 4} ${TEST_APP_QR.n + 4}" role="img" aria-label="QR code for ${esc(TEST_APP_URL)}"><rect x="-2" y="-2" width="${TEST_APP_QR.n + 4}" height="${TEST_APP_QR.n + 4}" class="cq-bg"/><path d="${TEST_APP_QR.d}" class="cq-ink"/></svg><p class="cq-url mono">duly.swiftbotlab.workers.dev<br>/test/app</p></figure></div><div class="foot-dir"><div class="foot-brand"><a class="brand" href="${base}/">Duly</a><p>Built at NEAR HACKATHON. Source, tests and reproduction steps: <a href="${esc(REPO)}">${esc(REPO.replace('https://', ''))}</a> (MIT).</p></div><nav class="foot-links" aria-label="Footer"><a href="/test/app">Test app</a><a href="/i/1-4">Sample invoice</a><a href="/i/1-3">Paid invoice</a><a href="#evidence">Mainnet proof</a><a href="${esc(REPO)}">Source</a></nav></div></div></footer></div>`;}/** The chain's view of one invoice. The document key never reaches the server, so names and line items stay out. */export function invoiceHtml({ id, test = false, inv, payments = [], verified = null }) { const base = test ? '/test' : ''; const explorer = test ? 'https://testnet.nearblocks.io' : 'https://nearblocks.io'; const contract = test ? 'dulypay.testnet' : E.contract; const head = `${test ? '<div class="testbar">Test mode. No real money.</div>' : ''}<header class="top"><a class="brand" href="${base}/">Duly</a></header>`; if (!ID.test(String(id)) || !inv) { return `<div data-ssr="invoice">${head}<main class="wrap narrow stack"><section class="card stack"><h2>Invoice ${esc(id)}</h2><p class="muted">${inv === null ? 'No invoice with this number exists on the contract.' : 'The NEAR chain did not answer just now. Reload this page, or ask the chain directly as shown below.'}</p></section>${checkHtml(id, contract, explorer, test)}</main></div>`; } const paid = ['paid', 'partially_refunded'].includes(inv.status); const status = esc(STATUS_WORDS[inv.status] || inv.status); const chip = paid ? '<div class="stamp">PAID</div>' : `<span class="chip ${esc(STATUS_KIND[inv.status] || 'neutral')}">${status}</span>`; const big = (v) => { try { return BigInt(v) > 0n; } catch { return false; } }; // The watcher's records, plus the run in evidence/mainnet.js for the invoices it covers. const known = !test && E.invoices.find((i) => i.id === id && i.settledBy); const fromEvidence = known ? [{ near_tx: nearTx(known.settledBy), origin_tx: baseTx(known.paidFrom) }] : []; const settle = [...payments, ...fromEvidence] .filter((p) => typeof p.near_tx === 'string' && HASH.test(p.near_tx)) .filter((p, i, all) => all.findIndex((q) => q.near_tx === p.near_tx) === i); const sample = !test && E.invoices.find((i) => i.id === id && i.sample); const open = big(inv.due_now) && !['cancelled'].includes(inv.status); const payList = (inv.payments || []) .map((p) => `<div class="item"><div><p>${esc(usdc(p.amount))} USDC</p><p class="muted small">${esc(utc(p.at_ms))}, NEAR block ${esc(p.block)}</p></div><p class="muted small right">Refunds go to ${esc(String(p.refund_to || '').split(':').slice(0, 2).join(' ') || 'the payer')}</p></div>`) .join(''); const txList = settle .map((p) => `${typeof p.origin_tx === 'string' && EVM_HASH.test(p.origin_tx) ? txRow('base', p.origin_tx, 'client payment') : ''}${txRow('near', p.near_tx, 'settlement', explorer)}`) .join(''); return `<div data-ssr="invoice">${head}<main class="wrap narrow stack">${verified === false ? '<div class="banner bad">The contract on-chain is not the code this site expects. Do not pay this invoice.</div>' : ''}<section class="card invoice"><div class="inv-head"><div><h2>Invoice ${esc(String(inv.number).padStart(4, '0'))}</h2><p class="muted small mono">Number ${esc(inv.id)}, issued by account ${esc(shortHash(String(inv.merchant)))}</p></div><div class="inv-no">${chip}</div></div><div class="parties"><div><p class="muted small">Status</p><p>${status}</p></div><div><p class="muted small">Issued</p><p>${esc(utc(inv.issued_at_ms))}</p></div><div><p class="muted small">Due</p><p>${esc(utc(inv.due_at_ms))}</p></div></div><div class="totals"><div class="grand"><span>Total</span><span>${esc(usdc(inv.amount))} USDC</span></div><div class="small"><span>Received</span><span>${esc(usdc(inv.paid))}</span></div><div class="small"><span>Set aside at settlement (tax, subcontractors)</span><span>${esc(usdc(inv.set_aside))}</span></div>${big(inv.refunded) ? `<div class="small"><span>Refunded</span><span>${esc(usdc(inv.refunded))}</span></div>` : ''}${big(inv.excess) ? `<div class="small"><span>Extra paid by client, refundable</span><span>${esc(usdc(inv.excess))}</span></div>` : ''}${open ? `<div class="small"><span>Still due</span><span>${esc(usdc(inv.due_now))}</span></div>` : ''}</div><p class="muted small inv-note">Client name and line items are encrypted. They open only in a browser, with the key in the part of the link after #, which never reaches a server. Everything above is read from contract ${esc(contract)} on NEAR${test ? ' testnet' : ''}.</p></section>${payList || txList ? `<section class="card stack"><h3>Payment record</h3>${payList ? `<div class="list">${payList}</div>` : ''}${txList ? `<ul class="txs flush">${txList}</ul>` : ''}</section>` : ''}${open ? `<section class="card stack nojs-only"><h3>Pay this invoice</h3><p>Open this page in a browser with JavaScript: it asks NEAR Intents for a signed quote, checks the signature and the amount, and only then shows a deposit address. ${sample ? `This is the public sample invoice. 1Click quoted 0.10 USDC from ${esc(E.sampleRoutes.join(', '))}; on Base and Ethereum it is below the route minimum. The first payment closes it; later payments are recorded as extra and stay refundable.` : ''}</p></section>` : ''}${checkHtml(id, contract, explorer, test)}</main></div>`;}function checkHtml(id, contract, explorer, test) { const args = btoa(JSON.stringify({ id: String(id) })); const rpcUrl = test ? 'https://test.rpc.fastnear.com' : 'https://free.rpc.fastnear.com'; return `<details class="card small"><summary>Check this invoice yourself</summary><div class="stack"><p class="muted">Ask the chain directly, without anything of ours in the path:</p><pre class="mono">curl -s ${esc(rpcUrl)} -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"query","params":{"request_type":"call_function","finality":"final","account_id":"${esc(contract)}","method_name":"get_invoice","args_base64":"${esc(args)}"}}'</pre><p><a href="${esc(`${explorer}/address/${contract}`)}">Open the contract on the NEAR explorer</a></p></div></details>`;}/** Puts server-rendered markup into the app root of index.html. */export function injectApp(indexHtml, inner, { title, description } = {}) { const marker = /<div id="app">[\s\S]*?<\/div>\s*<div id="toast"/; if (!marker.test(indexHtml)) throw new Error('index.html has no app root'); let out = indexHtml.replace(marker, () => `<div id="app">${inner}</div>\n <div id="toast"`); out = out.replace(/<noscript>[\s\S]*?<\/noscript>/, '<noscript><p class="boot">Paying or issuing an invoice needs JavaScript. Everything above is readable without it.</p></noscript>'); if (title) out = out.replace(/<title>[^<]*<\/title>/, () => `<title>${esc(title)}</title>`); if (description) out = out.replace(/<meta name="description" content="[^"]*">/, () => `<meta name="description" content="${esc(description)}">`); return out;}/** Which server-rendered page a path gets, if any. */export function pageFor(pathname) { const test = pathname === '/test' || pathname.startsWith('/test/'); const path = pathname.replace(/^\/test(?=\/|$)/, '') || '/'; if (path === '/' || path === '/index.html') return { kind: 'landing', test }; const m = path.match(/^\/i\/(\d{1,10}-\d{1,10})$/); if (m) return { kind: 'invoice', test, id: m[1] }; return null;}