deviant/scripts/asm-sequencer/client.mjs
Local Dev b8b110dc97 feat(asm): tier-2 sequencer service drafted + chipnet-proved 5/5
Adds the coordinator-funded HTTP relay from
Parameters/ASM_Native/ASM-single-asset-throughput.md §2a — the
low-latency path that complements the tier-4 wallet retry (0272/0275/
0277) by turning N simultaneous claim requests into an ordered chain
from ONE reservoir. Trust surface: the covenant enforces the swap
math either way; the relay can only order and rebroadcast.

scripts/asm-sequencer/
  server.mjs   — Node HTTP daemon: GET /health, POST /claim, POST /admin/reload-tip
  client.mjs   — reference client (portable to wallet + dApp)
  lib/state.mjs — SequencerState + async mutex over the chain

scripts/chipnet-asm-sequencer-daemon-test.mjs
  spawns daemon, fires 5 parallel /claim requests, verifies txid chain

Chipnet result: 5 wire arrivals within 3 ms produce 5/5 accepted in
daemon-assigned order; each tx's `before.reservoir` matches the
previous tx's txid. Reservoir advanced 5 times from one starting
outpoint, zero collisions. Baseline naïve parallel is 1/5.

Snapshot 0278 documents the trust model, gotchas
(chipnet wallet BCH depletion, cashaddr → tokenaddr conversion), and
the roadmap: wallet-side client wire, then SIGHASH_ANYONECANPAY
user-funded variant, then multi-category.

Wiki handoff (wiki/asm/zero-contention-protocol.md) updated with the
tier-2 status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-28 01:47:40 +02:00

63 lines
2.2 KiB
JavaScript

// ASM sequencer reference client.
//
// Pairs with server.mjs. Callable from node scripts (and portable to the
// wallet's core/wallet.ts / dApp's orbital.ts — same shape). Follows the
// "race the relay against the retry-wrapped local broadcast" pattern from
// Parameters/ASM_Native/ASM-single-asset-throughput.md §2d: the sequencer
// is the low-latency path, tier-4 retry (scripts/lib/asm-retry.mjs) is the
// no-coordinator backstop.
const DEFAULT_TIMEOUT_MS = 15_000;
/** POST a claim request to the sequencer. Throws on non-200, or when the
* server returned {ok:false, error}. Server serialises internally, so a
* concurrent burst of these calls settles in-order without collisions. */
export async function sequencerClaim({
endpoint,
ticker,
amountUnits,
recipientCashaddr,
timeoutMs = DEFAULT_TIMEOUT_MS,
}) {
const ctrl = new AbortController();
const to = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${endpoint}/claim`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ticker, amountUnits: String(amountUnits), recipientCashaddr }),
signal: ctrl.signal,
});
const body = await res.json();
if (!res.ok || !body.ok) {
throw new Error(body.error ? `sequencer: ${body.error}` : `sequencer HTTP ${res.status}`);
}
return { txid: body.txid, order: body.order, gross: body.gross, fee: body.fee, before: body.before };
} finally {
clearTimeout(to);
}
}
export async function sequencerHealth(endpoint, timeoutMs = 5_000) {
const ctrl = new AbortController();
const to = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${endpoint}/health`, { signal: ctrl.signal });
if (!res.ok) throw new Error(`sequencer HTTP ${res.status}`);
return res.json();
} finally {
clearTimeout(to);
}
}
/** Reachability probe — returns true only if a fresh /health responds ok
* within the short probe budget. Use to decide whether to go relay-first
* or straight to the retry-wrapped local broadcast. */
export async function sequencerReachable(endpoint, probeMs = 1_500) {
try {
const h = await sequencerHealth(endpoint, probeMs);
return h?.ok === true;
} catch {
return false;
}
}