@dcasegr/amm-client 0.41.1 → 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/README.md CHANGED
@@ -72,7 +72,13 @@ The client (each an async function unless noted):
72
72
  every sats row: txid, amount, BEEF, submit), `moveBackStagedSats` (the sats alone), `sendTokens`;
73
73
  - settings (root): `deployBeef`, `enableToken`, `tokensToEnable`, `disableToken`, `defaults`,
74
74
  `setDefaultTerms`, `clearDefaultTerms`, `setDefaultMarket`, `clearDefaultMarket`, `tokenTerms`,
75
- `setTokenTerms` (0.40.0: the terms `{lpBps, validatorBps, commissionBps, minLegSats?}`), `discoveryState`, `setDiscovery`.
75
+ `setTokenTerms` (0.40.0: the terms `{lpBps, validatorBps, commissionBps, minLegSats?}`), `discoveryState`, `setDiscovery`,
76
+ `setEnablePrice`, `exchangeWallet`, `withdrawAll`, `settleWithdrawals` (0.42.x);
77
+ - the paid enable (0.42.2, anyone signed; open-exchange 0.15.3): `enableTokenPaid(tokenId, {deployBeef?})` — the price
78
+ from `/amm/defaults`, a BRC-29 payment to the exchange built with `noSend` (not sent: the exchange's take-in sends it),
79
+ ONE BEEF with the deploy and the payment, `POST /amm/enable?tokenId=&paymentTxid=&paymentVout=&derivationPrefix=
80
+ &derivationSuffix=` signed; the enabled row back; on any refusal or failure the unsent payment is aborted
81
+ (`abortAction`). Refusals are `ClientError` kind `refused`, the exchange's code first in the message.
76
82
 
77
83
  Pure helpers beside it: `hostedFrom`, `parsePoolEventMessage`, `poolViews`, `poolFees`, `poolsWithMinimums`, `pairPrice`, `legCarrySats`, `hasTerms`,
78
84
  `enableCandidates`, `topicOf`, `lookupOf`, `ammLookupOf`, `walletDeployBeef`, `exchangeBase`,
package/dist/client.d.ts CHANGED
@@ -45,7 +45,7 @@ import { type WalletInterface } from "@bsv/sdk";
45
45
  import { type Send1ColorResult } from "@1sat/actions";
46
46
  import { type Leg, type PoolState, type Plan, type PricePair } from "@dcasegr/amm-engine";
47
47
  import { type Candle, type CandleSeries, type CandleTf, type PoolAt, type PoolHistoryPage, type SpendAll, type Holders, type TokenPosition, type TokenStats, type Trade, type TradesPage, type PoolRow, type PoolsAnswer, type PoolEventMessage, type SignedFetch } from "./lib/overlay.js";
48
- import { type Defaults, type Discovery, type EnabledToken, type ListedToken } from "./lib/skein.js";
48
+ import { type Defaults, type Discovery, type EnabledToken, type ExchangeWallet, type ListedToken } from "./lib/skein.js";
49
49
  import type { AuthFetchLike } from "./market/relay.js";
50
50
  import { type MovedBack } from "./market/activeSwap.js";
51
51
  import { type PositionRow } from "./lp/positions.js";
@@ -308,7 +308,7 @@ export interface RegisteredToken {
308
308
  icon?: string;
309
309
  status: EnabledToken["status"];
310
310
  }
311
- export type { EnabledToken, Defaults, Discovery, PoolRow, PoolsAnswer, Candle, CandleSeries, CandleTf, Holders, PoolAt, PoolHistoryPage, SpendAll, TokenPosition, TokenStats, Trade, TradesPage };
311
+ export type { EnabledToken, Defaults, Discovery, ExchangeWallet, PoolRow, PoolsAnswer, Candle, CandleSeries, CandleTf, Holders, PoolAt, PoolHistoryPage, SpendAll, TokenPosition, TokenStats, Trade, TradesPage };
312
312
  export type { PoolEvent, PoolEventMessage, PoolRevert } from "./lib/overlay.js";
313
313
  export type { Price } from "./lib/overlay.js";
314
314
  export { parsePoolEventMessage } from "./lib/overlay.js";
@@ -591,6 +591,22 @@ declare function makeClient(opts: ClientOptions, s: Shared): {
591
591
  clearDefaultMarket: () => Promise<void>;
592
592
  discoveryState: () => Promise<Discovery>;
593
593
  setDiscovery: (on: boolean) => Promise<void>;
594
+ setEnablePrice: (sats: number | null) => Promise<void>;
595
+ exchangeWallet: () => Promise<ExchangeWallet>;
596
+ withdrawAll: () => Promise<{
597
+ n: number;
598
+ txid: string;
599
+ sats: number;
600
+ at: number;
601
+ outputIndex: number;
602
+ derivationPrefix: string;
603
+ derivationSuffix: string;
604
+ senderIdentityKey: string;
605
+ }>;
606
+ enableTokenPaid: (tokenId: string, o?: {
607
+ deployBeef?: number[];
608
+ } | undefined) => Promise<EnabledToken & Record<string, unknown>>;
609
+ settleWithdrawals: () => Promise<string[]>;
594
610
  };
595
611
  onDataChange: (fn: () => void) => () => void;
596
612
  forgetReads: () => void;
@@ -675,4 +691,20 @@ declare function makeClient(opts: ClientOptions, s: Shared): {
675
691
  clearDefaultMarket: () => Promise<void>;
676
692
  discoveryState: () => Promise<Discovery>;
677
693
  setDiscovery: (on: boolean) => Promise<void>;
694
+ setEnablePrice: (sats: number | null) => Promise<void>;
695
+ exchangeWallet: () => Promise<ExchangeWallet>;
696
+ withdrawAll: () => Promise<{
697
+ n: number;
698
+ txid: string;
699
+ sats: number;
700
+ at: number;
701
+ outputIndex: number;
702
+ derivationPrefix: string;
703
+ derivationSuffix: string;
704
+ senderIdentityKey: string;
705
+ }>;
706
+ enableTokenPaid: (tokenId: string, o?: {
707
+ deployBeef?: number[];
708
+ } | undefined) => Promise<EnabledToken & Record<string, unknown>>;
709
+ settleWithdrawals: () => Promise<string[]>;
678
710
  };
package/dist/client.js CHANGED
@@ -41,13 +41,14 @@
41
41
  * an outpoint is `<txid>.<vout>`. Amounts are bigints in base units; prices
42
42
  * are numbers: sats per whole token.
43
43
  */
44
- 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";
45
46
  import { send1Color } from "@1sat/actions";
46
47
  import { nextLeg, priceOf } from "@dcasegr/amm-engine";
47
48
  import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
48
49
  import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
49
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";
50
- import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
51
+ import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, readWallet, sendMessage } from "./lib/skein.js";
51
52
  import { depthOf } from "./market/prices.js";
