Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3d076be
feat(ohttp): bound a sealed request by an inner x-date and time attem…
Kukks Nov 11, 2026
5323a32
feat(shielded): add the browser's policy-gated sealed transport
Kukks Nov 11, 2026
5930fe4
fix(shielded): route case-insensitively and connect to the relay URL …
Kukks Nov 11, 2026
eb7f89f
feat(shielded): size sealed pool pages and stop repeating a sealed 502
Kukks Nov 11, 2026
8becf89
fix(shielded): give up on a 502 only for the browser's sealed fetch
Kukks Nov 11, 2026
2f6aefe
feat(native): let a journal release a spend a 404 proves lost
Kukks Nov 11, 2026
689ab53
feat(shielded): give the sealed transport a grace, a page brand and a…
Kukks Nov 11, 2026
7f361f5
feat(shielded): strip the deposit sign request to the depositor's own…
Kukks Nov 11, 2026
4ecf17f
fix(shielded): widen the browser's 404 grace to 15 minutes
Kukks Nov 11, 2026
08e1481
feat(shielded): refuse batch swaps on a wallet whose transport cannot…
Kukks Nov 11, 2026
97728f1
fix(shielded): refuse an on-chain send at quote time when batch swaps…
Kukks Nov 11, 2026
08b2f49
feat(app): boot the browser private transport behind an internal flag
Kukks Nov 11, 2026
6f05b04
perf(shielded): let a build without the private transport drop its ci…
Kukks Nov 11, 2026
3abfd55
docs(dokploy): describe the private wallet build and its flag
Kukks Nov 11, 2026
69fb5cc
fix(app): let no link turn the private transport off
Kukks Nov 11, 2026
cd27183
fix(app): keep the wallet closed when an unpinned build sees the priv…
Kukks Nov 11, 2026
8fc3c6a
fix(shielded): end the browser's 404 release at 45 minutes, not the p…
Kukks Nov 11, 2026
5410468
fix(app): release a lost private spend on a wallet tick, without reop…
Kukks Nov 11, 2026
2a0da99
fix(shielded): send a GET of the native release list direct in privat…
Kukks Nov 11, 2026
d8b307d
fix(shielded): give up on a sealed read that came back unavailable
Kukks Nov 11, 2026
38d7c37
fix(shielded): let a wallet with batch swaps off still send to itself…
Kukks Nov 11, 2026
6b79763
test: import the carrier's ANSWER_BYTES instead of copying it
Kukks Nov 11, 2026
9746ee7
fix(shielded): cap a private wallet's shield at 20 coins, soonest to …
Kukks Nov 11, 2026
c498d7d
fix(app): open a private wallet's receive tab on shielded, with no un…
Kukks Nov 11, 2026
c74d22a
fix(app): say what main.tsx's first import guarantees, and pin the li…
Kukks Nov 11, 2026
52cc5f7
test: pin the sealed POST's 45 s bound and the 30 s rounding
Kukks Nov 11, 2026
8d91f5b
fix(rollup): start the gateway's replay window when the carrier start…
Kukks Nov 11, 2026
a2cdf73
docs(dokploy): bring the private gateway and wallet build sections up…
Kukks Nov 11, 2026
75d22dc
fix(app): lay out the receive strip in one column per offered tab
Kukks Nov 11, 2026
2b2a93a
test: pin the private tick reconcile and the private shield cap wiring
Kukks Nov 11, 2026
8de5e04
fix(shielded): let a capped shield take the largest coins when only t…
Kukks Nov 11, 2026
cdd024a
fix(app): offer no Lightning, Bitcoin or renewal action a private wal…
Kukks Nov 11, 2026
f20805a
test: give the relay-retry test room for a slow second relay
Kukks Nov 11, 2026
4ccc208
fix(shielded): shield the largest pool payout alone when no merged gr…
Kukks Nov 11, 2026
3e026f3
fix(app): say how to turn private networking off when sats cannot be …
Kukks Nov 11, 2026
d86fb59
fix(app): tell testers to drop ?private=1 when turning private networ…
Kukks Nov 11, 2026
566b3f9
Merge origin/main into feat/wallet-private-transport
Kukks Nov 11, 2026
df9c97b
Merge origin/main into feat/wallet-private-transport
Kukks Nov 11, 2026
9521983
fix(app): clear a private reconcile error once a later reconcile succ…
Kukks Nov 11, 2026
ae139c2
test: bound the sealed x-date by the clock around the fetch
Kukks Nov 11, 2026
f522cab
Merge origin/main into feat/wallet-private-transport
Kukks Nov 11, 2026
42a2f9d
fix(app): keep a private journal error apart from the page's pool error
Kukks Nov 11, 2026
81abbe2
docs(dokploy): list replay protection across restarts as a production…
Kukks Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Dockerfile.dokploy
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# The app cache's escape hatches (docs/DOKPLOY.md, App cache); RUN only sees build arguments declared here.
# The app cache's escape hatches and the private transport's pins (docs/DOKPLOY.md: App cache, Nostr carrier); RUN only sees build arguments declared here.
ARG SHIELDED_APP_CACHE_FORCE
ARG SHIELDED_APP_CACHE_OFF
ARG SHIELDED_OHTTP_CONFIG
ARG SHIELDED_NOSTR_GATEWAY
ARG SHIELDED_NOSTR_RELAYS
ARG SHIELDED_PUBLIC_ORIGIN
RUN npm run build

