feat(bcmr): variadic multi-URL + Sia mirror helper (ADR-0057 step 2)
Encoder changes to prepare the BCMR OP_RETURN to carry two URLs so a
silentmode.st outage no longer takes our token metadata with it. Client
side is ready; the server-side Sia mirror (POST to /api/bcmr-upload
also writing the Sia bucket bns/bcmr/<network>/) still TODO — the opt-
in flag below keeps current deploys unchanged until that lands.
- design/potidaea/src/bcmr.ts
• bcmrOpReturnChunks(bcmr, ...urls): now variadic. Returns [marker,
hashHex, ...urls]. Throws when called with zero URLs.
• bcmrSiaUrl(categoryId, net): forward-compatible Sia-mirror URL
(https://navigate.st/bns/bcmr.deviant.x/<network>/<catId>.json).
Resolves whichever bytes the gateway's s3 record returns from the
Sia bucket. Harmless to emit now: a wallet that fetches before the
mirror is live sees a 404 and falls through to the primary URL —
the ADR-0057 step 3 consumer we just shipped handles multi-URL
fallback cleanly.
- design/potidaea/src/genesis.ts
• Reads VITE_BCMR_SIA_MIRROR=1 at build time; when set, pushes BOTH
the primary and the Sia URL into the OP_RETURN. Default off so no
existing deploy's behaviour changes.
- wiki/decisions.md
• ADR-0057 "Deferred" section updated: step 2 marked client-ready;
server-side mirror hook called out as the remaining task.
Runtime sanity-check with the real encoder:
single-url: [BCMR, 0x…, primaryUrl] (3 chunks)
two-url: [BCMR, 0x…, primaryUrl, siaUrl] (4 chunks)
three-url: variadic works end-to-end
zero-url: correctly refused with explanatory message
Potidaea tsc clean. No regressions in callers (bcmrOpReturnChunks's
old (bcmr, url) call site is a strict subset of the new (bcmr, ...urls)
signature).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6f9794693f
commit
8bd292d3a1
3 changed files with 60 additions and 13 deletions
|
|
@ -171,11 +171,30 @@ const BCMR_BASE_URL: Record<Network, string> = {
|
|||
};
|
||||
|
||||
/** The public URL at which a wallet can GET the BCMR JSON for `categoryId`.
|
||||
* Kept stable per network — this is what goes into the OP_RETURN. */
|
||||
* Kept stable per network — this is what goes into the OP_RETURN as the
|
||||
* PRIMARY URL. */
|
||||
export function bcmrPublicUrl(categoryId: string, net: Network = currentNetwork()): string {
|
||||
return BCMR_BASE_URL[net] + categoryId.toLowerCase() + '.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-0057 step 2 — Sia-mirror URL. The BNS gateway resolves `bcmr.deviant.x`
|
||||
* via an `s3` record pointing at the Sia bucket `bns/bcmr/<network>/`. Any
|
||||
* wallet with Theseus or Ariadne hits the same bytes via a different host +
|
||||
* storage backend, so a silentmode.st outage no longer takes the metadata
|
||||
* with it.
|
||||
*
|
||||
* Forward-compatible: adding this URL to the on-chain push today is harmless
|
||||
* even before the server-side mirror lands. Wallets try URLs in order; the
|
||||
* primary resolves first in the happy path, and the Sia URL becomes active
|
||||
* the moment the mirror writes bytes whose SHA-256 matches the on-chain
|
||||
* anchor. A wallet seeing an inactive Sia URL simply moves to the next.
|
||||
*/
|
||||
export function bcmrSiaUrl(categoryId: string, net: Network = currentNetwork()): string {
|
||||
const sub = net === 'chipnet' ? 'chipnet/' : '';
|
||||
return `https://navigate.st/bns/bcmr.deviant.x/${sub}${categoryId.toLowerCase()}.json`;
|
||||
}
|
||||
|
||||
// ── OP_RETURN encoder ──────────────────────────────────────────────────────
|
||||
// cashscript's TransactionBuilder.addOpReturnOutput takes an array of chunks
|
||||
// where a bare string is pushed as UTF-8 bytes and a "0x..." prefixed string
|
||||
|
|
@ -199,11 +218,21 @@ function bytesToHex(b: Uint8Array): string {
|
|||
}
|
||||
|
||||
/** Produce the chunk array to hand to `tb.addOpReturnOutput()`. Pair it with
|
||||
* the JSON bytes from `serializeBcmr()` — the caller uploads those to the
|
||||
* the JSON bytes from `serializeBcmr()` — the caller uploads those to each
|
||||
* URL BEFORE broadcasting the tx that carries this OP_RETURN, so wallets
|
||||
* that fetch the URL right away find the content. */
|
||||
export function bcmrOpReturnChunks(bcmr: BcmrRegistry, url: string): [string, string, string] {
|
||||
* that fetch any URL right away find the content.
|
||||
*
|
||||
* Variadic in `urls` per ADR-0057 step 2: CHIP-2023-04 allows multiple URL
|
||||
* pushes and wallets try them in order. Supply the primary first and any
|
||||
* mirrors after. At least one URL is required.
|
||||
*
|
||||
* Returns `[marker, hashHex, ...urls]`. All string chunks are UTF-8 pushed
|
||||
* except the hash (hex-prefixed → raw bytes). */
|
||||
export function bcmrOpReturnChunks(bcmr: BcmrRegistry, ...urls: string[]): string[] {
|
||||
if (urls.length === 0) {
|
||||
throw new Error('bcmrOpReturnChunks: at least one URL is required');
|
||||
}
|
||||
const hash = bcmrContentHash(bcmr);
|
||||
return ['BCMR', '0x' + bytesToHex(hash), url];
|
||||
return ['BCMR', '0x' + bytesToHex(hash), ...urls];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import type { Signer } from './signer-store';
|
|||
import type { KnownMba } from './orbital';
|
||||
import { fanoutBroadcast } from './broadcast';
|
||||
import { cashscriptNetwork, currentNetwork } from './network';
|
||||
import { buildBcmrJson, serializeBcmr, bcmrPublicUrl, bcmrOpReturnChunks } from './bcmr';
|
||||
import { buildBcmrJson, serializeBcmr, bcmrPublicUrl, bcmrSiaUrl, bcmrOpReturnChunks } from './bcmr';
|
||||
import { uploadBcmr } from './bcmr-upload';
|
||||
|
||||
const ART = artifact as unknown as Artifact;
|
||||
|
|
@ -49,7 +49,7 @@ const DEFAULT_RESERVOIR_SUPPLY = 10n ** 18n; // just under CashTokens' 2^63-1
|
|||
async function buildAndUploadBcmr(
|
||||
categoryId: string,
|
||||
args: { ticker: string; name?: string; iconUrl?: string; dvmbExtension?: Record<string, unknown> },
|
||||
): Promise<[string, string, string] | null> {
|
||||
): Promise<string[] | null> {
|
||||
if (!args.ticker) return null;
|
||||
const net = currentNetwork();
|
||||
const json = buildBcmrJson({
|
||||
|
|
@ -60,10 +60,20 @@ async function buildAndUploadBcmr(
|
|||
dvmbExtension: args.dvmbExtension,
|
||||
});
|
||||
const canonical = serializeBcmr(json);
|
||||
const url = bcmrPublicUrl(categoryId, net);
|
||||
const primaryUrl = bcmrPublicUrl(categoryId, net);
|
||||
try { await uploadBcmr(categoryId, net, canonical); }
|
||||
catch (e) { console.warn('[bcmr] upload failed (on-chain OP_RETURN still committed):', e instanceof Error ? e.message : e); }
|
||||
return bcmrOpReturnChunks(json, url);
|
||||
|
||||
// ADR-0057 step 2 — emit the Sia-mirror URL alongside the primary when the
|
||||
// mirror is enabled. Opt-in via VITE_BCMR_SIA_MIRROR so existing deploys
|
||||
// don't start pushing an unresolvable URL until the server-side mirror is
|
||||
// writing bytes whose SHA-256 matches the on-chain anchor. Any wallet
|
||||
// reading the OP_RETURN tries URLs in order (CHIP-2023-04) so this is
|
||||
// forward-compatible: a wallet that fetches the Sia URL before the mirror
|
||||
// is live simply falls through to the primary.
|
||||
const siaEnabled = (import.meta as unknown as { env?: Record<string, string | undefined> }).env?.VITE_BCMR_SIA_MIRROR === '1';
|
||||
const urls = siaEnabled ? [primaryUrl, bcmrSiaUrl(categoryId, net)] : [primaryUrl];
|
||||
return bcmrOpReturnChunks(json, ...urls);
|
||||
}
|
||||
|
||||
function hexToBin(h: string): Uint8Array {
|
||||
|
|
|
|||
|
|
@ -3291,10 +3291,18 @@ same minting NFT.
|
|||
|
||||
### Deferred to follow-on sessions
|
||||
|
||||
**Step 2 — Sia mirror in the BCMR OP_RETURN.** CHIP-2023-04 allows multiple URL
|
||||
pushes; wallets try them in order. Upload the same canonical BCMR JSON to Sia
|
||||
at `s3://bns/bcmr/<network>/<categoryId>.json` and emit both URLs on-chain.
|
||||
~90 extra bytes on-chain, zero ongoing cost, host-outage resilience.
|
||||
**Step 2 — Sia mirror in the BCMR OP_RETURN.** ⚠ **Client-side ready; server-side
|
||||
mirror still TODO.** Encoder now accepts multiple URLs variadically
|
||||
(`bcmrOpReturnChunks(bcmr, ...urls)`) and `bcmrSiaUrl(categoryId, net)` yields
|
||||
the forward-compatible BNS-gateway URL
|
||||
(`https://navigate.st/bns/bcmr.deviant.x/<network>/<categoryId>.json`). The
|
||||
genesis emitter emits both URLs when `VITE_BCMR_SIA_MIRROR=1` is set at build;
|
||||
default (unset) keeps the current single-URL behaviour so no live deploy flips
|
||||
behaviour unexpectedly. Still owed: a server-side hook that mirrors every POST
|
||||
to `/api/bcmr-upload/<network>/<categoryId>.json` into Sia bucket
|
||||
`bns/bcmr/<network>/<categoryId>.json`, keyed so the SHA-256 of the stored
|
||||
bytes matches the on-chain anchor. Once that mirror writes, flip the env flag
|
||||
and every new genesis commits both URLs on-chain.
|
||||
|
||||
**Step 3 — BCMR consumer in `shells/app`.** Biggest UX win. Scan each unknown
|
||||
token's genesis tx for the BCMR OP_RETURN → fetch the referenced JSON (fall
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue