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>
63 lines
2.2 KiB
JavaScript
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;
|
|
}
|
|
}
|