52
53
  import { buildPlanRequest, legMinerFeeSats, quote as planQuote } from "./market/plan.js";
53
54
  import { prepareSwap, selectLegInputs } from "./market/swapAction.js";
@@ -1286,6 +1287,152 @@ function makeClient(opts, s) {
1286
1287
  async function clearDefaultMarket() {
1287
1288
  await sendDefaults({ market: null });
1288
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
+ }
1289
1436
  /** The discovery instance's record (0.18.0, shruggr/skein#137; `GET <config>/discovery`): `{on: false}` until root turns it on. */
1290
1437
  async function discoveryState() {
1291
1438
  return readDiscovery(swapBase, ff);
@@ -1370,5 +1517,10 @@ function makeClient(opts, s) {
1370
1517
  clearDefaultMarket: guard(clearDefaultMarket),
1371
1518
  discoveryState: guard(discoveryState),
1372
1519
  setDiscovery: guard(setDiscovery),
1520
+ setEnablePrice: guard(setEnablePrice),
1521
+ exchangeWallet: guard(exchangeWallet),
1522
+ withdrawAll: guard(withdrawAll),
1523
+ enableTokenPaid: guard(enableTokenPaid),
1524
+ settleWithdrawals,
1373
1525
  };
1374
1526
  }
@@ -73,7 +73,27 @@ export interface Defaults {
73
73
  market?: {
74
74
  minCommissionBps: number;
75
75
  };
76
+ /** 0.42.0 (open-exchange 0.15.0): the price in sats to enable a token on `POST /amm/enable`, present when the admin set one. */
77
+ enablePriceSats?: number;
76
78
  }
79
+ /** The exchange's wallet (0.42.0, open-exchange 0.15.0; `GET <swap>/wallet`, the host's read of swap's wallet): its sats and coin count. */
80
+ export interface ExchangeWallet {
81
+ sats: number;
82
+ outputs: number;
83
+ /** The admin's withdrawals, each kept by the swap app once proven; n counts from 1. */
84
+ withdrawals: {
85
+ n: number;
86
+ txid: string;
87
+ sats: number;
88
+ at: number;
89
+ outputIndex: number;
90
+ derivationPrefix: string;
91
+ derivationSuffix: string;
92
+ senderIdentityKey: string;
93
+ }[];
94
+ }
95
+ /** `GET <swap>/wallet`: `{sats, outputs, withdrawals}`. */
96
+ export declare function readWallet(base: string, fetchFn?: typeof fetch): Promise<ExchangeWallet>;
77
97
  /** Discovery (`GET <swap>/discovery`): root's last decision, on or off, and since when (ms); the host starts or stops the discovery instance. */
78
98
  export interface Discovery {
79
99
  on: boolean;
package/dist/lib/skein.js CHANGED
@@ -122,6 +122,19 @@ export async function readTokens(base, fetchFn = fetch) {
122
122
  since: typeof t.since === "number" ? t.since : 0,
123
123
  }));
