@dcasegr/amm-client 0.39.0 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -15,22 +15,23 @@
15
15
  * (`hostedTokens`): each token's price, liquidity (TVL) and pool count; no host-wide stream.
16
16
  * - **History** (0.39.0, D1–D6): `trades`, `trade`, `candles`, `stats`, `statsAll`, `holders`,
17
17
  * `positionsAll` — plain GETs of the history routes (H1–H6, src/lib/overlay.ts).
18
- * - **Swap** (D11–D16): the token's pools route `GET /@<token>/amm/pools` held as an event stream
19
- * (`watchTokenPools`; 0.39.0, D7: the whole answer once, then one `change` per pool change,
20
- * applied to the held answer): the pools whose validator is beating and whose commission meets the
21
- * market's minimum (the host's filter), the market's commission key hash; a quote is the
22
- * matching engine's plan over that list (no request); a swap reads ONE `GET
23
- * /@<token>/amm/pools/beef?lp=` for its legs (freshness, the pools' BEEF), then each leg's relay
24
- * request (`amm.swap.submit`, held by the host until the relay rests; 15 s expiry); a lost answer
25
- * is "no answer yet" with "Check again" (`GET /@<token>/amm/swaps?id=`, once).
26
- * - **Liquidity** (D9, D23): the wallet's pool rows and deploy history, then ONE `GET
27
- * /@<token>/amm/positions?lp=` per token; a deploy the history has but the basket lacks is
28
- * filed then; Close and Rebalance read their pool's BEEF by LP key (`/pools/beef?lp=`); the
29
- * deploy's terms `GET /@<token>/amm/terms`.
18
+ * - **Swap** (D11–D16; 0.40.0; 0.41.0): the token's pools route `GET /@<token>/amm/pools`, a plain
19
+ * read (`tokenPools`): the pools whose validator is beating and whose commission meets the
20
+ * market's minimum (the host's filter), each validator's minimum leg, the market's commission key
21
+ * hash; a quote is the matching engine's plan over that list (no request); a swap (`executeSwap`)
22
+ * stages what the swap basket lacks by ONE wallet action, then per leg the pools route read fresh,
23
+ * ONE `GET /@<token>/amm/pools/beef?pool=` and the leg's relay request (`amm.swap.submit`, held by
24
+ * the host until the relay rests; 15 s expiry); no answer → the host's `GET /amm/spends`, once
25
+ * more, a race. The pool events (`watchPoolEvents`, 0.41.0: the host's feeds) are signals for
26
+ * display: a page reads the route again on one.
27
+ * - **Liquidity** (D9, D23; 0.40.0: by pool id): the wallet's pool rows and deploy history, then
28
+ * ONE `GET /@<token>/amm/positions?pool=` per token; a deploy the history has but the basket
29
+ * lacks is filed then; Close and Rebalance read their pool's BEEF by its id (`/pools/beef?pool=`);
30
+ * the deploy's terms `GET /@<token>/amm/terms`.
30
31
  * - **Your tokens** (D17, D18, D19, D24): the wallet's module and the swap basket; the token list
31
- * once and ONE discovery `GET /@discovery/1color/tokens?ids=` for held tokens the list lacks; a
32
- * silent pass over the swap basket's staged legs (src/wallet/swapPayouts.ts); Move back submits
33
- * at the host root.
32
+ * once and ONE discovery `GET /@discovery/1color/tokens?ids=` for held tokens the list lacks; the
33
+ * pass over the swap basket (0.40.0: ONE `GET /amm/spends` over every row,
34
+ * src/wallet/swapPayouts.ts); Move back submits at the host root.
34
35
  * - **Settings** (D20, D21): the claim card (`claimStatus`: swap's identity, the recipient of
35
36
  * root's messages), the token list once, discovery's list and ONE `?ids=` read; Enable's deploy
36
37
  * BEEF from the wallet's basket else `GET /@discovery/1color/<token>/deploy`.
@@ -40,20 +41,20 @@
40
41
  * an outpoint is `<txid>.<vout>`. Amounts are bigints in base units; prices
41
42
  * are numbers: sats per whole token.
42
43
  */
43
- import { Beef, Transaction } from "@bsv/sdk";
44
+ import { Beef, Transaction, createNonce } from "@bsv/sdk";
45
+ import { BRC29_PROTOCOL, WALLET_PAYMENT, p2pkhHexOf } from "./wallet/brc29.js";
44
46
  import { send1Color } from "@1sat/actions";
45
- import { computeSwap } from "@dcasegr/amm-engine";
47
+ import { nextLeg, priceOf } from "@dcasegr/amm-engine";
46
48
  import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
47
49
  import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
48
- import { ammLookupService, applyPoolChange, liveOfPools, parsePools, parseTokenTopic, poolOutputOf, poolsUrl, readDiscoveryDeploy, readDiscoveryIds, readDiscoveryList, readAllPositions, readAllStats, readCandles, readHolders, readPoolsBeef, readPools, readPositions, readStats, readTerms, readTrade, readTrades, readTx, watchPools, } from "./lib/overlay.js";
49
- import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
50
+ import { ammLookupService, liveOfPools, parseTokenTopic, poolOutputOf, readDiscoveryDeploy, readDiscoveryIds, readDiscoveryList, readAllPositions, readAllStats, readCandles, readHolders, readPoolHistory, readPoolsAt, readPoolsBeef, readPools, readPositions, readSpendsAll, readStats, readTerms, readTrade, readTrades, readTx, eventsUrl, watchPoolEvents as watchFeed, } from "./lib/overlay.js";
51
+ import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, readWallet, sendMessage } from "./lib/skein.js";
50
52
  import { depthOf } from "./market/prices.js";
51
- import { buildPlanRequest, goneFromLookup, quote as planQuote } from "./market/plan.js";
52
- import { validatorStatus } from "./market/view.js";
53
- import { planSwapLeg, prepareSwap, selectExactSatsInput, selectExactTokenInputs } from "./market/swapAction.js";
54
- import { moveBack, sendSatsToSwapBasket, sendToSwapBasket, swapHoldingBeef, swapHoldings, swapSatsHoldings, swapTotals } from "./market/activeSwap.js";
55
- import { checkAgain, relaySwap, swapIdOf } from "./market/swapFlow.js";
56
- import { settleStagedSwaps } from "./wallet/swapPayouts.js";
53
+ import { buildPlanRequest, legMinerFeeSats, quote as planQuote } from "./market/plan.js";
54
+ import { prepareSwap, selectLegInputs } from "./market/swapAction.js";
55
+ import { fileStagedToken, moveBack, moveBackSats, stage, swapHoldingBeef, swapHoldings, swapSatsHoldings, swapSatsTotal, swapTotals, unfiledStagedTokens } from "./market/activeSwap.js";
56
+ import { relaySwap, swapIdOf } from "./market/swapFlow.js";
57
+ import { resolveFromOverlay } from "./wallet/swapPayouts.js";
57
58
  import { formatAmount } from "./lp/amounts.js";
58
59
  import { basketRows } from "./lp/wallet.js";
59
60
  import { buildInventory, decodeTokenRow, deployIcon } from "./lp/inventory.js";
@@ -63,7 +64,7 @@ import { rebalancePool } from "./lp/rebalance.js";
63
64
  import { historyPoolRows, mergePoolRows, positionRowsOf } from "./lp/positions.js";
64
65
  import { POOLS_BASKET, assetBasket, heldOutputs } from "./wallet/color.js";
65
66
  import { prepareClose, submitToOverlay } from "./lp/close.js";
66
- import { ClientError, errorKind, kindOfOutcome, toClientError } from "./errors.js";
67
+ import { ClientError, toClientError } from "./errors.js";
67
68
  /**
68
69
  * `amm.pool.terms`'s refusal when no terms are set (0.15.0, David Case
69
70
  * 2026-10-23: terms are a runtime record root sets; unset, a market only).
@@ -75,7 +76,7 @@ export const NO_TERMS_TEXT = "This exchange validates nothing: no terms are set.
75
76
  export function hasTerms(t) {
76
77
  return !!t && !("none" in t);
77
78
  }
78
- export { applyPoolChange, parseChange } from "./lib/overlay.js";
79
+ export { parsePoolEventMessage } from "./lib/overlay.js";
79
80
  /** The metadata of the deploy output `vout` of an Atomic BEEF's subject; null when that output is no 1Color deploy. */
80
81
  function deployMetaOf(beef, vout) {
81
82
  const tx = Transaction.fromAtomicBEEF(beef);
@@ -123,6 +124,7 @@ export function hostedFrom(list, stats) {
123
124
  export function poolViews(answer, tokenId) {
124
125
  return (answer?.pools ?? []).map((p) => ({
125
126
  outpoint: outpointText(p.outpoint),
127
+ poolId: p.poolId,
126
128
  tokenId,
127
129
  sats: p.bsvReserve,
128
130
  tokens: p.tokenReserve,
@@ -133,13 +135,29 @@ export function poolViews(answer, tokenId) {
133
135
  export function poolFees(p) {
134
136
  return { lpBps: Number(p.liquidityFeeBps), validatorBps: Number(p.validationFeeBps), commissionBps: Number(p.commissionBps ?? 0n) };
135
137
  }
136
- function swapResultOf(legs) {
137
- const filled = legs.filter((l) => l.status === "filled").length;
138
- return { status: filled === legs.length && legs.length > 0 ? "filled" : filled === 0 ? "failed" : "partial", legs };
138
+ /**
139
+ * A pools answer's rows as the planner takes them (0.40.0): each pool with its validator's
140
+ * minimum leg (`validators[].minLegSats`, the validator's announced terms).
141
+ */
142
+ export function poolsWithMinimums(answer) {
143
+ const min = new Map(answer.validators.map((v) => [v.identity, v.minLegSats]));
144
+ return answer.pools.map((p) => {
145
+ const m = min.get(p.validatorIdentityKey.toLowerCase());
146
+ return m ? { ...p, minLegSats: m } : p;
147
+ });
139
148
  }
140
- /** A swap's result with one leg replaced (after "Check again"). */
141
- export function withLeg(r, leg) {
142
- return swapResultOf(r.legs.map((l) => (l.poolOutpoint === leg.poolOutpoint ? leg : l)));
149
+ /** Sats per whole token of an exact pair (`sats` per `tokens` base units), 0 when `tokens` is 0. */
150
+ export function pairPrice(p, dec) {
151
+ return p.tokens > 0n ? Number(p.sats) / (Number(p.tokens) / 10 ** dec) : 0;
152
+ }
153
+ /**
154
+ * A leg's sats beyond its amount in (0.40.0, the stage's fee sats): the miner fee estimate plus its
155
+ * 1-sat carriers — on a buy the token payout; on a sell the token fee and commission outputs that
156
+ * are not zero, and the token change.
157
+ */
158
+ export function legCarrySats(leg, buy, minerFeeSats) {
159
+ const carriers = buy ? 1n : BigInt([leg.liquidityFee, leg.validationFee, leg.commission].filter((x) => x > 0n).length + 1);
160
+ return minerFeeSats + carriers;
143
161
  }
144
162
  /**
145
163
  * wallet-toolbox's `specOpWalletBalance` (`sdk.specOpWalletBalance`, @bsv/wallet-toolbox-client): the listOutputs "basket" of its
@@ -224,7 +242,17 @@ export function enableCandidates(listed, walletIds, onList) {
224
242
  }
225
243
  return out;
226
244
  }
227
- const newShared = () => ({ listRead: null, claimRead: null, planned: new WeakMap(), checks: new Map(), loaded: new Map(), listeners: new Set() });
245
+ /** The terms as root's messages carry them (0.40.0: `minLegSats` only when above 0). Refused unless each fee is an integer 0 to 10 000 and the minimum a whole number of sats. */
246
+ function termsArgs(t) {
247
+ for (const [k, v] of Object.entries({ lpBps: t.lpBps, validatorBps: t.validatorBps, commissionBps: t.commissionBps }))
248
+ if (!Number.isInteger(v) || v < 0 || v > 10_000)
249
+ throw new Error(`${k}: an integer, 0 to 10000 basis points`);
250
+ const min = t.minLegSats ?? 0;
251
+ if (!Number.isSafeInteger(min) || min < 0)
252
+ throw new Error("minLegSats: a whole number of sats, 0 for none");
253
+ return { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps, ...(min > 0 ? { minLegSats: min } : {}) };
254
+ }
255
+ const newShared = () => ({ listRead: null, claimRead: null, planned: new WeakMap(), swapping: false, loaded: new Map(), listeners: new Set() });
228
256
  /**
229
257
  * The client of one exchange (D9): its reads over `origin` (src/lib/overlay.ts, src/lib/skein.ts),
230
258
  * the swap flow, the pool flows (deploy, close, rebalance, move back, positions) and the wallet
@@ -244,7 +272,6 @@ function makeClient(opts, s) {
244
272
  const boundWallet = opts.wallet ?? null;
245
273
  const signedFor = boundWallet ? (opts.signedFor ?? authFetchPerInstance(boundWallet, f)) : null;
246
274
  const planned = s.planned;
247
- const checks = s.checks;
248
275
  const loaded = s.loaded;
249
276
  function needWallet() {
250
277
  if (!boundWallet || !signedFor)
@@ -470,16 +497,20 @@ function makeClient(opts, s) {
470
497
  const [list, stats] = await Promise.all([enabledList(), statsAll().catch(() => null)]);
471
498
  return hostedFrom(list, stats);
472
499
  }
500
+ /** P2 (0.41.0): a token's pools as the host lists them now, read past the browser's cache (`freshFetch`). */
501
+ async function tokenPools(tokenId) {
502
+ return readPools(tokenBase(tokenId, swapBase), freshFetch);
503
+ }
473
504
  /**
474
- * A token's pools route held as an event stream (D16; 0.39.0, D7): the whole answer (`pools`)
475
- * held, each `change` applied to it (`applyPoolChange`), one it cannot apply → the route read once
476
- * again; each new answer to `onAnswer`. → the unsubscribe.
505
+ * The host's pool event feed (E1, 0.41.0): one token's (`GET /@<token>/amm/events`) or, with
506
+ * `"all"`, every enabled token's (`GET <origin>/amm/events`); each message `{tokenId, …row}` to
507
+ * `onEvent`. Signals for display: the pools route is the truth. → the unsubscribe.
477
508
  */
478
- function watchTokenPools(tokenId, onAnswer, onError) {
479
- return watchPools(poolsUrl(tokenBase(tokenId, swapBase)), parsePools, onAnswer, { ...(onError ? { onError } : {}), fetch: f, change: applyPoolChange });
509
+ function watchPoolEvents(tokenId, onEvent, onError) {
510
+ return watchFeed(eventsUrl(tokenId === "all" ? swapBase : tokenBase(tokenId, swapBase)), onEvent, onError ? { onError } : {});
480
511
  }
481
512
  // ---------------------------------------------------------------- history (0.39.0, H1–H6)
482
- /** H1: a token's trades, newest first (`from ≤ at < to` ms, `limit` 1–500, `before` the cursor `next`, `lp` one LP key's). */
513
+ /** H1: a token's trades, newest first (`from ≤ at < to` ms, `limit` 1–500, `before` the cursor `next`, 0.40.0 `pool` one pool's by id). */
483
514
  async function trades(tokenId, o = {}) {
484
515
  return readTrades(tokenBase(tokenId, swapBase), o, ff);
485
516
  }
@@ -487,107 +518,128 @@ function makeClient(opts, s) {
487
518
  async function trade(tokenId, txid) {
488
519
  return readTrade(tokenBase(tokenId, swapBase), txid, ff);
489
520
  }
490
- /** H2: a token's candles, one per non-empty bucket of `tf` over `from ≤ at < to` (ms). */
521
+ /** H2: a token's candles, one per non-empty bucket of `tf` over `from ≤ at < to` (ms); 0.40.0 `series`: `fill` (the trades', default) or `market` (the price after each pool event, the host's). */
491
522
  async function candles(tokenId, o) {
492
523
  return readCandles(tokenBase(tokenId, swapBase), o, ff);
493
524
  }
525
+ /** H7 (0.40.0): every pool of the token listed at `t` (ms). */
526
+ async function poolsAt(tokenId, t) {
527
+ return readPoolsAt(tokenBase(tokenId, swapBase), t, ff);
528
+ }
529
+ /** H8 (0.40.0): the token's pool events in `from ≤ at < to`, oldest first (`pool` one pool's; forward paging: the page's `next` as `after`). */
530
+ async function poolHistory(tokenId, o = {}) {
531
+ return readPoolHistory(tokenBase(tokenId, swapBase), o, ff);
532
+ }
533
+ /** H9 (0.40.0): each outpoint's spender as the host's `/amm/spends` answers it (every enabled token's `/spends`). */
534
+ async function spendsAll(outpoints) {
535
+ return readSpendsAll(swapBase, outpoints, ff);
536
+ }
494
537
  /** H3: a token's stats. */
495
538
  async function stats(tokenId) {
496
539
  return readStats(tokenBase(tokenId, swapBase), ff);
497
540
  }
498
541
  /** H4: every enabled token's stats (`ids`: those only, in that order). */
499
542
  async function statsAll(o = {}) {
500
- return readAllStats(swapBase, o.ids, ff);
543
+ return readAllStats(swapBase, o.ids, o.fresh ? freshFetch : ff);
501
544
  }
502
545
  /** H6: a token's holders, largest first (`limit` 1–100, `skip`). */
503
546
  async function holders(tokenId, o = {}) {
504
547
  return readHolders(tokenBase(tokenId, swapBase), tokenId, o, ff);
505
548
  }
506
- /** H5: the LP keys' positions on every enabled token. */
507
- async function positionsAll(lpKeys) {
508
- return readAllPositions(swapBase, lpKeys, ff);
549
+ /** H5: the pools' positions on every enabled token (0.40.0: by pool id). */
550
+ async function positionsAll(poolIds) {
551
+ return readAllPositions(swapBase, poolIds, ff);
509
552
  }
510
553
  /**
511
554
  * A quote for one token (D12): the matching engine's plan over the pools the Swap page holds
512
555
  * (`held`: the token's pools route's answer — the host listed only pools whose validator is beating
513
- * and whose commission meets the market's minimum); no request. `meta`: the token's symbol and
514
- * decimals.
556
+ * and whose commission meets the market's minimum), each pool with its validator's minimum leg
557
+ * (0.40.0); no request. `meta`: the token's symbol and decimals. 0.40.0: the price and impact from
558
+ * the engine's `priceOf` over the same pools (the buy side for a buy, the sell side for a sell);
559
+ * each leg's own fees; what Swap will stage (the swap amount exact, 0.40.2: the fee sats of
560
+ * `maxSources` legs) and the per-leg miner fee estimate.
515
561
  */
516
562
  function quoteSwap(req, held, meta) {
517
563
  if (req.amountIn <= 0n)
518
564
  throw new Error("Enter an amount");
519
- const pools = held?.pools ?? [];
520
- if (pools.length === 0)
565
+ if (!held || held.pools.length === 0)
521
566
  throw new Error("No pool to swap against");
567
+ const pools = poolsWithMinimums(held);
522
568
  const live = liveOfPools(pools);
523
569
  const dec = meta.dec;
524
570
  const buy = req.side === "buy";
525
- const r = buildPlanRequest(req.tokenId, { direction: buy ? "bsvToToken" : "tokenToBsv", amount: formatAmount(req.amountIn, buy ? 0 : dec), slippageBps: String(req.maxSlippageBps), allowPartial: req.allowPartial }, pools, live, dec, feeRate);
571
+ const r = buildPlanRequest(req.tokenId, { direction: buy ? "bsvToToken" : "tokenToBsv", amount: formatAmount(req.amountIn, buy ? 0 : dec), slippageBps: String(req.maxSlippageBps), maxSources: req.maxSources }, pools, live, dec, feeRate);
526
572
  if (!r.ok)
527
573
  throw new Error(r.error);
528
574
  const { plan, view } = planQuote(r.request, live, dec);
575
+ if (plan.unfillable === "min")
576
+ throw new ClientError("below_minimum", `No pool here takes a swap this small: at least ${formatAmount(plan.minAmountIn ?? 0n, buy ? 0 : dec)} ${buy ? "sats" : meta.sym}.`);
529
577
  const used = view.legs.filter((l) => l.amountIn > 0n);
530
578
  if (used.length === 0 || view.totalOut === 0n)
531
579
  throw new Error("The amount is too small to receive anything");
532
- const sumSats = pools.reduce((a, p) => a + p.bsvReserve, 0n);
533
- const sumTokens = pools.reduce((a, p) => a + p.tokenReserve, 0n);
534
- const marginal = sumTokens > 0n ? Number(sumSats) / (Number(sumTokens) / 10 ** dec) : 0;
580
+ const price = priceOf(pools);
581
+ const marginal = price ? pairPrice(buy ? price.buy : price.sell, dec) : 0;
535
582
  const totalIn = used.reduce((a, l) => a + l.amountIn, 0n);
536
583
  const sats = buy ? totalIn : view.totalOut;
537
584
  const tokens = buy ? view.totalOut : totalIn;
538
585
  const effective = Number(sats) / (Number(tokens) / 10 ** dec);
539
- const first = used[0].pool;
586
+ const minerFee = legMinerFeeSats(feeRate);
587
+ const carry = plan.legs.map((l) => legCarrySats(l, buy, minerFee));
588
+ // 0.40.2: the fee sats for EXACTLY maxSources legs — the planned legs' own, each further leg at the largest of theirs.
589
+ const more = BigInt(Math.max(0, req.maxSources - carry.length));
590
+ const feeSats = carry.reduce((a, x) => a + x, 0n) + more * carry.reduce((a, x) => (x > a ? x : a), 0n);
540
591
  const q = {
541
592
  request: req,
542
593
  legs: used.map((l) => ({
543
594
  poolOutpoint: outpointText(l.outpoint),
595
+ poolId: l.pool.poolId,
544
596
  amountIn: l.amountIn,
545
597
  amountOut: l.amountOut,
546
598
  shareBps: Number((l.amountIn * 10000n) / req.amountIn),
547
599
  reserves: { sats: l.pool.bsvReserve, tokens: l.pool.tokenReserve },
548
600
  validator: { identityKey: l.pool.validatorIdentityKey, live: l.validator.live },
601
+ fees: poolFees(l.pool),
549
602
  })),
550
603
  amountOut: view.totalOut,
551
604
  minAmountOut: view.minAmountOut,
552
605
  effectivePrice: effective,
553
606
  marginalPrice: marginal,
554
607
  impactBps: marginal ? Math.round((Math.abs(effective - marginal) / marginal) * 10_000) : 0,
555
- fees: { lpBps: Number(first.liquidityFeeBps), validatorBps: Number(first.validationFeeBps), commissionBps: Number(first.commissionBps ?? 0n) },
608
+ stage: { amount: req.amountIn, feeSats },
609
+ legMinerFeeSats: minerFee,
556
610
  };
557
- planned.set(q, { plan, pools, live, market: held.market, dec, sym: meta.sym });
611
+ planned.set(q, { plan, answer: held, dec, sym: meta.sym });
558
612
  return q;
559
613
  }
560
- /** A relay outcome on a leg's result (0.31.1's mapping; 0.34.0: no answer carries its id for "Check again"). */
561
- function onLeg(result, o, prepared, base, af) {
562
- delete result.checkId;
563
- if (o.status === "accepted") {
564
- result.status = "filled";
565
- result.txid = o.txid;
566
- delete result.error;
567
- delete result.kind;
568
- checks.delete(o.id);
569
- return;
570
- }
571
- result.status = "failed";
572
- const kind = kindOfOutcome(o);
573
- if (kind)
574
- result.kind = kind;
575
- const rest = o.status === "unknown" ? o.reason.replace(/^no answer yet:? ?/, "") : "";
576
- result.error = o.status === "refused" ? `refused: ${o.reason}` : o.status === "unknown" ? `no answer yet${rest ? `: ${rest}` : ""}` : o.status;
577
- if (o.status === "unknown") {
578
- result.checkId = o.id;
579
- checks.set(o.id, { prepared, base, af });
580
- }
581
- else if ("id" in o)
582
- checks.delete(o.id);
614
+ /** The overlay pass over the swap basket (item 5, src/wallet/swapPayouts.ts): ONE `/amm/spends`, each spender's `/tx`, this wallet's legs taken in. */
615
+ function resolve(wallet) {
616
+ return resolveFromOverlay(wallet, { spendsAll: (ops) => readSpendsAll(swapBase, ops, freshFetch), tx: (tokenId, txid) => readTx(tokenBase(tokenId, swapBase), txid, freshFetch) });
583
617
  }
584
618
  /**
585
- * Runs a quoted swap with the connected wallet (D14, D15): ONE `GET /@<token>/amm/pools/beef?lp=`
586
- * for the legs' LP keys — a leg whose key is missing or whose outpoint moved is gone since the
587
- * quote (as before 0.34.0) — the commission key hash and each validator's peer ID from the pools
588
- * the page holds; then one transaction per leg, relayed to the leg's validator (`amm.swap.submit`,
589
- * its answer the settled record), the payout taken in on acceptance. A planned pool gone stops the
590
- * swap unless partial fills are allowed.
619
+ * Runs a quoted swap with the connected wallet (0.40.0, agent-env plans/swap-balance-build-2026-11-05.md
620
+ * §0 7, Builder 2 item 4): ONE swap at a time, legs ONE AT A TIME.
621
+ *
622
+ * 1. The overlay pass (`resolve`); then what `amm-swap` lacks of what the plan needs — the swap
623
+ * amount exact, the sats for the carriers and miner fees of `maxSources` legs —
624
+ * staged by ONE `stage` (`staging`).
625
+ * 2. The loop: cancelled → `cancelled`; the SERVER's pool list read fresh (`GET
626
+ * /@<token>/amm/pools`, no cache; 0.41.0: before EACH leg, no list of its own kept);
627
+ * `nextLeg` over it minus the excluded outpoints, `minOut` the quote's floor less what has
628
+ * filled, `maxSources` (0.40.2) the request's less the legs filled → its stop ends the swap;
629
+ * the pool's current output read by its id (`GET /pools/beef?pool=`; moved → the list read
630
+ * again); the staged rows that cover the leg, else `fees`; built, signed, relayed
631
+ * (`relaying`):
632
+ * - accepted → taken in (ONE internalizeAction) before the next leg;
633
+ * - refused `pool_spent` (`repricing`) → the list read again: the relaying host took the
634
+ * validator's state in before it answered (0.41.0);
635
+ * - refused otherwise (`below_minimum` included) → that outpoint excluded;
636
+ * - no answer → (a) the overlay: a staged input spent → the leg landed (taken in by the
637
+ * pass), the pool spent by another's swap → continue with the same rows; (b) else the
638
+ * same leg sent ONCE more to the same validator; (c) still none → that outpoint excluded
639
+ * (the next leg races it with the same rows);
640
+ * - after any refusal: the overlay pass.
641
+ * 3. Filled completely: the last leg's change went to the wallet, nothing left. Stopped early with
642
+ * rows left: `moveBack` (one wallet action, `returning`); declined or failed → the rows stay.
591
643
  */
592
644
  async function executeSwap(quote, o = {}) {
593
645
  const progress = (x) => {
@@ -598,26 +650,26 @@ function makeClient(opts, s) {
598
650
  /* the caller's callback: not the swap's failure */
599
651
  }
600
652
  };
653
+ if (s.swapping)
654
+ throw new Error("A swap is already running: one at a time");
655
+ s.swapping = true;
601
656
  let result;
602
657
  try {
603
- result = await runSwap(quote, progress);
658
+ result = await runSwap(quote, progress, o.signal);
604
659
  }
605
660
  catch (e) {
606
661
  const err = toClientError(e);
607
662
  progress({ phase: "failed", error: err instanceof Error ? err : new Error(String(err)) });
608
663
  throw err;
609
664
  }
665
+ finally {
666
+ s.swapping = false;
667
+ }
668
+ emit();
610
669
  progress(result.status === "failed" ? { phase: "failed", result } : { phase: "done", result });
611
670
  return result;
612
671
  }
613
- /** A leg failed with `e`: its text, and its kind when it has one (D13). */
614
- function failLeg(result, e) {
615
- result.error = errText(e);
616
- const kind = errorKind(e);
617
- if (kind)
618
- result.kind = kind;
619
- }
620
- async function runSwap(quote, progress) {
672
+ async function runSwap(quote, progress, signal) {
621
673
  const { wallet, signedFor } = needWallet();
622
674
  const p = planned.get(quote);
623
675
  if (!p)
@@ -625,212 +677,153 @@ function makeClient(opts, s) {
625
677
  const tokenId = quote.request.tokenId;
626
678
  const base = tokenBase(tokenId, swapBase);
627
679
  const af = signedFor(base);
628
- const lpOf = (outpoint) => p.pools.find((x) => x.outpoint === outpoint)?.lpPubKey ?? "";
629
- const keys = [...new Set(p.plan.legs.map((l) => lpOf(l.outpoint)).filter((k) => k !== ""))];
630
- const fresh = await readPoolsBeef(base, keys, freshFetch);
631
- const gone = goneFromLookup(p.plan, fresh.pools);
632
- if (gone.length > 0 && !quote.request.allowPartial)
633
- throw new ClientError("pool_spent", `${gone.map(outpointText).join(", ")} ${gone.length === 1 ? "was" : "were"} spent since the quote: quote again`);
634
- const commissionPkh = p.market.commissionPkh;
635
- const direction = quote.request.side === "buy" ? "bsvToToken" : "tokenToBsv";
680
+ const buy = quote.request.side === "buy";
681
+ const direction = buy ? "bsvToToken" : "tokenToBsv";
682
+ const minerFee = legMinerFeeSats(feeRate);
683
+ const meta = { sym: p.sym, dec: p.dec };
684
+ // 0.41.0: the server's list, read fresh before each leg; the outpoints excluded (a pool that refused other than pool_spent, or did not answer).
685
+ const excluded = new Set();
686
+ /** 0.40.2: the legs the order may still fill (the request's maxSources less the legs filled). */
687
+ let budget = quote.request.maxSources;
636
688
  const legs = [];
637
- const running = [];
638
- // The legs that can run: not spent since the quote, their validator live.
639
- const runnable = [];
640
- for (const leg of p.plan.legs) {
641
- const poolOutpoint = outpointText(leg.outpoint);
642
- if (gone.includes(leg.outpoint)) {
643
- legs.push({ poolOutpoint, status: "failed", error: "spent since the quote", kind: "pool_spent" });
644
- continue;
645
- }
646
- const pool = p.pools.find((x) => x.outpoint === leg.outpoint);
647
- const validator = validatorStatus(pool.validatorIdentityKey, p.live);
648
- if (!validator.live || !validator.peerId) {
649
- legs.push({ poolOutpoint, status: "failed", error: "the pool's validator is no longer live" });
650
- continue;
689
+ let filledIn = 0n;
690
+ let filledOut = 0n;
691
+ let order;
692
+ /** Legs sent with no answer yet: what they spend, to account a later take-in by the overlay pass. */
693
+ const pending = [];
694
+ const accountResolved = (taken) => {
695
+ for (const t of taken) {
696
+ const i = pending.findIndex((x) => x.prepared.satsInputs.some((r) => t.inputs.includes(r.outpoint)));
697
+ if (i < 0)
698
+ continue;
699
+ const [x] = pending.splice(i, 1);
700
+ order ??= x.prepared.order;
701
+ budget -= 1;
702
+ filledIn += x.leg.amountIn;
703
+ filledOut += x.leg.amountOut;
704
+ legs.push({ poolOutpoint: outpointText(x.leg.outpoint), poolId: x.poolId, amountIn: x.leg.amountIn, amountOut: x.leg.amountOut, txid: t.txid });
651
705
  }
652
- const result = { poolOutpoint, status: "failed" };
653
- legs.push(result);
654
- runnable.push({ leg, result, pool, peerId: validator.peerId });
706
+ };
707
+ accountResolved(await resolve(wallet));
708
+ // 1. Stage what the basket lacks.
709
+ const needTokens = buy ? 0n : quote.stage.amount;
710
+ const needSats = (buy ? quote.stage.amount : 0n) + quote.stage.feeSats;
711
+ const haveSats = BigInt(await swapSatsTotal(wallet));
712
+ const haveTokens = buy ? 0n : (await swapHoldings(wallet, tokenId)).reduce((a, t) => a + t.amount, 0n);
713
+ const lackSats = needSats > haveSats ? needSats - haveSats : 0n;
714
+ const lackTokens = needTokens > haveTokens ? needTokens - haveTokens : 0n;
715
+ if (lackSats > 0n || lackTokens > 0n) {
716
+ progress({ phase: "staging", sats: lackSats, tokens: lackTokens });
717
+ await stage(wallet, { sats: Number(lackSats), ...(lackTokens > 0n ? { tokens: { tokenId, amount: lackTokens } } : {}) }, { sym: p.sym, submit: submitAt(base) });
655
718
  }
656
- // 0.29.0: every leg spends only `amm-swap` outputs — tokens in, outputs carrying exactly its
657
- // amount in (0.12.0); and one sats output of exactly the leg's sats. What the basket does not
658
- // cover is staged by ONE send (tokens missing: the module send, the sats with it; else a plain
659
- // sats send), then each leg is built from the basket.
660
- const tokensIn = direction === "tokenToBsv";
661
- if (runnable.length > 0)
662
- progress({ phase: "staging" });
663
- let tokenCandidates = tokensIn && runnable.length > 0 ? await swapHoldings(wallet, tokenId) : [];
664
- let satsCandidates = runnable.length > 0 ? await swapSatsHoldings(wallet) : [];
665
- const staged = [];
666
- for (const r of runnable) {
667
- try {
668
- const poolOutput = poolOutputOf(fresh, r.leg.outpoint);
669
- if (!poolOutput)
670
- throw new Error("the pools read has no transaction for this pool");
671
- let tokens = null;
672
- if (tokensIn) {
673
- tokens = selectExactTokenInputs(tokenCandidates, r.leg.amountIn);
674
- if (tokens)
675
- tokenCandidates = tokenCandidates.filter((c) => !tokens.includes(c));
676
- }
677
- // A leg whose tokens are still to be staged gets one output of 1 sat (sendToSwapBasket).
678
- const planned = await planSwapLeg({ wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg: r.leg, pool: r.pool, poolOutput, commissionPkh, ...(tokensIn ? { tokenInputs: tokens ?? [{ satoshis: 1, amount: r.leg.amountIn }] } : {}), satsPerKb: feeRate });
679
- const sats = selectExactSatsInput(satsCandidates, planned.funding.satoshis);
680
- if (sats)
681
- satsCandidates = satsCandidates.filter((c) => c !== sats);
682
- staged.push({ r, poolOutput, planned, tokens, sats });
719
+ // 2. The loop.
720
+ let why = "filled";
721
+ for (;;) {
722
+ if (signal?.aborted) {
723
+ why = "cancelled";
724
+ break;
683
725
  }
684
- catch (e) {
685
- failLeg(r.result, e);
726
+ const remaining = quote.request.amountIn - filledIn;
727
+ const answer = await tokenPools(tokenId);
728
+ const pools = poolsWithMinimums(answer).filter((r) => !excluded.has(r.outpoint));
729
+ const minOut = quote.minAmountOut > filledOut ? quote.minAmountOut - filledOut : 0n;
730
+ const next = nextLeg({ pools, direction, remaining, fixedCost: { minerFeeSats: minerFee }, minOut, maxSources: budget });
731
+ if ("stop" in next) {
732
+ why = next.stop;
733
+ break;
686
734
  }
687
- }
688
- const shortTokens = staged.filter((x) => tokensIn && !x.tokens);
689
- const shortSats = staged.filter((x) => !x.sats);
690
- let sent;
691
- if (shortTokens.length > 0) {
692
- const s = await sendToSwapBasket(wallet, base, tokenId, { tokens: shortTokens.map((x) => ({ amount: x.r.leg.amountIn, pool: x.r.leg.outpoint })), sats: shortSats.map((x) => ({ satoshis: x.planned.funding.satoshis, pool: x.r.leg.outpoint })) }, { sym: p.sym }, submitAt(base));
693
- sent = s.tx;
694
- const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
695
- shortTokens.forEach((x, n) => {
696
- const h = s.holdings[n];
697
- if (h)
698
- x.tokens = [h];
699
- else
700
- x.r.result.error = `the swap basket send has no output for this leg${why}`;
701
- });
702
- shortSats.forEach((x, n) => {
703
- const h = s.sats[n];
704
- if (h)
705
- x.sats = h;
706
- else
707
- x.r.result.error ??= `the swap basket send has no sats output for this leg${why}`;
708
- });
709
- }
710
- else if (shortSats.length > 0) {
711
- const s = await sendSatsToSwapBasket(wallet, tokenId, shortSats.map((x) => ({ satoshis: x.planned.funding.satoshis, pool: x.r.leg.outpoint })), { sym: p.sym });
712
- sent = s.tx;
713
- const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
714
- shortSats.forEach((x, n) => {
715
- const h = s.sats[n];
716
- if (h)
717
- x.sats = h;
718
- else
719
- x.r.result.error = `the swap basket send has no sats output for this leg${why}`;
720
- });
721
- }
722
- const holdingBeef = staged.length > 0 ? await swapHoldingBeef(wallet, sent) : undefined;
723
- // The legs sent to the relay: their expected amountOut, and whether their outcome is known.
724
- const tracks = [];
725
- /** Relays the leg (0.34.0: no browser note; the staged rows are the leg's record, D24). */
726
- const relayLeg = (prepared, peerId) => relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
727
- /**
728
- * 0.31.1: one retry of a leg refused `pool_spent` — re-quoted alone against the validator's
729
- * newest pool state (the engine's single-pool math, same side and amountIn), sent only while
730
- * the order's bound holds (filled legs + this one + the legs still out ≥ the plan's
731
- * resolvedMinAmountOut), rebuilt on the new pool output with the same staged rows (0.34.0: its
732
- * BEEF from `GET /pools/beef?lp=<the pool's LP key>`, constant along its continuations). Null:
733
- * the leg failed here (its error set); else the second relay's outcome.
734
- */
735
- const retryLeg = async (x) => {
736
- const { track, pool } = x;
737
- const result = track.result;
738
- const priced = computeSwap(pool, direction, x.leg.amountIn);
739
- const amountOut = priced?.amountOut ?? 0n;
740
- const others = tracks.filter((t) => t !== track).reduce((a, t) => a + (t.result.status === "filled" || !t.done ? t.amountOut : 0n), 0n);
741
- if (!priced || others + amountOut < p.plan.resolvedMinAmountOut) {
742
- result.error = `repriced below your minimum: ${formatAmount(amountOut, direction === "bsvToToken" ? p.dec : 0)} for this leg`;
743
- result.kind = "slippage";
744
- return null;
735
+ const leg = next.leg;
736
+ const row = pools.find((x) => x.outpoint === leg.outpoint);
737
+ const fresh = await readPoolsBeef(base, [row.poolId], freshFetch);
738
+ const now = fresh.pools.find((x) => x.poolId === row.poolId);
739
+ if (!now) {
740
+ excluded.add(row.outpoint);
741
+ continue;
745
742
  }
746
- let peerId = x.peerId;
747
- if (pool.validatorIdentityKey !== x.was.validatorIdentityKey) {
748
- const v = validatorStatus(pool.validatorIdentityKey, p.live);
749
- if (!v.live || !v.peerId) {
750
- result.error = "the pool's validator changed";
751
- return null;
752
- }
753
- peerId = v.peerId;
743
+ // Moved since the list was read: read the list again.
744
+ if (now.outpoint !== row.outpoint)
745
+ continue;
746
+ const poolOutput = poolOutputOf(fresh, leg.outpoint);
747
+ const peerId = row.validatorPeerId ?? answer.validators.find((v) => v.identity === row.validatorIdentityKey.toLowerCase())?.peerId;
748
+ if (!poolOutput || !peerId) {
749
+ excluded.add(row.outpoint);
750
+ continue;
754
751
  }
755
- const leg = { outpoint: pool.outpoint, amountIn: x.leg.amountIn, ...priced };
756
- try {
757
- const poolOutput = poolOutputOf(await readPoolsBeef(base, [x.was.lpPubKey], freshFetch), pool.outpoint);
758
- if (!poolOutput)
759
- throw new Error("the pool moved again: quote again");
760
- const common = { wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg, pool, poolOutput, commissionPkh, ...(x.tokens ? { tokenInputs: x.tokens } : {}), satsPerKb: feeRate };
761
- const planned = await planSwapLeg(common);
762
- const prepared = await prepareSwap({ ...common, satsInput: x.sats, ...(holdingBeef ? { holdingBeef } : {}), planned });
763
- track.amountOut = amountOut;
764
- result.repriced = true;
765
- return { o: await relayLeg(prepared, peerId), prepared };
752
+ const common = { wallet, tokenId, meta, direction, leg, pool: row, poolOutput, commissionPkh: answer.market.commissionPkh, satsPerKb: feeRate };
753
+ const inputs = selectLegInputs(common, await swapSatsHoldings(wallet), buy ? [] : await swapHoldings(wallet, tokenId));
754
+ if (!inputs) {
755
+ why = "fees";
756
+ break;
766
757
  }
767
- catch (e) {
768
- failLeg(result, e);
769
- return null;
758
+ const prepared = await prepareSwap({ ...common, inputs, ...(order ? { order } : {}), last: leg.amountIn === remaining, holdingBeef: await swapHoldingBeef(wallet) });
759
+ const at = { leg: outpointText(leg.outpoint), amountIn: leg.amountIn, amountOut: leg.amountOut };
760
+ progress({ phase: "relaying", ...at, filled: [...legs] });
761
+ let outcome = await relayLeg(wallet, af, base, prepared, peerId);
762
+ if (outcome.status === "unknown" || outcome.status === "timeout") {
763
+ // (a) The overlay: what the leg spends.
764
+ const spent = await readSpendsAll(swapBase, [outpointText(leg.outpoint), ...inputs.sats.map((r) => r.outpoint), ...inputs.tokens.map((r) => r.outpoint)], freshFetch).catch(() => []);
765
+ const ours = spent.find((x) => x.spentBy && x.outpoint !== outpointText(leg.outpoint));
766
+ if (ours) {
767
+ pending.push({ prepared, leg, poolId: row.poolId });
768
+ accountResolved(await resolve(wallet));
769
+ continue;
770
+ }
771
+ if (spent.some((x) => x.spentBy && x.outpoint === outpointText(leg.outpoint)))
772
+ continue;
773
+ // (b) Once more to the same validator.
774
+ outcome = await relayLeg(wallet, af, base, prepared, peerId);
775
+ if (outcome.status === "unknown" || outcome.status === "timeout") {
776
+ // (c) Race: the next leg to another pool with the same rows.
777
+ pending.push({ prepared, leg, poolId: row.poolId });
778
+ excluded.add(row.outpoint);
779
+ continue;
780
+ }
770
781
  }
771
- };
772
- for (const { r, poolOutput, planned, tokens, sats } of staged) {
773
- const { leg, result, pool, peerId } = r;
774
- if (tokensIn && !tokens) {
775
- result.error ??= `no ${p.sym} in the swap basket for exactly ${formatAmount(leg.amountIn, p.dec)}`;
782
+ if (outcome.status === "accepted") {
783
+ if (!outcome.completed.internalized) {
784
+ pending.push({ prepared, leg, poolId: row.poolId });
785
+ accountResolved(await resolve(wallet));
786
+ continue;
787
+ }
788
+ order ??= prepared.order;
789
+ budget -= 1;
790
+ filledIn += leg.amountIn;
791
+ filledOut += leg.amountOut;
792
+ legs.push({ poolOutpoint: at.leg, poolId: row.poolId, amountIn: leg.amountIn, amountOut: leg.amountOut, txid: outcome.txid });
776
793
  continue;
777
794
  }
778
- if (!sats) {
779
- result.error ??= `no sats in the swap basket for this leg`;
780
- continue;
795
+ if (outcome.status === "refused" && outcome.reason.startsWith("pool_spent")) {
796
+ // The relaying host took the validator's state in before it answered: the next read lists it.
797
+ progress({ phase: "repricing", ...at, filled: [...legs] });
781
798
  }
782
- try {
783
- const prepared = await prepareSwap({ wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg, pool, poolOutput, commissionPkh, ...(tokens ? { tokenInputs: tokens } : {}), satsInput: sats, ...(holdingBeef ? { holdingBeef } : {}), planned, satsPerKb: feeRate });
784
- const track = { result, amountOut: leg.amountOut, done: false };
785
- tracks.push(track);
786
- running.push((async () => {
787
- progress({ phase: "relaying", leg: result.poolOutpoint });
788
- let o = await relayLeg(prepared, peerId);
789
- let sentAs = prepared;
790
- // 0.31.1: refused `pool_spent` with the pool's newest state attached → repriced against
791
- // that state and sent once more (the staged `amm-swap` rows are still there).
792
- if (o.status === "refused" && o.reason.startsWith("pool_spent") && o.pool) {
793
- progress({ phase: "repricing", leg: result.poolOutpoint });
794
- const again = await retryLeg({ leg, track, was: pool, pool: o.pool, peerId, tokens, sats });
795
- if (!again) {
796
- track.done = true;
797
- return;
798
- }
799
- o = again.o;
800
- sentAs = again.prepared;
801
- }
802
- track.done = true;
803
- onLeg(result, o, sentAs, base, af);
804
- })());
799
+ else {
800
+ excluded.add(row.outpoint);
805
801
  }
806
- catch (e) {
807
- failLeg(result, e);
802
+ accountResolved(await resolve(wallet));
803
+ }
804
+ // 3. The end.
805
+ const out = { status: filledIn >= quote.request.amountIn ? "filled" : legs.length > 0 ? "partial" : "failed", stop: why, legs, totalIn: filledIn, totalOut: filledOut, unfilled: quote.request.amountIn - filledIn };
806
+ if (why !== "filled" && pending.length === 0) {
807
+ const left = (await swapSatsHoldings(wallet)).length + (await swapHoldings(wallet, tokenId)).length;
808
+ if (left > 0) {
809
+ progress({ phase: "returning", filled: [...legs] });
810
+ try {
811
+ const m = await moveBack({ wallet, base: swapBase, tokenId, meta: { sym: p.sym }, submit: submitAt(swapBase) });
812
+ out.returned = { txid: m.txid, tokens: m.amount };
813
+ }
814
+ catch (e) {
815
+ out.returned = { error: errText(e) };
816
+ }
808
817
  }
809
818
  }
810
- await Promise.all(running);
811
- emit();
812
- return swapResultOf(legs);
819
+ return out;
820
+ }
821
+ /** One relay of a prepared leg (0.34.0: no browser note); a thrown error is no answer. */
822
+ function relayLeg(wallet, af, base, prepared, peerId) {
823
+ return relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
813
824
  }
814
825
  /** A plain GET past the browser's short cache (`state` class: max-age 5): what a swap is about to spend must be read now. */
815
826
  const freshFetch = (url, init) => f(url, { ...init, cache: "no-cache" });
816
- /**
817
- * "Check again" (D15) for a leg with no answer yet: its record read ONCE (`GET
818
- * /@<token>/amm/swaps?id=`, plain), acted on (accepted: the payout taken in). → the leg's result
819
- * now (`poolOutpoint` as given).
820
- */
821
- async function checkSwapAgain(leg) {
822
- const { wallet } = needWallet();
823
- const id = leg.checkId;
824
- const c = id ? checks.get(id) : undefined;
825
- if (!id || !c)
826
- throw new Error("Nothing to check: this leg is not waiting on an answer here");
827
- const o = await checkAgain({ wallet, authFetch: c.af, base: c.base, fetch: freshFetch }, c.prepared, id);
828
- const next = { poolOutpoint: leg.poolOutpoint, status: "failed", ...(leg.repriced ? { repriced: true } : {}) };
829
- onLeg(next, o, c.prepared, c.base, c.af);
830
- if (next.status === "filled")
831
- emit();
832
- return next;
833
- }
834
827
  /** The connected wallet's spendable sats: the toolbox's balance special operation (`Wallet.balance()`'s call), its `totalOutputs`; a page may not read the admin-only `default` basket. */
835
828
  async function walletSats() {
836
829
  const { wallet } = needWallet();
@@ -853,15 +846,16 @@ function makeClient(opts, s) {
853
846
  const [poolRows, history, list] = await Promise.all([basketRows(wallet, POOLS_BASKET), historyPoolRows(wallet), enabledList().catch(() => [])]);
854
847
  const enabled = new Map(list.filter((t) => t.status === "enabled").map((t) => [t.tokenId, t]));
855
848
  const filed = new Set(poolRows.map((r) => r.outpoint.replace("_", ".")));
856
- // The rows by token, one per LP key (the pool's key for its life: a filed row and a history row of one pool are one).
849
+ // The rows by token, one per pool id (0.40.0: the deploy outpoint is the pool's id).
857
850
  const byToken = new Map();
858
851
  for (const row of positionRowsOf(mergePoolRows(history, poolRows))) {
859
852
  if (!row.lpPubKey || !enabled.has(row.tokenId))
860
853
  continue;
861
- const keys = byToken.get(row.tokenId) ?? new Map();
862
- if (!keys.has(row.lpPubKey))
863
- keys.set(row.lpPubKey, row);
864
- byToken.set(row.tokenId, keys);
854
+ const ids = byToken.get(row.tokenId) ?? new Map();
855
+ const poolId = row.deployOutpoint.replace(".", "_");
856
+ if (!ids.has(poolId))
857
+ ids.set(poolId, row);
858
+ byToken.set(row.tokenId, ids);
865
859
  }
866
860
  const out = [];
867
861
  const closed = [];
@@ -879,10 +873,12 @@ function makeClient(opts, s) {
879
873
  const meta = { sym: t?.sym ?? tokenId.slice(0, 8), dec: t?.dec ?? 0, ...(t?.icon ? { icon: t.icon } : {}) };
880
874
  const beating = new Set((await readPools(base, f).catch(() => null))?.validators.map((x) => x.identity) ?? []);
881
875
  for (const p of pools) {
882
- const row = keys.get(p.lpPubKey);
876
+ const row = keys.get(p.poolId);
883
877
  const fees = { feesEarnedSats: p.sats, feesEarnedTokens: p.tokens };
878
+ if (!row)
879
+ continue;
884
880
  if (p.status === "closed" || !p.outpoint || !p.current) {
885
- closed.push({ outpoint: row.deployOutpoint, tokenId, ...meta, sats: 0n, tokens: 0n, validator: { identityKey: row.validatorIdentity ?? "", live: false }, closed: true, ...fees });
881
+ closed.push({ outpoint: row.deployOutpoint, poolId: p.poolId, swaps: p.swaps, tokenId, ...meta, sats: 0n, tokens: 0n, validator: { identityKey: row.validatorIdentity ?? "", live: false }, closed: true, ...fees });
886
882
  continue;
887
883
  }
888
884
  const outpoint = p.outpoint.replace("_", ".");
@@ -892,6 +888,8 @@ function makeClient(opts, s) {
892
888
  await fileDeployRow(wallet, base, history, outpoint, meta);
893
889
  out.push({
894
890
  outpoint,
891
+ poolId: p.poolId,
892
+ swaps: p.swaps,
895
893
  tokenId,
896
894
  ...meta,
897
895
  sats: p.current.bsvReserve,
@@ -939,7 +937,7 @@ function makeClient(opts, s) {
939
937
  const t = (await instanceTerms(tokenId)).validator;
940
938
  if (!t)
941
939
  return { none: true };
942
- return { validator: { identityKey: t.identity, live: Boolean(t.peerId) }, isThisExchange: true, fees: { lpBps: t.lpFeeBps, validatorBps: t.validatorFeeBps, commissionBps: t.commissionBps } };
940
+ return { validator: { identityKey: t.identity, live: Boolean(t.peerId) }, isThisExchange: true, fees: { lpBps: t.lpFeeBps, validatorBps: t.validatorFeeBps, commissionBps: t.commissionBps }, minLegSats: t.minLegSats };
943
941
  }
944
942
  /**
945
943
  * Deploys a new position (src/lp/poolDeploy.ts, 0.13.0): this skein's
@@ -976,8 +974,8 @@ function makeClient(opts, s) {
976
974
  }
977
975
  /**
978
976
  * Closes a position (src/lp/close.ts): everything in the pool returns to the wallet (its own action
979
- * pays the miner fee). 0.34.0: the pool's current output and its BEEF by its LP key (`GET
980
- * /@<token>/amm/pools/beef?lp=<key>`, D2: the pool's id); the page's submit at the host root (D19).
977
+ * pays the miner fee). 0.40.0: the pool's current output and its BEEF by its pool id (`GET
978
+ * /@<token>/amm/pools/beef?pool=<deploy outpoint>`); the page's submit at the host root (D19).
981
979
  */
982
980
  async function closePosition(req) {
983
981
  const { wallet } = needWallet();
@@ -986,11 +984,11 @@ function makeClient(opts, s) {
986
984
  await myPositions();
987
985
  row = loaded.get(req.outpoint);
988
986
  }
989
- if (!row || !row.lpPubKey)
987
+ if (!row)
990
988
  throw new Error("No such position");
991
989
  const [meta, base] = await Promise.all([tokenMeta(), tokenBaseOf(row.tokenId)]);
992
990
  const m = meta.get(row.tokenId);
993
- const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.lpPubKey], freshFetch), req.outpoint);
991
+ const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
994
992
  if (!poolOutput)
995
993
  throw new Error(`the pool is no longer at ${req.outpoint}: reload your positions`);
996
994
  const prepared = await prepareClose({
@@ -1011,8 +1009,8 @@ function makeClient(opts, s) {
1011
1009
  * `sats` deployed in ONE wallet createAction — the old pool the explicit input (as Close spends
1012
1010
  * it), the new one output 0 affirmed by the validator at its terms now (`amm.pool.affirm`, as a
1013
1011
  * deploy), the token difference through the wallet's module, the sats difference to or from the
1014
- * wallet's change. The old pool's output and BEEF by its LP key (`GET
1015
- * /@<token>/amm/pools/beef?lp=<key>`); the page's submit at the host root; the new pool's row
1012
+ * wallet's change. The old pool's output and BEEF by its pool id (`GET
1013
+ * /@<token>/amm/pools/beef?pool=<id>`); the page's submit at the host root; the new pool's row
1016
1014
  * filed in `amm-pools`.
1017
1015
  */
1018
1016
  async function rebalancePosition(req) {
@@ -1024,14 +1022,14 @@ function makeClient(opts, s) {
1024
1022
  await myPositions();
1025
1023
  row = loaded.get(req.outpoint);
1026
1024
  }
1027
- if (!row || !row.lpPubKey)
1025
+ if (!row)
1028
1026
  throw new Error("No such position");
1029
1027
  const [terms, held, meta, base] = await Promise.all([instanceTerms(row.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(row.tokenId)]);
1030
1028
  const v = terms.validator;
1031
1029
  if (!v)
1032
1030
  throw new Error(NO_VALIDATOR);
1033
1031
  const m = meta.get(row.tokenId);
1034
- const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.lpPubKey], freshFetch), req.outpoint);
1032
+ const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
1035
1033
  if (!poolOutput)
1036
1034
  throw new ClientError("stale_pool", `the pool is no longer at ${req.outpoint}: reload your positions`);
1037
1035
  const r = await rebalancePool({
@@ -1086,26 +1084,49 @@ function makeClient(opts, s) {
1086
1084
  balance,
1087
1085
  outputs: t?.valueOutputs ?? 0,
1088
1086
  inSwaps: swaps.get(tokenId)?.amount ?? 0n,
1089
- swapSats: swaps.get(tokenId)?.sats ?? 0,
1090
1087
  onExchange: registered.has(tokenId),
1091
1088
  };
1092
1089
  });
1093
- return out.filter((t) => t.balance > 0n || t.inSwaps > 0n || t.swapSats > 0).sort((a, b) => a.sym.localeCompare(b.sym) || a.tokenId.localeCompare(b.tokenId));
1090
+ return out.filter((t) => t.balance > 0n || t.inSwaps > 0n).sort((a, b) => a.sym.localeCompare(b.sym) || a.tokenId.localeCompare(b.tokenId));
1094
1091
  }
1095
1092
  /**
1096
- * The silent pass over the swap basket's staged legs (0.34.0, D18, D24; src/wallet/swapPayouts.ts):
1097
- * per token ONE `GET /spends?outpoint=` over the pools its rows were staged for, the spenders'
1098
- * transactions, this wallet's payouts taken in. Nothing shown; a payout taken in reloads the
1099
- * pages' data. Never throws.
1093
+ * The pass on load (0.40.0, src/wallet/swapPayouts.ts, src/market/activeSwap.ts): a labelled
1094
+ * staged token output the swap basket lacks filed from its action's description (its BEEF `GET
1095
+ * /@<token>/amm/tx?id=`), then the overlay pass over every `amm-swap` row (ONE `/amm/spends`, each
1096
+ * spender's `/tx`, this wallet's legs taken in). Nothing shown; a take-in reloads the pages' data.
1097
+ * Never throws. → the txids taken in.
1100
1098
  */
1101
1099
  async function settleSwaps() {
1102
1100
  if (!boundWallet)
1103
1101
  return [];
1104
- const taken = await settleStagedSwaps(boundWallet, (t) => tokenBase(t, swapBase), f);
1105
- if (taken.length > 0)
1102
+ let filed = false;
1103
+ try {
1104
+ for (const u of await unfiledStagedTokens(boundWallet)) {
1105
+ const beef = await readTx(tokenBase(u.tokenId, swapBase), u.txid, freshFetch).catch(() => null);
1106
+ if (beef && (await fileStagedToken(boundWallet, u, beef).catch(() => false)))
1107
+ filed = true;
1108
+ }
1109
+ }
1110
+ catch {
1111
+ /* the next load */
1112
+ }
1113
+ const taken = (await resolve(boundWallet)).map((t) => t.txid);
1114
+ if (filed || taken.length > 0)
1106
1115
  emit();
1107
1116
  return taken;
1108
1117
  }
1118
+ /** The sats in the swap basket (0.40.0: shown once, not per token). */
1119
+ async function swapBasketSats() {
1120
+ const { wallet } = needWallet();
1121
+ return swapSatsTotal(wallet);
1122
+ }
1123
+ /** "Move back" of the swap basket's sats alone (0.40.0, src/market/activeSwap.ts `moveBackSats`): one plain createAction, the sats rows its inputs, no outputs. */
1124
+ async function moveBackStagedSats() {
1125
+ const { wallet } = needWallet();
1126
+ const r = await moveBackSats(wallet);
1127
+ emit();
1128
+ return r;
1129
+ }
1109
1130
  /**
1110
1131
  * "Move back" (src/market/activeSwap.ts; 0.29.3: a module plan): every `amm-swap` row of the
1111
1132
  * token (and its staged sats) spent in one action whose one output the wallet's module files
@@ -1227,15 +1248,13 @@ function makeClient(opts, s) {
1227
1248
  }
1228
1249
  /**
1229
1250
  * Sets the default validator terms (0.17.0): root's `{fn: "defaults", args: {terms: {lpFeeBps,
1230
- * validatorFeeBps, commissionBps}}}` to the config instance's `<app>/admin` — a new token
1231
- * instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
1232
- * written). Refused here unless each fee is an integer 0 to 10 000.
1251
+ * validatorFeeBps, commissionBps, minLegSats?}}}` to the config instance's `<app>/admin` — a new
1252
+ * token instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
1253
+ * written). Refused here unless each fee is an integer 0 to 10 000 (0.40.0: and the minimum leg a
1254
+ * whole number of sats; 0 or absent is none and is not sent).
1233
1255
  */
1234
1256
  async function setDefaultTerms(t) {
1235
- for (const [k, v] of Object.entries(t))
1236
- if (!Number.isInteger(v) || v < 0 || v > 10_000)
1237
- throw new Error(`${k}: an integer, 0 to 10000 basis points`);
1238
- await sendDefaults({ terms: { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } });
1257
+ await sendDefaults({ terms: termsArgs(t) });
1239
1258
  }
1240
1259
  /** One token's validator terms as its instance holds them (0.35.0; P7 `GET /@<token>/amm/terms`, plain): the three fees, or null (a market only). */
1241
1260
  async function tokenTerms(tokenId) {
@@ -1243,19 +1262,15 @@ function makeClient(opts, s) {
1243
1262
  }
1244
1263
  /**
1245
1264
  * Sets one token's validator terms (0.35.0, open-exchange 0.12.0): root's `{fn: "terms", args:
1246
- * {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps} | null}}` to the swap instance's
1265
+ * {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps, minLegSats?} | null}}` to the swap instance's
1247
1266
  * `<app>/admin`; swap emits it as a fact and the host sends it to the token's instance as root's
1248
1267
  * `terms` (null: a market only). Nothing answers back: the token's `/terms` shows the result.
1249
1268
  * Refused here unless each fee is an integer 0 to 10 000.
1250
1269
  */
1251
1270
  async function setTokenTerms(tokenId, t) {
1252
- if (t)
1253
- for (const [k, v] of Object.entries(t))
1254
- if (!Number.isInteger(v) || v < 0 || v > 10_000)
1255
- throw new Error(`${k}: an integer, 0 to 10000 basis points`);
1271
+ const terms = t ? termsArgs(t) : null;
1256
1272
  const { signedFor } = needWallet();
1257
1273
  const me = await swapIdentity();
1258
- const terms = t ? { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } : null;
1259
1274
  await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "terms", args: { tokenId: sdkTokenId(tokenId), terms } });
1260
1275
  }
1261
1276
  /** Clears the default validator terms (0.17.0): `{fn: "defaults", args: {terms: null}}`; a new token instance is a market only. */
@@ -1272,6 +1287,152 @@ function makeClient(opts, s) {
1272
1287
  async function clearDefaultMarket() {
1273
1288
  await sendDefaults({ market: null });
1274
1289
  }
1290
+ /**
1291
+ * Sets the price to enable a token (0.42.0, open-exchange 0.15.0): the admin's `{fn: "price", args: {sats}}`
1292
+ * to swap's `<app>/admin`; the host sets it as the price of `POST /amm/enable` (any signed sender enables a
1293
+ * token for it). `null` turns the paid enable off. Refused here unless a whole number of sats, 1 or more.
1294
+ */
1295
+ async function setEnablePrice(sats) {
1296
+ if (sats !== null && (!Number.isSafeInteger(sats) || sats < 1))
1297
+ throw new Error("sats: a whole number, 1 or more (or null: off)");
1298
+ const { signedFor } = needWallet();
1299
+ const me = await swapIdentity();
1300
+ await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "price", args: { sats } });
1301
+ emit();
1302
+ }
1303
+ /** The exchange's wallet (0.42.0, open-exchange 0.15.0; `GET <swap>/wallet`, the host's read): the sats swap's wallet holds and its coin count. */
1304
+ async function exchangeWallet() {
1305
+ return readWallet(swapBase, ff);
1306
+ }
1307
+ /**
1308
+ * Enables a token for the exchange's fee (0.42.2, open-exchange 0.15.3: a BEEF submission; anyone signed, not the
1309
+ * admin): the price from `GET <swap>/defaults` (`enablePriceSats`; none → refused, "paid enable is off"); the
1310
+ * exchange's identity from `GET <swap's origin>/.well-known/skein`; the deploy's BEEF given, else discovery's
1311
+ * (`GET <origin>/@discovery/1color/<token>/deploy`). The payment, ts-stack's standard (sdk BasicBRC29): a prefix and
1312
+ * a suffix from `createNonce`, the payee key `getPublicKey({protocolID: [2, "3241645161d8"], keyID: "<prefix>
1313
+ * <suffix>", counterparty: <the exchange>})`, ONE P2PKH output of exactly the price, `createAction` with
1314
+ * `{randomizeOutputs: false, noSend: true}` — built and signed, NOT sent: the exchange's take-in sends it. Then ONE
1315
+ * BEEF merging the deploy's and the payment's, `POST <swap>/enable?tokenId=&paymentTxid=&paymentVout=0
1316
+ * &derivationPrefix=&derivationSuffix=` signed (BRC-104). On any refusal or failure after the payment was built, the
1317
+ * unsent payment is aborted in the wallet (`abortAction`), so its coins are free again. → the enabled row.
1318
+ * Refusals are `ClientError` kind `refused` (the exchange's code first in the message); a fetch that does not get
1319
+ * through, `network`; the user declining in the wallet, `user_rejected`.
1320
+ */
1321
+ async function enableTokenPaid(tokenId, o = {}) {
1322
+ const { wallet, signedFor } = needWallet();
1323
+ const id = parseOutpoint(sdkTokenId(tokenId).replace("_", "."));
1324
+ if (!id)
1325
+ throw new Error(`not a token id: ${tokenId}`);
1326
+ const canonical = tokenIdText(id);
1327
+ const price = (await readDefaults(swapBase, freshFetch)).enablePriceSats;
1328
+ if (!price)
1329
+ throw new ClientError("refused", "paid enable is off");
1330
+ const exchange = await swapIdentity();
1331
+ let deploy = o.deployBeef;
1332
+ if (!deploy) {
1333
+ const res = await freshFetch(`${discoveryBase()}/${canonical}/deploy`);
1334
+ if (!res.ok)
1335
+ throw new ClientError("refused", `no deploy for ${canonical} on discovery (${res.status}): pass deployBeef`);
1336
+ deploy = Array.from(new Uint8Array(await res.arrayBuffer()));
1337
+ }
1338
+ const derivationPrefix = await createNonce(wallet, "self");
1339
+ const derivationSuffix = await createNonce(wallet, "self");
1340
+ const { publicKey } = await wallet.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: exchange });
1341
+ const created = await wallet.createAction({
1342
+ description: "Open Exchange: enable a token",
1343
+ outputs: [{ satoshis: price, lockingScript: p2pkhHexOf(publicKey), outputDescription: "Open Exchange enable fee", customInstructions: JSON.stringify({ derivationPrefix, derivationSuffix, payee: exchange }) }],
1344
+ options: { randomizeOutputs: false, noSend: true },
1345
+ });
1346
+ const txid = created.txid;
1347
+ if (!txid || !created.tx)
1348
+ throw new Error("the wallet built no payment");
1349
+ try {
1350
+ const b = new Beef();
1351
+ b.mergeBeef(deploy);
1352
+ b.mergeBeef(created.tx);
1353
+ const q = new URLSearchParams({ tokenId: canonical, paymentTxid: txid, paymentVout: "0", derivationPrefix, derivationSuffix });
1354
+ let res;
1355
+ try {
1356
+ res = await signedFor(swapBase).fetch(`${swapBase}/enable?${q.toString()}`, { method: "POST", headers: { "content-type": "application/octet-stream" }, body: Uint8Array.from(b.toBinary()) });
1357
+ }
1358
+ catch (e) {
1359
+ // The stock AuthFetch takes every 402 for its own payment wire and throws on this route's (no x-bsv-payment-* headers).
1360
+ if (/x-bsv-payment/.test(errText(e)))
1361
+ throw new ClientError("refused", "ERR_PAYMENT_REQUIRED: the exchange did not take this payment (402; its price may have changed)", { cause: e });
1362
+ throw e;
1363
+ }
1364
+ const v = (await res.json().catch(() => ({})));
1365
+ if (res.status !== 200)
1366
+ throw new ClientError("refused", `${String(v.code ?? res.status)}: ${String(v.description ?? v.message ?? "refused")}`);
1367
+ s.listRead = null;
1368
+ emit();
1369
+ return v.result;
1370
+ }
1371
+ catch (e) {
1372
+ await wallet.abortAction({ reference: txid }).catch(() => { });
1373
+ throw e;
1374
+ }
1375
+ }
1376
+ /** A withdrawal's payment taken in by the admin's wallet: `internalizeAction`, output `outputIndex` as a wallet payment from swap (BRC-29, ts-stack's recipient side). */
1377
+ async function takeInWithdrawal(wallet, x, tx) {
1378
+ const r = await wallet.internalizeAction({
1379
+ tx,
1380
+ outputs: [{ outputIndex: x.outputIndex, protocol: WALLET_PAYMENT, paymentRemittance: { derivationPrefix: x.derivationPrefix, derivationSuffix: x.derivationSuffix, senderIdentityKey: x.senderIdentityKey } }],
1381
+ description: `Open Exchange withdraw ${x.n}`,
1382
+ });
1383
+ return r.accepted;
1384
+ }
1385
+ /** Withdrawal `n`'s Atomic BEEF (`GET <swap>/wallet/tx?n=`). */
1386
+ async function withdrawalTx(n) {
1387
+ const res = await freshFetch(`${swapBase}/wallet/tx?n=${n}`);
1388
+ if (!res.ok)
1389
+ throw new Error(`GET ${swapBase}/wallet/tx?n=${n}: ${res.status}`);
1390
+ return Array.from(new Uint8Array(await res.arrayBuffer()));
1391
+ }
1392
+ /**
1393
+ * Withdraws everything in the exchange's wallet to the admin's own (0.42.1, open-exchange 0.15.2): the admin's
1394
+ * `{fn: "withdraw"}` to swap's `<app>/admin`. swap's program pays every coin to a BRC-29 key derived for the
1395
+ * sender and hands the transaction over unsent; this function waits for the read to list it (`GET <swap>/wallet`,
1396
+ * at most 10 s — the message's answer reaches no page), then takes it in (`internalizeAction`, a wallet payment:
1397
+ * that is what sends it). → the withdrawal.
1398
+ */
1399
+ async function withdrawAll() {
1400
+ const { wallet, signedFor } = needWallet();
1401
+ const w = await readWallet(swapBase, freshFetch);
1402
+ if (w.sats <= 0)
1403
+ throw new Error("the exchange's wallet holds nothing to withdraw");
1404
+ const me = await swapIdentity();
1405
+ await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "withdraw" });
1406
+ let x;
1407
+ for (let i = 0; i < 40 && !x; i++) {
1408
+ await new Promise((r) => setTimeout(r, 250));
1409
+ x = (await readWallet(swapBase, freshFetch)).withdrawals.find((y) => y.n > w.withdrawals.length);
1410
+ }
1411
+ if (!x)
1412
+ throw new Error("the exchange has not answered the withdraw yet: Settings takes it in when it does");
1413
+ if (!(await takeInWithdrawal(wallet, x, await withdrawalTx(x.n))))
1414
+ throw new Error("the wallet did not accept the withdrawal");
1415
+ emit();
1416
+ return x;
1417
+ }
1418
+ /**
1419
+ * The latest withdrawal taken in again (0.42.1; on Settings load): for a page lost between the withdraw's answer
1420
+ * and its take-in (taking in a payment the wallet holds is harmless). Never throws. → its txid, or none.
1421
+ */
1422
+ async function settleWithdrawals() {
1423
+ if (!boundWallet)
1424
+ return [];
1425
+ try {
1426
+ const w = await readWallet(swapBase, freshFetch);
1427
+ const x = w.withdrawals.at(-1);
1428
+ if (!x)
1429
+ return [];
1430
+ return (await takeInWithdrawal(boundWallet, x, await withdrawalTx(x.n))) ? [x.txid] : [];
1431
+ }
1432
+ catch {
1433
+ return [];
1434
+ }
1435
+ }
1275
1436
  /** The discovery instance's record (0.18.0, shruggr/skein#137; `GET <config>/discovery`): `{on: false}` until root turns it on. */
1276
1437
  async function discoveryState() {
1277
1438
  return readDiscovery(swapBase, ff);
@@ -1311,11 +1472,15 @@ function makeClient(opts, s) {
1311
1472
  isRoot,
1312
1473
  claimExchange: guard(claimExchange),
1313
1474
  hostedTokens: guard(hostedTokens),
1314
- watchTokenPools,
1475
+ tokenPools: guard(tokenPools),
1476
+ watchPoolEvents,
1315
1477
  // history (0.39.0)
1316
1478
  trades: guard(trades),
1317
1479
  trade: guard(trade),
1318
1480
  candles: guard(candles),
1481
+ poolsAt: guard(poolsAt),
1482
+ poolHistory: guard(poolHistory),
1483
+ spendsAll: guard(spendsAll),
1319
1484
  stats: guard(stats),
1320
1485
  statsAll: guard(statsAll),
1321
1486
  holders: guard(holders),
@@ -1324,7 +1489,6 @@ function makeClient(opts, s) {
1324
1489
  // swap
1325
1490
  quoteSwap,
1326
1491
  executeSwap,
1327
- checkSwapAgain: guard(checkSwapAgain),
1328
1492
  walletSats: guard(walletSats),
1329
1493
  // liquidity
1330
1494
  myPositions: guard(myPositions),
@@ -1335,6 +1499,8 @@ function makeClient(opts, s) {
1335
1499
  // your tokens
1336
1500
  myTokens: guard(myTokens),
1337
1501
  settleSwaps: guard(settleSwaps),
1502
+ swapBasketSats: guard(swapBasketSats),
1503
+ moveBackStagedSats: guard(moveBackStagedSats),
1338
1504
  moveBackTokens: guard(moveBackTokens),
1339
1505
  sendTokens: guard(sendTokens),
1340
1506
  // settings
@@ -1351,5 +1517,10 @@ function makeClient(opts, s) {
1351
1517
  clearDefaultMarket: guard(clearDefaultMarket),
1352
1518
  discoveryState: guard(discoveryState),
1353
1519
  setDiscovery: guard(setDiscovery),
1520
+ setEnablePrice: guard(setEnablePrice),
1521
+ exchangeWallet: guard(exchangeWallet),
1522
+ withdrawAll: guard(withdrawAll),
1523
+ enableTokenPaid: guard(enableTokenPaid),
1524
+ settleWithdrawals,
1354
1525
  };
1355
1526
  }