@dcasegr/amm-client 0.39.0 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -30
- package/dist/client.d.ts +157 -61
- package/dist/client.js +339 -320
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +6 -3
- package/dist/index.d.ts +16 -4
- package/dist/index.js +16 -4
- package/dist/lib/overlay.d.ts +132 -50
- package/dist/lib/overlay.js +169 -160
- package/dist/lib/skein.d.ts +2 -0
- package/dist/lib/skein.js +1 -1
- package/dist/market/activeSwap.d.ts +77 -96
- package/dist/market/activeSwap.js +146 -166
- package/dist/market/plan.d.ts +2 -1
- package/dist/market/plan.js +1 -1
- package/dist/market/swapAction.d.ts +150 -145
- package/dist/market/swapAction.js +292 -213
- package/dist/market/swapFlow.d.ts +3 -6
- package/dist/market/swapFlow.js +4 -22
- package/dist/pool/pool.artifact.json +760 -528
- package/dist/pool/template.d.ts +10 -2
- package/dist/pool/template.js +16 -6
- package/dist/wallet/color.d.ts +13 -9
- package/dist/wallet/color.js +13 -10
- package/dist/wallet/swapPayouts.d.ts +45 -28
- package/dist/wallet/swapPayouts.js +129 -117
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -15,22 +15,23 @@
|
|
|
15
15
|
* (`hostedTokens`): each token's price, liquidity (TVL) and pool count; no host-wide stream.
|
|
16
16
|
* - **History** (0.39.0, D1–D6): `trades`, `trade`, `candles`, `stats`, `statsAll`, `holders`,
|
|
17
17
|
* `positionsAll` — plain GETs of the history routes (H1–H6, src/lib/overlay.ts).
|
|
18
|
-
* - **Swap** (D11–D16): the token's pools route `GET /@<token>/amm/pools
|
|
19
|
-
* (`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* /@<token>/amm/pools/beef?
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
18
|
+
* - **Swap** (D11–D16; 0.40.0; 0.41.0): the token's pools route `GET /@<token>/amm/pools`, a plain
|
|
19
|
+
* read (`tokenPools`): the pools whose validator is beating and whose commission meets the
|
|
20
|
+
* market's minimum (the host's filter), each validator's minimum leg, the market's commission key
|
|
21
|
+
* hash; a quote is the matching engine's plan over that list (no request); a swap (`executeSwap`)
|
|
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
|
|
26
|
+
* display: a page reads the route again on one.
|
|
27
|
+
* - **Liquidity** (D9, D23; 0.40.0: by pool id): the wallet's pool rows and deploy history, then
|
|
28
|
+
* ONE `GET /@<token>/amm/positions?pool=` per token; a deploy the history has but the basket
|
|
29
|
+
* lacks is filed then; Close and Rebalance read their pool's BEEF by its id (`/pools/beef?pool=`);
|
|
30
|
+
* the deploy's terms `GET /@<token>/amm/terms`.
|
|
30
31
|
* - **Your tokens** (D17, D18, D19, D24): the wallet's module and the swap basket; the token list
|
|
31
|
-
* once and ONE discovery `GET /@discovery/1color/tokens?ids=` for held tokens the list lacks;
|
|
32
|
-
*
|
|
33
|
-
* at the host root.
|
|
32
|
+
* once and ONE discovery `GET /@discovery/1color/tokens?ids=` for held tokens the list lacks; the
|
|
33
|
+
* pass over the swap basket (0.40.0: ONE `GET /amm/spends` over every row,
|
|
34
|
+
* src/wallet/swapPayouts.ts); Move back submits at the host root.
|
|
34
35
|
* - **Settings** (D20, D21): the claim card (`claimStatus`: swap's identity, the recipient of
|
|
35
36
|
* root's messages), the token list once, discovery's list and ONE `?ids=` read; Enable's deploy
|
|
36
37
|
* BEEF from the wallet's basket else `GET /@discovery/1color/<token>/deploy`.
|
|
@@ -42,18 +43,17 @@
|
|
|
42
43
|
*/
|
|
43
44
|
import { Beef, Transaction } from "@bsv/sdk";
|
|
44
45
|
import { send1Color } from "@1sat/actions";
|
|
45
|
-
import {
|
|
46
|
+
import { nextLeg, priceOf } from "@dcasegr/amm-engine";
|
|
46
47
|
import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
|
|
47
48
|
import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
|
|
48
|
-
import { ammLookupService,
|
|
49
|
+
import { ammLookupService, liveOfPools, parseTokenTopic, poolOutputOf, readDiscoveryDeploy, readDiscoveryIds, readDiscoveryList, readAllPositions, readAllStats, readCandles, readHolders, readPoolHistory, readPoolsAt, readPoolsBeef, readPools, readPositions, readSpendsAll, readStats, readTerms, readTrade, readTrades, readTx, eventsUrl, watchPoolEvents as watchFeed, } from "./lib/overlay.js";
|
|
49
50
|
import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
|
|
50
51
|
import { depthOf } from "./market/prices.js";
|
|
51
|
-
import { buildPlanRequest,
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import { settleStagedSwaps } from "./wallet/swapPayouts.js";
|
|
52
|
+
import { buildPlanRequest, legMinerFeeSats, quote as planQuote } from "./market/plan.js";
|
|
53
|
+
import { prepareSwap, selectLegInputs } from "./market/swapAction.js";
|
|
54
|
+
import { fileStagedToken, moveBack, moveBackSats, stage, swapHoldingBeef, swapHoldings, swapSatsHoldings, swapSatsTotal, swapTotals, unfiledStagedTokens } from "./market/activeSwap.js";
|
|
55
|
+
import { relaySwap, swapIdOf } from "./market/swapFlow.js";
|
|
56
|
+
import { resolveFromOverlay } from "./wallet/swapPayouts.js";
|
|
57
57
|
import { formatAmount } from "./lp/amounts.js";
|
|
58
58
|
import { basketRows } from "./lp/wallet.js";
|
|
59
59
|
import { buildInventory, decodeTokenRow, deployIcon } from "./lp/inventory.js";
|
|
@@ -63,7 +63,7 @@ import { rebalancePool } from "./lp/rebalance.js";
|
|
|
63
63
|
import { historyPoolRows, mergePoolRows, positionRowsOf } from "./lp/positions.js";
|
|
64
64
|
import { POOLS_BASKET, assetBasket, heldOutputs } from "./wallet/color.js";
|
|
65
65
|
import { prepareClose, submitToOverlay } from "./lp/close.js";
|
|
66
|
-
import { ClientError,
|
|
66
|
+
import { ClientError, toClientError } from "./errors.js";
|
|
67
67
|
/**
|
|
68
68
|
* `amm.pool.terms`'s refusal when no terms are set (0.15.0, David Case
|
|
69
69
|
* 2026-10-23: terms are a runtime record root sets; unset, a market only).
|
|
@@ -75,7 +75,7 @@ export const NO_TERMS_TEXT = "This exchange validates nothing: no terms are set.
|
|
|
75
75
|
export function hasTerms(t) {
|
|
76
76
|
return !!t && !("none" in t);
|
|
77
77
|
}
|
|
78
|
-
export {
|
|
78
|
+
export { parsePoolEventMessage } from "./lib/overlay.js";
|
|
79
79
|
/** The metadata of the deploy output `vout` of an Atomic BEEF's subject; null when that output is no 1Color deploy. */
|
|
80
80
|
function deployMetaOf(beef, vout) {
|
|
81
81
|
const tx = Transaction.fromAtomicBEEF(beef);
|
|
@@ -123,6 +123,7 @@ export function hostedFrom(list, stats) {
|
|
|
123
123
|
export function poolViews(answer, tokenId) {
|
|
124
124
|
return (answer?.pools ?? []).map((p) => ({
|
|
125
125
|
outpoint: outpointText(p.outpoint),
|
|
126
|
+
poolId: p.poolId,
|
|
126
127
|
tokenId,
|
|
127
128
|
sats: p.bsvReserve,
|
|
128
129
|
tokens: p.tokenReserve,
|
|
@@ -133,13 +134,29 @@ export function poolViews(answer, tokenId) {
|
|
|
133
134
|
export function poolFees(p) {
|
|
134
135
|
return { lpBps: Number(p.liquidityFeeBps), validatorBps: Number(p.validationFeeBps), commissionBps: Number(p.commissionBps ?? 0n) };
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* A pools answer's rows as the planner takes them (0.40.0): each pool with its validator's
|
|
139
|
+
* minimum leg (`validators[].minLegSats`, the validator's announced terms).
|
|
140
|
+
*/
|
|
141
|
+
export function poolsWithMinimums(answer) {
|
|
142
|
+
const min = new Map(answer.validators.map((v) => [v.identity, v.minLegSats]));
|
|
143
|
+
return answer.pools.map((p) => {
|
|
144
|
+
const m = min.get(p.validatorIdentityKey.toLowerCase());
|
|
145
|
+
return m ? { ...p, minLegSats: m } : p;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Sats per whole token of an exact pair (`sats` per `tokens` base units), 0 when `tokens` is 0. */
|
|
149
|
+
export function pairPrice(p, dec) {
|
|
150
|
+
return p.tokens > 0n ? Number(p.sats) / (Number(p.tokens) / 10 ** dec) : 0;
|
|
139
151
|
}
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
/**
|
|
153
|
+
* A leg's sats beyond its amount in (0.40.0, the stage's fee sats): the miner fee estimate plus its
|
|
154
|
+
* 1-sat carriers — on a buy the token payout; on a sell the token fee and commission outputs that
|
|
155
|
+
* are not zero, and the token change.
|
|
156
|
+
*/
|
|
157
|
+
export function legCarrySats(leg, buy, minerFeeSats) {
|
|
158
|
+
const carriers = buy ? 1n : BigInt([leg.liquidityFee, leg.validationFee, leg.commission].filter((x) => x > 0n).length + 1);
|
|
159
|
+
return minerFeeSats + carriers;
|
|
143
160
|
}
|
|
144
161
|
/**
|
|
145
162
|
* wallet-toolbox's `specOpWalletBalance` (`sdk.specOpWalletBalance`, @bsv/wallet-toolbox-client): the listOutputs "basket" of its
|
|
@@ -224,7 +241,17 @@ export function enableCandidates(listed, walletIds, onList) {
|
|
|
224
241
|
}
|
|
225
242
|
return out;
|
|
226
243
|
}
|
|
227
|
-
|
|
244
|
+
/** The terms as root's messages carry them (0.40.0: `minLegSats` only when above 0). Refused unless each fee is an integer 0 to 10 000 and the minimum a whole number of sats. */
|
|
245
|
+
function termsArgs(t) {
|
|
246
|
+
for (const [k, v] of Object.entries({ lpBps: t.lpBps, validatorBps: t.validatorBps, commissionBps: t.commissionBps }))
|
|
247
|
+
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
248
|
+
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
249
|
+
const min = t.minLegSats ?? 0;
|
|
250
|
+
if (!Number.isSafeInteger(min) || min < 0)
|
|
251
|
+
throw new Error("minLegSats: a whole number of sats, 0 for none");
|
|
252
|
+
return { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps, ...(min > 0 ? { minLegSats: min } : {}) };
|
|
253
|
+
}
|
|
254
|
+
const newShared = () => ({ listRead: null, claimRead: null, planned: new WeakMap(), swapping: false, loaded: new Map(), listeners: new Set() });
|
|
228
255
|
/**
|
|
229
256
|
* The client of one exchange (D9): its reads over `origin` (src/lib/overlay.ts, src/lib/skein.ts),
|
|
230
257
|
* the swap flow, the pool flows (deploy, close, rebalance, move back, positions) and the wallet
|
|
@@ -244,7 +271,6 @@ function makeClient(opts, s) {
|
|
|
244
271
|
const boundWallet = opts.wallet ?? null;
|
|
245
272
|
const signedFor = boundWallet ? (opts.signedFor ?? authFetchPerInstance(boundWallet, f)) : null;
|
|
246
273
|
const planned = s.planned;
|
|
247
|
-
const checks = s.checks;
|
|
248
274
|
const loaded = s.loaded;
|
|
249
275
|
function needWallet() {
|
|
250
276
|
if (!boundWallet || !signedFor)
|
|
@@ -470,16 +496,20 @@ function makeClient(opts, s) {
|
|
|
470
496
|
const [list, stats] = await Promise.all([enabledList(), statsAll().catch(() => null)]);
|
|
471
497
|
return hostedFrom(list, stats);
|
|
472
498
|
}
|
|
499
|
+
/** P2 (0.41.0): a token's pools as the host lists them now, read past the browser's cache (`freshFetch`). */
|
|
500
|
+
async function tokenPools(tokenId) {
|
|
501
|
+
return readPools(tokenBase(tokenId, swapBase), freshFetch);
|
|
502
|
+
}
|
|
473
503
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
504
|
+
* The host's pool event feed (E1, 0.41.0): one token's (`GET /@<token>/amm/events`) or, with
|
|
505
|
+
* `"all"`, every enabled token's (`GET <origin>/amm/events`); each message `{tokenId, …row}` to
|
|
506
|
+
* `onEvent`. Signals for display: the pools route is the truth. → the unsubscribe.
|
|
477
507
|
*/
|
|
478
|
-
function
|
|
479
|
-
return
|
|
508
|
+
function watchPoolEvents(tokenId, onEvent, onError) {
|
|
509
|
+
return watchFeed(eventsUrl(tokenId === "all" ? swapBase : tokenBase(tokenId, swapBase)), onEvent, onError ? { onError } : {});
|
|
480
510
|
}
|
|
481
511
|
// ---------------------------------------------------------------- history (0.39.0, H1–H6)
|
|
482
|
-
/** H1: a token's trades, newest first (`from ≤ at < to` ms, `limit` 1–500, `before` the cursor `next`, `
|
|
512
|
+
/** H1: a token's trades, newest first (`from ≤ at < to` ms, `limit` 1–500, `before` the cursor `next`, 0.40.0 `pool` one pool's by id). */
|
|
483
513
|
async function trades(tokenId, o = {}) {
|
|
484
514
|
return readTrades(tokenBase(tokenId, swapBase), o, ff);
|
|
485
515
|
}
|
|
@@ -487,107 +517,128 @@ function makeClient(opts, s) {
|
|
|
487
517
|
async function trade(tokenId, txid) {
|
|
488
518
|
return readTrade(tokenBase(tokenId, swapBase), txid, ff);
|
|
489
519
|
}
|
|
490
|
-
/** H2: a token's candles, one per non-empty bucket of `tf` over `from ≤ at < to` (ms). */
|
|
520
|
+
/** H2: a token's candles, one per non-empty bucket of `tf` over `from ≤ at < to` (ms); 0.40.0 `series`: `fill` (the trades', default) or `market` (the price after each pool event, the host's). */
|
|
491
521
|
async function candles(tokenId, o) {
|
|
492
522
|
return readCandles(tokenBase(tokenId, swapBase), o, ff);
|
|
493
523
|
}
|
|
524
|
+
/** H7 (0.40.0): every pool of the token listed at `t` (ms). */
|
|
525
|
+
async function poolsAt(tokenId, t) {
|
|
526
|
+
return readPoolsAt(tokenBase(tokenId, swapBase), t, ff);
|
|
527
|
+
}
|
|
528
|
+
/** H8 (0.40.0): the token's pool events in `from ≤ at < to`, oldest first (`pool` one pool's; forward paging: the page's `next` as `after`). */
|
|
529
|
+
async function poolHistory(tokenId, o = {}) {
|
|
530
|
+
return readPoolHistory(tokenBase(tokenId, swapBase), o, ff);
|
|
531
|
+
}
|
|
532
|
+
/** H9 (0.40.0): each outpoint's spender as the host's `/amm/spends` answers it (every enabled token's `/spends`). */
|
|
533
|
+
async function spendsAll(outpoints) {
|
|
534
|
+
return readSpendsAll(swapBase, outpoints, ff);
|
|
535
|
+
}
|
|
494
536
|
/** H3: a token's stats. */
|
|
495
537
|
async function stats(tokenId) {
|
|
496
538
|
return readStats(tokenBase(tokenId, swapBase), ff);
|
|
497
539
|
}
|
|
498
540
|
/** H4: every enabled token's stats (`ids`: those only, in that order). */
|
|
499
541
|
async function statsAll(o = {}) {
|
|
500
|
-
return readAllStats(swapBase, o.ids, ff);
|
|
542
|
+
return readAllStats(swapBase, o.ids, o.fresh ? freshFetch : ff);
|
|
501
543
|
}
|
|
502
544
|
/** H6: a token's holders, largest first (`limit` 1–100, `skip`). */
|
|
503
545
|
async function holders(tokenId, o = {}) {
|
|
504
546
|
return readHolders(tokenBase(tokenId, swapBase), tokenId, o, ff);
|
|
505
547
|
}
|
|
506
|
-
/** H5: the
|
|
507
|
-
async function positionsAll(
|
|
508
|
-
return readAllPositions(swapBase,
|
|
548
|
+
/** H5: the pools' positions on every enabled token (0.40.0: by pool id). */
|
|
549
|
+
async function positionsAll(poolIds) {
|
|
550
|
+
return readAllPositions(swapBase, poolIds, ff);
|
|
509
551
|
}
|
|
510
552
|
/**
|
|
511
553
|
* A quote for one token (D12): the matching engine's plan over the pools the Swap page holds
|
|
512
554
|
* (`held`: the token's pools route's answer — the host listed only pools whose validator is beating
|
|
513
|
-
* and whose commission meets the market's minimum)
|
|
514
|
-
* decimals.
|
|
555
|
+
* and whose commission meets the market's minimum), each pool with its validator's minimum leg
|
|
556
|
+
* (0.40.0); no request. `meta`: the token's symbol and decimals. 0.40.0: the price and impact from
|
|
557
|
+
* the engine's `priceOf` over the same pools (the buy side for a buy, the sell side for a sell);
|
|
558
|
+
* each leg's own fees; what Swap will stage (the swap amount exact, 0.40.2: the fee sats of
|
|
559
|
+
* `maxSources` legs) and the per-leg miner fee estimate.
|
|
515
560
|
*/
|
|
516
561
|
function quoteSwap(req, held, meta) {
|
|
517
562
|
if (req.amountIn <= 0n)
|
|
518
563
|
throw new Error("Enter an amount");
|
|
519
|
-
|
|
520
|
-
if (pools.length === 0)
|
|
564
|
+
if (!held || held.pools.length === 0)
|
|
521
565
|
throw new Error("No pool to swap against");
|
|
566
|
+
const pools = poolsWithMinimums(held);
|
|
522
567
|
const live = liveOfPools(pools);
|
|
523
568
|
const dec = meta.dec;
|
|
524
569
|
const buy = req.side === "buy";
|
|
525
|
-
const r = buildPlanRequest(req.tokenId, { direction: buy ? "bsvToToken" : "tokenToBsv", amount: formatAmount(req.amountIn, buy ? 0 : dec), slippageBps: String(req.maxSlippageBps),
|
|
570
|
+
const r = buildPlanRequest(req.tokenId, { direction: buy ? "bsvToToken" : "tokenToBsv", amount: formatAmount(req.amountIn, buy ? 0 : dec), slippageBps: String(req.maxSlippageBps), maxSources: req.maxSources }, pools, live, dec, feeRate);
|
|
526
571
|
if (!r.ok)
|
|
527
572
|
throw new Error(r.error);
|
|
528
573
|
const { plan, view } = planQuote(r.request, live, dec);
|
|
574
|
+
if (plan.unfillable === "min")
|
|
575
|
+
throw new ClientError("below_minimum", `No pool here takes a swap this small: at least ${formatAmount(plan.minAmountIn ?? 0n, buy ? 0 : dec)} ${buy ? "sats" : meta.sym}.`);
|
|
529
576
|
const used = view.legs.filter((l) => l.amountIn > 0n);
|
|
530
577
|
if (used.length === 0 || view.totalOut === 0n)
|
|
531
578
|
throw new Error("The amount is too small to receive anything");
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const marginal = sumTokens > 0n ? Number(sumSats) / (Number(sumTokens) / 10 ** dec) : 0;
|
|
579
|
+
const price = priceOf(pools);
|
|
580
|
+
const marginal = price ? pairPrice(buy ? price.buy : price.sell, dec) : 0;
|
|
535
581
|
const totalIn = used.reduce((a, l) => a + l.amountIn, 0n);
|
|
536
582
|
const sats = buy ? totalIn : view.totalOut;
|
|
537
583
|
const tokens = buy ? view.totalOut : totalIn;
|
|
538
584
|
const effective = Number(sats) / (Number(tokens) / 10 ** dec);
|
|
539
|
-
const
|
|
585
|
+
const minerFee = legMinerFeeSats(feeRate);
|
|
586
|
+
const carry = plan.legs.map((l) => legCarrySats(l, buy, minerFee));
|
|
587
|
+
// 0.40.2: the fee sats for EXACTLY maxSources legs — the planned legs' own, each further leg at the largest of theirs.
|
|
588
|
+
const more = BigInt(Math.max(0, req.maxSources - carry.length));
|
|
589
|
+
const feeSats = carry.reduce((a, x) => a + x, 0n) + more * carry.reduce((a, x) => (x > a ? x : a), 0n);
|
|
540
590
|
const q = {
|
|
541
591
|
request: req,
|
|
542
592
|
legs: used.map((l) => ({
|
|
543
593
|
poolOutpoint: outpointText(l.outpoint),
|
|
594
|
+
poolId: l.pool.poolId,
|
|
544
595
|
amountIn: l.amountIn,
|
|
545
596
|
amountOut: l.amountOut,
|
|
546
597
|
shareBps: Number((l.amountIn * 10000n) / req.amountIn),
|
|
547
598
|
reserves: { sats: l.pool.bsvReserve, tokens: l.pool.tokenReserve },
|
|
548
599
|
validator: { identityKey: l.pool.validatorIdentityKey, live: l.validator.live },
|
|
600
|
+
fees: poolFees(l.pool),
|
|
549
601
|
})),
|
|
550
602
|
amountOut: view.totalOut,
|
|
551
603
|
minAmountOut: view.minAmountOut,
|
|
552
604
|
effectivePrice: effective,
|
|
553
605
|
marginalPrice: marginal,
|
|
554
606
|
impactBps: marginal ? Math.round((Math.abs(effective - marginal) / marginal) * 10_000) : 0,
|
|
555
|
-
|
|
607
|
+
stage: { amount: req.amountIn, feeSats },
|
|
608
|
+
legMinerFeeSats: minerFee,
|
|
556
609
|
};
|
|
557
|
-
planned.set(q, { plan,
|
|
610
|
+
planned.set(q, { plan, answer: held, dec, sym: meta.sym });
|
|
558
611
|
return q;
|
|
559
612
|
}
|
|
560
|
-
/**
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
if (o.status === "accepted") {
|
|
564
|
-
result.status = "filled";
|
|
565
|
-
result.txid = o.txid;
|
|
566
|
-
delete result.error;
|
|
567
|
-
delete result.kind;
|
|
568
|
-
checks.delete(o.id);
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
result.status = "failed";
|
|
572
|
-
const kind = kindOfOutcome(o);
|
|
573
|
-
if (kind)
|
|
574
|
-
result.kind = kind;
|
|
575
|
-
const rest = o.status === "unknown" ? o.reason.replace(/^no answer yet:? ?/, "") : "";
|
|
576
|
-
result.error = o.status === "refused" ? `refused: ${o.reason}` : o.status === "unknown" ? `no answer yet${rest ? `: ${rest}` : ""}` : o.status;
|
|
577
|
-
if (o.status === "unknown") {
|
|
578
|
-
result.checkId = o.id;
|
|
579
|
-
checks.set(o.id, { prepared, base, af });
|
|
580
|
-
}
|
|
581
|
-
else if ("id" in o)
|
|
582
|
-
checks.delete(o.id);
|
|
613
|
+
/** The overlay pass over the swap basket (item 5, src/wallet/swapPayouts.ts): ONE `/amm/spends`, each spender's `/tx`, this wallet's legs taken in. */
|
|
614
|
+
function resolve(wallet) {
|
|
615
|
+
return resolveFromOverlay(wallet, { spendsAll: (ops) => readSpendsAll(swapBase, ops, freshFetch), tx: (tokenId, txid) => readTx(tokenBase(tokenId, swapBase), txid, freshFetch) });
|
|
583
616
|
}
|
|
584
617
|
/**
|
|
585
|
-
* Runs a quoted swap with the connected wallet (
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
618
|
+
* Runs a quoted swap with the connected wallet (0.40.0, agent-env plans/swap-balance-build-2026-11-05.md
|
|
619
|
+
* §0 7, Builder 2 item 4): ONE swap at a time, legs ONE AT A TIME.
|
|
620
|
+
*
|
|
621
|
+
* 1. The overlay pass (`resolve`); then what `amm-swap` lacks of what the plan needs — the swap
|
|
622
|
+
* amount exact, the sats for the carriers and miner fees of `maxSources` legs —
|
|
623
|
+
* staged by ONE `stage` (`staging`).
|
|
624
|
+
* 2. The loop: cancelled → `cancelled`; the SERVER's pool list read fresh (`GET
|
|
625
|
+
* /@<token>/amm/pools`, no cache; 0.41.0: before EACH leg, no list of its own kept);
|
|
626
|
+
* `nextLeg` over it minus the excluded outpoints, `minOut` the quote's floor less what has
|
|
627
|
+
* filled, `maxSources` (0.40.2) the request's less the legs filled → its stop ends the swap;
|
|
628
|
+
* the pool's current output read by its id (`GET /pools/beef?pool=`; moved → the list read
|
|
629
|
+
* again); the staged rows that cover the leg, else `fees`; built, signed, relayed
|
|
630
|
+
* (`relaying`):
|
|
631
|
+
* - accepted → taken in (ONE internalizeAction) before the next leg;
|
|
632
|
+
* - refused `pool_spent` (`repricing`) → the list read again: the relaying host took the
|
|
633
|
+
* validator's state in before it answered (0.41.0);
|
|
634
|
+
* - refused otherwise (`below_minimum` included) → that outpoint excluded;
|
|
635
|
+
* - no answer → (a) the overlay: a staged input spent → the leg landed (taken in by the
|
|
636
|
+
* pass), the pool spent by another's swap → continue with the same rows; (b) else the
|
|
637
|
+
* same leg sent ONCE more to the same validator; (c) still none → that outpoint excluded
|
|
638
|
+
* (the next leg races it with the same rows);
|
|
639
|
+
* - after any refusal: the overlay pass.
|
|
640
|
+
* 3. Filled completely: the last leg's change went to the wallet, nothing left. Stopped early with
|
|
641
|
+
* rows left: `moveBack` (one wallet action, `returning`); declined or failed → the rows stay.
|
|
591
642
|
*/
|
|
592
643
|
async function executeSwap(quote, o = {}) {
|
|
593
644
|
const progress = (x) => {
|
|
@@ -598,26 +649,26 @@ function makeClient(opts, s) {
|
|
|
598
649
|
/* the caller's callback: not the swap's failure */
|
|
599
650
|
}
|
|
600
651
|
};
|
|
652
|
+
if (s.swapping)
|
|
653
|
+
throw new Error("A swap is already running: one at a time");
|
|
654
|
+
s.swapping = true;
|
|
601
655
|
let result;
|
|
602
656
|
try {
|
|
603
|
-
result = await runSwap(quote, progress);
|
|
657
|
+
result = await runSwap(quote, progress, o.signal);
|
|
604
658
|
}
|
|
605
659
|
catch (e) {
|
|
606
660
|
const err = toClientError(e);
|
|
607
661
|
progress({ phase: "failed", error: err instanceof Error ? err : new Error(String(err)) });
|
|
608
662
|
throw err;
|
|
609
663
|
}
|
|
664
|
+
finally {
|
|
665
|
+
s.swapping = false;
|
|
666
|
+
}
|
|
667
|
+
emit();
|
|
610
668
|
progress(result.status === "failed" ? { phase: "failed", result } : { phase: "done", result });
|
|
611
669
|
return result;
|
|
612
670
|
}
|
|
613
|
-
|
|
614
|
-
function failLeg(result, e) {
|
|
615
|
-
result.error = errText(e);
|
|
616
|
-
const kind = errorKind(e);
|
|
617
|
-
if (kind)
|
|
618
|
-
result.kind = kind;
|
|
619
|
-
}
|
|
620
|
-
async function runSwap(quote, progress) {
|
|
671
|
+
async function runSwap(quote, progress, signal) {
|
|
621
672
|
const { wallet, signedFor } = needWallet();
|
|
622
673
|
const p = planned.get(quote);
|
|
623
674
|
if (!p)
|
|
@@ -625,212 +676,153 @@ function makeClient(opts, s) {
|
|
|
625
676
|
const tokenId = quote.request.tokenId;
|
|
626
677
|
const base = tokenBase(tokenId, swapBase);
|
|
627
678
|
const af = signedFor(base);
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
679
|
+
const buy = quote.request.side === "buy";
|
|
680
|
+
const direction = buy ? "bsvToToken" : "tokenToBsv";
|
|
681
|
+
const minerFee = legMinerFeeSats(feeRate);
|
|
682
|
+
const meta = { sym: p.sym, dec: p.dec };
|
|
683
|
+
// 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).
|
|
684
|
+
const excluded = new Set();
|
|
685
|
+
/** 0.40.2: the legs the order may still fill (the request's maxSources less the legs filled). */
|
|
686
|
+
let budget = quote.request.maxSources;
|
|
636
687
|
const legs = [];
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
688
|
+
let filledIn = 0n;
|
|
689
|
+
let filledOut = 0n;
|
|
690
|
+
let order;
|
|
691
|
+
/** Legs sent with no answer yet: what they spend, to account a later take-in by the overlay pass. */
|
|
692
|
+
const pending = [];
|
|
693
|
+
const accountResolved = (taken) => {
|
|
694
|
+
for (const t of taken) {
|
|
695
|
+
const i = pending.findIndex((x) => x.prepared.satsInputs.some((r) => t.inputs.includes(r.outpoint)));
|
|
696
|
+
if (i < 0)
|
|
697
|
+
continue;
|
|
698
|
+
const [x] = pending.splice(i, 1);
|
|
699
|
+
order ??= x.prepared.order;
|
|
700
|
+
budget -= 1;
|
|
701
|
+
filledIn += x.leg.amountIn;
|
|
702
|
+
filledOut += x.leg.amountOut;
|
|
703
|
+
legs.push({ poolOutpoint: outpointText(x.leg.outpoint), poolId: x.poolId, amountIn: x.leg.amountIn, amountOut: x.leg.amountOut, txid: t.txid });
|
|
651
704
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
705
|
+
};
|
|
706
|
+
accountResolved(await resolve(wallet));
|
|
707
|
+
// 1. Stage what the basket lacks.
|
|
708
|
+
const needTokens = buy ? 0n : quote.stage.amount;
|
|
709
|
+
const needSats = (buy ? quote.stage.amount : 0n) + quote.stage.feeSats;
|
|
710
|
+
const haveSats = BigInt(await swapSatsTotal(wallet));
|
|
711
|
+
const haveTokens = buy ? 0n : (await swapHoldings(wallet, tokenId)).reduce((a, t) => a + t.amount, 0n);
|
|
712
|
+
const lackSats = needSats > haveSats ? needSats - haveSats : 0n;
|
|
713
|
+
const lackTokens = needTokens > haveTokens ? needTokens - haveTokens : 0n;
|
|
714
|
+
if (lackSats > 0n || lackTokens > 0n) {
|
|
715
|
+
progress({ phase: "staging", sats: lackSats, tokens: lackTokens });
|
|
716
|
+
await stage(wallet, { sats: Number(lackSats), ...(lackTokens > 0n ? { tokens: { tokenId, amount: lackTokens } } : {}) }, { sym: p.sym, submit: submitAt(base) });
|
|
655
717
|
}
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
progress({ phase: "staging" });
|
|
663
|
-
let tokenCandidates = tokensIn && runnable.length > 0 ? await swapHoldings(wallet, tokenId) : [];
|
|
664
|
-
let satsCandidates = runnable.length > 0 ? await swapSatsHoldings(wallet) : [];
|
|
665
|
-
const staged = [];
|
|
666
|
-
for (const r of runnable) {
|
|
667
|
-
try {
|
|
668
|
-
const poolOutput = poolOutputOf(fresh, r.leg.outpoint);
|
|
669
|
-
if (!poolOutput)
|
|
670
|
-
throw new Error("the pools read has no transaction for this pool");
|
|
671
|
-
let tokens = null;
|
|
672
|
-
if (tokensIn) {
|
|
673
|
-
tokens = selectExactTokenInputs(tokenCandidates, r.leg.amountIn);
|
|
674
|
-
if (tokens)
|
|
675
|
-
tokenCandidates = tokenCandidates.filter((c) => !tokens.includes(c));
|
|
676
|
-
}
|
|
677
|
-
// A leg whose tokens are still to be staged gets one output of 1 sat (sendToSwapBasket).
|
|
678
|
-
const planned = await planSwapLeg({ wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg: r.leg, pool: r.pool, poolOutput, commissionPkh, ...(tokensIn ? { tokenInputs: tokens ?? [{ satoshis: 1, amount: r.leg.amountIn }] } : {}), satsPerKb: feeRate });
|
|
679
|
-
const sats = selectExactSatsInput(satsCandidates, planned.funding.satoshis);
|
|
680
|
-
if (sats)
|
|
681
|
-
satsCandidates = satsCandidates.filter((c) => c !== sats);
|
|
682
|
-
staged.push({ r, poolOutput, planned, tokens, sats });
|
|
718
|
+
// 2. The loop.
|
|
719
|
+
let why = "filled";
|
|
720
|
+
for (;;) {
|
|
721
|
+
if (signal?.aborted) {
|
|
722
|
+
why = "cancelled";
|
|
723
|
+
break;
|
|
683
724
|
}
|
|
684
|
-
|
|
685
|
-
|
|
725
|
+
const remaining = quote.request.amountIn - filledIn;
|
|
726
|
+
const answer = await tokenPools(tokenId);
|
|
727
|
+
const pools = poolsWithMinimums(answer).filter((r) => !excluded.has(r.outpoint));
|
|
728
|
+
const minOut = quote.minAmountOut > filledOut ? quote.minAmountOut - filledOut : 0n;
|
|
729
|
+
const next = nextLeg({ pools, direction, remaining, fixedCost: { minerFeeSats: minerFee }, minOut, maxSources: budget });
|
|
730
|
+
if ("stop" in next) {
|
|
731
|
+
why = next.stop;
|
|
732
|
+
break;
|
|
686
733
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
|
|
695
|
-
shortTokens.forEach((x, n) => {
|
|
696
|
-
const h = s.holdings[n];
|
|
697
|
-
if (h)
|
|
698
|
-
x.tokens = [h];
|
|
699
|
-
else
|
|
700
|
-
x.r.result.error = `the swap basket send has no output for this leg${why}`;
|
|
701
|
-
});
|
|
702
|
-
shortSats.forEach((x, n) => {
|
|
703
|
-
const h = s.sats[n];
|
|
704
|
-
if (h)
|
|
705
|
-
x.sats = h;
|
|
706
|
-
else
|
|
707
|
-
x.r.result.error ??= `the swap basket send has no sats output for this leg${why}`;
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
else if (shortSats.length > 0) {
|
|
711
|
-
const s = await sendSatsToSwapBasket(wallet, tokenId, shortSats.map((x) => ({ satoshis: x.planned.funding.satoshis, pool: x.r.leg.outpoint })), { sym: p.sym });
|
|
712
|
-
sent = s.tx;
|
|
713
|
-
const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
|
|
714
|
-
shortSats.forEach((x, n) => {
|
|
715
|
-
const h = s.sats[n];
|
|
716
|
-
if (h)
|
|
717
|
-
x.sats = h;
|
|
718
|
-
else
|
|
719
|
-
x.r.result.error = `the swap basket send has no sats output for this leg${why}`;
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
const holdingBeef = staged.length > 0 ? await swapHoldingBeef(wallet, sent) : undefined;
|
|
723
|
-
// The legs sent to the relay: their expected amountOut, and whether their outcome is known.
|
|
724
|
-
const tracks = [];
|
|
725
|
-
/** Relays the leg (0.34.0: no browser note; the staged rows are the leg's record, D24). */
|
|
726
|
-
const relayLeg = (prepared, peerId) => relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
|
|
727
|
-
/**
|
|
728
|
-
* 0.31.1: one retry of a leg refused `pool_spent` — re-quoted alone against the validator's
|
|
729
|
-
* newest pool state (the engine's single-pool math, same side and amountIn), sent only while
|
|
730
|
-
* the order's bound holds (filled legs + this one + the legs still out ≥ the plan's
|
|
731
|
-
* resolvedMinAmountOut), rebuilt on the new pool output with the same staged rows (0.34.0: its
|
|
732
|
-
* BEEF from `GET /pools/beef?lp=<the pool's LP key>`, constant along its continuations). Null:
|
|
733
|
-
* the leg failed here (its error set); else the second relay's outcome.
|
|
734
|
-
*/
|
|
735
|
-
const retryLeg = async (x) => {
|
|
736
|
-
const { track, pool } = x;
|
|
737
|
-
const result = track.result;
|
|
738
|
-
const priced = computeSwap(pool, direction, x.leg.amountIn);
|
|
739
|
-
const amountOut = priced?.amountOut ?? 0n;
|
|
740
|
-
const others = tracks.filter((t) => t !== track).reduce((a, t) => a + (t.result.status === "filled" || !t.done ? t.amountOut : 0n), 0n);
|
|
741
|
-
if (!priced || others + amountOut < p.plan.resolvedMinAmountOut) {
|
|
742
|
-
result.error = `repriced below your minimum: ${formatAmount(amountOut, direction === "bsvToToken" ? p.dec : 0)} for this leg`;
|
|
743
|
-
result.kind = "slippage";
|
|
744
|
-
return null;
|
|
734
|
+
const leg = next.leg;
|
|
735
|
+
const row = pools.find((x) => x.outpoint === leg.outpoint);
|
|
736
|
+
const fresh = await readPoolsBeef(base, [row.poolId], freshFetch);
|
|
737
|
+
const now = fresh.pools.find((x) => x.poolId === row.poolId);
|
|
738
|
+
if (!now) {
|
|
739
|
+
excluded.add(row.outpoint);
|
|
740
|
+
continue;
|
|
745
741
|
}
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
742
|
+
// Moved since the list was read: read the list again.
|
|
743
|
+
if (now.outpoint !== row.outpoint)
|
|
744
|
+
continue;
|
|
745
|
+
const poolOutput = poolOutputOf(fresh, leg.outpoint);
|
|
746
|
+
const peerId = row.validatorPeerId ?? answer.validators.find((v) => v.identity === row.validatorIdentityKey.toLowerCase())?.peerId;
|
|
747
|
+
if (!poolOutput || !peerId) {
|
|
748
|
+
excluded.add(row.outpoint);
|
|
749
|
+
continue;
|
|
754
750
|
}
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const common = { wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg, pool, poolOutput, commissionPkh, ...(x.tokens ? { tokenInputs: x.tokens } : {}), satsPerKb: feeRate };
|
|
761
|
-
const planned = await planSwapLeg(common);
|
|
762
|
-
const prepared = await prepareSwap({ ...common, satsInput: x.sats, ...(holdingBeef ? { holdingBeef } : {}), planned });
|
|
763
|
-
track.amountOut = amountOut;
|
|
764
|
-
result.repriced = true;
|
|
765
|
-
return { o: await relayLeg(prepared, peerId), prepared };
|
|
751
|
+
const common = { wallet, tokenId, meta, direction, leg, pool: row, poolOutput, commissionPkh: answer.market.commissionPkh, satsPerKb: feeRate };
|
|
752
|
+
const inputs = selectLegInputs(common, await swapSatsHoldings(wallet), buy ? [] : await swapHoldings(wallet, tokenId));
|
|
753
|
+
if (!inputs) {
|
|
754
|
+
why = "fees";
|
|
755
|
+
break;
|
|
766
756
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
757
|
+
const prepared = await prepareSwap({ ...common, inputs, ...(order ? { order } : {}), last: leg.amountIn === remaining, holdingBeef: await swapHoldingBeef(wallet) });
|
|
758
|
+
const at = { leg: outpointText(leg.outpoint), amountIn: leg.amountIn, amountOut: leg.amountOut };
|
|
759
|
+
progress({ phase: "relaying", ...at, filled: [...legs] });
|
|
760
|
+
let outcome = await relayLeg(wallet, af, base, prepared, peerId);
|
|
761
|
+
if (outcome.status === "unknown" || outcome.status === "timeout") {
|
|
762
|
+
// (a) The overlay: what the leg spends.
|
|
763
|
+
const spent = await readSpendsAll(swapBase, [outpointText(leg.outpoint), ...inputs.sats.map((r) => r.outpoint), ...inputs.tokens.map((r) => r.outpoint)], freshFetch).catch(() => []);
|
|
764
|
+
const ours = spent.find((x) => x.spentBy && x.outpoint !== outpointText(leg.outpoint));
|
|
765
|
+
if (ours) {
|
|
766
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
767
|
+
accountResolved(await resolve(wallet));
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (spent.some((x) => x.spentBy && x.outpoint === outpointText(leg.outpoint)))
|
|
771
|
+
continue;
|
|
772
|
+
// (b) Once more to the same validator.
|
|
773
|
+
outcome = await relayLeg(wallet, af, base, prepared, peerId);
|
|
774
|
+
if (outcome.status === "unknown" || outcome.status === "timeout") {
|
|
775
|
+
// (c) Race: the next leg to another pool with the same rows.
|
|
776
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
777
|
+
excluded.add(row.outpoint);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
770
780
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
781
|
+
if (outcome.status === "accepted") {
|
|
782
|
+
if (!outcome.completed.internalized) {
|
|
783
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
784
|
+
accountResolved(await resolve(wallet));
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
order ??= prepared.order;
|
|
788
|
+
budget -= 1;
|
|
789
|
+
filledIn += leg.amountIn;
|
|
790
|
+
filledOut += leg.amountOut;
|
|
791
|
+
legs.push({ poolOutpoint: at.leg, poolId: row.poolId, amountIn: leg.amountIn, amountOut: leg.amountOut, txid: outcome.txid });
|
|
776
792
|
continue;
|
|
777
793
|
}
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
|
|
794
|
+
if (outcome.status === "refused" && outcome.reason.startsWith("pool_spent")) {
|
|
795
|
+
// The relaying host took the validator's state in before it answered: the next read lists it.
|
|
796
|
+
progress({ phase: "repricing", ...at, filled: [...legs] });
|
|
781
797
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const track = { result, amountOut: leg.amountOut, done: false };
|
|
785
|
-
tracks.push(track);
|
|
786
|
-
running.push((async () => {
|
|
787
|
-
progress({ phase: "relaying", leg: result.poolOutpoint });
|
|
788
|
-
let o = await relayLeg(prepared, peerId);
|
|
789
|
-
let sentAs = prepared;
|
|
790
|
-
// 0.31.1: refused `pool_spent` with the pool's newest state attached → repriced against
|
|
791
|
-
// that state and sent once more (the staged `amm-swap` rows are still there).
|
|
792
|
-
if (o.status === "refused" && o.reason.startsWith("pool_spent") && o.pool) {
|
|
793
|
-
progress({ phase: "repricing", leg: result.poolOutpoint });
|
|
794
|
-
const again = await retryLeg({ leg, track, was: pool, pool: o.pool, peerId, tokens, sats });
|
|
795
|
-
if (!again) {
|
|
796
|
-
track.done = true;
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
o = again.o;
|
|
800
|
-
sentAs = again.prepared;
|
|
801
|
-
}
|
|
802
|
-
track.done = true;
|
|
803
|
-
onLeg(result, o, sentAs, base, af);
|
|
804
|
-
})());
|
|
798
|
+
else {
|
|
799
|
+
excluded.add(row.outpoint);
|
|
805
800
|
}
|
|
806
|
-
|
|
807
|
-
|
|
801
|
+
accountResolved(await resolve(wallet));
|
|
802
|
+
}
|
|
803
|
+
// 3. The end.
|
|
804
|
+
const out = { status: filledIn >= quote.request.amountIn ? "filled" : legs.length > 0 ? "partial" : "failed", stop: why, legs, totalIn: filledIn, totalOut: filledOut, unfilled: quote.request.amountIn - filledIn };
|
|
805
|
+
if (why !== "filled" && pending.length === 0) {
|
|
806
|
+
const left = (await swapSatsHoldings(wallet)).length + (await swapHoldings(wallet, tokenId)).length;
|
|
807
|
+
if (left > 0) {
|
|
808
|
+
progress({ phase: "returning", filled: [...legs] });
|
|
809
|
+
try {
|
|
810
|
+
const m = await moveBack({ wallet, base: swapBase, tokenId, meta: { sym: p.sym }, submit: submitAt(swapBase) });
|
|
811
|
+
out.returned = { txid: m.txid, tokens: m.amount };
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
out.returned = { error: errText(e) };
|
|
815
|
+
}
|
|
808
816
|
}
|
|
809
817
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
818
|
+
return out;
|
|
819
|
+
}
|
|
820
|
+
/** One relay of a prepared leg (0.34.0: no browser note); a thrown error is no answer. */
|
|
821
|
+
function relayLeg(wallet, af, base, prepared, peerId) {
|
|
822
|
+
return relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
|
|
813
823
|
}
|
|
814
824
|
/** 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. */
|
|
815
825
|
const freshFetch = (url, init) => f(url, { ...init, cache: "no-cache" });
|
|
816
|
-
/**
|
|
817
|
-
* "Check again" (D15) for a leg with no answer yet: its record read ONCE (`GET
|
|
818
|
-
* /@<token>/amm/swaps?id=`, plain), acted on (accepted: the payout taken in). → the leg's result
|
|
819
|
-
* now (`poolOutpoint` as given).
|
|
820
|
-
*/
|
|
821
|
-
async function checkSwapAgain(leg) {
|
|
822
|
-
const { wallet } = needWallet();
|
|
823
|
-
const id = leg.checkId;
|
|
824
|
-
const c = id ? checks.get(id) : undefined;
|
|
825
|
-
if (!id || !c)
|
|
826
|
-
throw new Error("Nothing to check: this leg is not waiting on an answer here");
|
|
827
|
-
const o = await checkAgain({ wallet, authFetch: c.af, base: c.base, fetch: freshFetch }, c.prepared, id);
|
|
828
|
-
const next = { poolOutpoint: leg.poolOutpoint, status: "failed", ...(leg.repriced ? { repriced: true } : {}) };
|
|
829
|
-
onLeg(next, o, c.prepared, c.base, c.af);
|
|
830
|
-
if (next.status === "filled")
|
|
831
|
-
emit();
|
|
832
|
-
return next;
|
|
833
|
-
}
|
|
834
826
|
/** The connected wallet's spendable sats: the toolbox's balance special operation (`Wallet.balance()`'s call), its `totalOutputs`; a page may not read the admin-only `default` basket. */
|
|
835
827
|
async function walletSats() {
|
|
836
828
|
const { wallet } = needWallet();
|
|
@@ -853,15 +845,16 @@ function makeClient(opts, s) {
|
|
|
853
845
|
const [poolRows, history, list] = await Promise.all([basketRows(wallet, POOLS_BASKET), historyPoolRows(wallet), enabledList().catch(() => [])]);
|
|
854
846
|
const enabled = new Map(list.filter((t) => t.status === "enabled").map((t) => [t.tokenId, t]));
|
|
855
847
|
const filed = new Set(poolRows.map((r) => r.outpoint.replace("_", ".")));
|
|
856
|
-
// The rows by token, one per
|
|
848
|
+
// The rows by token, one per pool id (0.40.0: the deploy outpoint is the pool's id).
|
|
857
849
|
const byToken = new Map();
|
|
858
850
|
for (const row of positionRowsOf(mergePoolRows(history, poolRows))) {
|
|
859
851
|
if (!row.lpPubKey || !enabled.has(row.tokenId))
|
|
860
852
|
continue;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
853
|
+
const ids = byToken.get(row.tokenId) ?? new Map();
|
|
854
|
+
const poolId = row.deployOutpoint.replace(".", "_");
|
|
855
|
+
if (!ids.has(poolId))
|
|
856
|
+
ids.set(poolId, row);
|
|
857
|
+
byToken.set(row.tokenId, ids);
|
|
865
858
|
}
|
|
866
859
|
const out = [];
|
|
867
860
|
const closed = [];
|
|
@@ -879,10 +872,12 @@ function makeClient(opts, s) {
|
|
|
879
872
|
const meta = { sym: t?.sym ?? tokenId.slice(0, 8), dec: t?.dec ?? 0, ...(t?.icon ? { icon: t.icon } : {}) };
|
|
880
873
|
const beating = new Set((await readPools(base, f).catch(() => null))?.validators.map((x) => x.identity) ?? []);
|
|
881
874
|
for (const p of pools) {
|
|
882
|
-
const row = keys.get(p.
|
|
875
|
+
const row = keys.get(p.poolId);
|
|
883
876
|
const fees = { feesEarnedSats: p.sats, feesEarnedTokens: p.tokens };
|
|
877
|
+
if (!row)
|
|
878
|
+
continue;
|
|
884
879
|
if (p.status === "closed" || !p.outpoint || !p.current) {
|
|
885
|
-
closed.push({ outpoint: row.deployOutpoint, tokenId, ...meta, sats: 0n, tokens: 0n, validator: { identityKey: row.validatorIdentity ?? "", live: false }, closed: true, ...fees });
|
|
880
|
+
closed.push({ outpoint: row.deployOutpoint, poolId: p.poolId, swaps: p.swaps, tokenId, ...meta, sats: 0n, tokens: 0n, validator: { identityKey: row.validatorIdentity ?? "", live: false }, closed: true, ...fees });
|
|
886
881
|
continue;
|
|
887
882
|
}
|
|
888
883
|
const outpoint = p.outpoint.replace("_", ".");
|
|
@@ -892,6 +887,8 @@ function makeClient(opts, s) {
|
|
|
892
887
|
await fileDeployRow(wallet, base, history, outpoint, meta);
|
|
893
888
|
out.push({
|
|
894
889
|
outpoint,
|
|
890
|
+
poolId: p.poolId,
|
|
891
|
+
swaps: p.swaps,
|
|
895
892
|
tokenId,
|
|
896
893
|
...meta,
|
|
897
894
|
sats: p.current.bsvReserve,
|
|
@@ -939,7 +936,7 @@ function makeClient(opts, s) {
|
|
|
939
936
|
const t = (await instanceTerms(tokenId)).validator;
|
|
940
937
|
if (!t)
|
|
941
938
|
return { none: true };
|
|
942
|
-
return { validator: { identityKey: t.identity, live: Boolean(t.peerId) }, isThisExchange: true, fees: { lpBps: t.lpFeeBps, validatorBps: t.validatorFeeBps, commissionBps: t.commissionBps } };
|
|
939
|
+
return { validator: { identityKey: t.identity, live: Boolean(t.peerId) }, isThisExchange: true, fees: { lpBps: t.lpFeeBps, validatorBps: t.validatorFeeBps, commissionBps: t.commissionBps }, minLegSats: t.minLegSats };
|
|
943
940
|
}
|
|
944
941
|
/**
|
|
945
942
|
* Deploys a new position (src/lp/poolDeploy.ts, 0.13.0): this skein's
|
|
@@ -976,8 +973,8 @@ function makeClient(opts, s) {
|
|
|
976
973
|
}
|
|
977
974
|
/**
|
|
978
975
|
* Closes a position (src/lp/close.ts): everything in the pool returns to the wallet (its own action
|
|
979
|
-
* pays the miner fee). 0.
|
|
980
|
-
* /@<token>/amm/pools/beef?
|
|
976
|
+
* pays the miner fee). 0.40.0: the pool's current output and its BEEF by its pool id (`GET
|
|
977
|
+
* /@<token>/amm/pools/beef?pool=<deploy outpoint>`); the page's submit at the host root (D19).
|
|
981
978
|
*/
|
|
982
979
|
async function closePosition(req) {
|
|
983
980
|
const { wallet } = needWallet();
|
|
@@ -986,11 +983,11 @@ function makeClient(opts, s) {
|
|
|
986
983
|
await myPositions();
|
|
987
984
|
row = loaded.get(req.outpoint);
|
|
988
985
|
}
|
|
989
|
-
if (!row
|
|
986
|
+
if (!row)
|
|
990
987
|
throw new Error("No such position");
|
|
991
988
|
const [meta, base] = await Promise.all([tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
992
989
|
const m = meta.get(row.tokenId);
|
|
993
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
990
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
994
991
|
if (!poolOutput)
|
|
995
992
|
throw new Error(`the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
996
993
|
const prepared = await prepareClose({
|
|
@@ -1011,8 +1008,8 @@ function makeClient(opts, s) {
|
|
|
1011
1008
|
* `sats` deployed in ONE wallet createAction — the old pool the explicit input (as Close spends
|
|
1012
1009
|
* it), the new one output 0 affirmed by the validator at its terms now (`amm.pool.affirm`, as a
|
|
1013
1010
|
* deploy), the token difference through the wallet's module, the sats difference to or from the
|
|
1014
|
-
* wallet's change. The old pool's output and BEEF by its
|
|
1015
|
-
* /@<token>/amm/pools/beef?
|
|
1011
|
+
* wallet's change. The old pool's output and BEEF by its pool id (`GET
|
|
1012
|
+
* /@<token>/amm/pools/beef?pool=<id>`); the page's submit at the host root; the new pool's row
|
|
1016
1013
|
* filed in `amm-pools`.
|
|
1017
1014
|
*/
|
|
1018
1015
|
async function rebalancePosition(req) {
|
|
@@ -1024,14 +1021,14 @@ function makeClient(opts, s) {
|
|
|
1024
1021
|
await myPositions();
|
|
1025
1022
|
row = loaded.get(req.outpoint);
|
|
1026
1023
|
}
|
|
1027
|
-
if (!row
|
|
1024
|
+
if (!row)
|
|
1028
1025
|
throw new Error("No such position");
|
|
1029
1026
|
const [terms, held, meta, base] = await Promise.all([instanceTerms(row.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
1030
1027
|
const v = terms.validator;
|
|
1031
1028
|
if (!v)
|
|
1032
1029
|
throw new Error(NO_VALIDATOR);
|
|
1033
1030
|
const m = meta.get(row.tokenId);
|
|
1034
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
1031
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
1035
1032
|
if (!poolOutput)
|
|
1036
1033
|
throw new ClientError("stale_pool", `the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
1037
1034
|
const r = await rebalancePool({
|
|
@@ -1086,26 +1083,49 @@ function makeClient(opts, s) {
|
|
|
1086
1083
|
balance,
|
|
1087
1084
|
outputs: t?.valueOutputs ?? 0,
|
|
1088
1085
|
inSwaps: swaps.get(tokenId)?.amount ?? 0n,
|
|
1089
|
-
swapSats: swaps.get(tokenId)?.sats ?? 0,
|
|
1090
1086
|
onExchange: registered.has(tokenId),
|
|
1091
1087
|
};
|
|
1092
1088
|
});
|
|
1093
|
-
return out.filter((t) => t.balance > 0n || t.inSwaps > 0n
|
|
1089
|
+
return out.filter((t) => t.balance > 0n || t.inSwaps > 0n).sort((a, b) => a.sym.localeCompare(b.sym) || a.tokenId.localeCompare(b.tokenId));
|
|
1094
1090
|
}
|
|
1095
1091
|
/**
|
|
1096
|
-
* The
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1092
|
+
* The pass on load (0.40.0, src/wallet/swapPayouts.ts, src/market/activeSwap.ts): a labelled
|
|
1093
|
+
* staged token output the swap basket lacks filed from its action's description (its BEEF `GET
|
|
1094
|
+
* /@<token>/amm/tx?id=`), then the overlay pass over every `amm-swap` row (ONE `/amm/spends`, each
|
|
1095
|
+
* spender's `/tx`, this wallet's legs taken in). Nothing shown; a take-in reloads the pages' data.
|
|
1096
|
+
* Never throws. → the txids taken in.
|
|
1100
1097
|
*/
|
|
1101
1098
|
async function settleSwaps() {
|
|
1102
1099
|
if (!boundWallet)
|
|
1103
1100
|
return [];
|
|
1104
|
-
|
|
1105
|
-
|
|
1101
|
+
let filed = false;
|
|
1102
|
+
try {
|
|
1103
|
+
for (const u of await unfiledStagedTokens(boundWallet)) {
|
|
1104
|
+
const beef = await readTx(tokenBase(u.tokenId, swapBase), u.txid, freshFetch).catch(() => null);
|
|
1105
|
+
if (beef && (await fileStagedToken(boundWallet, u, beef).catch(() => false)))
|
|
1106
|
+
filed = true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
/* the next load */
|
|
1111
|
+
}
|
|
1112
|
+
const taken = (await resolve(boundWallet)).map((t) => t.txid);
|
|
1113
|
+
if (filed || taken.length > 0)
|
|
1106
1114
|
emit();
|
|
1107
1115
|
return taken;
|
|
1108
1116
|
}
|
|
1117
|
+
/** The sats in the swap basket (0.40.0: shown once, not per token). */
|
|
1118
|
+
async function swapBasketSats() {
|
|
1119
|
+
const { wallet } = needWallet();
|
|
1120
|
+
return swapSatsTotal(wallet);
|
|
1121
|
+
}
|
|
1122
|
+
/** "Move back" of the swap basket's sats alone (0.40.0, src/market/activeSwap.ts `moveBackSats`): one plain createAction, the sats rows its inputs, no outputs. */
|
|
1123
|
+
async function moveBackStagedSats() {
|
|
1124
|
+
const { wallet } = needWallet();
|
|
1125
|
+
const r = await moveBackSats(wallet);
|
|
1126
|
+
emit();
|
|
1127
|
+
return r;
|
|
1128
|
+
}
|
|
1109
1129
|
/**
|
|
1110
1130
|
* "Move back" (src/market/activeSwap.ts; 0.29.3: a module plan): every `amm-swap` row of the
|
|
1111
1131
|
* token (and its staged sats) spent in one action whose one output the wallet's module files
|
|
@@ -1227,15 +1247,13 @@ function makeClient(opts, s) {
|
|
|
1227
1247
|
}
|
|
1228
1248
|
/**
|
|
1229
1249
|
* Sets the default validator terms (0.17.0): root's `{fn: "defaults", args: {terms: {lpFeeBps,
|
|
1230
|
-
* validatorFeeBps, commissionBps}}}` to the config instance's `<app>/admin` — a new
|
|
1231
|
-
* instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
|
|
1232
|
-
* written). Refused here unless each fee is an integer 0 to 10 000.
|
|
1250
|
+
* validatorFeeBps, commissionBps, minLegSats?}}}` to the config instance's `<app>/admin` — a new
|
|
1251
|
+
* token instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
|
|
1252
|
+
* written). Refused here unless each fee is an integer 0 to 10 000 (0.40.0: and the minimum leg a
|
|
1253
|
+
* whole number of sats; 0 or absent is none and is not sent).
|
|
1233
1254
|
*/
|
|
1234
1255
|
async function setDefaultTerms(t) {
|
|
1235
|
-
|
|
1236
|
-
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
1237
|
-
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
1238
|
-
await sendDefaults({ terms: { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } });
|
|
1256
|
+
await sendDefaults({ terms: termsArgs(t) });
|
|
1239
1257
|
}
|
|
1240
1258
|
/** One token's validator terms as its instance holds them (0.35.0; P7 `GET /@<token>/amm/terms`, plain): the three fees, or null (a market only). */
|
|
1241
1259
|
async function tokenTerms(tokenId) {
|
|
@@ -1243,19 +1261,15 @@ function makeClient(opts, s) {
|
|
|
1243
1261
|
}
|
|
1244
1262
|
/**
|
|
1245
1263
|
* Sets one token's validator terms (0.35.0, open-exchange 0.12.0): root's `{fn: "terms", args:
|
|
1246
|
-
* {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps} | null}}` to the swap instance's
|
|
1264
|
+
* {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps, minLegSats?} | null}}` to the swap instance's
|
|
1247
1265
|
* `<app>/admin`; swap emits it as a fact and the host sends it to the token's instance as root's
|
|
1248
1266
|
* `terms` (null: a market only). Nothing answers back: the token's `/terms` shows the result.
|
|
1249
1267
|
* Refused here unless each fee is an integer 0 to 10 000.
|
|
1250
1268
|
*/
|
|
1251
1269
|
async function setTokenTerms(tokenId, t) {
|
|
1252
|
-
|
|
1253
|
-
for (const [k, v] of Object.entries(t))
|
|
1254
|
-
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
1255
|
-
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
1270
|
+
const terms = t ? termsArgs(t) : null;
|
|
1256
1271
|
const { signedFor } = needWallet();
|
|
1257
1272
|
const me = await swapIdentity();
|
|
1258
|
-
const terms = t ? { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } : null;
|
|
1259
1273
|
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "terms", args: { tokenId: sdkTokenId(tokenId), terms } });
|
|
1260
1274
|
}
|
|
1261
1275
|
/** Clears the default validator terms (0.17.0): `{fn: "defaults", args: {terms: null}}`; a new token instance is a market only. */
|
|
@@ -1311,11 +1325,15 @@ function makeClient(opts, s) {
|
|
|
1311
1325
|
isRoot,
|
|
1312
1326
|
claimExchange: guard(claimExchange),
|
|
1313
1327
|
hostedTokens: guard(hostedTokens),
|
|
1314
|
-
|
|
1328
|
+
tokenPools: guard(tokenPools),
|
|
1329
|
+
watchPoolEvents,
|
|
1315
1330
|
// history (0.39.0)
|
|
1316
1331
|
trades: guard(trades),
|
|
1317
1332
|
trade: guard(trade),
|
|
1318
1333
|
candles: guard(candles),
|
|
1334
|
+
poolsAt: guard(poolsAt),
|
|
1335
|
+
poolHistory: guard(poolHistory),
|
|
1336
|
+
spendsAll: guard(spendsAll),
|
|
1319
1337
|
stats: guard(stats),
|
|
1320
1338
|
statsAll: guard(statsAll),
|
|
1321
1339
|
holders: guard(holders),
|
|
@@ -1324,7 +1342,6 @@ function makeClient(opts, s) {
|
|
|
1324
1342
|
// swap
|
|
1325
1343
|
quoteSwap,
|
|
1326
1344
|
executeSwap,
|
|
1327
|
-
checkSwapAgain: guard(checkSwapAgain),
|
|
1328
1345
|
walletSats: guard(walletSats),
|
|
1329
1346
|
// liquidity
|
|
1330
1347
|
myPositions: guard(myPositions),
|
|
@@ -1335,6 +1352,8 @@ function makeClient(opts, s) {
|
|
|
1335
1352
|
// your tokens
|
|
1336
1353
|
myTokens: guard(myTokens),
|
|
1337
1354
|
settleSwaps: guard(settleSwaps),
|
|
1355
|
+
swapBasketSats: guard(swapBasketSats),
|
|
1356
|
+
moveBackStagedSats: guard(moveBackStagedSats),
|
|
1338
1357
|
moveBackTokens: guard(moveBackTokens),
|
|
1339
1358
|
sendTokens: guard(sendTokens),
|
|
1340
1359
|
// settings
|