124
124
  }
125
+ /** `GET <swap>/wallet`: `{sats, outputs, withdrawals}`. */
126
+ export async function readWallet(base, fetchFn = fetch) {
127
+ const a = (await getJson(`${base}/wallet`, fetchFn));
128
+ const ws = Array.isArray(a?.withdrawals) ? a.withdrawals : [];
129
+ return {
130
+ sats: typeof a?.sats === "number" ? a.sats : 0,
131
+ outputs: typeof a?.outputs === "number" ? a.outputs : 0,
132
+ withdrawals: ws.filter((w) => typeof w.n === "number" && typeof w.txid === "string").map((w) => ({
133
+ n: w.n, txid: w.txid, sats: Number(w.sats ?? 0), at: Number(w.at ?? 0), outputIndex: Number(w.outputIndex ?? 0),
134
+ derivationPrefix: String(w.derivationPrefix ?? ""), derivationSuffix: String(w.derivationSuffix ?? ""), senderIdentityKey: String(w.senderIdentityKey ?? ""),
135
+ })),
136
+ };
137
+ }
125
138
  /** `GET <swap>/discovery`: `{on, since?}`, or `{on: false}` (none, or another shape). */
126
139
  export async function readDiscovery(base, fetchFn = fetch) {
127
140
  const a = (await getJson(`${base}/discovery`, fetchFn));
@@ -138,5 +151,7 @@ export async function readDefaults(base, fetchFn = fetch) {
138
151
  const m = a?.market;
139
152
  if (m && typeof m.minCommissionBps === "number")
140
153
  out.market = { minCommissionBps: m.minCommissionBps };
154
+ if (typeof a?.enablePriceSats === "number" && a.enablePriceSats > 0)
155
+ out.enablePriceSats = a.enablePriceSats;
141
156
  return out;
142
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcasegr/amm-client",
3
- "version": "0.41.1",
3
+ "version": "0.42.2",
4
4
  "type": "module",
5
5
  "description": "The Open Exchange client (skein-amm): reads, swap, pool deploy / close / rebalance, move back and positions over a BRC-100 wallet, bound to one exchange by createClient. No React, no styles, no wallet provider.",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@1sat/actions": "0.1.3",
33
- "@dcasegr/amm-engine": "0.41.1",
33
+ "@dcasegr/amm-engine": "0.42.2",
34
34
  "cbor2": "^2.3.0"
35
35
  },
36
36
  "peerDependencies": {