Skip to content

Commit bb362a3

Browse files
authored
feat(meta-legion): move /meta-legion to Legion Exchange v2 (#1095)
* feat(meta-legion): move /meta-legion to Legion Exchange v2 v2 (SP3EF02CC2CGWJ327TXXW7JD4B9K9F1R0FSVY3659.legion-exchange-v2) replaces v1's human resolvers. Every legion now asks one fixed question: will any of these Bitcoin addresses bond in pox-5 after the legion was created and by its deadline? YES settles by an on-chain Bitcoin proof (resolve-bonded), accepted until 1,008 blocks after the deadline; NO by resolve-idle after that. - Fold the v2 prints: create-legion carries label, scripts, deadline and created-at; resolve-bonded and resolve-idle replace resolve and void-legion - Phases follow the contract: trading, proof window, awaiting resolve-idle, settled - Show every legion's addresses, decoded from the stored output scripts (P2PK outputs such as Satoshi's genesis key are named, since they have no standard address), and both countdowns: deadline and proof window - Legion 0's label and created-at are v2's deploy values - The exchange chainhook targets v2 as aibtc-legion-exchange-v2-mainnet * refactor(meta-legion): trim the page copy Drop the explanatory paragraphs (hero, scoreboard, market, footer and the per-legion sub-line) down to one short line where one is needed, and fix the closing call to action, which still described v1's resolvers.
1 parent 0603de4 commit bb362a3

9 files changed

Lines changed: 278 additions & 158 deletions

File tree

‎app/api/meta-legion/route.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@ export async function GET(request: NextRequest) {
1919
endpoint: "/api/meta-legion",
2020
method: "GET",
2121
description:
22-
"State of the Legion Exchange: every legion is a yes/no question with its own YES/NO share market paid in sBTC. Legion 0, the meta legion, asks whether at least `target` legions clear the trading bar in each of the three counted epochs before the close. The contract settles it from its own scoreboard. Folded from the exchange's print events, delivered by a Hiro chainhook; read-only.",
22+
"State of the Legion Exchange v2: every legion asks whether any of a set of Bitcoin addresses bonds in pox-5 before a deadline, with its own YES/NO share market paid in sBTC. YES settles by an on-chain Bitcoin proof (resolve-bonded), NO by resolve-idle after the proof grace. Legion 0, the meta legion, asks whether at least `target` legions clear the trading bar in each of the three counted epochs before the close. The contract settles it from its own scoreboard. Folded from the exchange's print events, delivered by a Hiro chainhook; read-only.",
2323
contracts: { exchange: EXCHANGE_CONTRACT, collateral: SBTC_TOKEN },
2424
response: {
2525
tip: "number | null: Bitcoin burn height (epochs and resolve dates count burn blocks)",
2626
tipTime: "number | null: unix seconds of the tip block",
27-
terms: "meta-terms: { target, closeHeight, epochs[], epochBlocks, minVolume, minTraders, feeBps, maxCollateral, feeSink }",
27+
terms: "meta-terms: { target, closeHeight, epochs[], epochBlocks, minVolume, minTraders, feeBps, maxCollateral, feeSink, proofGrace, maxScripts }",
2828
score: "meta-count: the lowest qualified count across the counted epochs",
2929
currentEpoch: "floor(tip / epochBlocks)",
3030
epochs: "[{ epoch, start, end, state (upcoming | live | done), qualifiedCount }] for the counted epochs",
3131
meta: "LegionRow for legion 0",
3232
legions: "LegionRow[] for every other legion, newest first",
3333
LegionRow:
34-
"{ id, subject, creator, resolver, resolveHeight, status (0 open, 1 YES, 2 NO, 3 void), collateral, supply, stats[{ epoch, volume, traders, qualified }] }",
34+
"{ id, label, creator, scripts (Bitcoin output scripts, 0x hex; the question is whether any bonds in pox-5 after createdAt and by deadline), deadline, createdAt, status (0 open, 1 YES bonded, 2 NO idle), collateral, supply, stats[{ epoch, volume, traders, qualified }] }",
3535
orders:
3636
"Orders with shares left, across every legion: { id, kind (offer | bid), legion, side (1 YES, 0 NO), maker, price (ten-thousandths of a sat per share), remaining }",
3737
feed: "The 60 most recent exchange events, newest first: { txid, event, blockHeight, data }",

‎app/meta-legion/MetaLegionView.tsx‎

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ import type { MetaLegionState } from "@/lib/meta-legion/server-state";
1717
import {
1818
EXCHANGE_CONTRACT,
1919
EXCHANGE_SOURCE_HREF,
20+
LEGION_STATUS,
2021
SIDE,
21-
VOID_GRACE_BLOCKS,
2222
type Side,
2323
} from "@/lib/meta-legion/constants";
2424
import {
2525
bookFor,
26-
costOf,
2726
fmtPct,
2827
legionPhase,
2928
yesQuote,
@@ -33,8 +32,8 @@ import {
3332
type LegionRow,
3433
type Order,
3534
} from "@/lib/meta-legion/state";
35+
import { describeScript } from "@/lib/meta-legion/scripts";
3636
import {
37-
addrLink,
3837
contractLink,
3938
fmtBlocksLeft,
4039
fmtClock,
@@ -46,22 +45,20 @@ import {
4645

4746
const PHASE_LABEL: Record<LegionPhase, string> = {
4847
trading: "Trading",
49-
"awaiting-resolver": "Awaiting resolver",
50-
voidable: "Voidable",
48+
proving: "Proof window",
49+
idle: "Awaiting resolve-idle",
5150
resolvable: "Resolvable",
52-
yes: "Resolved YES",
53-
no: "Resolved NO",
54-
void: "Void",
51+
yes: "Bonded, YES",
52+
no: "Idle, NO",
5553
};
5654

5755
const PHASE_TONE: Record<LegionPhase, string> = {
5856
trading: "live",
59-
"awaiting-resolver": "warn",
60-
voidable: "warn",
57+
proving: "warn",
58+
idle: "warn",
6159
resolvable: "warn",
6260
yes: "good",
6361
no: "bad",
64-
void: "dim",
6562
};
6663

6764
const SIDE_LABEL: Record<Side, string> = { [SIDE.YES]: "YES", [SIDE.NO]: "NO" };
@@ -164,17 +161,60 @@ function EpochCard({ e, target }: { e: EpochWindow; target: number }) {
164161
);
165162
}
166163

164+
/** The addresses a legion asks about. They, not the label, decide the answer. */
165+
function Addresses({ scripts }: { scripts: string[] }) {
166+
return (
167+
<ul className="ml-addrs">
168+
{scripts.map((sc) => {
169+
const v = describeScript(sc);
170+
return (
171+
<li key={sc} title={sc}>
172+
{v.address ? (
173+
<a href={`https://mempool.space/address/${v.address}`} target="_blank" rel="noopener">
174+
{v.address}
175+
</a>
176+
) : (
177+
v.label
178+
)}
179+
</li>
180+
);
181+
})}
182+
</ul>
183+
);
184+
}
185+
186+
/** The deadline cell: trading countdown, then the proof window's. */
187+
function DeadlineCell({ l, state }: { l: LegionRow; state: MetaLegionState }) {
188+
const { tip, terms, blockSeconds } = state;
189+
const toDeadline = tip == null ? null : l.deadline - tip;
190+
const proofEnd = l.deadline + terms.proofGrace;
191+
const toProofEnd = tip == null ? null : proofEnd - tip;
192+
return (
193+
<td className="ml-mono">
194+
{fmtInt(l.deadline)}
195+
{toDeadline != null && toDeadline > 0 ? (
196+
<span className="ml-dim"> {fmtBlocksLeft(toDeadline, blockSeconds)}</span>
197+
) : null}
198+
<span className="ml-sub-line">
199+
YES provable to {fmtInt(proofEnd)}
200+
{toDeadline != null && toDeadline <= 0 && toProofEnd != null && toProofEnd >= 0
201+
? ` (${fmtBlocksLeft(toProofEnd, blockSeconds)})`
202+
: ""}
203+
</span>
204+
</td>
205+
);
206+
}
207+
167208
function LegionTable({ state, legions }: { state: MetaLegionState; legions: LegionRow[] }) {
168-
const { tip, terms, currentEpoch, blockSeconds } = state;
209+
const { tip, terms, currentEpoch } = state;
169210
return (
170211
<div className="ml-table-wrap">
171212
<table className="ml-table">
172213
<thead>
173214
<tr>
174215
<th>#</th>
175216
<th>Question</th>
176-
<th>Resolver</th>
177-
<th>Resolves</th>
217+
<th>Deadline</th>
178218
<th>Status</th>
179219
<th className="num">Epoch {currentEpoch ?? "-"} volume</th>
180220
<th className="num">Traders</th>
@@ -183,41 +223,31 @@ function LegionTable({ state, legions }: { state: MetaLegionState; legions: Legi
183223
</thead>
184224
<tbody>
185225
{legions.map((l) => {
186-
const phase = legionPhase(l, tip);
226+
const phase = legionPhase(l, tip, terms.proofGrace);
187227
const now = l.stats.find((s) => s.epoch === currentEpoch);
188-
const left = tip == null ? null : l.resolveHeight - tip;
189-
const stopsEarly = l.resolveHeight < terms.closeHeight;
228+
const stopsEarly = l.deadline < terms.closeHeight;
190229
return (
191230
<tr key={l.id}>
192231
<td className="ml-mono ml-dim">{l.id}</td>
193232
<td className="q">
194-
{l.subject}
233+
<span className="ml-q-label">{l.label}</span>
234+
<Addresses scripts={l.scripts} />
195235
{stopsEarly && phase === "trading" ? (
196236
<span className="ml-flag" title={`Trading stops before burn ${fmtInt(terms.closeHeight - 1)}`}>
197-
Stops before the close
237+
Closes before the meta legion, does not count
198238
</span>
199239
) : null}
200240
</td>
201-
<td className="ml-mono">
202-
<a href={addrLink(l.resolver)} target="_blank" rel="noopener">
203-
{shortAddr(l.resolver)}
204-
</a>
205-
</td>
206-
<td className="ml-mono">
207-
{fmtInt(l.resolveHeight)}
208-
{left != null && left > 0 ? (
209-
<span className="ml-dim"> {fmtBlocksLeft(left, blockSeconds)}</span>
210-
) : null}
211-
</td>
241+
<DeadlineCell l={l} state={state} />
212242
<td>
213243
<span className={`ml-pill ${PHASE_TONE[phase]}`}>{PHASE_LABEL[phase]}</span>
214244
</td>
215245
<td className="ml-mono num">
216-
{now ? fmtInt(now.volume) : "-"}
246+
{fmtInt(now?.volume ?? 0)}
217247
<span className="ml-dim"> / {fmtInt(terms.minVolume)}</span>
218248
</td>
219249
<td className="ml-mono num">
220-
{now ? fmtInt(now.traders) : "-"}
250+
{fmtInt(now?.traders ?? 0)}
221251
<span className="ml-dim"> / {terms.minTraders}</span>
222252
</td>
223253
<td>
@@ -265,16 +295,12 @@ function MetaMarket({ state }: { state: MetaLegionState }) {
265295
const { meta, orders, terms, tip } = state;
266296
const quote = yesQuote(orders, 0);
267297
const room = Math.max(0, terms.maxCollateral - meta.collateral);
268-
const phase = legionPhase(meta, tip);
298+
const phase = legionPhase(meta, tip, terms.proofGrace);
269299
const sides: Side[] = [SIDE.YES, SIDE.NO];
270300

271301
return (
272302
<section className="ml-section">
273303
<h2 className="ml-h2">The meta market</h2>
274-
<p className="ml-lede">
275-
Legion 0 trades like any other legion: mint a complete set, sell the side you don&apos;t believe in,
276-
or buy the side you do. Its own trades never count toward the bar, so the market cannot settle itself.
277-
</p>
278304

279305
<div className="figs four">
280306
<div className="fig accent">
@@ -329,7 +355,6 @@ function MetaMarket({ state }: { state: MetaLegionState }) {
329355

330356
function HowToTrade({ state }: { state: MetaLegionState }) {
331357
const { terms } = state;
332-
const example = { shares: 10_000, price: 5_000 };
333358
const calls: { fn: string; args: string; what: string; pc: string }[] = [
334359
{
335360
fn: "mint-set",
@@ -373,6 +398,12 @@ function HowToTrade({ state }: { state: MetaLegionState }) {
373398
what: "After resolution, collect the winning side",
374399
pc: "Exchange sends exactly claimable(u0, you)",
375400
},
401+
{
402+
fn: "resolve-meta",
403+
args: "none",
404+
what: "After the close, settle legion 0 from the scoreboard (anyone)",
405+
pc: "None",
406+
},
376407
];
377408
return (
378409
<div className="ml-howto">
@@ -383,12 +414,8 @@ function HowToTrade({ state }: { state: MetaLegionState }) {
383414
</a>
384415
</div>
385416
<p className="ml-howto-note">
386-
Call the contract directly, e.g. with the AIBTC MCP server&apos;s <code>call_contract</code>. Always use
387-
post-condition <b>deny</b> mode: shares are balances inside the contract, so only sBTC needs a
388-
post-condition. <code>side</code> is <code>u1</code> YES, <code>u0</code> NO. <code>price</code> is
389-
ten-thousandths of a sat per share, so <code>u5000</code> is 50%: {fmtInt(example.shares)} shares at 50%
390-
cost {fmtSats(costOf(example.shares, example.price))}. Each fill pays a {terms.feeBps / 100}% fee to the
391-
treasury, taken from the maker&apos;s side. Minimum trade is 1,000 sats.
417+
Post-condition <b>deny</b> mode. <code>side</code> <code>u1</code> YES, <code>u0</code> NO.{" "}
418+
<code>price</code> <code>u5000</code> = 50%. {terms.feeBps / 100}% fee from the maker, 1,000 sat minimum.
392419
</p>
393420
<div className="ml-table-wrap">
394421
<table className="ml-table ml-calls">
@@ -478,16 +505,16 @@ function wireLine(e: FeedItem): React.ReactNode {
478505
<b>legion #{Number(d.legion)}</b> cleared the bar in epoch {Number(d.epoch)} ({Number(d.count)} this epoch)
479506
</>
480507
);
481-
case "resolve":
508+
case "resolve-bonded":
482509
return (
483510
<>
484-
{who("by")} resolved {legionRef(d)} {Number(d.outcome) === 1 ? "YES" : "NO"}
511+
{who("by")} proved a bond: {legionRef(d)} settled <b>YES</b>
485512
</>
486513
);
487-
case "void-legion":
514+
case "resolve-idle":
488515
return (
489516
<>
490-
{who("by")} voided {legionRef(d)}
517+
{who("by")} settled {legionRef(d)} <b>NO</b>, no bond proven
491518
</>
492519
);
493520
case "resolve-meta":
@@ -610,13 +637,11 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
610637
const qualifiedNow = (l: LegionRow) => l.stats.some((s) => s.epoch === currentEpoch && s.qualified);
611638
const groups: Record<Filter, LegionRow[]> = {
612639
all: legions,
613-
trading: legions.filter((l) => legionPhase(l, tip) === "trading"),
640+
trading: legions.filter((l) => legionPhase(l, tip, terms.proofGrace) === "trading"),
614641
qualified: legions.filter(qualifiedNow),
615-
settled: legions.filter((l) => ["yes", "no", "void"].includes(legionPhase(l, tip))),
642+
settled: legions.filter((l) => l.status !== LEGION_STATUS.OPEN),
616643
};
617644
const shown = groups[filter];
618-
const counting = data.epochs.some((e) => e.state !== "upcoming");
619-
const firstCounted = data.epochs[0];
620645

621646
return (
622647
<main className="content">
@@ -641,10 +666,8 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
641666
trade?
642667
</h1>
643668
<p className="hero-lede">
644-
{meta.subject}. A legion clears the bar in an epoch with {fmtSats(terms.minVolume)} traded among{" "}
645-
{terms.minTraders} distinct traders. Nobody resolves this one: after burn block{" "}
646-
{fmtInt(terms.closeHeight)} anyone calls <code>resolve-meta</code> and the contract reads its own
647-
scoreboard. No admin key, no upgrade path.
669+
YES if {terms.target} legions each trade {fmtSats(terms.minVolume)} among {terms.minTraders} traders in
670+
all three counted epochs. The contract settles it from its own scoreboard.
648671
</p>
649672
<div className="hero-cta">
650673
<a className="cta-btn" href={EXCHANGE_SOURCE_HREF} target="_blank" rel="noopener">
@@ -685,12 +708,6 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
685708

686709
<section className="ml-section">
687710
<h2 className="ml-h2">Scoreboard</h2>
688-
<p className="ml-lede">
689-
The score is the lowest of the three counted epochs, so the bar has to be cleared in each of them.
690-
{!counting && firstCounted
691-
? ` Counting starts at burn block ${fmtInt(firstCounted.start)} (epoch ${firstCounted.epoch}); until then every count is zero.`
692-
: ""}
693-
</p>
694711
<div className="ml-epochs">
695712
{data.epochs.map((e) => (
696713
<EpochCard key={e.epoch} e={e} target={terms.target} />
@@ -701,8 +718,8 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
701718
<div ref={barSentinel} className="legion-bar-sentinel" aria-hidden="true" />
702719
<div className={`legion-bar ${stuck ? "stuck" : ""}`}>
703720
<CtaBand
704-
t="Anyone can create a legion. Each one that clears the bar moves the meta score."
705-
d={`no signup, no admin, ${fmtSats(terms.minVolume)} among ${terms.minTraders} traders per epoch`}
721+
t="Anyone can create a legion."
722+
d={`${fmtSats(terms.minVolume)} among ${terms.minTraders} traders per epoch to qualify`}
706723
/>
707724
{legions.length ? (
708725
<div className="tabs">
@@ -731,12 +748,6 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
731748
) : (
732749
<LegionTable state={data} legions={shown} />
733750
)}
734-
<p className="ml-foot">
735-
Every legion is only as honest as its resolver. If the resolver misses the {fmtInt(VOID_GRACE_BLOCKS)}-block
736-
grace after the resolve date, anyone can void it and every share, either side, redeems at half. Trades with
737-
the treasury (<span className="ml-mono">{shortAddr(terms.feeSink)}</span>) never count toward the bar. To
738-
count, a legion has to still be trading through burn block {fmtInt(terms.closeHeight - 1)}.
739-
</p>
740751

741752
<MetaMarket state={data} />
742753

@@ -745,8 +756,7 @@ export default function MetaLegionView({ initial }: { initial: MetaLegionState |
745756
<div className="cta-close">
746757
<div className="t">Put a legion on the board</div>
747758
<p className="d">
748-
Call <code>create-legion</code> with a question, a resolver and a resolve date, seed both sides, and let
749-
it trade. Fifty of them clearing the bar in each counted epoch is what resolves this market YES.
759+
Up to {terms.maxScripts} Bitcoin addresses and a deadline. Call <code>create-legion</code> and let it trade.
750760
</p>
751761
<a className="cta-btn" href={EXCHANGE_SOURCE_HREF} target="_blank" rel="noopener">
752762
Read the contract

0 commit comments

Comments
 (0)