@dcasegr/amm-client 0.42.2 → 0.44.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.
package/README.md CHANGED
@@ -75,7 +75,7 @@ The client (each an async function unless noted):
75
75
  `setTokenTerms` (0.40.0: the terms `{lpBps, validatorBps, commissionBps, minLegSats?}`), `discoveryState`, `setDiscovery`,
76
76
  `setEnablePrice`, `exchangeWallet`, `withdrawAll`, `settleWithdrawals` (0.42.x);
77
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),
78
+ from `/amm/defaults`, a BRC-29 payment to the exchange built as a protected noSend (0.42.3: BRC-177, `p nosend expiry seconds 600`; not sent: the exchange's take-in sends it),
79
79
  ONE BEEF with the deploy and the payment, `POST /amm/enable?tokenId=&paymentTxid=&paymentVout=&derivationPrefix=
80
80
  &derivationSuffix=` signed; the enabled row back; on any refusal or failure the unsent payment is aborted
81
81
  (`abortAction`). Refusals are `ClientError` kind `refused`, the exchange's code first in the message.
@@ -103,7 +103,7 @@ last-leg change keys), `swapInstructions` / `swapSatsInstructions` (their custom
103
103
  change row's with `order`), `LP_KEY_PROTOCOL` (BRC-29) + `LP_KEY_MARK` +
104
104
  `lpKeyDescription` / `lpKeyIdOfDescription` (the deploy's pool output description naming the LP
105
105
  keyID), `POOL_TAG` / `POOL_OP` / `isPoolRow` / `poolCustomInstructions` (an `amm-pools` row),
106
- `POOL_DEPLOY_LABEL` (`amm-pool-deploy`), `SWAP_TTL_MS` (15 s), `transferPlanLabels` (the `p color
106
+ `POOL_DEPLOY_LABEL` (`amm-pool-deploy`), `LEG_PATIENCE_MS` (15 s: how long a swap leg waits for the host's answer before closing the request; 0.44.0, was the swap's expiry), `transferPlanLabels` (the `p color
107
107
  plan` labels), the receipt helpers `newReceivingKey` / `receiptOutput`, and `xTopics` (the BRC-22
108
108
  `x-topics` header: a JSON array).
109
109
 
package/dist/client.d.ts CHANGED
@@ -20,9 +20,10 @@
20
20
  * market's minimum (the host's filter), each validator's minimum leg, the market's commission key
21
21
  * hash; a quote is the matching engine's plan over that list (no request); a swap (`executeSwap`)
22
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
23
+ * ONE `GET /@<token>/amm/pools/beef?pool=` and the leg's ONE plain `POST /@<token>/amm/swap`
24
+ * (0.44.0: the host is the relay, answering the validator's reply; the page's patience,
25
+ * `LEG_PATIENCE_MS`, is the only timer); no answer → the host's `GET /amm/spends`, once more, a
26
+ * race. The pool events (`watchPoolEvents`, 0.41.0: the host's feeds) are signals for
26
27
  * display: a page reads the route again on one.
27
28
  * - **Liquidity** (D9, D23; 0.40.0: by pool id): the wallet's pool rows and deploy history, then
28
29
  * ONE `GET /@<token>/amm/positions?pool=` per token; a deploy the history has but the basket
@@ -391,6 +392,8 @@ export interface EnableCandidate {
391
392
  onDiscovery: boolean;
392
393
  inWallet: boolean;
393
394
  }
395
+ /** 0.42.3: the paid enable's payment is a protected noSend (BRC-177): the wallet reclaims it if nobody sends it within 600 s. */
396
+ export declare const ENABLE_PAYMENT_EXPIRY_LABEL = "p nosend expiry seconds 600";
394
397
  /**
395
398
  * The candidates to enable: discovery's token list (its order, newest first, with its metadata),
396
399
  * then the tokens only this wallet holds (in the order held); every token already on the swap
package/dist/client.js CHANGED
@@ -20,9 +20,10 @@
20
20
  * market's minimum (the host's filter), each validator's minimum leg, the market's commission key
21
21
  * hash; a quote is the matching engine's plan over that list (no request); a swap (`executeSwap`)
22
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
23
+ * ONE `GET /@<token>/amm/pools/beef?pool=` and the leg's ONE plain `POST /@<token>/amm/swap`
24
+ * (0.44.0: the host is the relay, answering the validator's reply; the page's patience,
25
+ * `LEG_PATIENCE_MS`, is the only timer); no answer → the host's `GET /amm/spends`, once more, a
26
+ * race. The pool events (`watchPoolEvents`, 0.41.0: the host's feeds) are signals for
26
27
  * display: a page reads the route again on one.
27
28
  * - **Liquidity** (D9, D23; 0.40.0: by pool id): the wallet's pool rows and deploy history, then
28
29
  * ONE `GET /@<token>/amm/positions?pool=` per token; a deploy the history has but the basket
@@ -212,6 +213,8 @@ export async function walletDeployBeef(wallet, tokenId) {
212
213
  }
213
214
  return undefined;
214
215
  }
216
+ /** 0.42.3: the paid enable's payment is a protected noSend (BRC-177): the wallet reclaims it if nobody sends it within 600 s. */
217
+ export const ENABLE_PAYMENT_EXPIRY_LABEL = "p nosend expiry seconds 600";
215
218
  const canonicalId = (tokenId) => {
216
219
  const ref = parseTokenId(tokenId);
217
220
  return ref ? tokenIdText(ref) : null;
@@ -631,7 +634,8 @@ function makeClient(opts, s) {
631
634
  * (`relaying`):
632
635
  * - accepted → taken in (ONE internalizeAction) before the next leg;
633
636
  * - refused `pool_spent` (`repricing`) → the list read again: the relaying host took the
634
- * validator's state in before it answered (0.41.0);
637
+ * validator's state in before it answered (0.41.0); the list still showing the pool at the
638
+ * outpoint the leg spent → that pool excluded for the rest of this swap (0.43.0);
635
639
  * - refused otherwise (`below_minimum` included) → that outpoint excluded;
636
640
  * - no answer → (a) the overlay: a staged input spent → the leg landed (taken in by the
637
641
  * pass), the pool spent by another's swap → continue with the same rows; (b) else the
@@ -670,19 +674,20 @@ function makeClient(opts, s) {
670
674
  return result;
671
675
  }
672
676
  async function runSwap(quote, progress, signal) {
673
- const { wallet, signedFor } = needWallet();
677
+ const { wallet } = needWallet();
674
678
  const p = planned.get(quote);
675
679
  if (!p)
676
680
  throw new Error("Quote again: this quote was not planned here");
677
681
  const tokenId = quote.request.tokenId;
678
682
  const base = tokenBase(tokenId, swapBase);
679
- const af = signedFor(base);
680
683
  const buy = quote.request.side === "buy";
681
684
  const direction = buy ? "bsvToToken" : "tokenToBsv";
682
685
  const minerFee = legMinerFeeSats(feeRate);
683
686
  const meta = { sym: p.sym, dec: p.dec };
684
687
  // 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
688
  const excluded = new Set();
689
+ /** 0.43.0 (D4): the outpoint a leg was just refused `pool_spent` on: still listed there at the next read → excluded. */
690
+ let spentAt;
686
691
  /** 0.40.2: the legs the order may still fill (the request's maxSources less the legs filled). */
687
692
  let budget = quote.request.maxSources;
688
693
  const legs = [];
@@ -725,6 +730,11 @@ function makeClient(opts, s) {
725
730
  }
726
731
  const remaining = quote.request.amountIn - filledIn;
727
732
  const answer = await tokenPools(tokenId);
733
+ // 0.43.0 (D4): refused pool_spent, yet the server's fresh list still shows the pool at the outpoint the leg spent:
734
+ // the pool is not asked again in this swap.
735
+ if (spentAt !== undefined && answer.pools.some((r) => r.outpoint === spentAt))
736
+ excluded.add(spentAt);
737
+ spentAt = undefined;
728
738
  const pools = poolsWithMinimums(answer).filter((r) => !excluded.has(r.outpoint));
729
739
  const minOut = quote.minAmountOut > filledOut ? quote.minAmountOut - filledOut : 0n;
730
740
  const next = nextLeg({ pools, direction, remaining, fixedCost: { minerFeeSats: minerFee }, minOut, maxSources: budget });
@@ -758,8 +768,8 @@ function makeClient(opts, s) {
758
768
  const prepared = await prepareSwap({ ...common, inputs, ...(order ? { order } : {}), last: leg.amountIn === remaining, holdingBeef: await swapHoldingBeef(wallet) });
759
769
  const at = { leg: outpointText(leg.outpoint), amountIn: leg.amountIn, amountOut: leg.amountOut };
760
770
  progress({ phase: "relaying", ...at, filled: [...legs] });
761
- let outcome = await relayLeg(wallet, af, base, prepared, peerId);
762
- if (outcome.status === "unknown" || outcome.status === "timeout") {
771
+ let outcome = await relayLeg(wallet, base, prepared, peerId);
772
+ if (outcome.status === "unknown") {
763
773
  // (a) The overlay: what the leg spends.
764
774
  const spent = await readSpendsAll(swapBase, [outpointText(leg.outpoint), ...inputs.sats.map((r) => r.outpoint), ...inputs.tokens.map((r) => r.outpoint)], freshFetch).catch(() => []);
765
775
  const ours = spent.find((x) => x.spentBy && x.outpoint !== outpointText(leg.outpoint));
@@ -771,8 +781,8 @@ function makeClient(opts, s) {
771
781
  if (spent.some((x) => x.spentBy && x.outpoint === outpointText(leg.outpoint)))
772
782
  continue;
773
783
  // (b) Once more to the same validator.
774
- outcome = await relayLeg(wallet, af, base, prepared, peerId);
775
- if (outcome.status === "unknown" || outcome.status === "timeout") {
784
+ outcome = await relayLeg(wallet, base, prepared, peerId);
785
+ if (outcome.status === "unknown") {
776
786
  // (c) Race: the next leg to another pool with the same rows.
777
787
  pending.push({ prepared, leg, poolId: row.poolId });
778
788
  excluded.add(row.outpoint);
@@ -795,6 +805,7 @@ function makeClient(opts, s) {
795
805
  if (outcome.status === "refused" && outcome.reason.startsWith("pool_spent")) {
796
806
  // The relaying host took the validator's state in before it answered: the next read lists it.
797
807
  progress({ phase: "repricing", ...at, filled: [...legs] });
808
+ spentAt = row.outpoint;
798
809
  }
799
810
  else {
800
811
  excluded.add(row.outpoint);
@@ -818,9 +829,9 @@ function makeClient(opts, s) {
818
829
  }
819
830
  return out;
820
831
  }
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) }));
832
+ /** One leg to the host's swap route (0.44.0: a plain POST, the page's patience its only timer); a thrown error is no answer. */
833
+ function relayLeg(wallet, base, prepared, peerId) {
834
+ return relaySwap({ wallet, fetch: f, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
824
835
  }
825
836
  /** 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. */
826
837
  const freshFetch = (url, init) => f(url, { ...init, cache: "no-cache" });
@@ -1311,7 +1322,10 @@ function makeClient(opts, s) {
1311
1322
  * (`GET <origin>/@discovery/1color/<token>/deploy`). The payment, ts-stack's standard (sdk BasicBRC29): a prefix and
1312
1323
  * a suffix from `createNonce`, the payee key `getPublicKey({protocolID: [2, "3241645161d8"], keyID: "<prefix>
1313
1324
  * <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
1325
+ * `{noSend: true, randomizeOutputs: false}` and the BRC-177 label `ENABLE_PAYMENT_EXPIRY_LABEL` (0.42.3: a protected
1326
+ * noSend — the wallet prefunds an exact anchor and stores the payment, no change; a plain noSend lived only in the
1327
+ * toolbox's client-side batch, so the payer's storage never learned of it) — built and signed, NOT sent: the
1328
+ * exchange's take-in sends it; a wallet whose storage has no BRC-177 fails the createAction (surfaced). Then ONE
1315
1329
  * BEEF merging the deploy's and the payment's, `POST <swap>/enable?tokenId=&paymentTxid=&paymentVout=0
1316
1330
  * &derivationPrefix=&derivationSuffix=` signed (BRC-104). On any refusal or failure after the payment was built, the
1317
1331
  * unsent payment is aborted in the wallet (`abortAction`), so its coins are free again. → the enabled row.
@@ -1341,7 +1355,14 @@ function makeClient(opts, s) {
1341
1355
  const created = await wallet.createAction({
1342
1356
  description: "Open Exchange: enable a token",
1343
1357
  outputs: [{ satoshis: price, lockingScript: p2pkhHexOf(publicKey), outputDescription: "Open Exchange enable fee", customInstructions: JSON.stringify({ derivationPrefix, derivationSuffix, payee: exchange }) }],
1344
- options: { randomizeOutputs: false, noSend: true },
1358
+ // 0.42.3: a protected noSend (BRC-177, as @1sat/actions' payment to a handle): the wallet prefunds an exact anchor,
1359
+ // stores the payment in its storage with no change, and reclaims it if nobody sends it in time.
1360
+ labels: [ENABLE_PAYMENT_EXPIRY_LABEL],
1361
+ options: { noSend: true, randomizeOutputs: false },
1362
+ }).catch((e) => {
1363
+ // No fallback to a plain noSend: a wallet that cannot make a protected one (BRC-177) says why.
1364
+ const c = toClientError(e);
1365
+ throw c instanceof ClientError ? c : new ClientError("refused", `the wallet could not build the protected payment (BRC-177 noSend): ${errText(e)}`, { cause: e });
1345
1366
  });
1346
1367
  const txid = created.txid;
1347
1368
  if (!txid || !created.tx)
package/dist/errors.js CHANGED
@@ -14,7 +14,8 @@
14
14
  * pool_spent the pool was spent since the quote (the quote's freshness read; the validator's
15
15
  * `pool_spent` refusal)
16
16
  * user_rejected the user declined in the wallet (the wallet's `ERR_PERMISSION_DENIED`, "denied")
17
- * no_answer the relay gave no settled answer yet ("no answer yet"; a timed-out swap record)
17
+ * no_answer no answer to a leg yet ("no answer yet": the host could not reach the validator,
18
+ * the request failed, or the page's patience ran out)
18
19
  * below_minimum (0.40.0) under a validator's minimum leg (its `below_minimum` refusal; a quote no
19
20
  * pool takes for its size)
20
21
  * refused the validator, the relay or the host refused it (a refusal other than pool_spent
@@ -69,7 +70,6 @@ export function kindOfOutcome(o) {
69
70
  return undefined;
70
71
  case "refused":
71
72
  return o.reason.startsWith("pool_spent") ? "pool_spent" : o.reason.startsWith("below_minimum") ? "below_minimum" : "refused";
72
- case "timeout":
73
73
  case "unknown":
74
74
  return "no_answer";
75
75
  case "failed":
package/dist/index.d.ts CHANGED
@@ -51,8 +51,8 @@ export { isPoolRow } from "./lp/poolDeploy.js";
51
51
  export { poolCustomInstructions } from "./lp/poolDeploy.js";
52
52
  /** The label of a pool deploy's action (`amm-pool-deploy`): the positions reader's history. */
53
53
  export { POOL_DEPLOY_LABEL } from "./lp/positions.js";
54
- /** How long the relay may take a swap leg (the swap's `expires`), ms. */
55
- export { SWAP_TTL_MS } from "./market/swapAction.js";
54
+ /** How long the page waits for one swap leg's answer before it closes the request (0.44.0: the host's relay stops with it), ms. */
55
+ export { LEG_PATIENCE_MS } from "./market/swapFlow.js";
56
56
  /** The `p color plan` labels of a transfer of one asset (BRC-207 plan version 1): every token action's. */
57
57
  export { transferPlanLabels } from "./wallet/color.js";
58
58
  /** A module receiving key for one output of an asset from a sender: its receipt, key and hash160. */
package/dist/index.js CHANGED
@@ -53,8 +53,8 @@ export { isPoolRow } from "./lp/poolDeploy.js";
53
53
  export { poolCustomInstructions } from "./lp/poolDeploy.js";
54
54
  /** The label of a pool deploy's action (`amm-pool-deploy`): the positions reader's history. */
55
55
  export { POOL_DEPLOY_LABEL } from "./lp/positions.js";
56
- /** How long the relay may take a swap leg (the swap's `expires`), ms. */
57
- export { SWAP_TTL_MS } from "./market/swapAction.js";
56
+ /** How long the page waits for one swap leg's answer before it closes the request (0.44.0: the host's relay stops with it), ms. */
57
+ export { LEG_PATIENCE_MS } from "./market/swapFlow.js";
58
58
  /** The `p color plan` labels of a transfer of one asset (BRC-207 plan version 1): every token action's. */
59
59
  export { transferPlanLabels } from "./wallet/color.js";
60
60
  /** A module receiving key for one output of an asset from a sender: its receipt, key and hash160. */
@@ -178,8 +178,6 @@ export interface Spend {
178
178
  export declare function readSpends(base: string, outpoints: string[], fetchFn?: FetchFn): Promise<Spend[]>;
179
179
  /** P9: `GET <base>/tx?id=<txid>`: the transaction's BEEF (every input's source with it); null when the instance does not hold it. */
180
180
  export declare function readTx(base: string, txid: string, fetchFn?: FetchFn): Promise<number[] | null>;
181
- /** P10: `GET <base>/swaps?id=<id>`: the relay record (dag-json, as `amm.swap.status` answers it); null when unknown. */
182
- export declare function readSwap(base: string, id: string, fetchFn?: FetchFn): Promise<unknown | null>;
183
181
  /** A token on discovery (P13, P14): its metadata; `iconUrl` absolute (P13 gives it relative to the list). */
184
182
  export interface DiscoveryRow {
185
183
  tokenId: string;
@@ -17,7 +17,7 @@
17
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
- * | P10 | `GET /@<token>/amm/swaps?id=<id>` | the relay record, as `amm.swap.status` answers it |
20
+ * | (P10) | gone in 0.44.0: the host relays a leg (`POST /@<token>/amm/swap`, src/market/relay.ts), nothing is recorded |
21
21
  * | P11 | `GET /@<token>/amm/token` | the token's record |
22
22
  * | P12 | `GET <origin>/amm/tokens` | the exchange's token list (src/lib/skein.ts `readTokens`) |
23
23
  * | P13 | `GET /@discovery/1color/tokens?ids=a,b` (or paged `?limit=&skip=`) | `[{tokenId, topic, sym, dec, iconUrl?, txid, vout}]`; an id not on discovery `{tokenId, listed: false}` |
@@ -270,15 +270,6 @@ export async function readSpends(base, outpoints, fetchFn) {
270
270
  export async function readTx(base, txid, fetchFn) {
271
271
  return getBytes(`${trim(base)}/tx?id=${encodeURIComponent(txid)}`, fetchFn);
272
272
  }
273
- /** P10: `GET <base>/swaps?id=<id>`: the relay record (dag-json, as `amm.swap.status` answers it); null when unknown. */
274
- export async function readSwap(base, id, fetchFn) {
275
- const res = await (fetchFn ?? plain)(`${trim(base)}/swaps?id=${encodeURIComponent(id)}`, { headers: { accept: "application/json" } });
276
- if (res.status === 404)
277
- return null;
278
- if (!res.ok)
279
- throw new Error(`GET ${trim(base)}/swaps: ${res.status} ${(await res.text()).slice(0, 200)}`);
280
- return res.json();
281
- }
282
273
  function discoveryRow(x, listUrl) {
283
274
  const listed = x.listed !== false;
284
275
  const dec = typeof x.dec === "number" ? x.dec : typeof x.dec === "string" && /^\d+$/.test(x.dec) ? Number(x.dec) : undefined;
@@ -1,6 +1,4 @@
1
1
  import type { PoolState } from "@dcasegr/amm-engine";
2
- export declare const SWAP_SUBMIT_FN = "amm.swap.submit";
3
- export declare const SWAP_STATUS_FN = "amm.swap.status";
4
2
  export declare const SWAP_TERMS_FN = "amm.swap.terms";
5
3
  /** The relay's terms: where it takes the commission (hash160 hex), or null when it names none; this market's minimum commission (bps, 0 when unset). */
6
4
  export interface SwapTerms {
@@ -15,7 +13,8 @@ export interface AuthFetchLike {
15
13
  body?: string;
16
14
  }): Promise<Pick<Response, "status" | "text">>;
17
15
  }
18
- export interface SwapSubmit {
16
+ /** A leg as the host's swap route takes it. */
17
+ export interface SwapRequest {
19
18
  /** The funding transaction as AtomicBEEF (the swap's unproven parent, with its ancestry). */
20
19
  funding: number[];
21
20
  /** Raw swap transaction, the validator's slot empty. */
@@ -24,32 +23,32 @@ export interface SwapSubmit {
24
23
  pool: string;
25
24
  /** The pool's validator identity key (hex; sent as 33 bytes). */
26
25
  validator: string;
27
- /** The validator's libp2p peer ID (text), as the liveness read gave it: what the relay dials. */
26
+ /** The validator's libp2p peer ID (text), as the pools route gave it: what the host dials. */
28
27
  peerId: string;
29
- /** Unix ms after which the relay gives up. */
30
- expires: number;
31
28
  }
32
- export type SwapRecord = {
33
- id: string;
34
- status: "pending";
35
- } | {
36
- id: string;
37
- status: "accepted";
29
+ /** The host's answer to a leg: the validator's reply, or the host's own refusal / break (`unreachable`). */
30
+ export type SwapReply = {
31
+ ok: true;
38
32
  tx: number[];
39
33
  txid: string;
40
34
  } | {
41
- id: string;
42
- status: "refused";
35
+ ok: false;
43
36
  reason: string;
44
37
  pool?: PoolState;
45
- } | {
46
- id: string;
47
- status: "timeout";
48
- } | {
49
- id: string;
50
- status: "failed";
38
+ }
39
+ /** Any break in the call (dial refused, protocol not served, write failed, stream closed without an answer): no answer, the swap may or may not stand. */
40
+ | {
41
+ ok: false;
42
+ unreachable: true;
51
43
  reason: string;
52
44
  };
45
+ /** What the swap route needs of `fetch` (and what the tests fake): a plain fetch that honours `signal`. */
46
+ export type FetchLike = (url: string, init: {
47
+ method: string;
48
+ headers: Record<string, string>;
49
+ body: string;
50
+ signal?: AbortSignal;
51
+ }) => Promise<Pick<Response, "status" | "text">>;
53
52
  export declare class RelayError extends Error {
54
53
  readonly code: string;
55
54
  readonly httpStatus: number;
@@ -63,10 +62,10 @@ export declare function dagBytes(bytes: number[]): {
63
62
  /** DAG-JSON bytes, hex, or a byte array → bytes; null otherwise. */
64
63
  export declare function readBytes(x: unknown): number[] | null;
65
64
  export declare function callUrl(base: string): string;
66
- export declare function submitBody(s: SwapSubmit): {
67
- fn: string;
68
- args: Record<string, unknown>;
69
- };
65
+ export declare const SWAP_ROUTE = "/swap";
66
+ /** The host's swap route of a token instance's AMM base (`<origin>/@<tokenId>/amm`). */
67
+ export declare function swapUrl(base: string): string;
68
+ export declare function swapBody(s: SwapRequest): Record<string, unknown>;
70
69
  /** One `{fn, args}` call on the app's `/call` route. Throws `RelayError` on an error answer. */
71
70
  export declare function callApp(af: AuthFetchLike, base: string, fn: string, args: unknown): Promise<unknown>;
72
71
  /**
@@ -76,9 +75,14 @@ export declare function callApp(af: AuthFetchLike, base: string, fn: string, arg
76
75
  * missing a field, is undefined: nothing left to re-price.
77
76
  */
78
77
  export declare function toPoolState(raw: unknown): PoolState | undefined;
79
- export declare function parseSwapRecord(x: unknown): SwapRecord;
78
+ /** The host's answer (200, JSON) to a leg, as a `SwapReply`; throws when it is no reply. */
79
+ export declare function parseSwapReply(x: unknown): SwapReply;
80
80
  /** `amm.swap.terms`'s answer: `commissionPkh` as DAG-JSON bytes (or hex, or a byte array) of 20 bytes, or null / absent; `minCommissionBps` an integer 0 to 10 000, absent 0 (0.16.0). */
81
81
  export declare function parseSwapTerms(x: unknown): SwapTerms;
82
82
  export declare function swapTerms(af: AuthFetchLike, base: string): Promise<SwapTerms>;
83
- export declare function submitSwap(af: AuthFetchLike, base: string, s: SwapSubmit): Promise<SwapRecord>;
84
- export declare function swapStatus(af: AuthFetchLike, base: string, id: string): Promise<SwapRecord>;
83
+ /**
84
+ * One leg to the host's swap route (a plain POST; `signal` closes the request — the page's patience,
85
+ * the host's only timer). Throws `RelayError` on an answer that is not 200 (the host relayed nothing)
86
+ * and the fetch's own error on a network failure or an abort (no answer: the swap may stand).
87
+ */
88
+ export declare function postSwap(fetchFn: FetchLike, base: string, s: SwapRequest, signal?: AbortSignal): Promise<SwapReply>;
@@ -1,61 +1,41 @@
1
1
  /**
2
- * The marketplace relay (docs/notes.md, "Marketplace relay"): the user's own
3
- * skein instance takes the funding and swap transactions and carries them to
4
- * the pool's validator, so the browser needs no libp2p. Two functions on the
5
- * AMM app's box, hosted by programs/amm-p2p:
2
+ * The swap relay and the app's `/call` functions, as the pages reach them.
6
3
  *
7
- * amm.swap.terms (reads) {} → {commissionPkh: bytes(20) | null, minCommissionBps}
8
- * amm.swap.submit (writes) {funding, swap, pool, validator, peerId, expires} → the record
9
- * (0.34.0, D15: the host holds the request until the relay thread
10
- * rests, so the answer is the settled record; past the host's bound,
11
- * 503: no answer yet)
12
- * amm.swap.status (reads) {id} → the record (0.34.0: the pages read it plain, `GET
13
- * <base>/swaps?id=`, src/lib/overlay.ts `readSwap`, behind "Check again")
4
+ * 0.44.0 (David Case 2026-11-06, "the instance logs facts, never attempts"): the HOST is the relay.
5
+ * A leg is one plain POST to the open-exchange host's route
14
6
  *
15
- * commissionPkh the address the relay takes the pool's commission at
16
- * (Swap's `commissionPkh`; 0.16.0: hash160 of the instance's
17
- * own derived key, no config); null when the instance names
18
- * none, in which case a pool with a nonzero CommissionBps pays
19
- * the commission to the taker's own key
20
- * (src/market/swapAction.ts, "the commission")
21
- * minCommissionBps this market's minimum commission (0.16.0: the record
22
- * root sets, `amm/market`; absent or unset, 0): a listing
23
- * preference, the pools the Swap page lists, not a rule
7
+ * POST <origin>/@<tokenId>/amm/swap (`swapUrl(base)`; no BRC-104 session, as /submit is plain)
8
+ * body {funding, swap, pool, validator, peerId} JSON, bytes as DAG-JSON bytes
9
+ * answer the validator's reply as it is: {ok: true, tx, txid, beef?}
10
+ * {ok: false, reason, detail?, pool?, beef?}
11
+ * or the host's own: {ok: false, reason: "unreachable", detail} any break in the call
12
+ * {ok: false, reason: "commission_missing", detail} the one rule it checks
24
13
  *
25
- * funding bytes: the funding transaction as the wallet's AtomicBEEF (its
26
- * ancestry with it: the validator's overlay verifies the unproven
27
- * funding against parents in the BEEF); 0.29.0: the transaction
28
- * of the leg's `amm-swap` sats output, the swap's input 1
14
+ * funding bytes: the funding transaction as the wallet's AtomicBEEF (0.40.1 `fundingBeef`: every
15
+ * staged input's source with its ancestry)
29
16
  * swap bytes: the raw swap, the validator's slot `OP_0`
30
- * pool text `<txid>_<vout>`; validator bytes(33); expires unix ms
31
- * peerId text: the validator's libp2p peer ID, from the pools route's row
32
- * (`validatorPeerId`, 0.34.0: the host joined it from the beat): the
33
- * relay dials it (or, when it is the relay's own node, hands the
34
- * swap to its own validator program); it looks at no liveness
17
+ * pool text `<txid>_<vout>`; validator bytes(33)
18
+ * peerId text: the validator's libp2p peer ID, from the pools route's row (`validatorPeerId`)
19
+ *
20
+ * The host checks that the swap's commission pays this exchange's key (amm-p2p README "The
21
+ * commission rule") and nothing else, signs the validator's package with the token instance's key,
22
+ * carries it on one libp2p stream (or to a validator on its own node as an inbound frame), submits a
23
+ * `beef` the reply carries to the token instance's overlay and then answers. Nothing is recorded:
24
+ * no swap id to ask about, no status, no expiry. THE PAGE'S CONNECTION IS THE ONLY TIMER: closing
25
+ * it (`LEG_PATIENCE_MS`, src/market/swapFlow.ts) stops the host — a dial in progress cancelled, a
26
+ * request not yet written never written. Once written, the validator answers on that stream; a
27
+ * signed swap lands on the overlay and the swap loop's settle pass finds it.
35
28
  *
36
- * record (programs/amm-p2p relay.zig `Record.answer`; id = sha256 of the swap, hex):
37
- * {id, status: "pending" | "accepted" | "refused" | "timeout" | "failed", pool, expires, …,
38
- * tx?, txid? accepted: the validator's signed swap
39
- * reason?, detail?, refused / timeout / failed
40
- * poolState?} refused: the pool's current state (the validator's)
41
- * "failed" is the relay's transport failure (retryable by submitting again):
42
- * the validator may or may not have the swap, so the page treats it as no
43
- * answer, not as a refusal.
29
+ * The app's `/call` route (skein docs/APPS.md §4) stays for `amm.swap.terms`, `amm.pool.terms`
30
+ * and `amm.pool.affirm`: `POST <base>/call`, JSON body `{fn, args}`, answer `{fn, result}` (200) or
31
+ * `{fn, error: {code, message}}`; BRC-104 authenticated (the wallet-backed AuthFetch).
44
32
  *
45
- * Transport: the app's `/call` route (skein docs/APPS.md §4): `POST
46
- * <VITE_AMM_OVERLAY>/call`, JSON body `{fn, args}`, answer `{fn, result}`
47
- * (200) or `{fn, error: {code, message}}` (400 bad-request / bad-args, 403
48
- * not-admitted, 404 unknown-fn, 409 read-only, 500 failed). The route is
49
- * BRC-104 authenticated: the caller must be admitted to the app's box, so
50
- * both calls go through the wallet-backed AuthFetch (src/wallet/authFetch.ts).
33
+ * amm.swap.terms (reads) {} → {commissionPkh: bytes(20) | null, minCommissionBps}
51
34
  *
52
- * Bytes in `args` are DAG-JSON bytes (`{"/": {"bytes": "<base64, unpadded>"}}`),
53
- * how skein reads JSON bodies (docs/MESSAGES.md "The messagebox"); answers are
35
+ * Bytes in bodies are DAG-JSON bytes (`{"/": {"bytes": "<base64, unpadded>"}}`); answers are
54
36
  * read as DAG-JSON bytes, hex or a byte array.
55
37
  */
56
38
  import { Utils } from "@bsv/sdk";
57
- export const SWAP_SUBMIT_FN = "amm.swap.submit";
58
- export const SWAP_STATUS_FN = "amm.swap.status";
59
39
  export const SWAP_TERMS_FN = "amm.swap.terms";
60
40
  export class RelayError extends Error {
61
41
  code;
@@ -86,11 +66,13 @@ export function readBytes(x) {
86
66
  export function callUrl(base) {
87
67
  return `${base.replace(/\/+$/, "")}/call`;
88
68
  }
89
- export function submitBody(s) {
90
- return {
91
- fn: SWAP_SUBMIT_FN,
92
- args: { funding: dagBytes(s.funding), swap: dagBytes(s.swap), pool: s.pool, validator: dagBytes(Utils.toArray(s.validator, "hex")), peerId: s.peerId, expires: s.expires },
93
- };
69
+ export const SWAP_ROUTE = "/swap";
70
+ /** The host's swap route of a token instance's AMM base (`<origin>/@<tokenId>/amm`). */
71
+ export function swapUrl(base) {
72
+ return `${base.replace(/\/+$/, "")}${SWAP_ROUTE}`;
73
+ }
74
+ export function swapBody(s) {
75
+ return { funding: dagBytes(s.funding), swap: dagBytes(s.swap), pool: s.pool, validator: dagBytes(Utils.toArray(s.validator, "hex")), peerId: s.peerId };
94
76
  }
95
77
  /** One `{fn, args}` call on the app's `/call` route. Throws `RelayError` on an error answer. */
96
78
  export async function callApp(af, base, fn, args) {
@@ -147,32 +129,22 @@ export function toPoolState(raw) {
147
129
  validatorIdentityKey: identity,
148
130
  };
149
131
  }
150
- export function parseSwapRecord(x) {
132
+ /** The host's answer (200, JSON) to a leg, as a `SwapReply`; throws when it is no reply. */
133
+ export function parseSwapReply(x) {
151
134
  const r = x;
152
- const id = r?.id ?? r?.swapId;
153
- const status = r?.status ?? r?.state;
154
- if (!r || typeof id !== "string")
155
- throw new Error(`relay: not a swap record: ${JSON.stringify(x)?.slice(0, 200)}`);
156
- switch (status) {
157
- case "pending":
158
- case "timeout":
159
- return { id, status };
160
- case "failed":
161
- return { id, status, reason: (typeof r.reason === "string" ? r.reason : "failed") + (typeof r.detail === "string" && r.detail ? `: ${r.detail}` : "") };
162
- case "accepted": {
163
- const tx = readBytes(r.tx);
164
- if (!tx || typeof r.txid !== "string")
165
- throw new Error("relay: accepted without tx / txid");
166
- return { id, status, tx, txid: r.txid };
167
- }
168
- case "refused": {
169
- const pool = toPoolState(r.poolState ?? (typeof r.pool === "object" ? r.pool : undefined));
170
- const reason = (typeof r.reason === "string" ? r.reason : "refused") + (typeof r.detail === "string" && r.detail ? `: ${r.detail}` : "");
171
- return pool ? { id, status, reason, pool } : { id, status, reason };
172
- }
173
- default:
174
- throw new Error(`relay: unknown swap status ${JSON.stringify(status)}`);
135
+ if (!r || typeof r !== "object" || typeof r.ok !== "boolean")
136
+ throw new Error(`relay: not a swap reply: ${JSON.stringify(x)?.slice(0, 200)}`);
137
+ if (r.ok) {
138
+ const tx = readBytes(r.tx);
139
+ if (!tx || typeof r.txid !== "string")
140
+ throw new Error("relay: accepted without tx / txid");
141
+ return { ok: true, tx, txid: r.txid };
175
142
  }
143
+ const reason = (typeof r.reason === "string" ? r.reason : "refused") + (typeof r.detail === "string" && r.detail ? `: ${r.detail}` : "");
144
+ if (r.reason === "unreachable")
145
+ return { ok: false, unreachable: true, reason };
146
+ const pool = toPoolState(typeof r.pool === "object" ? r.pool : undefined);
147
+ return pool ? { ok: false, reason, pool } : { ok: false, reason };
176
148
  }
177
149
  /** `amm.swap.terms`'s answer: `commissionPkh` as DAG-JSON bytes (or hex, or a byte array) of 20 bytes, or null / absent; `minCommissionBps` an integer 0 to 10 000, absent 0 (0.16.0). */
178
150
  export function parseSwapTerms(x) {
@@ -192,10 +164,28 @@ export function parseSwapTerms(x) {
192
164
  export async function swapTerms(af, base) {
193
165
  return parseSwapTerms(await callApp(af, base, SWAP_TERMS_FN, {}));
194
166
  }
195
- export async function submitSwap(af, base, s) {
196
- const { fn, args } = submitBody(s);
197
- return parseSwapRecord(await callApp(af, base, fn, args));
198
- }
199
- export async function swapStatus(af, base, id) {
200
- return parseSwapRecord(await callApp(af, base, SWAP_STATUS_FN, { id }));
167
+ /**
168
+ * One leg to the host's swap route (a plain POST; `signal` closes the request — the page's patience,
169
+ * the host's only timer). Throws `RelayError` on an answer that is not 200 (the host relayed nothing)
170
+ * and the fetch's own error on a network failure or an abort (no answer: the swap may stand).
171
+ */
172
+ export async function postSwap(fetchFn, base, s, signal) {
173
+ const res = await fetchFn(swapUrl(base), {
174
+ method: "POST",
175
+ headers: { "content-type": "application/json", accept: "application/json" },
176
+ body: JSON.stringify(swapBody(s)),
177
+ ...(signal ? { signal } : {}),
178
+ });
179
+ const text = await res.text();
180
+ let answer = null;
181
+ try {
182
+ answer = JSON.parse(text);
183
+ }
184
+ catch {
185
+ /* not JSON */
186
+ }
187
+ if (res.status === 200 && answer && typeof answer === "object" && "ok" in answer)
188
+ return parseSwapReply(answer);
189
+ const e = answer;
190
+ throw new RelayError(typeof e?.code === "string" ? e.code : `HTTP ${res.status}`, typeof e?.description === "string" ? e.description : text.slice(0, 200), res.status);
201
191
  }
@@ -20,7 +20,7 @@
20
20
  * into `amm-swap` with the order's key input in its customInstructions; the LAST leg (it takes
21
21
  * all that remains) → the WALLET: sats to a BRC-29 key of the user's, tokens to a module
22
22
  * receiving key for self, both `legDerivation(order, "change")`.
23
- * 4. The relay (src/market/relay.ts) carries it to the validator with the first sats input's
23
+ * 4. The host's relay (src/market/relay.ts, `POST /@<tokenId>/amm/swap`, 0.44.0) carries it to the validator with the first sats input's
24
24
  * transaction as the swap's funding transaction: ONE BEEF of every staged input's source,
25
25
  * its subject that transaction (0.40.1, `fundingBeef`).
26
26
  * 5. Accepted: `completeSwap` takes in, in ONE `internalizeAction`, the payout, the commission
@@ -35,11 +35,6 @@ import { type PaymentRemittance } from "../wallet/brc29.js";
35
35
  import { type ColorReceipt } from "../wallet/color.js";
36
36
  /** A P2PKH unlocking script's upper bound: push(DER ≤ 72 + sighash byte) + push(33-byte key). */
37
37
  export declare const P2PKH_UNLOCK_LENGTH = 108;
38
- /**
39
- * How long the relay may take (the swap's `expires`): 15 s (0.34.0, D15, David Case 2026-11-04;
40
- * was 120 s, a 10-05 builder number): the held submit returns inside it.
41
- */
42
- export declare const SWAP_TTL_MS = 15000;
43
38
  export declare class LegShapeError extends Error {
44
39
  constructor(message: string);
45
40
  }
@@ -199,8 +194,6 @@ export interface PrepareSwapInput extends PlanLegInput {
199
194
  last: boolean;
200
195
  /** The swap basket's source transactions: the rows' (default: `amm-swap`'s, from the wallet). */
201
196
  holdingBeef?: Beef;
202
- now?: number;
203
- ttlMs?: number;
204
197
  }
205
198
  export type SwapPayout = {
206
199
  kind: "color";
@@ -228,7 +221,6 @@ export interface PreparedSwap {
228
221
  pool: string;
229
222
  validator: string;
230
223
  bsvIn: boolean;
231
- expires: number;
232
224
  plan: CallPlan;
233
225
  /** The order's key input (`<txid>.<vout>`): what the payout keys are derived from. */
234
226
  order: string;
@@ -20,7 +20,7 @@
20
20
  * into `amm-swap` with the order's key input in its customInstructions; the LAST leg (it takes
21
21
  * all that remains) → the WALLET: sats to a BRC-29 key of the user's, tokens to a module
22
22
  * receiving key for self, both `legDerivation(order, "change")`.
23
- * 4. The relay (src/market/relay.ts) carries it to the validator with the first sats input's
23
+ * 4. The host's relay (src/market/relay.ts, `POST /@<tokenId>/amm/swap`, 0.44.0) carries it to the validator with the first sats input's
24
24
  * transaction as the swap's funding transaction: ONE BEEF of every staged input's source,
25
25
  * its subject that transaction (0.40.1, `fundingBeef`).
26
26
  * 5. Accepted: `completeSwap` takes in, in ONE `internalizeAction`, the payout, the commission
@@ -36,11 +36,6 @@ import { WALLET_PAYMENT, identityKeyOf, newBrc29Payout, p2pkhHexOf } from "../wa
36
36
  import { SWAP_BASKET, SWAP_PROTOCOL, newReceivingKey, receiptOutput, swapInstructions, swapSatsInstructions } from "../wallet/color.js";
37
37
  /** A P2PKH unlocking script's upper bound: push(DER ≤ 72 + sighash byte) + push(33-byte key). */
38
38
  export const P2PKH_UNLOCK_LENGTH = 108;
39
- /**
40
- * How long the relay may take (the swap's `expires`): 15 s (0.34.0, D15, David Case 2026-11-04;
41
- * was 120 s, a 10-05 builder number): the held submit returns inside it.
42
- */
43
- export const SWAP_TTL_MS = 15_000;
44
39
  const toHex = (b) => Utils.toHex(b);
45
40
  export class LegShapeError extends Error {
46
41
  constructor(message) {
@@ -462,7 +457,6 @@ export async function prepareSwap(i) {
462
457
  pool: i.leg.outpoint,
463
458
  validator: i.pool.validatorIdentityKey,
464
459
  bsvIn,
465
- expires: (i.now ?? Date.now()) + (i.ttlMs ?? SWAP_TTL_MS),
466
460
  plan,
467
461
  order,
468
462
  last: i.last,
@@ -1,24 +1,27 @@
1
1
  /**
2
- * A prepared swap through the relay to its end (pure over the wallet and
3
- * AuthFetch, tested with fakes). 0.34.0 (D15, David Case 2026-11-04): the submit's answer is
4
- * the settled record — the host holds the `/call` request until the relay thread rests (the
5
- * validator's answer; a dial failure settles `failed` at once) — so nothing polls:
2
+ * A prepared swap through the host's relay to its end (pure over the wallet and fetch, tested with
3
+ * fakes). 0.44.0 (David Case 2026-11-06): the host is the relay — one plain POST per leg to
4
+ * `<origin>/@<tokenId>/amm/swap`, answered with the validator's reply (src/market/relay.ts); no
5
+ * record, no status, no expiry:
6
6
  *
7
- * submit (amm.swap.submit) →
8
- * accepted: completeSwap (internalizeAction; 0.29.0: no relinquishOutput)
9
- * refused / timeout / an error answer to submit (nothing recorded):
7
+ * POST /swap →
8
+ * {ok: true, tx, txid}: completeSwap (internalizeAction; 0.29.0: no relinquishOutput) → accepted
9
+ * {ok: false, reason, pool?} (the validator's refusal, or the host's commission rule): refused —
10
10
  * nothing to undo: the leg's `amm-swap` outputs stay there
11
- * no answer (a network failure, the host's 503 past its bound, a `pending` record, the
12
- * relay's transport `failed`): "no answer yet", left alone — the relay may still carry it
13
- * (0.40.0: the swap's loop resolves it from the overlay, sends it once more, or races it;
14
- * src/client.ts `executeSwap`).
11
+ * {ok: false, reason: "unreachable"} (any break in the host's call), a network failure, or no
12
+ * answer within LEG_PATIENCE_MS (the request is closed: the host stops): unknown, "no answer
13
+ * yet" — the swap may stand (0.40.0: the swap's loop resolves it from the overlay, asks once
14
+ * more, or races it; src/client.ts `executeSwap`)
15
+ * an answer that is not 200 (a bad request, an unknown token): failed — the host relayed nothing
15
16
  *
16
- * The swap's id is the relay's (amm-p2p relay.zig `idOf`): sha256 of the swap as submitted (the
17
- * validator's slot empty), hex — known to the page, so a lost answer can be checked again.
17
+ * The page's patience is the only timer: the AbortController's signal closes the request, and with
18
+ * it the host's attempt (a plain fetch honours `signal`; AuthFetch does not, which is why the leg is
19
+ * a plain fetch). The swap's id is sha256 of the swap as sent (the validator's slot empty), hex: the
20
+ * page's own name for a leg.
18
21
  */
19
22
  import { type WalletInterface } from "@bsv/sdk";
20
23
  import type { PoolState } from "@dcasegr/amm-engine";
21
- import { type AuthFetchLike } from "./relay.js";
24
+ import { type FetchLike } from "./relay.js";
22
25
  import { type CompletedSwap, type PreparedSwap } from "./swapAction.js";
23
26
  export type SwapOutcome = {
24
27
  status: "accepted";
@@ -30,9 +33,6 @@ export type SwapOutcome = {
30
33
  id: string;
31
34
  reason: string;
32
35
  pool?: PoolState;
33
- } | {
34
- status: "timeout";
35
- id: string;
36
36
  } | {
37
37
  status: "failed";
38
38
  reason: string;
@@ -41,15 +41,20 @@ export type SwapOutcome = {
41
41
  id: string;
42
42
  reason: string;
43
43
  };
44
+ /** How long the page waits for one leg's answer before it closes the request (the host then stops), ms. */
45
+ export declare const LEG_PATIENCE_MS = 15000;
44
46
  export interface RelayContext {
45
47
  wallet: WalletInterface;
46
- authFetch: AuthFetchLike;
47
- /** The token instance's AMM base. */
48
+ /** A plain fetch (it must honour `signal`). Default: the global fetch. */
49
+ fetch?: FetchLike;
50
+ /** The token instance's AMM base, `<origin>/@<tokenId>/amm`. */
48
51
  base: string;
52
+ /** The page's patience for this leg (default LEG_PATIENCE_MS). */
53
+ patienceMs?: number;
49
54
  }
50
- /** The relay's id of a prepared swap: sha256 of the swap's bytes as submitted, hex. */
55
+ /** A prepared swap's id: sha256 of the swap's bytes as sent, hex. */
51
56
  export declare function swapIdOf(p: PreparedSwap): string;
52
- /** "no answer yet": what a held request that came back without the settled record means. */
57
+ /** "no answer yet": what a leg that came back without the validator's reply means. */
53
58
  export declare const NO_ANSWER_YET = "no answer yet";
54
- /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the relay dials it). */
59
+ /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the host dials it). */
55
60
  export declare function relaySwap(c: RelayContext, p: PreparedSwap, peerId: string): Promise<SwapOutcome>;
@@ -1,68 +1,65 @@
1
1
  /**
2
- * A prepared swap through the relay to its end (pure over the wallet and
3
- * AuthFetch, tested with fakes). 0.34.0 (D15, David Case 2026-11-04): the submit's answer is
4
- * the settled record — the host holds the `/call` request until the relay thread rests (the
5
- * validator's answer; a dial failure settles `failed` at once) — so nothing polls:
2
+ * A prepared swap through the host's relay to its end (pure over the wallet and fetch, tested with
3
+ * fakes). 0.44.0 (David Case 2026-11-06): the host is the relay — one plain POST per leg to
4
+ * `<origin>/@<tokenId>/amm/swap`, answered with the validator's reply (src/market/relay.ts); no
5
+ * record, no status, no expiry:
6
6
  *
7
- * submit (amm.swap.submit) →
8
- * accepted: completeSwap (internalizeAction; 0.29.0: no relinquishOutput)
9
- * refused / timeout / an error answer to submit (nothing recorded):
7
+ * POST /swap →
8
+ * {ok: true, tx, txid}: completeSwap (internalizeAction; 0.29.0: no relinquishOutput) → accepted
9
+ * {ok: false, reason, pool?} (the validator's refusal, or the host's commission rule): refused —
10
10
  * nothing to undo: the leg's `amm-swap` outputs stay there
11
- * no answer (a network failure, the host's 503 past its bound, a `pending` record, the
12
- * relay's transport `failed`): "no answer yet", left alone — the relay may still carry it
13
- * (0.40.0: the swap's loop resolves it from the overlay, sends it once more, or races it;
14
- * src/client.ts `executeSwap`).
11
+ * {ok: false, reason: "unreachable"} (any break in the host's call), a network failure, or no
12
+ * answer within LEG_PATIENCE_MS (the request is closed: the host stops): unknown, "no answer
13
+ * yet" — the swap may stand (0.40.0: the swap's loop resolves it from the overlay, asks once
14
+ * more, or races it; src/client.ts `executeSwap`)
15
+ * an answer that is not 200 (a bad request, an unknown token): failed — the host relayed nothing
15
16
  *
16
- * The swap's id is the relay's (amm-p2p relay.zig `idOf`): sha256 of the swap as submitted (the
17
- * validator's slot empty), hex — known to the page, so a lost answer can be checked again.
17
+ * The page's patience is the only timer: the AbortController's signal closes the request, and with
18
+ * it the host's attempt (a plain fetch honours `signal`; AuthFetch does not, which is why the leg is
19
+ * a plain fetch). The swap's id is sha256 of the swap as sent (the validator's slot empty), hex: the
20
+ * page's own name for a leg.
18
21
  */
19
22
  import { Hash, Utils } from "@bsv/sdk";
20
- import { RelayError, submitSwap } from "./relay.js";
23
+ import { RelayError, postSwap } from "./relay.js";
21
24
  import { completeSwap } from "./swapAction.js";
25
+ /** How long the page waits for one leg's answer before it closes the request (the host then stops), ms. */
26
+ export const LEG_PATIENCE_MS = 15_000;
22
27
  const errText = (e) => (e instanceof Error ? e.message : String(e));
23
- /** The relay's id of a prepared swap: sha256 of the swap's bytes as submitted, hex. */
28
+ /** A prepared swap's id: sha256 of the swap's bytes as sent, hex. */
24
29
  export function swapIdOf(p) {
25
30
  return Utils.toHex(Hash.sha256(p.swap.toBinary()));
26
31
  }
27
- /** "no answer yet": what a held request that came back without the settled record means. */
32
+ /** "no answer yet": what a leg that came back without the validator's reply means. */
28
33
  export const NO_ANSWER_YET = "no answer yet";
29
- /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the relay dials it). */
34
+ /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the host dials it). */
30
35
  export async function relaySwap(c, p, peerId) {
31
36
  const id = swapIdOf(p);
32
- let record;
37
+ const patience = c.patienceMs ?? LEG_PATIENCE_MS;
38
+ const ac = new AbortController();
39
+ const timer = setTimeout(() => ac.abort(new Error(`no answer within ${patience} ms`)), patience);
40
+ let reply;
33
41
  try {
34
- record = await submitSwap(c.authFetch, c.base, {
42
+ reply = await postSwap(c.fetch ?? globalThis.fetch, c.base, {
35
43
  funding: p.funding.atomicBeef,
36
44
  swap: p.swap.toBinary(),
37
45
  pool: p.pool,
38
46
  validator: p.validator,
39
47
  peerId,
40
- expires: p.expires,
41
- });
48
+ }, ac.signal);
42
49
  }
43
50
  catch (err) {
44
- // The host's bound passed with the relay still out: no answer yet (the swap stands).
45
- if (err instanceof RelayError && err.httpStatus === 503)
46
- return { status: "unknown", id, reason: NO_ANSWER_YET };
47
- // The relay answered with an error: it recorded nothing and sent nothing on.
51
+ // An answer that is not 200: the host relayed nothing.
48
52
  if (err instanceof RelayError)
49
- return { status: "failed", reason: `amm.swap.submit: ${err.message}` };
50
- return { status: "unknown", id, reason: `${NO_ANSWER_YET}: amm.swap.submit: ${errText(err)}` };
53
+ return { status: "failed", reason: `swap: ${err.message}` };
54
+ // A network failure, or the page's patience ran out (the request closed: the host stopped).
55
+ return { status: "unknown", id, reason: `${NO_ANSWER_YET}: ${ac.signal.aborted ? errText(ac.signal.reason) : errText(err)}` };
51
56
  }
52
- return act(c, p, record);
53
- }
54
- /** A record's outcome, acted on (accepted: the wallet takes the payout in). */
55
- async function act(c, p, r) {
56
- switch (r.status) {
57
- case "pending":
58
- return { status: "unknown", id: r.id, reason: NO_ANSWER_YET };
59
- case "accepted":
60
- return { status: "accepted", id: r.id, txid: r.txid, completed: await completeSwap(c.wallet, p, r.tx, r.txid) };
61
- case "refused":
62
- return r.pool ? { status: "refused", id: r.id, reason: r.reason, pool: r.pool } : { status: "refused", id: r.id, reason: r.reason };
63
- case "timeout":
64
- return { status: "timeout", id: r.id };
65
- case "failed":
66
- return { status: "unknown", id: r.id, reason: `the relay could not reach the validator (${r.reason})` };
57
+ finally {
58
+ clearTimeout(timer);
67
59
  }
60
+ if (reply.ok)
61
+ return { status: "accepted", id, txid: reply.txid, completed: await completeSwap(c.wallet, p, reply.tx, reply.txid) };
62
+ if ("unreachable" in reply)
63
+ return { status: "unknown", id, reason: `${NO_ANSWER_YET}: the host could not reach the validator (${reply.reason})` };
64
+ return reply.pool ? { status: "refused", id, reason: reply.reason, pool: reply.pool } : { status: "refused", id, reason: reply.reason };
68
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcasegr/amm-client",
3
- "version": "0.42.2",
3
+ "version": "0.44.1",
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.42.2",
33
+ "@dcasegr/amm-engine": "0.44.1",
34
34
  "cbor2": "^2.3.0"
35
35
  },
36
36
  "peerDependencies": {