FROM --platform=linux/amd64 golang:1.26.9-bookworm AS vm-build
Expand Down
10 changes: 6 additions & 4 deletions app/src/Disclose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import type {Hash} from '../../packages/protocol/src/rollup/notes.ts';
import {rollupAddressOf} from '../../packages/protocol/src/rollup/wallet.ts';
import {epochMatchesPool,epochPoolOf} from '../../src/shielded/epoch-checkpoint.ts';
import {epochIncomingNotes} from '../../src/shielded/epoch-disclosure.ts';
import {privateTransport} from '../../src/shielded/private-transport.ts';
import {epochSyncAccount,poolApiOf,rollupApi,type PoolApi,type RollupPoolStatus} from './rollup-client.ts';

const EXPLORER='https://explorer.mutinynet.arkade.sh';
const sats=(value:bigint|number)=>Number(value).toLocaleString('en-US'),short=(id:string)=>`${id.slice(0,8)}…${id.slice(-4)}`;
const TxLink=({txid}:{txid:string})=>privateTransport()?<code>{txid}</code>:<a className="stock-txlink" href={`${EXPLORER}/tx/${txid}`} target="_blank" rel="noreferrer">{short(txid)} ↗</a>;
const when=(at?:number)=>at?new Date(at).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'}):'';
const unit=(asset:bigint)=>asset===0n?'sats':'asset units';
const totals=(items:{asset:bigint;amount:bigint}[])=>{const m=new Map<bigint,bigint>();for(const i of items)m.set(i.asset,(m.get(i.asset)??0n)+i.amount);return [...m].map(([asset,amount])=>`${sats(amount)} ${unit(asset)}`).join(' + ');};
Expand Down Expand Up @@ -51,19 +53,19 @@ export function Results({checked,watched}:{checked?:Checked;watched?:Watched}){
<p className="stock-muted">{checked.ok.every(Boolean)?'Every note below is exactly the one the pool published at its position, owned by this address.':'At least one note differs from what the pool published, so this payment is not proven. A link minted against a different pool, such as an earlier genesis, cannot verify here.'}</p>
<dl className="stock-facts"><dt>Pool</dt><dd>{checked.pool?<code>{checked.pool}</code>:'Not reported'}</dd><dt>To</dt><dd><code>{checked.disclosure.to}</code></dd>
{checked.disclosure.notes.map((note,i)=>{const batch=Math.floor(note.index/32),record=checked.batches[batch];
return <div key={i} className="stock-fact-row"><dt>Note {i+1}</dt><dd>{checked.ok[i]?'✓':'✗'} {sats(note.amount)} {unit(note.asset)} · batch #{batch}{record?.at?` · ${when(record.at)}`:''}{record?.txid&&<> · <a className="stock-txlink" href={`${EXPLORER}/tx/${record.txid}`} target="_blank" rel="noreferrer">{short(record.txid)} ↗</a></>}</dd></div>;})}
return <div key={i} className="stock-fact-row"><dt>Note {i+1}</dt><dd>{checked.ok[i]?'✓':'✗'} {sats(note.amount)} {unit(note.asset)} · batch #{batch}{record?.at?` · ${when(record.at)}`:''}{record?.txid&&<> · <TxLink txid={record.txid}/></>}</dd></div>;})}
</dl>
</section>}
{watched&&'entries' in watched&&<section className="stock-card stock-history"><h2>{watched.entries.length?'Notes and spends':'No activity yet'}</h2>
<p className="stock-muted">For <code>{watched.address}</code></p>
<ul>{watched.entries.slice().reverse().map(e=><li key={`${e.batch}-${e.slots[0]}`}><details><summary><span className={'stock-kind '+e.kind}>{kinds[e.kind]}</span><b className={e.kind==='shield'||e.kind==='receive'?'in':'out'}>{e.amounts.length?totals(e.amounts):'No net change'}</b><time>{when(e.at)}</time></summary>
<dl><dt>Batch</dt><dd>#{e.batch}{e.txid&&<> · <a className="stock-txlink" href={`${EXPLORER}/tx/${e.txid}`} target="_blank" rel="noreferrer">{short(e.txid)} ↗</a></>}</dd>
<dl><dt>Batch</dt><dd>#{e.batch}{e.txid&&<> · <TxLink txid={e.txid}/></>}</dd>
{e.created.map(n=><div key={n.index} className="stock-fact-row"><dt>Note {n.epoch}/{n.index}</dt><dd>{sats(n.amount)} {unit(n.asset)} · {watched.state(n)}</dd></div>)}</dl></details></li>)}</ul>
</section>}
{watched&&'notes' in watched&&<section className="stock-card stock-history"><h2>{watched.notes.length?`${watched.notes.length} ${watched.notes.length===1?'note':'notes'} seen`:'No notes seen yet'}</h2>
<p className="stock-muted">For <code>{watched.address}</code>. A refreshed or recovered note appears again as a new note, so this list does not add up to a balance.</p>
<ul>{watched.notes.slice().reverse().map(note=><li key={note.index}><details><summary><span className={'stock-kind '+(note.deposit?'shield':'receive')}>{note.deposit?'Shielded':'Received'}</span><b className="in">+{sats(note.amount)} {unit(note.asset)}</b><time>{when(note.at)}</time></summary>
<dl><dt>Batch</dt><dd>#{note.batch}{note.txid&&<> · <a className="stock-txlink" href={`${EXPLORER}/tx/${note.txid}`} target="_blank" rel="noreferrer">{short(note.txid)} ↗</a></>}</dd><dt>Position</dt><dd>{note.index}</dd></dl></details></li>)}</ul>
<dl><dt>Batch</dt><dd>#{note.batch}{note.txid&&<> · <TxLink txid={note.txid}/></>}</dd><dt>Position</dt><dd>{note.index}</dd></dl></details></li>)}</ul>
</section>}
</>;
}
Expand All @@ -83,7 +85,7 @@ export default function Disclose(){
if(live())setWatched({address:rollupAddressOf({owner:key.owner,viewPublic:x25519.getPublicKey(key.viewSecret)}),entries:[...account.history],state:noteStateOf(account)});
}else if(watch){
const key=parseViewKey(value),all:RollupEpochRecord[]=[];
for(;;){const page=await rollupApi<{total:number;batches:RollupEpochRecord[]}>(`/batches?from=${all.length}&limit=100`);all.push(...page.batches);if(all.length>=page.total||!page.batches.length)break;}
for(;;){const page=await rollupApi<{total:number;batches:RollupEpochRecord[]}>(`/batches?from=${all.length}&limit=${privateTransport()?.pageSize??100}`);all.push(...page.batches);if(all.length>=page.total||!page.batches.length)break;}
const notes=await epochIncomingNotes(hash,all,key);
if(live())setWatched({address:rollupAddressOf({owner:key.owner,viewPublic:x25519.getPublicKey(key.viewSecret)}),notes});
}else{const result=await checkDisclosure(hash,decodeDisclosure(value.includes('#')?value.slice(value.indexOf('#')+1):value));if(live())setChecked(result);}
Expand Down
Loading