@dcasegr/amm-client 0.39.0 → 0.41.1

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.
@@ -8,13 +8,13 @@
8
8
  *
9
9
  * | # | route | answer |
10
10
  * |---|---|---|
11
- * | P1 | `GET <origin>/amm/pools` (and as an event stream) | `[{tokenId, …P2 row}]`, every enabled token's live pools (the host's) |
12
- * | P2 | `GET <origin>/@<token>/amm/pools` (and as an event stream) | `{market: {commissionPkh, minCommissionBps}, validators: [{identity, peerId}] (0.34.1: beating now), pools: [{lpPubKey, outpoint, bsvReserve, tokenReserve, liquidityFeeBps, validationFeeBps, commissionBps, validatorIdentityKey, validatorPeerId}]}`: only pools whose validator beats now and whose commission is at or above the market's minimum (the host's) |
13
- * | P3 | `GET /@<token>/amm/pools/beef?lp=k1,k2` | `{pools: [P2 row without validatorPeerId], beef: <base64 BEEF of their transactions>}` |
14
- * | P4 | `GET /@<token>/amm/positions?lp=k1,k2` | `[{lpPubKey, status: live|rescinded|closed, outpoint?, current?, sats, tokens, swaps}]` |
11
+ * | P1 | `GET <origin>/amm/pools` | `[{tokenId, …P2 row}]`, every enabled token's live pools (the host's) |
12
+ * | P2 | `GET <origin>/@<token>/amm/pools` (0.41.0: a plain read only) | `{market: {commissionPkh, minCommissionBps}, validators: [{identity, peerId, minLegSats?}] (0.34.1: beating now; 0.40.0: the minimum leg its terms announce), pools: [{poolId, lpPubKey, outpoint, bsvReserve, tokenReserve, liquidityFeeBps, validationFeeBps, commissionBps, validatorIdentityKey, validatorPeerId, swaps}]}`: only pools whose validator beats now and whose commission is at or above the market's minimum (the host's) |
13
+ * | P3 | `GET /@<token>/amm/pools/beef?pool=id1,id2` (0.40.0: by pool id) | `{pools: [P2 row without validatorPeerId], beef: <base64 BEEF of their transactions>}` |
14
+ * | P4 | `GET /@<token>/amm/positions?pool=id1,id2` (0.40.0) | `[{poolId, lpPubKey, status: live|rescinded|closed, outpoint?, current?, sats, tokens, swaps}]` |
15
15
  * | P5 | `GET /@<token>/amm/fees/validator?id=k1,k2` | `[{validatorIdentityKey, sats, tokens, swaps, pools}]` |
16
16
  * | P6 | `GET /@<token>/amm/fees/commission?pkh=h1,h2` | `[{commissionPkh, sats, tokens, swaps}]` |
17
- * | P7 | `GET /@<token>/amm/terms` | `{validator: {identity, peerId, lpFeeBps, validatorFeeBps, commissionBps} | null, market: {commissionPkh, minCommissionBps}}` |
17
+ * | P7 | `GET /@<token>/amm/terms` | `{validator: {identity, peerId, lpFeeBps, validatorFeeBps, commissionBps, minLegSats} | null, market: {commissionPkh, minCommissionBps}}` |
18
18
  * | P8 | `GET /@<token>/amm/spends?outpoint=a,b` | `[{outpoint, spentBy?, current?, hops, closed}]` (one outpoint: the object) |
19
19
  * | P9 | `GET /@<token>/amm/tx?id=<txid>` | the transaction's BEEF (bytes) |
20
20
  * | P10 | `GET /@<token>/amm/swaps?id=<id>` | the relay record, as `amm.swap.status` answers it |
@@ -25,16 +25,24 @@
25
25
  * | P15 | `GET /@discovery/1color/<token>/icon` | the icon's bytes |
26
26
  * | P16 | `GET /@discovery/1color/<token>/deploy` | the deploy's BEEF (bytes) |
27
27
  * | P17 | `GET <origin>/.well-known/skein`, `/skeins?key=`, `/amm/admin`, `/amm/defaults`, `/amm/discovery` | as before |
