@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 +2 -2
- package/dist/client.d.ts +6 -3
- package/dist/client.js +36 -15
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/overlay.d.ts +0 -2
- package/dist/lib/overlay.js +1 -10
- package/dist/market/relay.d.ts +31 -27
- package/dist/market/relay.js +72 -82
- package/dist/market/swapAction.d.ts +1 -9
- package/dist/market/swapAction.js +1 -7
- package/dist/market/swapFlow.d.ts +27 -22
- package/dist/market/swapFlow.js +39 -42
- package/package.json +2 -2
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
|
|
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`), `
|
|
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
|
|
24
|
-
* the host
|
|
25
|
-
*
|
|
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
|
|
24
|
-
* the host
|
|
25
|
-
*
|
|
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
|
|
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,
|
|
762
|
-
if (outcome.status === "unknown"
|
|
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,
|
|
775
|
-
if (outcome.status === "unknown"
|
|
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
|
|
822
|
-
function relayLeg(wallet,
|
|
823
|
-
return relaySwap({ wallet,
|
|
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
|
-
* `{
|
|
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
|
-
|
|
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
|
|
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
|
|
55
|
-
export {
|
|
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
|
|
57
|
-
export {
|
|
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. */
|
package/dist/lib/overlay.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/overlay.js
CHANGED
|
@@ -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 |
|
|
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;
|
package/dist/market/relay.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
status: "refused";
|
|
35
|
+
ok: false;
|
|
43
36
|
reason: string;
|
|
44
37
|
pool?: PoolState;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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>;
|
package/dist/market/relay.js
CHANGED
|
@@ -1,61 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 (
|
|
26
|
-
*
|
|
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)
|
|
31
|
-
* peerId text: the validator's libp2p peer ID, from the pools route's row
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (0.40.0: the swap's loop resolves it from the overlay,
|
|
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
|
|
17
|
-
*
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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>;
|
package/dist/market/swapFlow.js
CHANGED
|
@@ -1,68 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A prepared swap through the relay to its end (pure over the wallet and
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (0.40.0: the swap's loop resolves it from the overlay,
|
|
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
|
|
17
|
-
*
|
|
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,
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
});
|
|
48
|
+
}, ac.signal);
|
|
42
49
|
}
|
|
43
50
|
catch (err) {
|
|
44
|
-
//
|
|
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: `
|
|
50
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
33
|
+
"@dcasegr/amm-engine": "0.44.1",
|
|
34
34
|
"cbor2": "^2.3.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|