@dcasegr/amm-client 0.38.2 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -24
- package/dist/client.d.ts +206 -62
- package/dist/client.js +388 -333
- 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 +226 -19
- package/dist/lib/overlay.js +278 -49
- 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/prices.d.ts +4 -4
- package/dist/market/prices.js +4 -4
- 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
|
@@ -11,23 +11,27 @@
|
|
|
11
11
|
*
|
|
12
12
|
* - **The token list** (`GET <swap>/tokens`): read ONCE per client (D4; `enabledList`), again
|
|
13
13
|
* only after its own enable or disable changed it.
|
|
14
|
-
* - **Prices** (
|
|
15
|
-
* `
|
|
16
|
-
* - **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
14
|
+
* - **Prices** (0.39.0, D3, D7): ONE read of the host-wide stats `GET <origin>/amm/stats`
|
|
15
|
+
* (`hostedTokens`): each token's price, liquidity (TVL) and pool count; no host-wide stream.
|
|
16
|
+
* - **History** (0.39.0, D1–D6): `trades`, `trade`, `candles`, `stats`, `statsAll`, `holders`,
|
|
17
|
+
* `positionsAll` — plain GETs of the history routes (H1–H6, src/lib/overlay.ts).
|
|
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`.
|
|
27
31
|
* - **Your tokens** (D17, D18, D19, D24): the wallet's module and the swap basket; the token list
|
|
28
|
-
* once and ONE discovery `GET /@discovery/1color/tokens?ids=` for held tokens the list lacks;
|
|
29
|
-
*
|
|
30
|
-
* 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.
|
|
31
35
|
* - **Settings** (D20, D21): the claim card (`claimStatus`: swap's identity, the recipient of
|
|
32
36
|
* root's messages), the token list once, discovery's list and ONE `?ids=` read; Enable's deploy
|
|
33
37
|
* BEEF from the wallet's basket else `GET /@discovery/1color/<token>/deploy`.
|
|
@@ -39,18 +43,17 @@
|
|
|
39
43
|
*/
|
|
40
44
|
import { Beef, Transaction } from "@bsv/sdk";
|
|
41
45
|
import { send1Color } from "@1sat/actions";
|
|
42
|
-
import {
|
|
46
|
+
import { nextLeg, priceOf } from "@dcasegr/amm-engine";
|
|
43
47
|
import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
|
|
44
48
|
import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
|
|
45
|
-
import { ammLookupService, liveOfPools,
|
|
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";
|
|
46
50
|
import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
|
|
47
51
|
import { depthOf } from "./market/prices.js";
|
|
48
|
-
import { buildPlanRequest,
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
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";
|
|
54
57
|
import { formatAmount } from "./lp/amounts.js";
|
|
55
58
|
import { basketRows } from "./lp/wallet.js";
|
|
56
59
|
import { buildInventory, decodeTokenRow, deployIcon } from "./lp/inventory.js";
|
|
@@ -60,7 +63,7 @@ import { rebalancePool } from "./lp/rebalance.js";
|
|
|
60
63
|
import { historyPoolRows, mergePoolRows, positionRowsOf } from "./lp/positions.js";
|
|
61
64
|
import { POOLS_BASKET, assetBasket, heldOutputs } from "./wallet/color.js";
|
|
62
65
|
import { prepareClose, submitToOverlay } from "./lp/close.js";
|
|
63
|
-
import { ClientError,
|
|
66
|
+
import { ClientError, toClientError } from "./errors.js";
|
|
64
67
|
/**
|
|
65
68
|
* `amm.pool.terms`'s refusal when no terms are set (0.15.0, David Case
|
|
66
69
|
* 2026-10-23: terms are a runtime record root sets; unset, a market only).
|
|
@@ -72,6 +75,7 @@ export const NO_TERMS_TEXT = "This exchange validates nothing: no terms are set.
|
|
|
72
75
|
export function hasTerms(t) {
|
|
73
76
|
return !!t && !("none" in t);
|
|
74
77
|
}
|
|
78
|
+
export { parsePoolEventMessage } from "./lib/overlay.js";
|
|
75
79
|
/** The metadata of the deploy output `vout` of an Atomic BEEF's subject; null when that output is no 1Color deploy. */
|
|
76
80
|
function deployMetaOf(beef, vout) {
|
|
77
81
|
const tx = Transaction.fromAtomicBEEF(beef);
|
|
@@ -85,11 +89,12 @@ function symOf(meta, tokenId) {
|
|
|
85
89
|
return meta.get(tokenId)?.sym ?? tokenId.slice(0, 8);
|
|
86
90
|
}
|
|
87
91
|
/**
|
|
88
|
-
* The hosted tokens (the enabled 1Color tokens at output 0), each
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
92
|
+
* The hosted tokens (the enabled 1Color tokens at output 0), each with its record of the host-wide
|
|
93
|
+
* stats (0.39.0, D3, D7: H4): the price (`price`, sats per whole token), the liquidity (`tvlSats`,
|
|
94
|
+
* `tvlTokens`) and the pool count. `stats` null: not read yet (no prices); a token the stats lack:
|
|
95
|
+
* no price, no pools. Pure.
|
|
91
96
|
*/
|
|
92
|
-
export function hostedFrom(list,
|
|
97
|
+
export function hostedFrom(list, stats) {
|
|
93
98
|
const out = [];
|
|
94
99
|
for (const t of list) {
|
|
95
100
|
if (t.status !== "enabled")
|
|
@@ -97,17 +102,18 @@ export function hostedFrom(list, rows) {
|
|
|
97
102
|
const topic = parseTokenTopic(t.topic);
|
|
98
103
|
if (!topic || topic.kind !== "native")
|
|
99
104
|
continue;
|
|
100
|
-
const
|
|
101
|
-
const sats =
|
|
102
|
-
const tokens =
|
|
105
|
+
const st = (stats ?? []).find((r) => canonicalId(r.tokenId) === t.tokenId);
|
|
106
|
+
const sats = st?.tvlSats ?? 0n;
|
|
107
|
+
const tokens = st?.tvlTokens ?? 0n;
|
|
108
|
+
const price = st?.price ?? null;
|
|
103
109
|
out.push({
|
|
104
110
|
tokenId: t.tokenId,
|
|
105
111
|
sym: t.sym || t.tokenId.slice(0, 8),
|
|
106
112
|
dec: t.dec,
|
|
107
113
|
...(t.icon ? { icon: t.icon } : {}),
|
|
108
|
-
marginalPrice: tokens > 0n ? Number(sats) / (Number(tokens) / 10 ** t.dec) : null,
|
|
114
|
+
marginalPrice: price && price.tokens > 0n ? Number(price.sats) / (Number(price.tokens) / 10 ** t.dec) : null,
|
|
109
115
|
depth: depthOf(sats),
|
|
110
|
-
pools:
|
|
116
|
+
pools: st?.pools ?? 0,
|
|
111
117
|
reserves: { sats, tokens },
|
|
112
118
|
});
|
|
113
119
|
}
|
|
@@ -117,6 +123,7 @@ export function hostedFrom(list, rows) {
|
|
|
117
123
|
export function poolViews(answer, tokenId) {
|
|
118
124
|
return (answer?.pools ?? []).map((p) => ({
|
|
119
125
|
outpoint: outpointText(p.outpoint),
|
|
126
|
+
poolId: p.poolId,
|
|
120
127
|
tokenId,
|
|
121
128
|
sats: p.bsvReserve,
|
|
122
129
|
tokens: p.tokenReserve,
|
|
@@ -127,13 +134,29 @@ export function poolViews(answer, tokenId) {
|
|
|
127
134
|
export function poolFees(p) {
|
|
128
135
|
return { lpBps: Number(p.liquidityFeeBps), validatorBps: Number(p.validationFeeBps), commissionBps: Number(p.commissionBps ?? 0n) };
|
|
129
136
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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;
|
|
133
151
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
160
|
}
|
|
138
161
|
/**
|
|
139
162
|
* wallet-toolbox's `specOpWalletBalance` (`sdk.specOpWalletBalance`, @bsv/wallet-toolbox-client): the listOutputs "basket" of its
|
|
@@ -218,7 +241,17 @@ export function enableCandidates(listed, walletIds, onList) {
|
|
|
218
241
|
}
|
|
219
242
|
return out;
|
|
220
243
|
}
|
|
221
|
-
|
|
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() });
|
|
222
255
|
/**
|
|
223
256
|
* The client of one exchange (D9): its reads over `origin` (src/lib/overlay.ts, src/lib/skein.ts),
|
|
224
257
|
* the swap flow, the pool flows (deploy, close, rebalance, move back, positions) and the wallet
|
|
@@ -238,7 +271,6 @@ function makeClient(opts, s) {
|
|
|
238
271
|
const boundWallet = opts.wallet ?? null;
|
|
239
272
|
const signedFor = boundWallet ? (opts.signedFor ?? authFetchPerInstance(boundWallet, f)) : null;
|
|
240
273
|
const planned = s.planned;
|
|
241
|
-
const checks = s.checks;
|
|
242
274
|
const loaded = s.loaded;
|
|
243
275
|
function needWallet() {
|
|
244
276
|
if (!boundWallet || !signedFor)
|
|
@@ -459,106 +491,154 @@ function makeClient(opts, s) {
|
|
|
459
491
|
throw new Error("The claim was sent, but this wallet is not the admin: someone else may have claimed this exchange first.");
|
|
460
492
|
return root;
|
|
461
493
|
}
|
|
462
|
-
/** The tokens this exchange hosts, priced: the token list (once) and ONE read of the host-wide
|
|
494
|
+
/** The tokens this exchange hosts, priced: the token list (once) and ONE read of the host-wide stats (0.39.0, H4). No wallet needed. */
|
|
463
495
|
async function hostedTokens() {
|
|
464
|
-
const [list,
|
|
465
|
-
return hostedFrom(list,
|
|
496
|
+
const [list, stats] = await Promise.all([enabledList(), statsAll().catch(() => null)]);
|
|
497
|
+
return hostedFrom(list, stats);
|
|
466
498
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
}
|
|
503
|
+
/**
|
|
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.
|
|
507
|
+
*/
|
|
508
|
+
function watchPoolEvents(tokenId, onEvent, onError) {
|
|
509
|
+
return watchFeed(eventsUrl(tokenId === "all" ? swapBase : tokenBase(tokenId, swapBase)), onEvent, onError ? { onError } : {});
|
|
510
|
+
}
|
|
511
|
+
// ---------------------------------------------------------------- history (0.39.0, H1–H6)
|
|
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). */
|
|
513
|
+
async function trades(tokenId, o = {}) {
|
|
514
|
+
return readTrades(tokenBase(tokenId, swapBase), o, ff);
|
|
472
515
|
}
|
|
473
|
-
/**
|
|
474
|
-
function
|
|
475
|
-
return
|
|
516
|
+
/** H1 with `txid`: one Swap's trade row, null when it has none. */
|
|
517
|
+
async function trade(tokenId, txid) {
|
|
518
|
+
return readTrade(tokenBase(tokenId, swapBase), txid, ff);
|
|
476
519
|
}
|
|
477
|
-
/**
|
|
478
|
-
function
|
|
479
|
-
return
|
|
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). */
|
|
521
|
+
async function candles(tokenId, o) {
|
|
522
|
+
return readCandles(tokenBase(tokenId, swapBase), o, ff);
|
|
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
|
+
}
|
|
536
|
+
/** H3: a token's stats. */
|
|
537
|
+
async function stats(tokenId) {
|
|
538
|
+
return readStats(tokenBase(tokenId, swapBase), ff);
|
|
539
|
+
}
|
|
540
|
+
/** H4: every enabled token's stats (`ids`: those only, in that order). */
|
|
541
|
+
async function statsAll(o = {}) {
|
|
542
|
+
return readAllStats(swapBase, o.ids, o.fresh ? freshFetch : ff);
|
|
543
|
+
}
|
|
544
|
+
/** H6: a token's holders, largest first (`limit` 1–100, `skip`). */
|
|
545
|
+
async function holders(tokenId, o = {}) {
|
|
546
|
+
return readHolders(tokenBase(tokenId, swapBase), tokenId, o, ff);
|
|
547
|
+
}
|
|
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);
|
|
480
551
|
}
|
|
481
552
|
/**
|
|
482
553
|
* A quote for one token (D12): the matching engine's plan over the pools the Swap page holds
|
|
483
554
|
* (`held`: the token's pools route's answer — the host listed only pools whose validator is beating
|
|
484
|
-
* and whose commission meets the market's minimum)
|
|
485
|
-
* 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.
|
|
486
560
|
*/
|
|
487
561
|
function quoteSwap(req, held, meta) {
|
|
488
562
|
if (req.amountIn <= 0n)
|
|
489
563
|
throw new Error("Enter an amount");
|
|
490
|
-
|
|
491
|
-
if (pools.length === 0)
|
|
564
|
+
if (!held || held.pools.length === 0)
|
|
492
565
|
throw new Error("No pool to swap against");
|
|
566
|
+
const pools = poolsWithMinimums(held);
|
|
493
567
|
const live = liveOfPools(pools);
|
|
494
568
|
const dec = meta.dec;
|
|
495
569
|
const buy = req.side === "buy";
|
|
496
|
-
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);
|
|
497
571
|
if (!r.ok)
|
|
498
572
|
throw new Error(r.error);
|
|
499
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}.`);
|
|
500
576
|
const used = view.legs.filter((l) => l.amountIn > 0n);
|
|
501
577
|
if (used.length === 0 || view.totalOut === 0n)
|
|
502
578
|
throw new Error("The amount is too small to receive anything");
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
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;
|
|
506
581
|
const totalIn = used.reduce((a, l) => a + l.amountIn, 0n);
|
|
507
582
|
const sats = buy ? totalIn : view.totalOut;
|
|
508
583
|
const tokens = buy ? view.totalOut : totalIn;
|
|
509
584
|
const effective = Number(sats) / (Number(tokens) / 10 ** dec);
|
|
510
|
-
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);
|
|
511
590
|
const q = {
|
|
512
591
|
request: req,
|
|
513
592
|
legs: used.map((l) => ({
|
|
514
593
|
poolOutpoint: outpointText(l.outpoint),
|
|
594
|
+
poolId: l.pool.poolId,
|
|
515
595
|
amountIn: l.amountIn,
|
|
516
596
|
amountOut: l.amountOut,
|
|
517
597
|
shareBps: Number((l.amountIn * 10000n) / req.amountIn),
|
|
518
598
|
reserves: { sats: l.pool.bsvReserve, tokens: l.pool.tokenReserve },
|
|
519
599
|
validator: { identityKey: l.pool.validatorIdentityKey, live: l.validator.live },
|
|
600
|
+
fees: poolFees(l.pool),
|
|
520
601
|
})),
|
|
521
602
|
amountOut: view.totalOut,
|
|
522
603
|
minAmountOut: view.minAmountOut,
|
|
523
604
|
effectivePrice: effective,
|
|
524
605
|
marginalPrice: marginal,
|
|
525
606
|
impactBps: marginal ? Math.round((Math.abs(effective - marginal) / marginal) * 10_000) : 0,
|
|
526
|
-
|
|
607
|
+
stage: { amount: req.amountIn, feeSats },
|
|
608
|
+
legMinerFeeSats: minerFee,
|
|
527
609
|
};
|
|
528
|
-
planned.set(q, { plan,
|
|
610
|
+
planned.set(q, { plan, answer: held, dec, sym: meta.sym });
|
|
529
611
|
return q;
|
|
530
612
|
}
|
|
531
|
-
/**
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
if (o.status === "accepted") {
|
|
535
|
-
result.status = "filled";
|
|
536
|
-
result.txid = o.txid;
|
|
537
|
-
delete result.error;
|
|
538
|
-
delete result.kind;
|
|
539
|
-
checks.delete(o.id);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
result.status = "failed";
|
|
543
|
-
const kind = kindOfOutcome(o);
|
|
544
|
-
if (kind)
|
|
545
|
-
result.kind = kind;
|
|
546
|
-
const rest = o.status === "unknown" ? o.reason.replace(/^no answer yet:? ?/, "") : "";
|
|
547
|
-
result.error = o.status === "refused" ? `refused: ${o.reason}` : o.status === "unknown" ? `no answer yet${rest ? `: ${rest}` : ""}` : o.status;
|
|
548
|
-
if (o.status === "unknown") {
|
|
549
|
-
result.checkId = o.id;
|
|
550
|
-
checks.set(o.id, { prepared, base, af });
|
|
551
|
-
}
|
|
552
|
-
else if ("id" in o)
|
|
553
|
-
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) });
|
|
554
616
|
}
|
|
555
617
|
/**
|
|
556
|
-
* Runs a quoted swap with the connected wallet (
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
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.
|
|
562
642
|
*/
|
|
563
643
|
async function executeSwap(quote, o = {}) {
|
|
564
644
|
const progress = (x) => {
|
|
@@ -569,26 +649,26 @@ function makeClient(opts, s) {
|
|
|
569
649
|
/* the caller's callback: not the swap's failure */
|
|
570
650
|
}
|
|
571
651
|
};
|
|
652
|
+
if (s.swapping)
|
|
653
|
+
throw new Error("A swap is already running: one at a time");
|
|
654
|
+
s.swapping = true;
|
|
572
655
|
let result;
|
|
573
656
|
try {
|
|
574
|
-
result = await runSwap(quote, progress);
|
|
657
|
+
result = await runSwap(quote, progress, o.signal);
|
|
575
658
|
}
|
|
576
659
|
catch (e) {
|
|
577
660
|
const err = toClientError(e);
|
|
578
661
|
progress({ phase: "failed", error: err instanceof Error ? err : new Error(String(err)) });
|
|
579
662
|
throw err;
|
|
580
663
|
}
|
|
664
|
+
finally {
|
|
665
|
+
s.swapping = false;
|
|
666
|
+
}
|
|
667
|
+
emit();
|
|
581
668
|
progress(result.status === "failed" ? { phase: "failed", result } : { phase: "done", result });
|
|
582
669
|
return result;
|
|
583
670
|
}
|
|
584
|
-
|
|
585
|
-
function failLeg(result, e) {
|
|
586
|
-
result.error = errText(e);
|
|
587
|
-
const kind = errorKind(e);
|
|
588
|
-
if (kind)
|
|
589
|
-
result.kind = kind;
|
|
590
|
-
}
|
|
591
|
-
async function runSwap(quote, progress) {
|
|
671
|
+
async function runSwap(quote, progress, signal) {
|
|
592
672
|
const { wallet, signedFor } = needWallet();
|
|
593
673
|
const p = planned.get(quote);
|
|
594
674
|
if (!p)
|
|
@@ -596,212 +676,153 @@ function makeClient(opts, s) {
|
|
|
596
676
|
const tokenId = quote.request.tokenId;
|
|
597
677
|
const base = tokenBase(tokenId, swapBase);
|
|
598
678
|
const af = signedFor(base);
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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;
|
|
607
687
|
const legs = [];
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 });
|
|
622
704
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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) });
|
|
626
717
|
}
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
progress({ phase: "staging" });
|
|
634
|
-
let tokenCandidates = tokensIn && runnable.length > 0 ? await swapHoldings(wallet, tokenId) : [];
|
|
635
|
-
let satsCandidates = runnable.length > 0 ? await swapSatsHoldings(wallet) : [];
|
|
636
|
-
const staged = [];
|
|
637
|
-
for (const r of runnable) {
|
|
638
|
-
try {
|
|
639
|
-
const poolOutput = poolOutputOf(fresh, r.leg.outpoint);
|
|
640
|
-
if (!poolOutput)
|
|
641
|
-
throw new Error("the pools read has no transaction for this pool");
|
|
642
|
-
let tokens = null;
|
|
643
|
-
if (tokensIn) {
|
|
644
|
-
tokens = selectExactTokenInputs(tokenCandidates, r.leg.amountIn);
|
|
645
|
-
if (tokens)
|
|
646
|
-
tokenCandidates = tokenCandidates.filter((c) => !tokens.includes(c));
|
|
647
|
-
}
|
|
648
|
-
// A leg whose tokens are still to be staged gets one output of 1 sat (sendToSwapBasket).
|
|
649
|
-
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 });
|
|
650
|
-
const sats = selectExactSatsInput(satsCandidates, planned.funding.satoshis);
|
|
651
|
-
if (sats)
|
|
652
|
-
satsCandidates = satsCandidates.filter((c) => c !== sats);
|
|
653
|
-
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;
|
|
654
724
|
}
|
|
655
|
-
|
|
656
|
-
|
|
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;
|
|
657
733
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
|
|
666
|
-
shortTokens.forEach((x, n) => {
|
|
667
|
-
const h = s.holdings[n];
|
|
668
|
-
if (h)
|
|
669
|
-
x.tokens = [h];
|
|
670
|
-
else
|
|
671
|
-
x.r.result.error = `the swap basket send has no output for this leg${why}`;
|
|
672
|
-
});
|
|
673
|
-
shortSats.forEach((x, n) => {
|
|
674
|
-
const h = s.sats[n];
|
|
675
|
-
if (h)
|
|
676
|
-
x.sats = h;
|
|
677
|
-
else
|
|
678
|
-
x.r.result.error ??= `the swap basket send has no sats output for this leg${why}`;
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
else if (shortSats.length > 0) {
|
|
682
|
-
const s = await sendSatsToSwapBasket(wallet, tokenId, shortSats.map((x) => ({ satoshis: x.planned.funding.satoshis, pool: x.r.leg.outpoint })), { sym: p.sym });
|
|
683
|
-
sent = s.tx;
|
|
684
|
-
const why = s.errors.length ? ` (${s.errors.join("; ")})` : "";
|
|
685
|
-
shortSats.forEach((x, n) => {
|
|
686
|
-
const h = s.sats[n];
|
|
687
|
-
if (h)
|
|
688
|
-
x.sats = h;
|
|
689
|
-
else
|
|
690
|
-
x.r.result.error = `the swap basket send has no sats output for this leg${why}`;
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
const holdingBeef = staged.length > 0 ? await swapHoldingBeef(wallet, sent) : undefined;
|
|
694
|
-
// The legs sent to the relay: their expected amountOut, and whether their outcome is known.
|
|
695
|
-
const tracks = [];
|
|
696
|
-
/** Relays the leg (0.34.0: no browser note; the staged rows are the leg's record, D24). */
|
|
697
|
-
const relayLeg = (prepared, peerId) => relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
|
|
698
|
-
/**
|
|
699
|
-
* 0.31.1: one retry of a leg refused `pool_spent` — re-quoted alone against the validator's
|
|
700
|
-
* newest pool state (the engine's single-pool math, same side and amountIn), sent only while
|
|
701
|
-
* the order's bound holds (filled legs + this one + the legs still out ≥ the plan's
|
|
702
|
-
* resolvedMinAmountOut), rebuilt on the new pool output with the same staged rows (0.34.0: its
|
|
703
|
-
* BEEF from `GET /pools/beef?lp=<the pool's LP key>`, constant along its continuations). Null:
|
|
704
|
-
* the leg failed here (its error set); else the second relay's outcome.
|
|
705
|
-
*/
|
|
706
|
-
const retryLeg = async (x) => {
|
|
707
|
-
const { track, pool } = x;
|
|
708
|
-
const result = track.result;
|
|
709
|
-
const priced = computeSwap(pool, direction, x.leg.amountIn);
|
|
710
|
-
const amountOut = priced?.amountOut ?? 0n;
|
|
711
|
-
const others = tracks.filter((t) => t !== track).reduce((a, t) => a + (t.result.status === "filled" || !t.done ? t.amountOut : 0n), 0n);
|
|
712
|
-
if (!priced || others + amountOut < p.plan.resolvedMinAmountOut) {
|
|
713
|
-
result.error = `repriced below your minimum: ${formatAmount(amountOut, direction === "bsvToToken" ? p.dec : 0)} for this leg`;
|
|
714
|
-
result.kind = "slippage";
|
|
715
|
-
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;
|
|
716
741
|
}
|
|
717
|
-
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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;
|
|
725
750
|
}
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const common = { wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg, pool, poolOutput, commissionPkh, ...(x.tokens ? { tokenInputs: x.tokens } : {}), satsPerKb: feeRate };
|
|
732
|
-
const planned = await planSwapLeg(common);
|
|
733
|
-
const prepared = await prepareSwap({ ...common, satsInput: x.sats, ...(holdingBeef ? { holdingBeef } : {}), planned });
|
|
734
|
-
track.amountOut = amountOut;
|
|
735
|
-
result.repriced = true;
|
|
736
|
-
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;
|
|
737
756
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|
|
741
780
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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 });
|
|
747
792
|
continue;
|
|
748
793
|
}
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
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] });
|
|
752
797
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const track = { result, amountOut: leg.amountOut, done: false };
|
|
756
|
-
tracks.push(track);
|
|
757
|
-
running.push((async () => {
|
|
758
|
-
progress({ phase: "relaying", leg: result.poolOutpoint });
|
|
759
|
-
let o = await relayLeg(prepared, peerId);
|
|
760
|
-
let sentAs = prepared;
|
|
761
|
-
// 0.31.1: refused `pool_spent` with the pool's newest state attached → repriced against
|
|
762
|
-
// that state and sent once more (the staged `amm-swap` rows are still there).
|
|
763
|
-
if (o.status === "refused" && o.reason.startsWith("pool_spent") && o.pool) {
|
|
764
|
-
progress({ phase: "repricing", leg: result.poolOutpoint });
|
|
765
|
-
const again = await retryLeg({ leg, track, was: pool, pool: o.pool, peerId, tokens, sats });
|
|
766
|
-
if (!again) {
|
|
767
|
-
track.done = true;
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
o = again.o;
|
|
771
|
-
sentAs = again.prepared;
|
|
772
|
-
}
|
|
773
|
-
track.done = true;
|
|
774
|
-
onLeg(result, o, sentAs, base, af);
|
|
775
|
-
})());
|
|
798
|
+
else {
|
|
799
|
+
excluded.add(row.outpoint);
|
|
776
800
|
}
|
|
777
|
-
|
|
778
|
-
|
|
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
|
+
}
|
|
779
816
|
}
|
|
780
817
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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) }));
|
|
784
823
|
}
|
|
785
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. */
|
|
786
825
|
const freshFetch = (url, init) => f(url, { ...init, cache: "no-cache" });
|
|
787
|
-
/**
|
|
788
|
-
* "Check again" (D15) for a leg with no answer yet: its record read ONCE (`GET
|
|
789
|
-
* /@<token>/amm/swaps?id=`, plain), acted on (accepted: the payout taken in). → the leg's result
|
|
790
|
-
* now (`poolOutpoint` as given).
|
|
791
|
-
*/
|
|
792
|
-
async function checkSwapAgain(leg) {
|
|
793
|
-
const { wallet } = needWallet();
|
|
794
|
-
const id = leg.checkId;
|
|
795
|
-
const c = id ? checks.get(id) : undefined;
|
|
796
|
-
if (!id || !c)
|
|
797
|
-
throw new Error("Nothing to check: this leg is not waiting on an answer here");
|
|
798
|
-
const o = await checkAgain({ wallet, authFetch: c.af, base: c.base, fetch: freshFetch }, c.prepared, id);
|
|
799
|
-
const next = { poolOutpoint: leg.poolOutpoint, status: "failed", ...(leg.repriced ? { repriced: true } : {}) };
|
|
800
|
-
onLeg(next, o, c.prepared, c.base, c.af);
|
|
801
|
-
if (next.status === "filled")
|
|
802
|
-
emit();
|
|
803
|
-
return next;
|
|
804
|
-
}
|
|
805
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. */
|
|
806
827
|
async function walletSats() {
|
|
807
828
|
const { wallet } = needWallet();
|
|
@@ -824,15 +845,16 @@ function makeClient(opts, s) {
|
|
|
824
845
|
const [poolRows, history, list] = await Promise.all([basketRows(wallet, POOLS_BASKET), historyPoolRows(wallet), enabledList().catch(() => [])]);
|
|
825
846
|
const enabled = new Map(list.filter((t) => t.status === "enabled").map((t) => [t.tokenId, t]));
|
|
826
847
|
const filed = new Set(poolRows.map((r) => r.outpoint.replace("_", ".")));
|
|
827
|
-
// 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).
|
|
828
849
|
const byToken = new Map();
|
|
829
850
|
for (const row of positionRowsOf(mergePoolRows(history, poolRows))) {
|
|
830
851
|
if (!row.lpPubKey || !enabled.has(row.tokenId))
|
|
831
852
|
continue;
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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);
|
|
836
858
|
}
|
|
837
859
|
const out = [];
|
|
838
860
|
const closed = [];
|
|
@@ -850,10 +872,12 @@ function makeClient(opts, s) {
|
|
|
850
872
|
const meta = { sym: t?.sym ?? tokenId.slice(0, 8), dec: t?.dec ?? 0, ...(t?.icon ? { icon: t.icon } : {}) };
|
|
851
873
|
const beating = new Set((await readPools(base, f).catch(() => null))?.validators.map((x) => x.identity) ?? []);
|
|
852
874
|
for (const p of pools) {
|
|
853
|
-
const row = keys.get(p.
|
|
875
|
+
const row = keys.get(p.poolId);
|
|
854
876
|
const fees = { feesEarnedSats: p.sats, feesEarnedTokens: p.tokens };
|
|
877
|
+
if (!row)
|
|
878
|
+
continue;
|
|
855
879
|
if (p.status === "closed" || !p.outpoint || !p.current) {
|
|
856
|
-
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 });
|
|
857
881
|
continue;
|
|
858
882
|
}
|
|
859
883
|
const outpoint = p.outpoint.replace("_", ".");
|
|
@@ -863,6 +887,8 @@ function makeClient(opts, s) {
|
|
|
863
887
|
await fileDeployRow(wallet, base, history, outpoint, meta);
|
|
864
888
|
out.push({
|
|
865
889
|
outpoint,
|
|
890
|
+
poolId: p.poolId,
|
|
891
|
+
swaps: p.swaps,
|
|
866
892
|
tokenId,
|
|
867
893
|
...meta,
|
|
868
894
|
sats: p.current.bsvReserve,
|
|
@@ -910,7 +936,7 @@ function makeClient(opts, s) {
|
|
|
910
936
|
const t = (await instanceTerms(tokenId)).validator;
|
|
911
937
|
if (!t)
|
|
912
938
|
return { none: true };
|
|
913
|
-
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 };
|
|
914
940
|
}
|
|
915
941
|
/**
|
|
916
942
|
* Deploys a new position (src/lp/poolDeploy.ts, 0.13.0): this skein's
|
|
@@ -947,8 +973,8 @@ function makeClient(opts, s) {
|
|
|
947
973
|
}
|
|
948
974
|
/**
|
|
949
975
|
* Closes a position (src/lp/close.ts): everything in the pool returns to the wallet (its own action
|
|
950
|
-
* pays the miner fee). 0.
|
|
951
|
-
* /@<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).
|
|
952
978
|
*/
|
|
953
979
|
async function closePosition(req) {
|
|
954
980
|
const { wallet } = needWallet();
|
|
@@ -957,11 +983,11 @@ function makeClient(opts, s) {
|
|
|
957
983
|
await myPositions();
|
|
958
984
|
row = loaded.get(req.outpoint);
|
|
959
985
|
}
|
|
960
|
-
if (!row
|
|
986
|
+
if (!row)
|
|
961
987
|
throw new Error("No such position");
|
|
962
988
|
const [meta, base] = await Promise.all([tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
963
989
|
const m = meta.get(row.tokenId);
|
|
964
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
990
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
965
991
|
if (!poolOutput)
|
|
966
992
|
throw new Error(`the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
967
993
|
const prepared = await prepareClose({
|
|
@@ -982,8 +1008,8 @@ function makeClient(opts, s) {
|
|
|
982
1008
|
* `sats` deployed in ONE wallet createAction — the old pool the explicit input (as Close spends
|
|
983
1009
|
* it), the new one output 0 affirmed by the validator at its terms now (`amm.pool.affirm`, as a
|
|
984
1010
|
* deploy), the token difference through the wallet's module, the sats difference to or from the
|
|
985
|
-
* wallet's change. The old pool's output and BEEF by its
|
|
986
|
-
* /@<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
|
|
987
1013
|
* filed in `amm-pools`.
|
|
988
1014
|
*/
|
|
989
1015
|
async function rebalancePosition(req) {
|
|
@@ -995,14 +1021,14 @@ function makeClient(opts, s) {
|
|
|
995
1021
|
await myPositions();
|
|
996
1022
|
row = loaded.get(req.outpoint);
|
|
997
1023
|
}
|
|
998
|
-
if (!row
|
|
1024
|
+
if (!row)
|
|
999
1025
|
throw new Error("No such position");
|
|
1000
1026
|
const [terms, held, meta, base] = await Promise.all([instanceTerms(row.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
1001
1027
|
const v = terms.validator;
|
|
1002
1028
|
if (!v)
|
|
1003
1029
|
throw new Error(NO_VALIDATOR);
|
|
1004
1030
|
const m = meta.get(row.tokenId);
|
|
1005
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
1031
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
1006
1032
|
if (!poolOutput)
|
|
1007
1033
|
throw new ClientError("stale_pool", `the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
1008
1034
|
const r = await rebalancePool({
|
|
@@ -1057,26 +1083,49 @@ function makeClient(opts, s) {
|
|
|
1057
1083
|
balance,
|
|
1058
1084
|
outputs: t?.valueOutputs ?? 0,
|
|
1059
1085
|
inSwaps: swaps.get(tokenId)?.amount ?? 0n,
|
|
1060
|
-
swapSats: swaps.get(tokenId)?.sats ?? 0,
|
|
1061
1086
|
onExchange: registered.has(tokenId),
|
|
1062
1087
|
};
|
|
1063
1088
|
});
|
|
1064
|
-
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));
|
|
1065
1090
|
}
|
|
1066
1091
|
/**
|
|
1067
|
-
* The
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
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.
|
|
1071
1097
|
*/
|
|
1072
1098
|
async function settleSwaps() {
|
|
1073
1099
|
if (!boundWallet)
|
|
1074
1100
|
return [];
|
|
1075
|
-
|
|
1076
|
-
|
|
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)
|
|
1077
1114
|
emit();
|
|
1078
1115
|
return taken;
|
|
1079
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
|
+
}
|
|
1080
1129
|
/**
|
|
1081
1130
|
* "Move back" (src/market/activeSwap.ts; 0.29.3: a module plan): every `amm-swap` row of the
|
|
1082
1131
|
* token (and its staged sats) spent in one action whose one output the wallet's module files
|
|
@@ -1198,15 +1247,13 @@ function makeClient(opts, s) {
|
|
|
1198
1247
|
}
|
|
1199
1248
|
/**
|
|
1200
1249
|
* Sets the default validator terms (0.17.0): root's `{fn: "defaults", args: {terms: {lpFeeBps,
|
|
1201
|
-
* validatorFeeBps, commissionBps}}}` to the config instance's `<app>/admin` — a new
|
|
1202
|
-
* instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
|
|
1203
|
-
* 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).
|
|
1204
1254
|
*/
|
|
1205
1255
|
async function setDefaultTerms(t) {
|
|
1206
|
-
|
|
1207
|
-
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
1208
|
-
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
1209
|
-
await sendDefaults({ terms: { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } });
|
|
1256
|
+
await sendDefaults({ terms: termsArgs(t) });
|
|
1210
1257
|
}
|
|
1211
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). */
|
|
1212
1259
|
async function tokenTerms(tokenId) {
|
|
@@ -1214,19 +1261,15 @@ function makeClient(opts, s) {
|
|
|
1214
1261
|
}
|
|
1215
1262
|
/**
|
|
1216
1263
|
* Sets one token's validator terms (0.35.0, open-exchange 0.12.0): root's `{fn: "terms", args:
|
|
1217
|
-
* {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps} | null}}` to the swap instance's
|
|
1264
|
+
* {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps, minLegSats?} | null}}` to the swap instance's
|
|
1218
1265
|
* `<app>/admin`; swap emits it as a fact and the host sends it to the token's instance as root's
|
|
1219
1266
|
* `terms` (null: a market only). Nothing answers back: the token's `/terms` shows the result.
|
|
1220
1267
|
* Refused here unless each fee is an integer 0 to 10 000.
|
|
1221
1268
|
*/
|
|
1222
1269
|
async function setTokenTerms(tokenId, t) {
|
|
1223
|
-
|
|
1224
|
-
for (const [k, v] of Object.entries(t))
|
|
1225
|
-
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
1226
|
-
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
1270
|
+
const terms = t ? termsArgs(t) : null;
|
|
1227
1271
|
const { signedFor } = needWallet();
|
|
1228
1272
|
const me = await swapIdentity();
|
|
1229
|
-
const terms = t ? { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } : null;
|
|
1230
1273
|
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "terms", args: { tokenId: sdkTokenId(tokenId), terms } });
|
|
1231
1274
|
}
|
|
1232
1275
|
/** Clears the default validator terms (0.17.0): `{fn: "defaults", args: {terms: null}}`; a new token instance is a market only. */
|
|
@@ -1282,13 +1325,23 @@ function makeClient(opts, s) {
|
|
|
1282
1325
|
isRoot,
|
|
1283
1326
|
claimExchange: guard(claimExchange),
|
|
1284
1327
|
hostedTokens: guard(hostedTokens),
|
|
1285
|
-
|
|
1286
|
-
|
|
1328
|
+
tokenPools: guard(tokenPools),
|
|
1329
|
+
watchPoolEvents,
|
|
1330
|
+
// history (0.39.0)
|
|
1331
|
+
trades: guard(trades),
|
|
1332
|
+
trade: guard(trade),
|
|
1333
|
+
candles: guard(candles),
|
|
1334
|
+
poolsAt: guard(poolsAt),
|
|
1335
|
+
poolHistory: guard(poolHistory),
|
|
1336
|
+
spendsAll: guard(spendsAll),
|
|
1337
|
+
stats: guard(stats),
|
|
1338
|
+
statsAll: guard(statsAll),
|
|
1339
|
+
holders: guard(holders),
|
|
1340
|
+
positionsAll: guard(positionsAll),
|
|
1287
1341
|
registeredTokens: guard(registeredTokens),
|
|
1288
1342
|
// swap
|
|
1289
1343
|
quoteSwap,
|
|
1290
1344
|
executeSwap,
|
|
1291
|
-
checkSwapAgain: guard(checkSwapAgain),
|
|
1292
1345
|
walletSats: guard(walletSats),
|
|
1293
1346
|
// liquidity
|
|
1294
1347
|
myPositions: guard(myPositions),
|
|
@@ -1299,6 +1352,8 @@ function makeClient(opts, s) {
|
|
|
1299
1352
|
// your tokens
|
|
1300
1353
|
myTokens: guard(myTokens),
|
|
1301
1354
|
settleSwaps: guard(settleSwaps),
|
|
1355
|
+
swapBasketSats: guard(swapBasketSats),
|
|
1356
|
+
moveBackStagedSats: guard(moveBackStagedSats),
|
|
1302
1357
|
moveBackTokens: guard(moveBackTokens),
|
|
1303
1358
|
sendTokens: guard(sendTokens),
|
|
1304
1359
|
// settings
|