28
- * | H1 | `GET /@<token>/amm/trades?from=&to=&limit=&before=&lp=` (or `?txid=`) | `{trades: [Trade], next}` newest first (one txid: the row) |
29
- * | H2 | `GET /@<token>/amm/candles?tf=&from=&to=` | `[{t, o, h, l, c, volumeSats, volumeTokens, n}]`, one per non-empty bucket |
30
- * | H3 | `GET /@<token>/amm/stats` | `{tokenId, price, change1h, change24h, volume24hSats, volume24hTokens, buys24h, sells24h, lastTradeAt, tvlSats, tvlTokens, pools}` |
28
+ * | H1 | `GET /@<token>/amm/trades?from=&to=&limit=&before=&pool=` (or `?txid=`) | `{trades: [Trade], next}` newest first (one txid: the row) |
29
+ * | H2 | `GET /@<token>/amm/candles?tf=&from=&to=&series=fill|market` | `[{t, o, h, l, c, volumeSats, volumeTokens, n}]`, one per non-empty bucket (0.40.0: `market`, the host's: the price after each pool event) |
30
+ * | H3 | `GET /@<token>/amm/stats` | `{tokenId, price, buyPrice, sellPrice, lastTradePrice, change1h, change24h, volume24hSats, volume24hTokens, buys24h, sells24h, lastTradeAt, tvlSats, tvlTokens, pools}` |
31
31
  * | H4 | `GET <origin>/amm/stats?ids=` | `[H3]`, every enabled token's (the host's) |
32
- * | H5 | `GET <origin>/amm/positions?lp=` | `[{tokenId, …P4 row}]`, every enabled token's (the host's) |
32
+ * | H5 | `GET <origin>/amm/positions?pool=` | `[{tokenId, …P4 row}]`, every enabled token's (the host's) |
33
+ * | H7 | `GET /@<token>/amm/pools/at?t=<ms>` (0.40.0) | `[{poolId, outpoint, bsvReserve, tokenReserve, lpPubKey, …the three bps, validatorIdentityKey, at, swaps}]`: every pool listed at `t` |
34
+ * | H8 | `GET /@<token>/amm/pools/history?from=&to=&pool=&limit=&after=` (0.40.0) | `{events: [PoolEvent], next}` oldest first; `next` → `after` |
35
+ * | H9 | `GET <origin>/amm/spends?outpoint=a,b` (0.40.0, the host's) | `[{outpoint, tokenId?, spentBy?}]`: each enabled token's `/spends`, the first that names a spender |
33
36
  * | H6 | `GET /@<token>/amm/color/<token>/holders?limit=&skip=` | `{count, rows: [{address, balance}]}` largest first |
37
+ * | E1 | `GET /@<token>/amm/events`, `GET <origin>/amm/events` (0.41.0, the host's) | Server-Sent Events, each `event: pool`, `data: {tokenId, …row}`: the AMM lookup's pool event (a deploy, swap, close or rescind: the H8 row; a `revert`), one token's or every enabled token's; nothing on connect, nothing replayed |
34
38
  *
35
- * 0.39.0 (agent-env plans/history-api-2026-11-05.md D7): P2's stream sends the whole answer once
36
- * (`pools`), then ONE `change` per pool change (`{kind, outpoint, pool?, trade?}`); the host-wide
37
- * stream is gone (P1, H4 and H5 are plain reads).
39
+ * 0.41.0 (agent-env plans/swap-balance-build-2026-11-05.md §5): P2's event-stream form is gone
40
+ * (0.39.0's `pools` / `change` events); the pool events are the lookup's, passed through by the
41
+ * host's feeds (E1): signals for display, the pools route the truth.
42
+ *
43
+ * 0.40.0 (agent-env plans/swap-balance-build-2026-11-05.md §0 3): a pool's id is its deploy outpoint
44
+ * `<txid>_<vout>` (`poolId`, copied forward along its swaps); every read by LP key is by pool id
45
+ * now (`?pool=`), the LP key a field. A clean break: no `?lp=` here.
38
46
  *
39
47
  * Writes stay what they were: `POST <origin>/submit` (BRC-22, plain), `/call` and `/sendMessage`
40
48
  * (signed, the wallet's BRC-104 client).
@@ -83,7 +91,9 @@ export function toPoolRow(raw) {
83
91
  return null;
84
92
  return {
85
93
  ...toPoolState(raw),
94
+ poolId: typeof raw.poolId === "string" ? raw.poolId.toLowerCase() : "",
86
95
  lpPubKey: typeof raw.lpPubKey === "string" ? raw.lpPubKey.toLowerCase() : "",
96
+ swaps: Number(raw.swaps ?? 0),
87
97
  ...(typeof raw.validatorPeerId === "string" && raw.validatorPeerId ? { validatorPeerId: raw.validatorPeerId } : {}),
88
98
  ...(typeof raw.tokenId === "string" ? { tokenId: raw.tokenId } : {}),
89
99
  };
@@ -102,7 +112,10 @@ export function parsePools(x) {
102
112
  const a = x;
103
113
  const validators = (Array.isArray(a?.validators) ? a.validators : [])
104
114
  .filter((v) => v && typeof v.identity === "string")
105
- .map((v) => ({ identity: String(v.identity).toLowerCase(), peerId: typeof v.peerId === "string" ? v.peerId : "" }));
115
+ .map((v) => {
116
+ const min = typeof v.minLegSats === "number" || typeof v.minLegSats === "string" ? BigInt(v.minLegSats) : 0n;
117
+ return { identity: String(v.identity).toLowerCase(), peerId: typeof v.peerId === "string" ? v.peerId : "", ...(min > 0n ? { minLegSats: min } : {}) };
118
+ });
106
119
  return { market: toMarket(a?.market), pools: poolRows(a?.pools), validators };
107
120
  }
108
121
  /** P1's JSON: every enabled token's rows, each naming its `tokenId`. */
@@ -141,7 +154,7 @@ const list = (xs) => xs.map(encodeURIComponent).join(",");
141
154
  export function poolsUrl(base) {
142
155
  return `${trim(base)}/pools`;
143
156
  }
144
- /** P2 once (a one-shot reader; the Swap page holds the stream, `watchPools`). */
157
+ /** P2: the token's pools as the host lists them now (0.41.0: a plain read only). */
145
158
  export async function readPools(base, fetchFn) {
146
159
  return parsePools(await getJson(poolsUrl(base), fetchFn));
147
160
  }
@@ -149,89 +162,43 @@ function eventSourceCtor() {
149
162
  const g = globalThis;
150
163
  return typeof g.EventSource === "function" ? g.EventSource : null;
151
164
  }
165
+ /** E1's URL: `<base>/events` (a token instance's app base: that token's; the swap app's: every token's). */
166
+ export function eventsUrl(base) {
167
+ return `${trim(base)}/events`;
168
+ }
152
169
  /**
153
- * Holds a pools route as an event stream (D16; 0.39.0, D7): `GET <url>` with `Accept:
154
- * text/event-stream` (the browser's EventSource sends it). The event `pools` carries the whole
155
- * answer (first the current one; again when the host cannot say what changed): it replaces the held
156
- * answer. With `change` (P2), the event `change` carries one pool change: `change(held, x)` applies
157
- * it to the held answer (null: it cannot); one it cannot apply (no answer held yet, an unknown
158
- * kind, a row it names not held) → the route is read again ONCE (a plain GET) and that answer is
159
- * held (changes arriving while it is read are dropped: the answer read is the newer state). Each
160
- * new held answer goes to `onAnswer`. Without an EventSource (an old browser, a test): ONE plain
161
- * GET, no poll. → the unsubscribe.
170
+ * Listens to a host's pool event feed (E1, 0.41.0): each `pool` message parsed
171
+ * (`parsePoolEventMessage`) to `onEvent`; one it cannot parse is passed over. Reconnecting is the
172
+ * browser's EventSource's. Without an EventSource: nothing. → the unsubscribe.
162
173
  */
163
- export function watchPools(url, parse, onAnswer, o = {}) {
174
+ export function watchPoolEvents(url, onEvent, o = {}) {
164
175
  const ES = o.EventSource === undefined ? eventSourceCtor() : o.EventSource;
176
+ if (!ES)
177
+ return () => { };
165
178
  let stopped = false;
166
- if (!ES) {
167
- getJson(url, o.fetch).then((x) => {
168
- if (!stopped)
169
- onAnswer(parse(x));
170
- }, (e) => {
171
- if (!stopped)
172
- o.onError?.(e);
173
- });
174
- return () => {
175
- stopped = true;
176
- };
177
- }
178
- let held = null;
179
- let rereading = false;
180
- const hold = (a) => {
181
- held = a;
182
- onAnswer(a);
183
- };
184
- const reread = () => {
185
- rereading = true;
186
- getJson(url, o.fetch).then((x) => {
187
- rereading = false;
188
- if (!stopped)
189
- hold(parse(x));
190
- }, (e) => {
191
- rereading = false;
192
- if (!stopped)
193
- o.onError?.(e);
194
- });
195
- };
196
179
  const es = new ES(url);
197
- es.addEventListener("pools", (e) => {
180
+ es.addEventListener("pool", (e) => {
198
181
  if (stopped)
199
182
  return;
183
+ let m = null;
200
184
  try {
201
- hold(parse(JSON.parse(String(e.data))));
185
+ m = parsePoolEventMessage(JSON.parse(String(e.data)));
202
186
  }
203
- catch (err) {
204
- o.onError?.(err);
187
+ catch {
188
+ m = null;
205
189
  }
190
+ if (m)
191
+ onEvent(m);
206
192
  });
207
- const apply = o.change;
208
- if (apply) {
209
- es.addEventListener("change", (e) => {
210
- if (stopped || rereading)
211
- return;
212
- let next = null;
213
- try {
214
- next = held === null ? null : apply(held, JSON.parse(String(e.data)));
215
- }
216
- catch {
217
- next = null;
218
- }
219
- if (next === null)
220
- reread();
221
- else
222
- hold(next);
223
- });
224
- }
225
- es.onopen = () => o.onOpen?.();
226
193
  es.onerror = (e) => o.onError?.(e);
227
194
  return () => {
228
195
  stopped = true;
229
196
  es.close();
230
197
  };
231
198
  }
232
- /** P3: `GET <base>/pools/beef?lp=k1,k2` (no `lpKeys`: every live pool). */
233
- export async function readPoolsBeef(base, lpKeys, fetchFn) {
234
- const url = `${trim(base)}/pools/beef${lpKeys ? `?lp=${list(lpKeys)}` : ""}`;
199
+ /** P3: `GET <base>/pools/beef?pool=id1,id2` (0.40.0: by pool id; no `poolIds`: every live pool). */
200
+ export async function readPoolsBeef(base, poolIds, fetchFn) {
201
+ const url = `${trim(base)}/pools/beef${poolIds ? `?pool=${list(poolIds)}` : ""}`;
235
202
  const a = (await getJson(url, fetchFn));
236
203
  return { pools: poolRows(a?.pools), beef: typeof a?.beef === "string" && a.beef ? Utils.toArray(a.beef, "base64") : [] };
237
204
  }
@@ -246,24 +213,27 @@ export function poolOutputOf(pb, outpoint) {
246
213
  return { beef: beef.toBinaryAtomic(m[1].toLowerCase()), outputIndex: Number(m[2]) };
247
214
  }
248
215
  const wireBig = (x) => (typeof x === "number" || typeof x === "string" || typeof x === "bigint" ? BigInt(x) : 0n);
249
- /** P4: `GET <base>/positions?lp=k1,k2` → one record per key in the same order. */
250
- export async function readPositions(base, lpPubKeys, fetchFn) {
251
- const url = `${trim(base)}/positions?lp=${list(lpPubKeys)}`;
216
+ /** One P4 / H5 record. */
217
+ function lpPoolOf(r, poolId) {
218
+ const status = r.status === "live" || r.status === "rescinded" ? r.status : "closed";
219
+ return {
220
+ poolId: typeof r.poolId === "string" ? r.poolId.toLowerCase() : poolId,
221
+ lpPubKey: typeof r.lpPubKey === "string" ? r.lpPubKey.toLowerCase() : null,
222
+ status,
223
+ outpoint: typeof r.outpoint === "string" ? r.outpoint : null,
224
+ current: isWirePool(r.current) ? toPoolState(r.current) : null,
225
+ sats: wireBig(r.sats),
226
+ tokens: wireBig(r.tokens),
227
+ swaps: Number(r.swaps ?? 0),
228
+ };
229
+ }
230
+ /** P4: `GET <base>/positions?pool=id1,id2` (0.40.0) → one record per pool id in the same order. */
231
+ export async function readPositions(base, poolIds, fetchFn) {
232
+ const url = `${trim(base)}/positions?pool=${list(poolIds)}`;
252
233
  const a = await getJson(url, fetchFn);
253
- if (!Array.isArray(a) || a.length !== lpPubKeys.length)
254
- throw new Error(`GET ${url}: not one record per key`);
255
- return a.map((r, i) => {
256
- const status = r.status === "live" || r.status === "rescinded" ? r.status : "closed";
257
- return {
258
- lpPubKey: typeof r.lpPubKey === "string" ? r.lpPubKey : lpPubKeys[i],
259
- status,
260
- outpoint: typeof r.outpoint === "string" ? r.outpoint : null,
261
- current: isWirePool(r.current) ? toPoolState(r.current) : null,
262
- sats: wireBig(r.sats),
263
- tokens: wireBig(r.tokens),
264
- swaps: Number(r.swaps ?? 0),
265
- };
266
- });
234
+ if (!Array.isArray(a) || a.length !== poolIds.length)
235
+ throw new Error(`GET ${url}: not one record per pool`);
236
+ return a.map((r, i) => lpPoolOf(r, poolIds[i]));
267
237
  }
268
238
  /** P7: `GET <base>/terms`, unsigned. */
269
239
  export async function readTerms(base, fetchFn) {
@@ -283,6 +253,7 @@ export async function readTerms(base, fetchFn) {
283
253
  lpFeeBps: int(v.lpFeeBps, "lpFeeBps"),
284
254
  validatorFeeBps: int(v.validatorFeeBps, "validatorFeeBps"),
285
255
  commissionBps: int(v.commissionBps, "commissionBps"),
256
+ minLegSats: typeof v.minLegSats === "number" && Number.isInteger(v.minLegSats) ? v.minLegSats : 0,
286
257
  }
287
258
  : null,
288
259
  market: toMarket(a?.market),
@@ -366,7 +337,9 @@ export function toTrade(x) {
366
337
  return null;
367
338
  return {
368
339
  txid: r.txid.toLowerCase(),
340
+ poolId: typeof r.poolId === "string" ? r.poolId.toLowerCase() : "",
369
341
  lpPubKey: r.lpPubKey.toLowerCase(),
342
+ payoutPkh: typeof r.payoutPkh === "string" ? r.payoutPkh.toLowerCase() : "",
370
343
  side: r.side,
371
344
  amountIn: wireBig(r.amountIn),
372
345
  amountOut: wireBig(r.amountOut),
@@ -396,6 +369,9 @@ export function toStats(x) {
396
369
  return {
397
370
  tokenId: r.tokenId,
398
371
  price: toPrice(r.price),
372
+ buyPrice: toPrice(r.buyPrice),
373
+ sellPrice: toPrice(r.sellPrice),
374
+ lastTradePrice: toPrice(r.lastTradePrice),
399
375
  change1h: intOrNull(r.change1h),
400
376
  change24h: intOrNull(r.change24h),
401
377
  volume24hSats: wireBig(r.volume24hSats),
@@ -408,59 +384,13 @@ export function toStats(x) {
408
384
  pools: int(r.pools),
409
385
  };
410
386
  }
411
- /** A `change` event's data, or null when it is not one this client knows. */
412
- export function parseChange(x) {
413
- const r = x;
414
- if (!r || typeof r !== "object" || typeof r.outpoint !== "string")
415
- return null;
416
- if (r.kind !== "deploy" && r.kind !== "swap" && r.kind !== "close" && r.kind !== "rescind")
417
- return null;
418
- const pool = r.pool === undefined ? null : toPoolRow(r.pool);
419
- const trade = r.trade === undefined ? null : toTrade(r.trade);
420
- return { kind: r.kind, outpoint: r.outpoint, ...(pool ? { pool } : {}), ...(trade ? { trade } : {}) };
421
- }
422
- const sameOutpoint = (a, b) => a.toLowerCase().replace(".", "_") === b.toLowerCase().replace(".", "_");
423
- /**
424
- * One `change` applied to a held P2 answer (D7) → the new answer, or null when it cannot be applied
425
- * (the caller reads the route again): `swap` — the row of `trade.lpPubKey` replaced by `pool` (its
426
- * outpoint moved); `deploy` — `pool` added; `close` / `rescind` — the row at `outpoint` removed.
427
- * The market and the validators are kept (a change of either comes as a whole `pools` answer).
428
- */
429
- export function applyPoolChange(held, x) {
430
- const ch = parseChange(x);
431
- if (!ch)
432
- return null;
433
- switch (ch.kind) {
434
- case "swap": {
435
- if (!ch.pool || !ch.trade)
436
- return null;
437
- const i = held.pools.findIndex((p) => p.lpPubKey === ch.trade.lpPubKey);
438
- if (i < 0)
439
- return null;
440
- const pools = held.pools.slice();
441
- pools[i] = ch.pool;
442
- return { ...held, pools };
443
- }
444
- case "deploy":
445
- if (!ch.pool)
446
- return null;
447
- return { ...held, pools: [...held.pools, ch.pool] };
448
- case "close":
449
- case "rescind": {
450
- const pools = held.pools.filter((p) => !sameOutpoint(p.outpoint, ch.outpoint));
451
- if (pools.length === held.pools.length)
452
- return null;
453
- return { ...held, pools };
454
- }
455
- }
456
- }
457
387
  const query = (q) => {
458
388
  const kv = Object.entries(q).filter(([, x]) => x !== undefined).map(([k, x]) => `${k}=${encodeURIComponent(String(x))}`);
459
389
  return kv.length ? `?${kv.join("&")}` : "";
460
390
  };
461
- /** H1: `GET <base>/trades?from=&to=&limit=&before=&lp=` → the page, newest first. */
391
+ /** H1: `GET <base>/trades?from=&to=&limit=&before=&pool=` (0.40.0: `pool` one pool's) → the page, newest first. */
462
392
  export async function readTrades(base, o = {}, fetchFn) {
463
- const a = (await getJson(`${trim(base)}/trades${query({ from: o.from, to: o.to, limit: o.limit, before: o.before, lp: o.lp })}`, fetchFn));
393
+ const a = (await getJson(`${trim(base)}/trades${query({ from: o.from, to: o.to, limit: o.limit, before: o.before, pool: o.pool })}`, fetchFn));
464
394
  return {
465
395
  trades: (Array.isArray(a?.trades) ? a.trades : []).map(toTrade).filter((t) => t !== null),
466
396
  next: typeof a?.next === "string" ? a.next : null,
@@ -476,9 +406,9 @@ export async function readTrade(base, txid, fetchFn) {
476
406
  throw new Error(`GET ${url}: ${res.status} ${(await res.text()).slice(0, 200)}`);
477
407
  return toTrade(await res.json());
478
408
  }
479
- /** H2: `GET <base>/candles?tf=&from=&to=` → one candle per non-empty bucket, oldest first. */
409
+ /** H2: `GET <base>/candles?tf=&from=&to=&series=` → one candle per non-empty bucket, oldest first. */
480
410
  export async function readCandles(base, o, fetchFn) {
481
- const a = await getJson(`${trim(base)}/candles${query({ tf: o.tf, from: o.from, to: o.to })}`, fetchFn);
411
+ const a = await getJson(`${trim(base)}/candles${query({ tf: o.tf, from: o.from, to: o.to, series: o.series })}`, fetchFn);
482
412
  return (Array.isArray(a) ? a : []).map(toCandle).filter((c) => c !== null);
483
413
  }
484
414
  /** H3: `GET <base>/stats`. */
@@ -502,21 +432,100 @@ export async function readHolders(base, tokenId, o = {}, fetchFn) {
502
432
  rows: (Array.isArray(a?.rows) ? a.rows : []).filter((r) => r && typeof r.address === "string").map((r) => ({ address: String(r.address), balance: wireBig(r.balance) })),
503
433
  };
504
434
  }
505
- /** H5: `GET <swap base>/positions?lp=k1,k2` → every enabled token's rows for the keys, each with its token. */
506
- export async function readAllPositions(swapBase, lpPubKeys, fetchFn) {
507
- if (lpPubKeys.length === 0)
435
+ /** H5: `GET <swap base>/positions?pool=id1,id2` (0.40.0) → every enabled token's rows for the pools, each with its token. */
436
+ export async function readAllPositions(swapBase, poolIds, fetchFn) {
437
+ if (poolIds.length === 0)
508
438
  return [];
509
- const a = await getJson(`${trim(swapBase)}/positions?lp=${list(lpPubKeys)}`, fetchFn);
439
+ const a = await getJson(`${trim(swapBase)}/positions?pool=${list(poolIds)}`, fetchFn);
510
440
  return (Array.isArray(a) ? a : [])
511
- .filter((r) => r && typeof r.tokenId === "string" && typeof r.lpPubKey === "string")
441
+ .filter((r) => r && typeof r.tokenId === "string" && typeof r.poolId === "string")
442
+ .map((r) => ({ tokenId: String(r.tokenId), ...lpPoolOf(r, String(r.poolId)) }));
443
+ }
444
+ /** H7: `GET <base>/pools/at?t=<ms>` → every pool listed at `t`. */
445
+ export async function readPoolsAt(base, t, fetchFn) {
446
+ const a = await getJson(`${trim(base)}/pools/at${query({ t })}`, fetchFn);
447
+ return (Array.isArray(a) ? a : []).flatMap((r) => {
448
+ const row = toPoolRow(r);
449
+ return row ? [{ ...row, at: int(r.at) }] : [];
450
+ });
451
+ }
452
+ function toPoolEvent(x) {
453
+ const r = x;
454
+ if (!r || typeof r !== "object" || typeof r.txid !== "string" || typeof r.poolId !== "string")
455
+ return null;
456
+ if (r.kind !== "deploy" && r.kind !== "swap" && r.kind !== "close" && r.kind !== "rescind")
457
+ return null;
458
+ const big = (k) => (r[k] === undefined || r[k] === null ? {} : { [k]: wireBig(r[k]) });
459
+ const text = (k) => (typeof r[k] === "string" ? { [k]: String(r[k]).toLowerCase() } : {});
460
+ return {
461
+ kind: r.kind,
462
+ txid: r.txid.toLowerCase(),
463
+ poolId: r.poolId.toLowerCase(),
464
+ ...(typeof r.outpoint === "string" ? { outpoint: r.outpoint } : {}),
465
+ ...big("bsvReserve"),
466
+ ...big("tokenReserve"),
467
+ lpPubKey: typeof r.lpPubKey === "string" ? r.lpPubKey.toLowerCase() : "",
468
+ liquidityFeeBps: wireBig(r.liquidityFeeBps),
469
+ validationFeeBps: wireBig(r.validationFeeBps),
470
+ commissionBps: wireBig(r.commissionBps),
471
+ validatorIdentityKey: typeof r.validatorIdentityKey === "string" ? r.validatorIdentityKey.toLowerCase() : "",
472
+ at: int(r.at),
473
+ swaps: int(r.swaps),
474
+ ...(r.side === "buy" || r.side === "sell" ? { side: r.side } : {}),
475
+ ...big("amountIn"),
476
+ ...big("amountOut"),
477
+ ...big("feeLp"),
478
+ ...big("feeValidator"),
479
+ ...big("feeCommission"),
480
+ ...text("commissionPkh"),
481
+ ...text("payoutPkh"),
482
+ };
483
+ }
484
+ function toPoolRevert(r) {
485
+ if (r.kind !== "revert" || typeof r.txid !== "string" || typeof r.poolId !== "string")
486
+ return null;
487
+ const base = { kind: "revert", txid: r.txid.toLowerCase(), poolId: r.poolId.toLowerCase(), at: int(r.at) };
488
+ if (typeof r.outpoint !== "string")
489
+ return base;
490
+ return {
491
+ ...base,
492
+ outpoint: r.outpoint,
493
+ bsvReserve: wireBig(r.bsvReserve),
494
+ tokenReserve: wireBig(r.tokenReserve),
495
+ lpPubKey: typeof r.lpPubKey === "string" ? r.lpPubKey.toLowerCase() : "",
496
+ liquidityFeeBps: wireBig(r.liquidityFeeBps),
497
+ validationFeeBps: wireBig(r.validationFeeBps),
498
+ commissionBps: wireBig(r.commissionBps),
499
+ validatorIdentityKey: typeof r.validatorIdentityKey === "string" ? r.validatorIdentityKey.toLowerCase() : "",
500
+ swaps: int(r.swaps),
501
+ };
502
+ }
503
+ /** A pool event feed message's data, or null when it is not one. */
504
+ export function parsePoolEventMessage(x) {
505
+ const r = x;
506
+ if (!r || typeof r !== "object" || typeof r.tokenId !== "string")
507
+ return null;
508
+ const row = r.kind === "revert" ? toPoolRevert(r) : toPoolEvent(r);
509
+ return row ? { tokenId: r.tokenId, ...row } : null;
510
+ }
511
+ /** H8: `GET <base>/pools/history?from=&to=&pool=&limit=&after=` → the page, oldest first. */
512
+ export async function readPoolHistory(base, o = {}, fetchFn) {
513
+ const a = (await getJson(`${trim(base)}/pools/history${query({ from: o.from, to: o.to, pool: o.pool, limit: o.limit, after: o.after })}`, fetchFn));
514
+ return {
515
+ events: (Array.isArray(a?.events) ? a.events : []).map(toPoolEvent).filter((e) => e !== null),
516
+ next: typeof a?.next === "string" ? a.next : null,
517
+ };
518
+ }
519
+ /** H9: `GET <swap base>/spends?outpoint=a,b` (the host's: every enabled token's `/spends`, the first naming a spender) → one record per outpoint. */
520
+ export async function readSpendsAll(swapBase, outpoints, fetchFn) {
521
+ if (outpoints.length === 0)
522
+ return [];
523
+ const a = await getJson(`${trim(swapBase)}/spends?outpoint=${list(outpoints)}`, fetchFn);
524
+ return (Array.isArray(a) ? a : [a])
525
+ .filter((r) => r && typeof r.outpoint === "string")
512
526
  .map((r) => ({
513
- tokenId: String(r.tokenId),
514
- lpPubKey: String(r.lpPubKey),
515
- status: r.status === "live" || r.status === "rescinded" ? r.status : "closed",
516
- outpoint: typeof r.outpoint === "string" ? r.outpoint : null,
517
- current: isWirePool(r.current) ? toPoolState(r.current) : null,
518
- sats: wireBig(r.sats),
519
- tokens: wireBig(r.tokens),
520
- swaps: Number(r.swaps ?? 0),
527
+ outpoint: String(r.outpoint),
528
+ ...(typeof r.tokenId === "string" ? { tokenId: r.tokenId } : {}),
529
+ ...(typeof r.spentBy === "string" ? { spentBy: r.spentBy.toLowerCase() } : {}),
521
530
  }));
522
531
  }
@@ -63,10 +63,12 @@ export interface EnabledToken {
63
63
  export declare function readTokens(base: string, fetchFn?: typeof fetch): Promise<EnabledToken[]>;
64
64
  /** The defaults a new token instance is created with (0.17.0, `GET <config>/defaults`): each absent when unset. */
65
65
  export interface Defaults {
66
+ /** 0.40.0: `minLegSats` present when the defaults set one. */
66
67
  terms?: {
67
68
  lpFeeBps: number;
68
69
  validatorFeeBps: number;
69
70
  commissionBps: number;
71
+ minLegSats?: number;
70
72
  };
71
73
  market?: {
72
74
  minCommissionBps: number;
package/dist/lib/skein.js CHANGED
@@ -134,7 +134,7 @@ export async function readDefaults(base, fetchFn = fetch) {
134
134
  const out = {};
135
135
  const t = a?.terms;
136
136
  if (t && typeof t.lpFeeBps === "number" && typeof t.validatorFeeBps === "number" && typeof t.commissionBps === "number")
137
- out.terms = { lpFeeBps: t.lpFeeBps, validatorFeeBps: t.validatorFeeBps, commissionBps: t.commissionBps };
137
+ out.terms = { lpFeeBps: t.lpFeeBps, validatorFeeBps: t.validatorFeeBps, commissionBps: t.commissionBps, ...(typeof t.minLegSats === "number" && t.minLegSats > 0 ? { minLegSats: t.minLegSats } : {}) };
138
138
  const m = a?.market;
139
139
  if (m && typeof m.minCommissionBps === "number")
140
140
  out.market = { minCommissionBps: m.minCommissionBps };