feat(anthemus): dual asset pickers + honest exec state in Swap
Swap was a single pair-select + reverse button, and called the demo
placeOrder() stub — market orders got marked "filled" instantly with no
chain interaction, which reads as "swap is broken".
- SwapPanel.tsx: rewritten as dual asset pickers (You pay [X ▾] / You
receive [Y ▾]). Receive options are filtered to tickers connected to
the current pay ticker (receivableFor); no direct pair → picker
disabled. Default is BCH → USD.
- engine.ts: added ASSETS, pairFor(pay, receive), receivableFor(pay),
isPairBroadcastable(pairId). No PAIRS or engine-behaviour changes.
- AnthemusScreen.tsx: passes session down to <SwapPanel/>.
Execution honesty (fixes the "swap does nothing" complaint):
- Pair NOT broadcastable in this build (missing env config) → demo
banner up-front + local-only recording. No more silent fake fills.
- Pair broadcastable → a ⌁ Broadcast on-chain OFF/ON toggle appears
(needs unlocked wallet). ON routes through placeOrderWired and
reports the actual txid or the reason it couldn't broadcast.
To flip broadcast ON for a pair: set VITE_ANTHEMUS_PAIR_CATEGORIES
(JSON with baseCategory/quoteCategory hex per pair) and
VITE_ANTHEMUS_PLATFORM_LB (hex) at build. The chipnet taker script
scripts/chipnet-anthemus-taker.mjs shows the LB shape used in
validation.
Also fixes a stale AgoraScreen.tsx header comment ("Two sub-tabs";
code has three: My Assets | Potidaea | Anthemus) and logs both this
and the Fiat 3-step change in WALLET-COORDINATION.md.
This commit is contained in:
parent
503f45809e
commit
63ae6cda5d
5 changed files with 251 additions and 44 deletions
|
|
@ -360,3 +360,45 @@ started; **core + shell tsc clean** after. Please read if you re-open these:
|
|||
the lists. `ArtemisionScreen` create-wizard name/ticker defaults `'Nova'/'NOVA'` → `''`. Agora demo
|
||||
banner reworded ("unlock a wallet…"). tsc clean. FYI `ActivityScreen.tsx` still has a couple of mock
|
||||
"Nova" *activity-feed* entries (not asset fixtures) — left as-is; flag if you want those cleared too.
|
||||
- **Uniform-Fiat deposit/withdraw → three-step flow (user-directed, YYYY-MM-DD).**
|
||||
Both flows now pick **1. Currency → 2. Stablecoin → 3. Network** as three separate
|
||||
dropdowns (aligned with `Anthemus/DepositPanel`). Deposit was 2-step (combined
|
||||
issuer·chain); Withdraw was a one-step mini nested inside the Hold card.
|
||||
Files touched — flagging per the currency-picker precedent:
|
||||
- `screens/UniformFiat/DepositPanel.tsx` — split combined `cellCode` state into
|
||||
separate `issuer` + `chain` state; cascade defaults keep valid on change.
|
||||
- `screens/UniformFiat/WithdrawPanel.tsx` — **NEW** file, symmetric with
|
||||
DepositPanel; uses `applyWithdraw` (unchanged in engine).
|
||||
- `screens/UniformFiat/UniformFiatScreen.tsx` — added `'withdraw'` to the `Mode`
|
||||
union + top-level tab; removed the nested `WithdrawMini` (replaced by a
|
||||
one-line pointer to the Withdraw tab from the Hold detail card); cleaned
|
||||
imports (`applyWithdraw`/`parseAmt`/`depositableCells` no longer used here).
|
||||
Also fixed a stale comment on `AgoraScreen.tsx` header (said "Two sub-tabs";
|
||||
code has three — My Assets | Potidaea | Anthemus). No engine changes.
|
||||
tsc not run in this session (sandbox); please verify with `rtk npx tsc --noEmit -p shells/app`.
|
||||
- **Anthemus Swap → dual asset pickers + honest exec state (user-directed).**
|
||||
The panel was a single pair-select + ⇅ reverse button (Uniswap-expert UX) and
|
||||
called the demo `placeOrder` stub — so market swaps got marked "filled"
|
||||
instantly with no chain interaction. Users read that as "broken".
|
||||
Files touched:
|
||||
- `screens/Anthemus/SwapPanel.tsx` — rewritten: **You pay [asset ▾] / You
|
||||
receive [asset ▾]**. The receive picker only shows tickers connected to the
|
||||
chosen pay ticker (`receivableFor`); no direct pair → picker disabled with
|
||||
"no direct pair". Default is BCH → USD. ⇅ swaps the two pickers cleanly.
|
||||
- `screens/Anthemus/engine.ts` — added `ASSETS`, `pairFor(pay, receive)`,
|
||||
`receivableFor(pay)`, and `isPairBroadcastable(pairId)`. No PAIRS changes,
|
||||
no engine-behaviour changes.
|
||||
- `screens/Anthemus/AnthemusScreen.tsx` — passes `session` down to
|
||||
`<SwapPanel/>` (needed for the broadcast path).
|
||||
**Execution honesty:**
|
||||
- Pair NOT broadcastable in this build (no `VITE_ANTHEMUS_PAIR_CATEGORIES`
|
||||
entry or no `VITE_ANTHEMUS_PLATFORM_LB`) → **demo banner up-front** + swap
|
||||
records locally (Phase A). No silent fake fills.
|
||||
- Pair broadcastable → a **⌁ Broadcast on-chain OFF/ON** toggle appears
|
||||
(disabled without an unlocked wallet). When ON, the swap uses
|
||||
`placeOrderWired(order, { session, dryRun: false })` and reports the txid
|
||||
or the reason it couldn't broadcast.
|
||||
**To flip broadcast ON for a pair** (Phase B): set the two env vars at build:
|
||||
`VITE_ANTHEMUS_PAIR_CATEGORIES='{"BCH/USD":{"baseCategory":"<hex>","quoteCategory":"<hex>"}}'`
|
||||
and `VITE_ANTHEMUS_PLATFORM_LB=<hex>`. The chipnet taker script at
|
||||
`scripts/chipnet-anthemus-taker.mjs:39` shows the LB shape used in validation.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import type {
|
|||
} from '@core/wallet/display.js';
|
||||
|
||||
// Agora — the marketplace hub (glossary: renamed from "Assets"; *see + participate*).
|
||||
// Two sub-tabs: My Assets (holdings) | Anthemus (the market venue). My Assets has a
|
||||
// Three sub-tabs: My Assets (holdings) | Potidaea (counterparty-free Orbital Swap,
|
||||
// BCH↔MBA and MBA↔MBA at protocol rate — the wallet-side surface of the AsmCore
|
||||
// engine) | Anthemus (the P2P market venue with book/AMM/Deposit). My Assets has a
|
||||
// MBA | Tokens | Fiat filter (user-directed reorg, 2026-07-22): backed → MBA,
|
||||
// unbacked → Tokens, and Fiat embeds the Uniform-Fiat surface (USD/EUR balances +
|
||||
// Deposit + Swap) — no separate Fiat tab. Katalogos lives under Artemision.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function AnthemusScreen({ bchAddress, session }: { bchAddress?: string; s
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'swap' ? <SwapPanel /> : mode === 'exchange' ? <ExchangePanel session={session} /> : <DepositPanel bchAddress={bchAddress} />}
|
||||
{mode === 'swap' ? <SwapPanel session={session} /> : mode === 'exchange' ? <ExchangePanel session={session} /> : <DepositPanel bchAddress={bchAddress} />}
|
||||
|
||||
<div className="hint" style={{ marginTop: 12 }}>
|
||||
Anthemus is a market — prices are discovered on a book/curve and every fee is a
|
||||
|
|
|
|||
|
|
@ -1,47 +1,115 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { CoreSession } from '../../core/wallet';
|
||||
import {
|
||||
PAIRS, type Pair, ammQuote, demoReserves, fmt, pct, assetLabel, CURVE_FEE, placeOrder,
|
||||
PAIRS, ASSETS, pairFor, receivableFor, ammQuote, demoReserves, fmt, pct,
|
||||
assetLabel, CURVE_FEE, placeOrder, placeOrderWired, isPairBroadcastable,
|
||||
} from './engine';
|
||||
|
||||
// SWAP — the Uniswap-style interface: token-in → token-out against the AMM maker.
|
||||
// HONEST LABELING (ADR-0016 Decision 5): this is rendered as a "Market swap" with
|
||||
// price impact + fee as separate disclosed lines — never as a counterparty-free
|
||||
// protocol swap. The AMM is a maker on the same book as the Exchange panel.
|
||||
// SWAP — DUAL asset pickers ("You pay X → You receive Y"). The pair (and whether
|
||||
// the direction is reversed) is resolved from the two tickers, matching how users
|
||||
// think about swaps in every mainstream wallet. If no direct pair connects the
|
||||
// chosen tickers, the receive picker only offers what's routable.
|
||||
//
|
||||
// HONEST LABELING (ADR-0016 Decision 5): rendered as a "Market swap" — price
|
||||
// impact and curve fee are separate disclosed lines, never folded into "rate".
|
||||
//
|
||||
// EXECUTION HONESTY:
|
||||
// • When VITE_ANTHEMUS_PAIR_CATEGORIES + VITE_ANTHEMUS_PLATFORM_LB are set for
|
||||
// the selected pair, a "⌁ broadcast on" toggle appears; when on, the swap
|
||||
// goes through `placeOrderWired` and hits chipnet.
|
||||
// • When the pair is NOT broadcastable in this build, a demo banner is shown
|
||||
// up-front and the swap only records to localStorage (Phase A). No silent
|
||||
// fake fills.
|
||||
|
||||
const SLIPPAGE_OPTS = [0.001, 0.005, 0.01];
|
||||
|
||||
export function SwapPanel({ onFilled }: { onFilled?: () => void }) {
|
||||
const [pairId, setPairId] = useState(PAIRS[0].id);
|
||||
const [reversed, setReversed] = useState(false); // false: sell base → get quote
|
||||
export function SwapPanel({
|
||||
session,
|
||||
onFilled,
|
||||
}: {
|
||||
session?: CoreSession | null;
|
||||
onFilled?: () => void;
|
||||
}) {
|
||||
// 1. Asset selection — separate pay / receive tickers.
|
||||
const [pay, setPay] = useState<string>(ASSETS[0]);
|
||||
const [receive, setReceive] = useState<string>(() => {
|
||||
// Prefer BCH → USD as the default (the most common wallet swap).
|
||||
const withBch = ASSETS.includes('BCH') && ASSETS.includes('USD');
|
||||
return withBch ? 'USD' : receivableFor(ASSETS[0])[0] ?? ASSETS[1] ?? '';
|
||||
});
|
||||
// If BCH is available, default pay to BCH so the initial view is "BCH → USD".
|
||||
useEffect(() => {
|
||||
if (ASSETS.includes('BCH')) setPay('BCH');
|
||||
}, []);
|
||||
|
||||
const receiveOpts = useMemo(() => receivableFor(pay), [pay]);
|
||||
// If the current receive isn't valid for the new pay, pick the first that is.
|
||||
useEffect(() => {
|
||||
if (!receiveOpts.includes(receive)) setReceive(receiveOpts[0] ?? '');
|
||||
}, [receiveOpts, receive]);
|
||||
|
||||
const resolved = useMemo(() => pairFor(pay, receive), [pay, receive]);
|
||||
const pair = resolved?.pair ?? null;
|
||||
const reversed = resolved?.reversed ?? false;
|
||||
const broadcastable = pair ? isPairBroadcastable(pair.id) : false;
|
||||
|
||||
// 2. Amount + quote.
|
||||
const [amountStr, setAmountStr] = useState('');
|
||||
const [slippage, setSlippage] = useState(0.005);
|
||||
const [done, setDone] = useState<string | null>(null);
|
||||
|
||||
const pair = PAIRS.find((p) => p.id === pairId) as Pair;
|
||||
const from = reversed ? pair.quote : pair.base;
|
||||
const to = reversed ? pair.base : pair.quote;
|
||||
const amount = Number(amountStr) || 0;
|
||||
|
||||
const quote = useMemo(() => {
|
||||
if (!pair) return null;
|
||||
const { rIn, rOut } = demoReserves(pair);
|
||||
// reserves are (base, quote); a reversed swap feeds the quote side in.
|
||||
return reversed ? ammQuote(rOut, rIn, amount) : ammQuote(rIn, rOut, amount);
|
||||
}, [pair, reversed, amount]);
|
||||
|
||||
const canSwap = quote !== null && amount > 0;
|
||||
// 3. Slippage + broadcast toggle.
|
||||
const [slippage, setSlippage] = useState(0.005);
|
||||
const [broadcastOn, setBroadcastOn] = useState(false);
|
||||
useEffect(() => { if (!broadcastable) setBroadcastOn(false); }, [broadcastable]);
|
||||
|
||||
const doSwap = () => {
|
||||
if (!quote) return;
|
||||
// MVP surface: record the fill locally (the covenant leg mirrors the validated
|
||||
// chipnet driver once the pair's categories are live in the registry).
|
||||
placeOrder({
|
||||
const [note, setNote] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const canSwap = !!pair && quote !== null && amount > 0 && !busy;
|
||||
|
||||
const flipDirection = () => {
|
||||
const nextPay = receive;
|
||||
const nextReceive = pay;
|
||||
setPay(nextPay);
|
||||
setReceive(nextReceive);
|
||||
setAmountStr('');
|
||||
setNote(null);
|
||||
};
|
||||
|
||||
const doSwap = async () => {
|
||||
if (!pair || !quote) return;
|
||||
const order = {
|
||||
pairId: pair.id,
|
||||
side: reversed ? 'buy' : 'sell',
|
||||
kind: 'market',
|
||||
side: (reversed ? 'buy' : 'sell') as 'buy' | 'sell',
|
||||
kind: 'market' as const,
|
||||
price: quote.execPrice,
|
||||
qty: amount,
|
||||
});
|
||||
setDone(`Swapped ${fmt(amount, 2)} ${assetLabel(from)} → ${fmt(quote.out, 4)} ${assetLabel(to)}`);
|
||||
};
|
||||
if (broadcastOn && broadcastable && session) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const { built } = await placeOrderWired(order, { session, dryRun: false });
|
||||
setNote(
|
||||
built.txid
|
||||
? `Broadcast ${assetLabel(pair.base)} / ${assetLabel(pair.quote)} — txid ${built.txid.slice(0, 12)}…`
|
||||
: `Not broadcast: ${built.note}`,
|
||||
);
|
||||
} catch (e) {
|
||||
setNote(`Broadcast failed: ${(e as Error).message}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
} else {
|
||||
placeOrder(order);
|
||||
setNote(
|
||||
`Recorded (demo) ${fmt(amount, 2)} ${assetLabel({ ticker: pay, decimals: 2 })} → ` +
|
||||
`${fmt(quote.out, 4)} ${assetLabel({ ticker: receive, decimals: 2 })}`,
|
||||
);
|
||||
}
|
||||
setAmountStr('');
|
||||
onFilled?.();
|
||||
};
|
||||
|
|
@ -54,12 +122,21 @@ export function SwapPanel({ onFilled }: { onFilled?: () => void }) {
|
|||
<span className="anx-fee-tag">curve fee {pct(CURVE_FEE, 2)} → LPs</span>
|
||||
</div>
|
||||
|
||||
{/* Honest state — up front, not buried under the CTA. */}
|
||||
{!broadcastable && (
|
||||
<div className="banner info" style={{ marginBottom: 8 }}>
|
||||
Demo swap — this pair is not wired to the on-chain relay in this build.
|
||||
Confirm still records the fill locally so the flow is testable.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* You pay */}
|
||||
<div className="anx-field">
|
||||
<div className="anx-field-top">
|
||||
<span>You pay</span>
|
||||
<select className="anx-pair-select" value={pairId} onChange={(e) => { setPairId(e.target.value); setDone(null); }}>
|
||||
{PAIRS.map((p) => (
|
||||
<option key={p.id} value={p.id}>{assetLabel(p.base)} / {assetLabel(p.quote)}</option>
|
||||
<select className="anx-pair-select" value={pay} onChange={(e) => { setPay(e.target.value); setNote(null); }}>
|
||||
{ASSETS.map((a) => (
|
||||
<option key={a} value={a}>{a}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -69,27 +146,55 @@ export function SwapPanel({ onFilled }: { onFilled?: () => void }) {
|
|||
inputMode="decimal"
|
||||
placeholder="0.00"
|
||||
value={amountStr}
|
||||
onChange={(e) => { setAmountStr(e.target.value.replace(/[^\d.]/g, '')); setDone(null); }}
|
||||
onChange={(e) => { setAmountStr(e.target.value.replace(/[^\d.]/g, '')); setNote(null); }}
|
||||
/>
|
||||
<span className="anx-asset">{assetLabel(from)}</span>
|
||||
<span className="anx-asset">{pay}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="anx-flip" aria-label="Reverse direction" onClick={() => { setReversed((r) => !r); setDone(null); }}>⇅</button>
|
||||
<button className="anx-flip" aria-label="Reverse direction" onClick={flipDirection}>⇅</button>
|
||||
|
||||
{/* You receive */}
|
||||
<div className="anx-field">
|
||||
<div className="anx-field-top"><span>You receive</span></div>
|
||||
<div className="anx-field-top">
|
||||
<span>You receive</span>
|
||||
<select
|
||||
className="anx-pair-select"
|
||||
value={receive}
|
||||
onChange={(e) => { setReceive(e.target.value); setNote(null); }}
|
||||
disabled={receiveOpts.length === 0}
|
||||
>
|
||||
{receiveOpts.length === 0 ? (
|
||||
<option value="">no direct pair</option>
|
||||
) : (
|
||||
receiveOpts.map((a) => (
|
||||
<option key={a} value={a}>{a}</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="anx-field-row">
|
||||
<div className="anx-amount out">{quote ? fmt(quote.out, 4) : '—'}</div>
|
||||
<span className="anx-asset">{assetLabel(to)}</span>
|
||||
<span className="anx-asset">{receive || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* the disclosed lines — price impact + fee are SEPARATE, never folded into "rate" */}
|
||||
{/* Disclosed lines — price impact + fee separate, never folded into "rate". */}
|
||||
<div className="anx-quote-lines">
|
||||
<div className="anx-line"><span>Rate</span><span>{quote ? `1 ${from.ticker} ≈ ${fmt(quote.execPrice, 4)} ${to.ticker}` : '—'}</span></div>
|
||||
<div className="anx-line"><span>Price impact</span><span className={quote && quote.priceImpact > 0.01 ? 'warn' : ''}>{quote ? pct(quote.priceImpact) : '—'}</span></div>
|
||||
<div className="anx-line"><span>Curve fee (0.30% → LPs)</span><span>{quote ? `${fmt(quote.curveFee, 4)} ${from.ticker}` : '—'}</span></div>
|
||||
<div className="anx-line">
|
||||
<span>Rate</span>
|
||||
<span>{quote ? `1 ${pay} ≈ ${fmt(quote.execPrice, 4)} ${receive}` : '—'}</span>
|
||||
</div>
|
||||
<div className="anx-line">
|
||||
<span>Price impact</span>
|
||||
<span className={quote && quote.priceImpact > 0.01 ? 'warn' : ''}>
|
||||
{quote ? pct(quote.priceImpact) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="anx-line">
|
||||
<span>Curve fee (0.30% → LPs)</span>
|
||||
<span>{quote ? `${fmt(quote.curveFee, 4)} ${pay}` : '—'}</span>
|
||||
</div>
|
||||
<div className="anx-line">
|
||||
<span>Slippage tolerance</span>
|
||||
<span className="anx-slip">
|
||||
|
|
@ -98,14 +203,37 @@ export function SwapPanel({ onFilled }: { onFilled?: () => void }) {
|
|||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div className="anx-line"><span>Minimum received</span><span>{quote ? `${fmt(quote.minReceived(slippage), 4)} ${to.ticker}` : '—'}</span></div>
|
||||
<div className="anx-line">
|
||||
<span>Minimum received</span>
|
||||
<span>{quote ? `${fmt(quote.minReceived(slippage), 4)} ${receive}` : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Broadcast toggle — only visible when the pair has env categories + platform LB. */}
|
||||
{broadcastable && (
|
||||
<div className="anx-line" style={{ marginTop: 4 }}>
|
||||
<span>⌁ Broadcast on-chain</span>
|
||||
<span className="anx-slip">
|
||||
<button
|
||||
className={`anx-slip-btn${broadcastOn ? ' on' : ''}`}
|
||||
onClick={() => setBroadcastOn((v) => !v)}
|
||||
disabled={!session}
|
||||
title={session ? '' : 'Unlock a wallet to broadcast'}
|
||||
>
|
||||
{broadcastOn ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button className="anx-cta" disabled={!canSwap} onClick={doSwap}>
|
||||
{canSwap ? `Swap ${assetLabel(from)} → ${assetLabel(to)}` : 'Enter an amount'}
|
||||
{busy ? 'Broadcasting…'
|
||||
: !pair ? 'Pick two connected assets'
|
||||
: canSwap ? `Swap ${pay} → ${receive}${broadcastOn ? ' (on-chain)' : ''}`
|
||||
: 'Enter an amount'}
|
||||
</button>
|
||||
|
||||
{done && <div className="banner info" style={{ marginTop: 8 }}>{done}</div>}
|
||||
{note && <div className="banner info" style={{ marginTop: 8 }}>{note}</div>}
|
||||
|
||||
<div className="hint" style={{ marginTop: 10 }}>
|
||||
A market swap against pooled liquidity — the price is discovered on a curve, not a
|
||||
|
|
|
|||
|
|
@ -50,6 +50,33 @@ export const PAIRS: Pair[] = [
|
|||
export const CURVE_FEE = 0.003; // 0.30% AMM curve fee → LPs
|
||||
export const TAKER_FEE = 0.0015; // 0.15% book taker fee (maker 0%)
|
||||
|
||||
// ── asset-picker helpers (Swap panel: dual "pay/receive" pickers) ─────────────
|
||||
/** Distinct tickers listed across all pairs, e.g. ['EUR','USD','BCH','ASM','ASM2']. */
|
||||
export const ASSETS: string[] = [
|
||||
...new Set(PAIRS.flatMap((p) => [p.base.ticker, p.quote.ticker])),
|
||||
];
|
||||
|
||||
/** Given a chosen (pay, receive) ticker pair, resolve to a PAIRS entry + whether the
|
||||
* direction is reversed (i.e. selling the quote side). Returns null when no direct
|
||||
* pair connects the two tickers — the UI can then disable the target option. */
|
||||
export function pairFor(pay: string, receive: string): { pair: Pair; reversed: boolean } | null {
|
||||
for (const p of PAIRS) {
|
||||
if (p.base.ticker === pay && p.quote.ticker === receive) return { pair: p, reversed: false };
|
||||
if (p.quote.ticker === pay && p.base.ticker === receive) return { pair: p, reversed: true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Which tickers can I receive if I'm paying `pay`? (For enabling/disabling picker options.) */
|
||||
export function receivableFor(pay: string): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const p of PAIRS) {
|
||||
if (p.base.ticker === pay) out.add(p.quote.ticker);
|
||||
if (p.quote.ticker === pay) out.add(p.base.ticker);
|
||||
}
|
||||
return [...out];
|
||||
}
|
||||
|
||||
// ── AMM quote (constant product x·y=k, fee on input — SharePairAMM semantics) ──
|
||||
export interface AmmQuote {
|
||||
out: number; // amount received (before slippage tolerance)
|
||||
|
|
@ -414,6 +441,14 @@ export const PLATFORM_FEE_LOCKING_BYTECODE: string | null =
|
|||
|
||||
export const ANTHEMUS_ORDER_BOOK_ARTIFACT: Artifact = anthemusOrderBookArtifact as unknown as Artifact;
|
||||
|
||||
/** True when the pair has BOTH categories AND the platform fee sink set —
|
||||
* the two things a broadcast path needs before it can build a real escrow
|
||||
* tx. The Swap panel uses this to show/hide the "⌁ broadcast on" toggle. */
|
||||
export function isPairBroadcastable(pairId: string): boolean {
|
||||
const cats = PAIR_CATEGORIES[pairId];
|
||||
return !!cats?.baseCategory && !!cats?.quoteCategory && !!PLATFORM_FEE_LOCKING_BYTECODE;
|
||||
}
|
||||
|
||||
export interface PlacementContext {
|
||||
session: CoreSession;
|
||||
/** Default TRUE — Phase A never broadcasts. Phase B flips this per pair. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue