@dcasegr/amm-client 0.38.2 → 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,6 +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=&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
+ * | H4 | `GET <origin>/amm/stats?ids=` | `[H3]`, 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 |
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 |
38
+ *
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.
28
46
  *
29
47
  * Writes stay what they were: `POST <origin>/submit` (BRC-22, plain), `/call` and `/sendMessage`
30
48
  * (signed, the wallet's BRC-104 client).
@@ -73,7 +91,9 @@ export function toPoolRow(raw) {
73
91
  return null;
74
92
  return {
75
93
  ...toPoolState(raw),
94
+ poolId: typeof raw.poolId === "string" ? raw.poolId.toLowerCase() : "",
76
95
  lpPubKey: typeof raw.lpPubKey === "string" ? raw.lpPubKey.toLowerCase() : "",
96
+ swaps: Number(raw.swaps ?? 0),
77
97
  ...(typeof raw.validatorPeerId === "string" && raw.validatorPeerId ? { validatorPeerId: raw.validatorPeerId } : {}),
78
98
  ...(typeof raw.tokenId === "string" ? { tokenId: raw.tokenId } : {}),
79
99
  };
@@ -92,7 +112,10 @@ export function parsePools(x) {
92
112
  const a = x;
93
113
  const validators = (Array.isArray(a?.validators) ? a.validators : [])
94
114
  .filter((v) => v && typeof v.identity === "string")
95
- .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
+ });
96
119
  return { market: toMarket(a?.market), pools: poolRows(a?.pools), validators };
97
120
  }
98
121
  /** P1's JSON: every enabled token's rows, each naming its `tokenId`. */
@@ -131,7 +154,7 @@ const list = (xs) => xs.map(encodeURIComponent).join(",");
131
154
  export function poolsUrl(base) {
132
155
  return `${trim(base)}/pools`;
133
156
  }
134
- /** 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). */
135
158
  export async function readPools(base, fetchFn) {
136
159
  return parsePools(await getJson(poolsUrl(base), fetchFn));
137
160
  }
@@ -139,48 +162,43 @@ function eventSourceCtor() {
139
162
  const g = globalThis;
140
163
  return typeof g.EventSource === "function" ? g.EventSource : null;
141
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
+ }
142
169
  /**
143
- * Holds a pools route (P1 or P2) as an event stream (D16, D22): `GET <url>` with `Accept:
144
- * text/event-stream` (the browser's EventSource sends it), the event `pools` carrying the answer,
145
- * first the current one, then again each time it changes; `onAnswer` gets each. Without an
146
- * EventSource (an old browser, a test): ONE plain 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.
147
173
  */
148
- export function watchPools(url, parse, onAnswer, o = {}) {
174
+ export function watchPoolEvents(url, onEvent, o = {}) {
149
175
  const ES = o.EventSource === undefined ? eventSourceCtor() : o.EventSource;
176
+ if (!ES)
177
+ return () => { };
150
178
  let stopped = false;
151
- if (!ES) {
152
- getJson(url, o.fetch).then((x) => {
153
- if (!stopped)
154
- onAnswer(parse(x));
155
- }, (e) => {
156
- if (!stopped)
157
- o.onError?.(e);
158
- });
159
- return () => {
160
- stopped = true;
161
- };
162
- }
163
179
  const es = new ES(url);
164
- es.addEventListener("pools", (e) => {
180
+ es.addEventListener("pool", (e) => {
165
181
  if (stopped)
166
182
  return;
183
+ let m = null;
167
184
  try {
168
- onAnswer(parse(JSON.parse(String(e.data))));
185
+ m = parsePoolEventMessage(JSON.parse(String(e.data)));
169
186
  }
170
- catch (err) {
171
- o.onError?.(err);
187
+ catch {
188
+ m = null;
172
189
  }
190
+ if (m)
191
+ onEvent(m);
173
192
  });
174
- es.onopen = () => o.onOpen?.();
175
193
  es.onerror = (e) => o.onError?.(e);
176
194
  return () => {
177
195
  stopped = true;
178
196
  es.close();
179
197
  };
180
198
  }
181
- /** P3: `GET <base>/pools/beef?lp=k1,k2` (no `lpKeys`: every live pool). */
182
- export async function readPoolsBeef(base, lpKeys, fetchFn) {
183
- 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)}` : ""}`;
184
202
  const a = (await getJson(url, fetchFn));
185
203
  return { pools: poolRows(a?.pools), beef: typeof a?.beef === "string" && a.beef ? Utils.toArray(a.beef, "base64") : [] };
186
204
  }
@@ -195,24 +213,27 @@ export function poolOutputOf(pb, outpoint) {
195
213
  return { beef: beef.toBinaryAtomic(m[1].toLowerCase()), outputIndex: Number(m[2]) };
196
214
  }
197
215
  const wireBig = (x) => (typeof x === "number" || typeof x === "string" || typeof x === "bigint" ? BigInt(x) : 0n);
198
- /** P4: `GET <base>/positions?lp=k1,k2` → one record per key in the same order. */
199
- export async function readPositions(base, lpPubKeys, fetchFn) {
200
- 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)}`;
201
233
  const a = await getJson(url, fetchFn);
202
- if (!Array.isArray(a) || a.length !== lpPubKeys.length)
203
- throw new Error(`GET ${url}: not one record per key`);
204
- return a.map((r, i) => {
205
- const status = r.status === "live" || r.status === "rescinded" ? r.status : "closed";
206
- return {
207
- lpPubKey: typeof r.lpPubKey === "string" ? r.lpPubKey : lpPubKeys[i],
208
- status,
209
- outpoint: typeof r.outpoint === "string" ? r.outpoint : null,
210
- current: isWirePool(r.current) ? toPoolState(r.current) : null,
211
- sats: wireBig(r.sats),
212
- tokens: wireBig(r.tokens),
213
- swaps: Number(r.swaps ?? 0),
214
- };
215
- });
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]));
216
237
  }
217
238
  /** P7: `GET <base>/terms`, unsigned. */
218
239
  export async function readTerms(base, fetchFn) {
@@ -232,6 +253,7 @@ export async function readTerms(base, fetchFn) {
232
253
  lpFeeBps: int(v.lpFeeBps, "lpFeeBps"),
233
254
  validatorFeeBps: int(v.validatorFeeBps, "validatorFeeBps"),
234
255
  commissionBps: int(v.commissionBps, "commissionBps"),
256
+ minLegSats: typeof v.minLegSats === "number" && Number.isInteger(v.minLegSats) ? v.minLegSats : 0,
235
257
  }
236
258
  : null,
237
259
  market: toMarket(a?.market),
@@ -300,3 +322,210 @@ export async function readDiscoveryList(discovery, fetchFn) {
300
322
  export async function readDiscoveryDeploy(discovery, tokenId, fetchFn) {
301
323
  return getBytes(`${trim(discovery)}/${encodeURIComponent(tokenId)}/deploy`, fetchFn);
302
324
  }
325
+ const intOrNull = (x) => (typeof x === "number" && Number.isFinite(x) ? x : null);
326
+ const int = (x) => intOrNull(x) ?? 0;
327
+ function toPrice(x) {
328
+ const p = x;
329
+ if (!p || typeof p !== "object" || p.sats === undefined || p.tokens === undefined)
330
+ return null;
331
+ return { sats: wireBig(p.sats), tokens: wireBig(p.tokens) };
332
+ }
333
+ /** A trade row, or null when it is not one. */
334
+ export function toTrade(x) {
335
+ const r = x;
336
+ if (!r || typeof r !== "object" || typeof r.txid !== "string" || typeof r.lpPubKey !== "string" || (r.side !== "buy" && r.side !== "sell"))
337
+ return null;
338
+ return {
339
+ txid: r.txid.toLowerCase(),
340
+ poolId: typeof r.poolId === "string" ? r.poolId.toLowerCase() : "",
341
+ lpPubKey: r.lpPubKey.toLowerCase(),
342
+ payoutPkh: typeof r.payoutPkh === "string" ? r.payoutPkh.toLowerCase() : "",
343
+ side: r.side,
344
+ amountIn: wireBig(r.amountIn),
345
+ amountOut: wireBig(r.amountOut),
346
+ feeLp: wireBig(r.feeLp),
347
+ feeValidator: wireBig(r.feeValidator),
348
+ feeCommission: wireBig(r.feeCommission),
349
+ bsvReserve: wireBig(r.bsvReserve),
350
+ tokenReserve: wireBig(r.tokenReserve),
351
+ at: int(r.at),
352
+ height: intOrNull(r.height),
353
+ };
354
+ }
355
+ function toCandle(x) {
356
+ const r = x;
357
+ if (!r || typeof r !== "object")
358
+ return null;
359
+ const [o, h, l, c] = [toPrice(r.o), toPrice(r.h), toPrice(r.l), toPrice(r.c)];
360
+ if (!o || !h || !l || !c || typeof r.t !== "number")
361
+ return null;
362
+ return { t: r.t, o, h, l, c, volumeSats: wireBig(r.volumeSats), volumeTokens: wireBig(r.volumeTokens), n: int(r.n) };
363
+ }
364
+ /** A stats record (H3), or null when it is not one. */
365
+ export function toStats(x) {
366
+ const r = x;
367
+ if (!r || typeof r !== "object" || typeof r.tokenId !== "string")
368
+ return null;
369
+ return {
370
+ tokenId: r.tokenId,
371
+ price: toPrice(r.price),
372
+ buyPrice: toPrice(r.buyPrice),
373
+ sellPrice: toPrice(r.sellPrice),
374
+ lastTradePrice: toPrice(r.lastTradePrice),
375
+ change1h: intOrNull(r.change1h),
376
+ change24h: intOrNull(r.change24h),
377
+ volume24hSats: wireBig(r.volume24hSats),
378
+ volume24hTokens: wireBig(r.volume24hTokens),
379
+ buys24h: int(r.buys24h),
380
+ sells24h: int(r.sells24h),
381
+ lastTradeAt: intOrNull(r.lastTradeAt),
382
+ tvlSats: wireBig(r.tvlSats),
383
+ tvlTokens: wireBig(r.tvlTokens),
384
+ pools: int(r.pools),
385
+ };
386
+ }
387
+ const query = (q) => {
388
+ const kv = Object.entries(q).filter(([, x]) => x !== undefined).map(([k, x]) => `${k}=${encodeURIComponent(String(x))}`);
389
+ return kv.length ? `?${kv.join("&")}` : "";
390
+ };
391
+ /** H1: `GET <base>/trades?from=&to=&limit=&before=&pool=` (0.40.0: `pool` one pool's) → the page, newest first. */
392
+ export async function readTrades(base, o = {}, 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));
394
+ return {
395
+ trades: (Array.isArray(a?.trades) ? a.trades : []).map(toTrade).filter((t) => t !== null),
396
+ next: typeof a?.next === "string" ? a.next : null,
397
+ };
398
+ }
399
+ /** H1 with `txid`: one Swap's trade row; null when the transaction has none (404). */
400
+ export async function readTrade(base, txid, fetchFn) {
401
+ const url = `${trim(base)}/trades?txid=${encodeURIComponent(txid)}`;
402
+ const res = await (fetchFn ?? plain)(url, { headers: { accept: "application/json" } });
403
+ if (res.status === 404)
404
+ return null;
405
+ if (!res.ok)
406
+ throw new Error(`GET ${url}: ${res.status} ${(await res.text()).slice(0, 200)}`);
407
+ return toTrade(await res.json());
408
+ }
409
+ /** H2: `GET <base>/candles?tf=&from=&to=&series=` → one candle per non-empty bucket, oldest first. */
410
+ export async function readCandles(base, o, fetchFn) {
411
+ const a = await getJson(`${trim(base)}/candles${query({ tf: o.tf, from: o.from, to: o.to, series: o.series })}`, fetchFn);
412
+ return (Array.isArray(a) ? a : []).map(toCandle).filter((c) => c !== null);
413
+ }
414
+ /** H3: `GET <base>/stats`. */
415
+ export async function readStats(base, fetchFn) {
416
+ const url = `${trim(base)}/stats`;
417
+ const s = toStats(await getJson(url, fetchFn));
418
+ if (!s)
419
+ throw new Error(`GET ${url}: not a stats record`);
420
+ return s;
421
+ }
422
+ /** H4: `GET <swap base>/stats` (`ids`: those enabled tokens only, in that order) → every enabled token's stats. */
423
+ export async function readAllStats(swapBase, ids, fetchFn) {
424
+ const a = await getJson(`${trim(swapBase)}/stats${ids ? `?ids=${list(ids)}` : ""}`, fetchFn);
425
+ return (Array.isArray(a) ? a : []).map(toStats).filter((s) => s !== null);
426
+ }
427
+ /** H6: `GET <token base>/color/<token>/holders?limit=&skip=` → the count and the rows, largest first. */
428
+ export async function readHolders(base, tokenId, o = {}, fetchFn) {
429
+ const a = (await getJson(`${trim(base)}/color/${encodeURIComponent(tokenId)}/holders${query({ limit: o.limit, skip: o.skip })}`, fetchFn));
430
+ return {
431
+ count: int(a?.count),
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) })),
433
+ };
434
+ }
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)
438
+ return [];
439
+ const a = await getJson(`${trim(swapBase)}/positions?pool=${list(poolIds)}`, fetchFn);
440
+ return (Array.isArray(a) ? a : [])
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")
526
+ .map((r) => ({
527
+ outpoint: String(r.outpoint),
528
+ ...(typeof r.tokenId === "string" ? { tokenId: r.tokenId } : {}),
529
+ ...(typeof r.spentBy === "string" ? { spentBy: r.spentBy.toLowerCase() } : {}),
530
+ }));
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 };