@dcasegr/amm-client 0.39.0 → 0.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -31
- package/dist/client.d.ts +190 -62
- package/dist/client.js +493 -322
- 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 +22 -0
- package/dist/lib/skein.js +16 -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`.
|
|
@@ -40,20 +41,20 @@
|
|
|
40
41
|
* an outpoint is `<txid>.<vout>`. Amounts are bigints in base units; prices
|
|
41
42
|
* are numbers: sats per whole token.
|
|
42
43
|
*/
|
|
43
|
-
import { Beef, Transaction } from "@bsv/sdk";
|
|
44
|
+
import { Beef, Transaction, createNonce } from "@bsv/sdk";
|
|
45
|
+
import { BRC29_PROTOCOL, WALLET_PAYMENT, p2pkhHexOf } from "./wallet/brc29.js";
|
|
44
46
|
import { send1Color } from "@1sat/actions";
|
|
45
|
-
import {
|
|
47
|
+
import { nextLeg, priceOf } from "@dcasegr/amm-engine";
|
|
46
48
|
import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
|
|
47
49
|
import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
|
|
48
|
-
import { ammLookupService,
|
|
49
|
-
import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
|
|
50
|
+
import { ammLookupService, liveOfPools, parseTokenTopic, poolOutputOf, readDiscoveryDeploy, readDiscoveryIds, readDiscoveryList, readAllPositions, readAllStats, readCandles, readHolders, readPoolHistory, readPoolsAt, readPoolsBeef, readPools, readPositions, readSpendsAll, readStats, readTerms, readTrade, readTrades, readTx, eventsUrl, watchPoolEvents as watchFeed, } from "./lib/overlay.js";
|
|
51
|
+
import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, readWallet, sendMessage } from "./lib/skein.js";
|
|
50
52
|
import { depthOf } from "./market/prices.js";
|
|
51
|
-
import { buildPlanRequest,
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import { settleStagedSwaps } from "./wallet/swapPayouts.js";
|
|
53
|
+
import { buildPlanRequest, legMinerFeeSats, quote as planQuote } from "./market/plan.js";
|
|
54
|
+
import { prepareSwap, selectLegInputs } from "./market/swapAction.js";
|
|
55
|
+
import { fileStagedToken, moveBack, moveBackSats, stage, swapHoldingBeef, swapHoldings, swapSatsHoldings, swapSatsTotal, swapTotals, unfiledStagedTokens } from "./market/activeSwap.js";
|
|
56
|
+
import { relaySwap, swapIdOf } from "./market/swapFlow.js";
|
|
57
|
+
import { resolveFromOverlay } from "./wallet/swapPayouts.js";
|
|
57
58
|
import { formatAmount } from "./lp/amounts.js";
|
|
58
59
|
import { basketRows } from "./lp/wallet.js";
|
|
59
60
|
import { buildInventory, decodeTokenRow, deployIcon } from "./lp/inventory.js";
|
|
@@ -63,7 +64,7 @@ import { rebalancePool } from "./lp/rebalance.js";
|
|
|
63
64
|
import { historyPoolRows, mergePoolRows, positionRowsOf } from "./lp/positions.js";
|
|
64
65
|
import { POOLS_BASKET, assetBasket, heldOutputs } from "./wallet/color.js";
|
|
65
66
|
import { prepareClose, submitToOverlay } from "./lp/close.js";
|
|
66
|
-
import { ClientError,
|
|
67
|
+
import { ClientError, toClientError } from "./errors.js";
|
|
67
68
|
/**
|
|
68
69
|
* `amm.pool.terms`'s refusal when no terms are set (0.15.0, David Case
|
|
69
70
|
* 2026-10-23: terms are a runtime record root sets; unset, a market only).
|
|
@@ -75,7 +76,7 @@ export const NO_TERMS_TEXT = "This exchange validates nothing: no terms are set.
|
|
|
75
76
|
export function hasTerms(t) {
|
|
76
77
|
return !!t && !("none" in t);
|
|
77
78
|
}
|
|
78
|
-
export {
|
|
79
|
+
export { parsePoolEventMessage } from "./lib/overlay.js";
|
|
79
80
|
/** The metadata of the deploy output `vout` of an Atomic BEEF's subject; null when that output is no 1Color deploy. */
|
|
80
81
|
function deployMetaOf(beef, vout) {
|
|
81
82
|
const tx = Transaction.fromAtomicBEEF(beef);
|
|
@@ -123,6 +124,7 @@ export function hostedFrom(list, stats) {
|
|
|
123
124
|
export function poolViews(answer, tokenId) {
|
|
124
125
|
return (answer?.pools ?? []).map((p) => ({
|
|
125
126
|
outpoint: outpointText(p.outpoint),
|
|
127
|
+
poolId: p.poolId,
|
|
126
128
|
tokenId,
|
|
127
129
|
sats: p.bsvReserve,
|
|
128
130
|
tokens: p.tokenReserve,
|
|
@@ -133,13 +135,29 @@ export function poolViews(answer, tokenId) {
|
|
|
133
135
|
export function poolFees(p) {
|
|
134
136
|
return { lpBps: Number(p.liquidityFeeBps), validatorBps: Number(p.validationFeeBps), commissionBps: Number(p.commissionBps ?? 0n) };
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* A pools answer's rows as the planner takes them (0.40.0): each pool with its validator's
|
|
140
|
+
* minimum leg (`validators[].minLegSats`, the validator's announced terms).
|
|
141
|
+
*/
|
|
142
|
+
export function poolsWithMinimums(answer) {
|
|
143
|
+
const min = new Map(answer.validators.map((v) => [v.identity, v.minLegSats]));
|
|
144
|
+
return answer.pools.map((p) => {
|
|
145
|
+
const m = min.get(p.validatorIdentityKey.toLowerCase());
|
|
146
|
+
return m ? { ...p, minLegSats: m } : p;
|
|
147
|
+
});
|
|
139
148
|
}
|
|
140
|
-
/**
|
|
141
|
-
export function
|
|
142
|
-
return
|
|
149
|
+
/** Sats per whole token of an exact pair (`sats` per `tokens` base units), 0 when `tokens` is 0. */
|
|
150
|
+
export function pairPrice(p, dec) {
|
|
151
|
+
return p.tokens > 0n ? Number(p.sats) / (Number(p.tokens) / 10 ** dec) : 0;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A leg's sats beyond its amount in (0.40.0, the stage's fee sats): the miner fee estimate plus its
|
|
155
|
+
* 1-sat carriers — on a buy the token payout; on a sell the token fee and commission outputs that
|
|
156
|
+
* are not zero, and the token change.
|
|
157
|
+
*/
|
|
158
|
+
export function legCarrySats(leg, buy, minerFeeSats) {
|
|
159
|
+
const carriers = buy ? 1n : BigInt([leg.liquidityFee, leg.validationFee, leg.commission].filter((x) => x > 0n).length + 1);
|
|
160
|
+
return minerFeeSats + carriers;
|
|
143
161
|
}
|
|
144
162
|
/**
|
|
145
163
|
* wallet-toolbox's `specOpWalletBalance` (`sdk.specOpWalletBalance`, @bsv/wallet-toolbox-client): the listOutputs "basket" of its
|
|
@@ -224,7 +242,17 @@ export function enableCandidates(listed, walletIds, onList) {
|
|
|
224
242
|
}
|
|
225
243
|
return out;
|
|
226
244
|
}
|
|
227
|
-
|
|
245
|
+
/** 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. */
|
|
246
|
+
function termsArgs(t) {
|
|
247
|
+
for (const [k, v] of Object.entries({ lpBps: t.lpBps, validatorBps: t.validatorBps, commissionBps: t.commissionBps }))
|
|
248
|
+
if (!Number.isInteger(v) || v < 0 || v > 10_000)
|
|
249
|
+
throw new Error(`${k}: an integer, 0 to 10000 basis points`);
|
|
250
|
+
const min = t.minLegSats ?? 0;
|
|
251
|
+
if (!Number.isSafeInteger(min) || min < 0)
|
|
252
|
+
throw new Error("minLegSats: a whole number of sats, 0 for none");
|
|
253
|
+
return { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps, ...(min > 0 ? { minLegSats: min } : {}) };
|
|
254
|
+
}
|
|
255
|
+
const newShared = () => ({ listRead: null, claimRead: null, planned: new WeakMap(), swapping: false, loaded: new Map(), listeners: new Set() });
|
|
228
256
|
/**
|
|
229
257
|
* The client of one exchange (D9): its reads over `origin` (src/lib/overlay.ts, src/lib/skein.ts),
|
|
230
258
|
* the swap flow, the pool flows (deploy, close, rebalance, move back, positions) and the wallet
|
|
@@ -244,7 +272,6 @@ function makeClient(opts, s) {
|
|
|
244
272
|
const boundWallet = opts.wallet ?? null;
|
|
245
273
|
const signedFor = boundWallet ? (opts.signedFor ?? authFetchPerInstance(boundWallet, f)) : null;
|
|
246
274
|
const planned = s.planned;
|
|
247
|
-
const checks = s.checks;
|
|
248
275
|
const loaded = s.loaded;
|
|
249
276
|
function needWallet() {
|
|
250
277
|
if (!boundWallet || !signedFor)
|
|
@@ -470,16 +497,20 @@ function makeClient(opts, s) {
|
|
|
470
497
|
const [list, stats] = await Promise.all([enabledList(), statsAll().catch(() => null)]);
|
|
471
498
|
return hostedFrom(list, stats);
|
|
472
499
|
}
|
|
500
|
+
/** P2 (0.41.0): a token's pools as the host lists them now, read past the browser's cache (`freshFetch`). */
|
|
501
|
+
async function tokenPools(tokenId) {
|
|
502
|
+
return readPools(tokenBase(tokenId, swapBase), freshFetch);
|
|
503
|
+
}
|
|
473
504
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
505
|
+
* The host's pool event feed (E1, 0.41.0): one token's (`GET /@<token>/amm/events`) or, with
|
|
506
|
+
* `"all"`, every enabled token's (`GET <origin>/amm/events`); each message `{tokenId, …row}` to
|
|
507
|
+
* `onEvent`. Signals for display: the pools route is the truth. → the unsubscribe.
|
|
477
508
|
*/
|
|
478
|
-
function
|
|
479
|
-
return
|
|
509
|
+
function watchPoolEvents(tokenId, onEvent, onError) {
|
|
510
|
+
return watchFeed(eventsUrl(tokenId === "all" ? swapBase : tokenBase(tokenId, swapBase)), onEvent, onError ? { onError } : {});
|
|
480
511
|
}
|
|
481
512
|
// ---------------------------------------------------------------- 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`, `
|
|
513
|
+
/** 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
514
|
async function trades(tokenId, o = {}) {
|
|
484
515
|
return readTrades(tokenBase(tokenId, swapBase), o, ff);
|
|
485
516
|
}
|
|
@@ -487,107 +518,128 @@ function makeClient(opts, s) {
|
|
|
487
518
|
async function trade(tokenId, txid) {
|
|
488
519
|
return readTrade(tokenBase(tokenId, swapBase), txid, ff);
|
|
489
520
|
}
|
|
490
|
-
/** H2: a token's candles, one per non-empty bucket of `tf` over `from ≤ at < to` (ms). */
|
|
521
|
+
/** 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
522
|
async function candles(tokenId, o) {
|
|
492
523
|
return readCandles(tokenBase(tokenId, swapBase), o, ff);
|
|
493
524
|
}
|
|
525
|
+
/** H7 (0.40.0): every pool of the token listed at `t` (ms). */
|
|
526
|
+
async function poolsAt(tokenId, t) {
|
|
527
|
+
return readPoolsAt(tokenBase(tokenId, swapBase), t, ff);
|
|
528
|
+
}
|
|
529
|
+
/** 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`). */
|
|
530
|
+
async function poolHistory(tokenId, o = {}) {
|
|
531
|
+
return readPoolHistory(tokenBase(tokenId, swapBase), o, ff);
|
|
532
|
+
}
|
|
533
|
+
/** H9 (0.40.0): each outpoint's spender as the host's `/amm/spends` answers it (every enabled token's `/spends`). */
|
|
534
|
+
async function spendsAll(outpoints) {
|
|
535
|
+
return readSpendsAll(swapBase, outpoints, ff);
|
|
536
|
+
}
|
|
494
537
|
/** H3: a token's stats. */
|
|
495
538
|
async function stats(tokenId) {
|
|
496
539
|
return readStats(tokenBase(tokenId, swapBase), ff);
|
|
497
540
|
}
|
|
498
541
|
/** H4: every enabled token's stats (`ids`: those only, in that order). */
|
|
499
542
|
async function statsAll(o = {}) {
|
|
500
|
-
return readAllStats(swapBase, o.ids, ff);
|
|
543
|
+
return readAllStats(swapBase, o.ids, o.fresh ? freshFetch : ff);
|
|
501
544
|
}
|
|
502
545
|
/** H6: a token's holders, largest first (`limit` 1–100, `skip`). */
|
|
503
546
|
async function holders(tokenId, o = {}) {
|
|
504
547
|
return readHolders(tokenBase(tokenId, swapBase), tokenId, o, ff);
|
|
505
548
|
}
|
|
506
|
-
/** H5: the
|
|
507
|
-
async function positionsAll(
|
|
508
|
-
return readAllPositions(swapBase,
|
|
549
|
+
/** H5: the pools' positions on every enabled token (0.40.0: by pool id). */
|
|
550
|
+
async function positionsAll(poolIds) {
|
|
551
|
+
return readAllPositions(swapBase, poolIds, ff);
|
|
509
552
|
}
|
|
510
553
|
/**
|
|
511
554
|
* A quote for one token (D12): the matching engine's plan over the pools the Swap page holds
|
|
512
555
|
* (`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.
|
|
556
|
+
* and whose commission meets the market's minimum), each pool with its validator's minimum leg
|
|
557
|
+
* (0.40.0); no request. `meta`: the token's symbol and decimals. 0.40.0: the price and impact from
|
|
558
|
+
* the engine's `priceOf` over the same pools (the buy side for a buy, the sell side for a sell);
|
|
559
|
+
* each leg's own fees; what Swap will stage (the swap amount exact, 0.40.2: the fee sats of
|
|
560
|
+
* `maxSources` legs) and the per-leg miner fee estimate.
|
|
515
561
|
*/
|
|
516
562
|
function quoteSwap(req, held, meta) {
|
|
517
563
|
if (req.amountIn <= 0n)
|
|
518
564
|
throw new Error("Enter an amount");
|
|
519
|
-
|
|
520
|
-
if (pools.length === 0)
|
|
565
|
+
if (!held || held.pools.length === 0)
|
|
521
566
|
throw new Error("No pool to swap against");
|
|
567
|
+
const pools = poolsWithMinimums(held);
|
|
522
568
|
const live = liveOfPools(pools);
|
|
523
569
|
const dec = meta.dec;
|
|
524
570
|
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),
|
|
571
|
+
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
572
|
if (!r.ok)
|
|
527
573
|
throw new Error(r.error);
|
|
528
574
|
const { plan, view } = planQuote(r.request, live, dec);
|
|
575
|
+
if (plan.unfillable === "min")
|
|
576
|
+
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
577
|
const used = view.legs.filter((l) => l.amountIn > 0n);
|
|
530
578
|
if (used.length === 0 || view.totalOut === 0n)
|
|
531
579
|
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;
|
|
580
|
+
const price = priceOf(pools);
|
|
581
|
+
const marginal = price ? pairPrice(buy ? price.buy : price.sell, dec) : 0;
|
|
535
582
|
const totalIn = used.reduce((a, l) => a + l.amountIn, 0n);
|
|
536
583
|
const sats = buy ? totalIn : view.totalOut;
|
|
537
584
|
const tokens = buy ? view.totalOut : totalIn;
|
|
538
585
|
const effective = Number(sats) / (Number(tokens) / 10 ** dec);
|
|
539
|
-
const
|
|
586
|
+
const minerFee = legMinerFeeSats(feeRate);
|
|
587
|
+
const carry = plan.legs.map((l) => legCarrySats(l, buy, minerFee));
|
|
588
|
+
// 0.40.2: the fee sats for EXACTLY maxSources legs — the planned legs' own, each further leg at the largest of theirs.
|
|
589
|
+
const more = BigInt(Math.max(0, req.maxSources - carry.length));
|
|
590
|
+
const feeSats = carry.reduce((a, x) => a + x, 0n) + more * carry.reduce((a, x) => (x > a ? x : a), 0n);
|
|
540
591
|
const q = {
|
|
541
592
|
request: req,
|
|
542
593
|
legs: used.map((l) => ({
|
|
543
594
|
poolOutpoint: outpointText(l.outpoint),
|
|
595
|
+
poolId: l.pool.poolId,
|
|
544
596
|
amountIn: l.amountIn,
|
|
545
597
|
amountOut: l.amountOut,
|
|
546
598
|
shareBps: Number((l.amountIn * 10000n) / req.amountIn),
|
|
547
599
|
reserves: { sats: l.pool.bsvReserve, tokens: l.pool.tokenReserve },
|
|
548
600
|
validator: { identityKey: l.pool.validatorIdentityKey, live: l.validator.live },
|
|
601
|
+
fees: poolFees(l.pool),
|
|
549
602
|
})),
|
|
550
603
|
amountOut: view.totalOut,
|
|
551
604
|
minAmountOut: view.minAmountOut,
|
|
552
605
|
effectivePrice: effective,
|
|
553
606
|
marginalPrice: marginal,
|
|
554
607
|
impactBps: marginal ? Math.round((Math.abs(effective - marginal) / marginal) * 10_000) : 0,
|
|
555
|
-
|
|
608
|
+
stage: { amount: req.amountIn, feeSats },
|
|
609
|
+
legMinerFeeSats: minerFee,
|
|
556
610
|
};
|
|
557
|
-
planned.set(q, { plan,
|
|
611
|
+
planned.set(q, { plan, answer: held, dec, sym: meta.sym });
|
|
558
612
|
return q;
|
|
559
613
|
}
|
|
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);
|
|
614
|
+
/** 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. */
|
|
615
|
+
function resolve(wallet) {
|
|
616
|
+
return resolveFromOverlay(wallet, { spendsAll: (ops) => readSpendsAll(swapBase, ops, freshFetch), tx: (tokenId, txid) => readTx(tokenBase(tokenId, swapBase), txid, freshFetch) });
|
|
583
617
|
}
|
|
584
618
|
/**
|
|
585
|
-
* Runs a quoted swap with the connected wallet (
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
619
|
+
* Runs a quoted swap with the connected wallet (0.40.0, agent-env plans/swap-balance-build-2026-11-05.md
|
|
620
|
+
* §0 7, Builder 2 item 4): ONE swap at a time, legs ONE AT A TIME.
|
|
621
|
+
*
|
|
622
|
+
* 1. The overlay pass (`resolve`); then what `amm-swap` lacks of what the plan needs — the swap
|
|
623
|
+
* amount exact, the sats for the carriers and miner fees of `maxSources` legs —
|
|
624
|
+
* staged by ONE `stage` (`staging`).
|
|
625
|
+
* 2. The loop: cancelled → `cancelled`; the SERVER's pool list read fresh (`GET
|
|
626
|
+
* /@<token>/amm/pools`, no cache; 0.41.0: before EACH leg, no list of its own kept);
|
|
627
|
+
* `nextLeg` over it minus the excluded outpoints, `minOut` the quote's floor less what has
|
|
628
|
+
* filled, `maxSources` (0.40.2) the request's less the legs filled → its stop ends the swap;
|
|
629
|
+
* the pool's current output read by its id (`GET /pools/beef?pool=`; moved → the list read
|
|
630
|
+
* again); the staged rows that cover the leg, else `fees`; built, signed, relayed
|
|
631
|
+
* (`relaying`):
|
|
632
|
+
* - accepted → taken in (ONE internalizeAction) before the next leg;
|
|
633
|
+
* - refused `pool_spent` (`repricing`) → the list read again: the relaying host took the
|
|
634
|
+
* validator's state in before it answered (0.41.0);
|
|
635
|
+
* - refused otherwise (`below_minimum` included) → that outpoint excluded;
|
|
636
|
+
* - no answer → (a) the overlay: a staged input spent → the leg landed (taken in by the
|
|
637
|
+
* pass), the pool spent by another's swap → continue with the same rows; (b) else the
|
|
638
|
+
* same leg sent ONCE more to the same validator; (c) still none → that outpoint excluded
|
|
639
|
+
* (the next leg races it with the same rows);
|
|
640
|
+
* - after any refusal: the overlay pass.
|
|
641
|
+
* 3. Filled completely: the last leg's change went to the wallet, nothing left. Stopped early with
|
|
642
|
+
* rows left: `moveBack` (one wallet action, `returning`); declined or failed → the rows stay.
|
|
591
643
|
*/
|
|
592
644
|
async function executeSwap(quote, o = {}) {
|
|
593
645
|
const progress = (x) => {
|
|
@@ -598,26 +650,26 @@ function makeClient(opts, s) {
|
|
|
598
650
|
/* the caller's callback: not the swap's failure */
|
|
599
651
|
}
|
|
600
652
|
};
|
|
653
|
+
if (s.swapping)
|
|
654
|
+
throw new Error("A swap is already running: one at a time");
|
|
655
|
+
s.swapping = true;
|
|
601
656
|
let result;
|
|
602
657
|
try {
|
|
603
|
-
result = await runSwap(quote, progress);
|
|
658
|
+
result = await runSwap(quote, progress, o.signal);
|
|
604
659
|
}
|
|
605
660
|
catch (e) {
|
|
606
661
|
const err = toClientError(e);
|
|
607
662
|
progress({ phase: "failed", error: err instanceof Error ? err : new Error(String(err)) });
|
|
608
663
|
throw err;
|
|
609
664
|
}
|
|
665
|
+
finally {
|
|
666
|
+
s.swapping = false;
|
|
667
|
+
}
|
|
668
|
+
emit();
|
|
610
669
|
progress(result.status === "failed" ? { phase: "failed", result } : { phase: "done", result });
|
|
611
670
|
return result;
|
|
612
671
|
}
|
|
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) {
|
|
672
|
+
async function runSwap(quote, progress, signal) {
|
|
621
673
|
const { wallet, signedFor } = needWallet();
|
|
622
674
|
const p = planned.get(quote);
|
|
623
675
|
if (!p)
|
|
@@ -625,212 +677,153 @@ function makeClient(opts, s) {
|
|
|
625
677
|
const tokenId = quote.request.tokenId;
|
|
626
678
|
const base = tokenBase(tokenId, swapBase);
|
|
627
679
|
const af = signedFor(base);
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
680
|
+
const buy = quote.request.side === "buy";
|
|
681
|
+
const direction = buy ? "bsvToToken" : "tokenToBsv";
|
|
682
|
+
const minerFee = legMinerFeeSats(feeRate);
|
|
683
|
+
const meta = { sym: p.sym, dec: p.dec };
|
|
684
|
+
// 0.41.0: the server's list, read fresh before each leg; the outpoints excluded (a pool that refused other than pool_spent, or did not answer).
|
|
685
|
+
const excluded = new Set();
|
|
686
|
+
/** 0.40.2: the legs the order may still fill (the request's maxSources less the legs filled). */
|
|
687
|
+
let budget = quote.request.maxSources;
|
|
636
688
|
const legs = [];
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
689
|
+
let filledIn = 0n;
|
|
690
|
+
let filledOut = 0n;
|
|
691
|
+
let order;
|
|
692
|
+
/** Legs sent with no answer yet: what they spend, to account a later take-in by the overlay pass. */
|
|
693
|
+
const pending = [];
|
|
694
|
+
const accountResolved = (taken) => {
|
|
695
|
+
for (const t of taken) {
|
|
696
|
+
const i = pending.findIndex((x) => x.prepared.satsInputs.some((r) => t.inputs.includes(r.outpoint)));
|
|
697
|
+
if (i < 0)
|
|
698
|
+
continue;
|
|
699
|
+
const [x] = pending.splice(i, 1);
|
|
700
|
+
order ??= x.prepared.order;
|
|
701
|
+
budget -= 1;
|
|
702
|
+
filledIn += x.leg.amountIn;
|
|
703
|
+
filledOut += x.leg.amountOut;
|
|
704
|
+
legs.push({ poolOutpoint: outpointText(x.leg.outpoint), poolId: x.poolId, amountIn: x.leg.amountIn, amountOut: x.leg.amountOut, txid: t.txid });
|
|
651
705
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
706
|
+
};
|
|
707
|
+
accountResolved(await resolve(wallet));
|
|
708
|
+
// 1. Stage what the basket lacks.
|
|
709
|
+
const needTokens = buy ? 0n : quote.stage.amount;
|
|
710
|
+
const needSats = (buy ? quote.stage.amount : 0n) + quote.stage.feeSats;
|
|
711
|
+
const haveSats = BigInt(await swapSatsTotal(wallet));
|
|
712
|
+
const haveTokens = buy ? 0n : (await swapHoldings(wallet, tokenId)).reduce((a, t) => a + t.amount, 0n);
|
|
713
|
+
const lackSats = needSats > haveSats ? needSats - haveSats : 0n;
|
|
714
|
+
const lackTokens = needTokens > haveTokens ? needTokens - haveTokens : 0n;
|
|
715
|
+
if (lackSats > 0n || lackTokens > 0n) {
|
|
716
|
+
progress({ phase: "staging", sats: lackSats, tokens: lackTokens });
|
|
717
|
+
await stage(wallet, { sats: Number(lackSats), ...(lackTokens > 0n ? { tokens: { tokenId, amount: lackTokens } } : {}) }, { sym: p.sym, submit: submitAt(base) });
|
|
655
718
|
}
|
|
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 });
|
|
719
|
+
// 2. The loop.
|
|
720
|
+
let why = "filled";
|
|
721
|
+
for (;;) {
|
|
722
|
+
if (signal?.aborted) {
|
|
723
|
+
why = "cancelled";
|
|
724
|
+
break;
|
|
683
725
|
}
|
|
684
|
-
|
|
685
|
-
|
|
726
|
+
const remaining = quote.request.amountIn - filledIn;
|
|
727
|
+
const answer = await tokenPools(tokenId);
|
|
728
|
+
const pools = poolsWithMinimums(answer).filter((r) => !excluded.has(r.outpoint));
|
|
729
|
+
const minOut = quote.minAmountOut > filledOut ? quote.minAmountOut - filledOut : 0n;
|
|
730
|
+
const next = nextLeg({ pools, direction, remaining, fixedCost: { minerFeeSats: minerFee }, minOut, maxSources: budget });
|
|
731
|
+
if ("stop" in next) {
|
|
732
|
+
why = next.stop;
|
|
733
|
+
break;
|
|
686
734
|
}
|
|
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;
|
|
735
|
+
const leg = next.leg;
|
|
736
|
+
const row = pools.find((x) => x.outpoint === leg.outpoint);
|
|
737
|
+
const fresh = await readPoolsBeef(base, [row.poolId], freshFetch);
|
|
738
|
+
const now = fresh.pools.find((x) => x.poolId === row.poolId);
|
|
739
|
+
if (!now) {
|
|
740
|
+
excluded.add(row.outpoint);
|
|
741
|
+
continue;
|
|
745
742
|
}
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
743
|
+
// Moved since the list was read: read the list again.
|
|
744
|
+
if (now.outpoint !== row.outpoint)
|
|
745
|
+
continue;
|
|
746
|
+
const poolOutput = poolOutputOf(fresh, leg.outpoint);
|
|
747
|
+
const peerId = row.validatorPeerId ?? answer.validators.find((v) => v.identity === row.validatorIdentityKey.toLowerCase())?.peerId;
|
|
748
|
+
if (!poolOutput || !peerId) {
|
|
749
|
+
excluded.add(row.outpoint);
|
|
750
|
+
continue;
|
|
754
751
|
}
|
|
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 };
|
|
752
|
+
const common = { wallet, tokenId, meta, direction, leg, pool: row, poolOutput, commissionPkh: answer.market.commissionPkh, satsPerKb: feeRate };
|
|
753
|
+
const inputs = selectLegInputs(common, await swapSatsHoldings(wallet), buy ? [] : await swapHoldings(wallet, tokenId));
|
|
754
|
+
if (!inputs) {
|
|
755
|
+
why = "fees";
|
|
756
|
+
break;
|
|
766
757
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
758
|
+
const prepared = await prepareSwap({ ...common, inputs, ...(order ? { order } : {}), last: leg.amountIn === remaining, holdingBeef: await swapHoldingBeef(wallet) });
|
|
759
|
+
const at = { leg: outpointText(leg.outpoint), amountIn: leg.amountIn, amountOut: leg.amountOut };
|
|
760
|
+
progress({ phase: "relaying", ...at, filled: [...legs] });
|
|
761
|
+
let outcome = await relayLeg(wallet, af, base, prepared, peerId);
|
|
762
|
+
if (outcome.status === "unknown" || outcome.status === "timeout") {
|
|
763
|
+
// (a) The overlay: what the leg spends.
|
|
764
|
+
const spent = await readSpendsAll(swapBase, [outpointText(leg.outpoint), ...inputs.sats.map((r) => r.outpoint), ...inputs.tokens.map((r) => r.outpoint)], freshFetch).catch(() => []);
|
|
765
|
+
const ours = spent.find((x) => x.spentBy && x.outpoint !== outpointText(leg.outpoint));
|
|
766
|
+
if (ours) {
|
|
767
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
768
|
+
accountResolved(await resolve(wallet));
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (spent.some((x) => x.spentBy && x.outpoint === outpointText(leg.outpoint)))
|
|
772
|
+
continue;
|
|
773
|
+
// (b) Once more to the same validator.
|
|
774
|
+
outcome = await relayLeg(wallet, af, base, prepared, peerId);
|
|
775
|
+
if (outcome.status === "unknown" || outcome.status === "timeout") {
|
|
776
|
+
// (c) Race: the next leg to another pool with the same rows.
|
|
777
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
778
|
+
excluded.add(row.outpoint);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
770
781
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
782
|
+
if (outcome.status === "accepted") {
|
|
783
|
+
if (!outcome.completed.internalized) {
|
|
784
|
+
pending.push({ prepared, leg, poolId: row.poolId });
|
|
785
|
+
accountResolved(await resolve(wallet));
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
order ??= prepared.order;
|
|
789
|
+
budget -= 1;
|
|
790
|
+
filledIn += leg.amountIn;
|
|
791
|
+
filledOut += leg.amountOut;
|
|
792
|
+
legs.push({ poolOutpoint: at.leg, poolId: row.poolId, amountIn: leg.amountIn, amountOut: leg.amountOut, txid: outcome.txid });
|
|
776
793
|
continue;
|
|
777
794
|
}
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
|
|
795
|
+
if (outcome.status === "refused" && outcome.reason.startsWith("pool_spent")) {
|
|
796
|
+
// The relaying host took the validator's state in before it answered: the next read lists it.
|
|
797
|
+
progress({ phase: "repricing", ...at, filled: [...legs] });
|
|
781
798
|
}
|
|
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
|
-
})());
|
|
799
|
+
else {
|
|
800
|
+
excluded.add(row.outpoint);
|
|
805
801
|
}
|
|
806
|
-
|
|
807
|
-
|
|
802
|
+
accountResolved(await resolve(wallet));
|
|
803
|
+
}
|
|
804
|
+
// 3. The end.
|
|
805
|
+
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 };
|
|
806
|
+
if (why !== "filled" && pending.length === 0) {
|
|
807
|
+
const left = (await swapSatsHoldings(wallet)).length + (await swapHoldings(wallet, tokenId)).length;
|
|
808
|
+
if (left > 0) {
|
|
809
|
+
progress({ phase: "returning", filled: [...legs] });
|
|
810
|
+
try {
|
|
811
|
+
const m = await moveBack({ wallet, base: swapBase, tokenId, meta: { sym: p.sym }, submit: submitAt(swapBase) });
|
|
812
|
+
out.returned = { txid: m.txid, tokens: m.amount };
|
|
813
|
+
}
|
|
814
|
+
catch (e) {
|
|
815
|
+
out.returned = { error: errText(e) };
|
|
816
|
+
}
|
|
808
817
|
}
|
|
809
818
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
/** One relay of a prepared leg (0.34.0: no browser note); a thrown error is no answer. */
|
|
822
|
+
function relayLeg(wallet, af, base, prepared, peerId) {
|
|
823
|
+
return relaySwap({ wallet, authFetch: af, base }, prepared, peerId).catch((e) => ({ status: "unknown", id: swapIdOf(prepared), reason: errText(e) }));
|
|
813
824
|
}
|
|
814
825
|
/** 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
826
|
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
827
|
/** 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
828
|
async function walletSats() {
|
|
836
829
|
const { wallet } = needWallet();
|
|
@@ -853,15 +846,16 @@ function makeClient(opts, s) {
|
|
|
853
846
|
const [poolRows, history, list] = await Promise.all([basketRows(wallet, POOLS_BASKET), historyPoolRows(wallet), enabledList().catch(() => [])]);
|
|
854
847
|
const enabled = new Map(list.filter((t) => t.status === "enabled").map((t) => [t.tokenId, t]));
|
|
855
848
|
const filed = new Set(poolRows.map((r) => r.outpoint.replace("_", ".")));
|
|
856
|
-
// The rows by token, one per
|
|
849
|
+
// The rows by token, one per pool id (0.40.0: the deploy outpoint is the pool's id).
|
|
857
850
|
const byToken = new Map();
|
|
858
851
|
for (const row of positionRowsOf(mergePoolRows(history, poolRows))) {
|
|
859
852
|
if (!row.lpPubKey || !enabled.has(row.tokenId))
|
|
860
853
|
continue;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
854
|
+
const ids = byToken.get(row.tokenId) ?? new Map();
|
|
855
|
+
const poolId = row.deployOutpoint.replace(".", "_");
|
|
856
|
+
if (!ids.has(poolId))
|
|
857
|
+
ids.set(poolId, row);
|
|
858
|
+
byToken.set(row.tokenId, ids);
|
|
865
859
|
}
|
|
866
860
|
const out = [];
|
|
867
861
|
const closed = [];
|
|
@@ -879,10 +873,12 @@ function makeClient(opts, s) {
|
|
|
879
873
|
const meta = { sym: t?.sym ?? tokenId.slice(0, 8), dec: t?.dec ?? 0, ...(t?.icon ? { icon: t.icon } : {}) };
|
|
880
874
|
const beating = new Set((await readPools(base, f).catch(() => null))?.validators.map((x) => x.identity) ?? []);
|
|
881
875
|
for (const p of pools) {
|
|
882
|
-
const row = keys.get(p.
|
|
876
|
+
const row = keys.get(p.poolId);
|
|
883
877
|
const fees = { feesEarnedSats: p.sats, feesEarnedTokens: p.tokens };
|
|
878
|
+
if (!row)
|
|
879
|
+
continue;
|
|
884
880
|
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 });
|
|
881
|
+
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
882
|
continue;
|
|
887
883
|
}
|
|
888
884
|
const outpoint = p.outpoint.replace("_", ".");
|
|
@@ -892,6 +888,8 @@ function makeClient(opts, s) {
|
|
|
892
888
|
await fileDeployRow(wallet, base, history, outpoint, meta);
|
|
893
889
|
out.push({
|
|
894
890
|
outpoint,
|
|
891
|
+
poolId: p.poolId,
|
|
892
|
+
swaps: p.swaps,
|
|
895
893
|
tokenId,
|
|
896
894
|
...meta,
|
|
897
895
|
sats: p.current.bsvReserve,
|
|
@@ -939,7 +937,7 @@ function makeClient(opts, s) {
|
|
|
939
937
|
const t = (await instanceTerms(tokenId)).validator;
|
|
940
938
|
if (!t)
|
|
941
939
|
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 } };
|
|
940
|
+
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
941
|
}
|
|
944
942
|
/**
|
|
945
943
|
* Deploys a new position (src/lp/poolDeploy.ts, 0.13.0): this skein's
|
|
@@ -976,8 +974,8 @@ function makeClient(opts, s) {
|
|
|
976
974
|
}
|
|
977
975
|
/**
|
|
978
976
|
* 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?
|
|
977
|
+
* pays the miner fee). 0.40.0: the pool's current output and its BEEF by its pool id (`GET
|
|
978
|
+
* /@<token>/amm/pools/beef?pool=<deploy outpoint>`); the page's submit at the host root (D19).
|
|
981
979
|
*/
|
|
982
980
|
async function closePosition(req) {
|
|
983
981
|
const { wallet } = needWallet();
|
|
@@ -986,11 +984,11 @@ function makeClient(opts, s) {
|
|
|
986
984
|
await myPositions();
|
|
987
985
|
row = loaded.get(req.outpoint);
|
|
988
986
|
}
|
|
989
|
-
if (!row
|
|
987
|
+
if (!row)
|
|
990
988
|
throw new Error("No such position");
|
|
991
989
|
const [meta, base] = await Promise.all([tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
992
990
|
const m = meta.get(row.tokenId);
|
|
993
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
991
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
994
992
|
if (!poolOutput)
|
|
995
993
|
throw new Error(`the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
996
994
|
const prepared = await prepareClose({
|
|
@@ -1011,8 +1009,8 @@ function makeClient(opts, s) {
|
|
|
1011
1009
|
* `sats` deployed in ONE wallet createAction — the old pool the explicit input (as Close spends
|
|
1012
1010
|
* it), the new one output 0 affirmed by the validator at its terms now (`amm.pool.affirm`, as a
|
|
1013
1011
|
* 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?
|
|
1012
|
+
* wallet's change. The old pool's output and BEEF by its pool id (`GET
|
|
1013
|
+
* /@<token>/amm/pools/beef?pool=<id>`); the page's submit at the host root; the new pool's row
|
|
1016
1014
|
* filed in `amm-pools`.
|
|
1017
1015
|
*/
|
|
1018
1016
|
async function rebalancePosition(req) {
|
|
@@ -1024,14 +1022,14 @@ function makeClient(opts, s) {
|
|
|
1024
1022
|
await myPositions();
|
|
1025
1023
|
row = loaded.get(req.outpoint);
|
|
1026
1024
|
}
|
|
1027
|
-
if (!row
|
|
1025
|
+
if (!row)
|
|
1028
1026
|
throw new Error("No such position");
|
|
1029
1027
|
const [terms, held, meta, base] = await Promise.all([instanceTerms(row.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
1030
1028
|
const v = terms.validator;
|
|
1031
1029
|
if (!v)
|
|
1032
1030
|
throw new Error(NO_VALIDATOR);
|
|
1033
1031
|
const m = meta.get(row.tokenId);
|
|
1034
|
-
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.
|
|
1032
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.deployOutpoint.replace(".", "_")], freshFetch), req.outpoint);
|
|
1035
1033
|
if (!poolOutput)
|
|
1036
1034
|
throw new ClientError("stale_pool", `the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
1037
1035
|
const r = await rebalancePool({
|
|
@@ -1086,26 +1084,49 @@ function makeClient(opts, s) {
|
|
|
1086
1084
|
balance,
|
|
1087
1085
|
outputs: t?.valueOutputs ?? 0,
|
|
1088
1086
|
inSwaps: swaps.get(tokenId)?.amount ?? 0n,
|
|
1089
|
-
swapSats: swaps.get(tokenId)?.sats ?? 0,
|
|
1090
1087
|
onExchange: registered.has(tokenId),
|
|
1091
1088
|
};
|
|
1092
1089
|
});
|
|
1093
|
-
return out.filter((t) => t.balance > 0n || t.inSwaps > 0n
|
|
1090
|
+
return out.filter((t) => t.balance > 0n || t.inSwaps > 0n).sort((a, b) => a.sym.localeCompare(b.sym) || a.tokenId.localeCompare(b.tokenId));
|
|
1094
1091
|
}
|
|
1095
1092
|
/**
|
|
1096
|
-
* The
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1093
|
+
* The pass on load (0.40.0, src/wallet/swapPayouts.ts, src/market/activeSwap.ts): a labelled
|
|
1094
|
+
* staged token output the swap basket lacks filed from its action's description (its BEEF `GET
|
|
1095
|
+
* /@<token>/amm/tx?id=`), then the overlay pass over every `amm-swap` row (ONE `/amm/spends`, each
|
|
1096
|
+
* spender's `/tx`, this wallet's legs taken in). Nothing shown; a take-in reloads the pages' data.
|
|
1097
|
+
* Never throws. → the txids taken in.
|
|
1100
1098
|
*/
|
|
1101
1099
|
async function settleSwaps() {
|
|
1102
1100
|
if (!boundWallet)
|
|
1103
1101
|
return [];
|
|
1104
|
-
|
|
1105
|
-
|
|
1102
|
+
let filed = false;
|
|
1103
|
+
try {
|
|
1104
|
+
for (const u of await unfiledStagedTokens(boundWallet)) {
|
|
1105
|
+
const beef = await readTx(tokenBase(u.tokenId, swapBase), u.txid, freshFetch).catch(() => null);
|
|
1106
|
+
if (beef && (await fileStagedToken(boundWallet, u, beef).catch(() => false)))
|
|
1107
|
+
filed = true;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
/* the next load */
|
|
1112
|
+
}
|
|
1113
|
+
const taken = (await resolve(boundWallet)).map((t) => t.txid);
|
|
1114
|
+
if (filed || taken.length > 0)
|
|
1106
1115
|
emit();
|
|
1107
1116
|
return taken;
|
|
1108
1117
|
}
|
|
1118
|
+
/** The sats in the swap basket (0.40.0: shown once, not per token). */
|
|
1119
|
+
async function swapBasketSats() {
|
|
1120
|
+
const { wallet } = needWallet();
|
|
1121
|
+
return swapSatsTotal(wallet);
|
|
1122
|
+
}
|
|
1123
|
+
/** "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. */
|
|
1124
|
+
async function moveBackStagedSats() {
|
|
1125
|
+
const { wallet } = needWallet();
|
|
1126
|
+
const r = await moveBackSats(wallet);
|
|
1127
|
+
emit();
|
|
1128
|
+
return r;
|
|
1129
|
+
}
|
|
1109
1130
|
/**
|
|
1110
1131
|
* "Move back" (src/market/activeSwap.ts; 0.29.3: a module plan): every `amm-swap` row of the
|
|
1111
1132
|
* token (and its staged sats) spent in one action whose one output the wallet's module files
|
|
@@ -1227,15 +1248,13 @@ function makeClient(opts, s) {
|
|
|
1227
1248
|
}
|
|
1228
1249
|
/**
|
|
1229
1250
|
* 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.
|
|
1251
|
+
* validatorFeeBps, commissionBps, minLegSats?}}}` to the config instance's `<app>/admin` — a new
|
|
1252
|
+
* token instance's initial terms (its own `amm/terms`, root's message to `/@<tokenId>`, wins once
|
|
1253
|
+
* written). Refused here unless each fee is an integer 0 to 10 000 (0.40.0: and the minimum leg a
|
|
1254
|
+
* whole number of sats; 0 or absent is none and is not sent).
|
|
1233
1255
|
*/
|
|
1234
1256
|
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 } });
|
|
1257
|
+
await sendDefaults({ terms: termsArgs(t) });
|
|
1239
1258
|
}
|
|
1240
1259
|
/** 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
1260
|
async function tokenTerms(tokenId) {
|
|
@@ -1243,19 +1262,15 @@ function makeClient(opts, s) {
|
|
|
1243
1262
|
}
|
|
1244
1263
|
/**
|
|
1245
1264
|
* 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
|
|
1265
|
+
* {tokenId, terms: {lpFeeBps, validatorFeeBps, commissionBps, minLegSats?} | null}}` to the swap instance's
|
|
1247
1266
|
* `<app>/admin`; swap emits it as a fact and the host sends it to the token's instance as root's
|
|
1248
1267
|
* `terms` (null: a market only). Nothing answers back: the token's `/terms` shows the result.
|
|
1249
1268
|
* Refused here unless each fee is an integer 0 to 10 000.
|
|
1250
1269
|
*/
|
|
1251
1270
|
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`);
|
|
1271
|
+
const terms = t ? termsArgs(t) : null;
|
|
1256
1272
|
const { signedFor } = needWallet();
|
|
1257
1273
|
const me = await swapIdentity();
|
|
1258
|
-
const terms = t ? { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } : null;
|
|
1259
1274
|
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "terms", args: { tokenId: sdkTokenId(tokenId), terms } });
|
|
1260
1275
|
}
|
|
1261
1276
|
/** Clears the default validator terms (0.17.0): `{fn: "defaults", args: {terms: null}}`; a new token instance is a market only. */
|
|
@@ -1272,6 +1287,152 @@ function makeClient(opts, s) {
|
|
|
1272
1287
|
async function clearDefaultMarket() {
|
|
1273
1288
|
await sendDefaults({ market: null });
|
|
1274
1289
|
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Sets the price to enable a token (0.42.0, open-exchange 0.15.0): the admin's `{fn: "price", args: {sats}}`
|
|
1292
|
+
* to swap's `<app>/admin`; the host sets it as the price of `POST /amm/enable` (any signed sender enables a
|
|
1293
|
+
* token for it). `null` turns the paid enable off. Refused here unless a whole number of sats, 1 or more.
|
|
1294
|
+
*/
|
|
1295
|
+
async function setEnablePrice(sats) {
|
|
1296
|
+
if (sats !== null && (!Number.isSafeInteger(sats) || sats < 1))
|
|
1297
|
+
throw new Error("sats: a whole number, 1 or more (or null: off)");
|
|
1298
|
+
const { signedFor } = needWallet();
|
|
1299
|
+
const me = await swapIdentity();
|
|
1300
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "price", args: { sats } });
|
|
1301
|
+
emit();
|
|
1302
|
+
}
|
|
1303
|
+
/** The exchange's wallet (0.42.0, open-exchange 0.15.0; `GET <swap>/wallet`, the host's read): the sats swap's wallet holds and its coin count. */
|
|
1304
|
+
async function exchangeWallet() {
|
|
1305
|
+
return readWallet(swapBase, ff);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Enables a token for the exchange's fee (0.42.2, open-exchange 0.15.3: a BEEF submission; anyone signed, not the
|
|
1309
|
+
* admin): the price from `GET <swap>/defaults` (`enablePriceSats`; none → refused, "paid enable is off"); the
|
|
1310
|
+
* exchange's identity from `GET <swap's origin>/.well-known/skein`; the deploy's BEEF given, else discovery's
|
|
1311
|
+
* (`GET <origin>/@discovery/1color/<token>/deploy`). The payment, ts-stack's standard (sdk BasicBRC29): a prefix and
|
|
1312
|
+
* a suffix from `createNonce`, the payee key `getPublicKey({protocolID: [2, "3241645161d8"], keyID: "<prefix>
|
|
1313
|
+
* <suffix>", counterparty: <the exchange>})`, ONE P2PKH output of exactly the price, `createAction` with
|
|
1314
|
+
* `{randomizeOutputs: false, noSend: true}` — built and signed, NOT sent: the exchange's take-in sends it. Then ONE
|
|
1315
|
+
* BEEF merging the deploy's and the payment's, `POST <swap>/enable?tokenId=&paymentTxid=&paymentVout=0
|
|
1316
|
+
* &derivationPrefix=&derivationSuffix=` signed (BRC-104). On any refusal or failure after the payment was built, the
|
|
1317
|
+
* unsent payment is aborted in the wallet (`abortAction`), so its coins are free again. → the enabled row.
|
|
1318
|
+
* Refusals are `ClientError` kind `refused` (the exchange's code first in the message); a fetch that does not get
|
|
1319
|
+
* through, `network`; the user declining in the wallet, `user_rejected`.
|
|
1320
|
+
*/
|
|
1321
|
+
async function enableTokenPaid(tokenId, o = {}) {
|
|
1322
|
+
const { wallet, signedFor } = needWallet();
|
|
1323
|
+
const id = parseOutpoint(sdkTokenId(tokenId).replace("_", "."));
|
|
1324
|
+
if (!id)
|
|
1325
|
+
throw new Error(`not a token id: ${tokenId}`);
|
|
1326
|
+
const canonical = tokenIdText(id);
|
|
1327
|
+
const price = (await readDefaults(swapBase, freshFetch)).enablePriceSats;
|
|
1328
|
+
if (!price)
|
|
1329
|
+
throw new ClientError("refused", "paid enable is off");
|
|
1330
|
+
const exchange = await swapIdentity();
|
|
1331
|
+
let deploy = o.deployBeef;
|
|
1332
|
+
if (!deploy) {
|
|
1333
|
+
const res = await freshFetch(`${discoveryBase()}/${canonical}/deploy`);
|
|
1334
|
+
if (!res.ok)
|
|
1335
|
+
throw new ClientError("refused", `no deploy for ${canonical} on discovery (${res.status}): pass deployBeef`);
|
|
1336
|
+
deploy = Array.from(new Uint8Array(await res.arrayBuffer()));
|
|
1337
|
+
}
|
|
1338
|
+
const derivationPrefix = await createNonce(wallet, "self");
|
|
1339
|
+
const derivationSuffix = await createNonce(wallet, "self");
|
|
1340
|
+
const { publicKey } = await wallet.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: exchange });
|
|
1341
|
+
const created = await wallet.createAction({
|
|
1342
|
+
description: "Open Exchange: enable a token",
|
|
1343
|
+
outputs: [{ satoshis: price, lockingScript: p2pkhHexOf(publicKey), outputDescription: "Open Exchange enable fee", customInstructions: JSON.stringify({ derivationPrefix, derivationSuffix, payee: exchange }) }],
|
|
1344
|
+
options: { randomizeOutputs: false, noSend: true },
|
|
1345
|
+
});
|
|
1346
|
+
const txid = created.txid;
|
|
1347
|
+
if (!txid || !created.tx)
|
|
1348
|
+
throw new Error("the wallet built no payment");
|
|
1349
|
+
try {
|
|
1350
|
+
const b = new Beef();
|
|
1351
|
+
b.mergeBeef(deploy);
|
|
1352
|
+
b.mergeBeef(created.tx);
|
|
1353
|
+
const q = new URLSearchParams({ tokenId: canonical, paymentTxid: txid, paymentVout: "0", derivationPrefix, derivationSuffix });
|
|
1354
|
+
let res;
|
|
1355
|
+
try {
|
|
1356
|
+
res = await signedFor(swapBase).fetch(`${swapBase}/enable?${q.toString()}`, { method: "POST", headers: { "content-type": "application/octet-stream" }, body: Uint8Array.from(b.toBinary()) });
|
|
1357
|
+
}
|
|
1358
|
+
catch (e) {
|
|
1359
|
+
// The stock AuthFetch takes every 402 for its own payment wire and throws on this route's (no x-bsv-payment-* headers).
|
|
1360
|
+
if (/x-bsv-payment/.test(errText(e)))
|
|
1361
|
+
throw new ClientError("refused", "ERR_PAYMENT_REQUIRED: the exchange did not take this payment (402; its price may have changed)", { cause: e });
|
|
1362
|
+
throw e;
|
|
1363
|
+
}
|
|
1364
|
+
const v = (await res.json().catch(() => ({})));
|
|
1365
|
+
if (res.status !== 200)
|
|
1366
|
+
throw new ClientError("refused", `${String(v.code ?? res.status)}: ${String(v.description ?? v.message ?? "refused")}`);
|
|
1367
|
+
s.listRead = null;
|
|
1368
|
+
emit();
|
|
1369
|
+
return v.result;
|
|
1370
|
+
}
|
|
1371
|
+
catch (e) {
|
|
1372
|
+
await wallet.abortAction({ reference: txid }).catch(() => { });
|
|
1373
|
+
throw e;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/** A withdrawal's payment taken in by the admin's wallet: `internalizeAction`, output `outputIndex` as a wallet payment from swap (BRC-29, ts-stack's recipient side). */
|
|
1377
|
+
async function takeInWithdrawal(wallet, x, tx) {
|
|
1378
|
+
const r = await wallet.internalizeAction({
|
|
1379
|
+
tx,
|
|
1380
|
+
outputs: [{ outputIndex: x.outputIndex, protocol: WALLET_PAYMENT, paymentRemittance: { derivationPrefix: x.derivationPrefix, derivationSuffix: x.derivationSuffix, senderIdentityKey: x.senderIdentityKey } }],
|
|
1381
|
+
description: `Open Exchange withdraw ${x.n}`,
|
|
1382
|
+
});
|
|
1383
|
+
return r.accepted;
|
|
1384
|
+
}
|
|
1385
|
+
/** Withdrawal `n`'s Atomic BEEF (`GET <swap>/wallet/tx?n=`). */
|
|
1386
|
+
async function withdrawalTx(n) {
|
|
1387
|
+
const res = await freshFetch(`${swapBase}/wallet/tx?n=${n}`);
|
|
1388
|
+
if (!res.ok)
|
|
1389
|
+
throw new Error(`GET ${swapBase}/wallet/tx?n=${n}: ${res.status}`);
|
|
1390
|
+
return Array.from(new Uint8Array(await res.arrayBuffer()));
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Withdraws everything in the exchange's wallet to the admin's own (0.42.1, open-exchange 0.15.2): the admin's
|
|
1394
|
+
* `{fn: "withdraw"}` to swap's `<app>/admin`. swap's program pays every coin to a BRC-29 key derived for the
|
|
1395
|
+
* sender and hands the transaction over unsent; this function waits for the read to list it (`GET <swap>/wallet`,
|
|
1396
|
+
* at most 10 s — the message's answer reaches no page), then takes it in (`internalizeAction`, a wallet payment:
|
|
1397
|
+
* that is what sends it). → the withdrawal.
|
|
1398
|
+
*/
|
|
1399
|
+
async function withdrawAll() {
|
|
1400
|
+
const { wallet, signedFor } = needWallet();
|
|
1401
|
+
const w = await readWallet(swapBase, freshFetch);
|
|
1402
|
+
if (w.sats <= 0)
|
|
1403
|
+
throw new Error("the exchange's wallet holds nothing to withdraw");
|
|
1404
|
+
const me = await swapIdentity();
|
|
1405
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "withdraw" });
|
|
1406
|
+
let x;
|
|
1407
|
+
for (let i = 0; i < 40 && !x; i++) {
|
|
1408
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1409
|
+
x = (await readWallet(swapBase, freshFetch)).withdrawals.find((y) => y.n > w.withdrawals.length);
|
|
1410
|
+
}
|
|
1411
|
+
if (!x)
|
|
1412
|
+
throw new Error("the exchange has not answered the withdraw yet: Settings takes it in when it does");
|
|
1413
|
+
if (!(await takeInWithdrawal(wallet, x, await withdrawalTx(x.n))))
|
|
1414
|
+
throw new Error("the wallet did not accept the withdrawal");
|
|
1415
|
+
emit();
|
|
1416
|
+
return x;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* The latest withdrawal taken in again (0.42.1; on Settings load): for a page lost between the withdraw's answer
|
|
1420
|
+
* and its take-in (taking in a payment the wallet holds is harmless). Never throws. → its txid, or none.
|
|
1421
|
+
*/
|
|
1422
|
+
async function settleWithdrawals() {
|
|
1423
|
+
if (!boundWallet)
|
|
1424
|
+
return [];
|
|
1425
|
+
try {
|
|
1426
|
+
const w = await readWallet(swapBase, freshFetch);
|
|
1427
|
+
const x = w.withdrawals.at(-1);
|
|
1428
|
+
if (!x)
|
|
1429
|
+
return [];
|
|
1430
|
+
return (await takeInWithdrawal(boundWallet, x, await withdrawalTx(x.n))) ? [x.txid] : [];
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return [];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1275
1436
|
/** The discovery instance's record (0.18.0, shruggr/skein#137; `GET <config>/discovery`): `{on: false}` until root turns it on. */
|
|
1276
1437
|
async function discoveryState() {
|
|
1277
1438
|
return readDiscovery(swapBase, ff);
|
|
@@ -1311,11 +1472,15 @@ function makeClient(opts, s) {
|
|
|
1311
1472
|
isRoot,
|
|
1312
1473
|
claimExchange: guard(claimExchange),
|
|
1313
1474
|
hostedTokens: guard(hostedTokens),
|
|
1314
|
-
|
|
1475
|
+
tokenPools: guard(tokenPools),
|
|
1476
|
+
watchPoolEvents,
|
|
1315
1477
|
// history (0.39.0)
|
|
1316
1478
|
trades: guard(trades),
|
|
1317
1479
|
trade: guard(trade),
|
|
1318
1480
|
candles: guard(candles),
|
|
1481
|
+
poolsAt: guard(poolsAt),
|
|
1482
|
+
poolHistory: guard(poolHistory),
|
|
1483
|
+
spendsAll: guard(spendsAll),
|
|
1319
1484
|
stats: guard(stats),
|
|
1320
1485
|
statsAll: guard(statsAll),
|
|
1321
1486
|
holders: guard(holders),
|
|
@@ -1324,7 +1489,6 @@ function makeClient(opts, s) {
|
|
|
1324
1489
|
// swap
|
|
1325
1490
|
quoteSwap,
|
|
1326
1491
|
executeSwap,
|
|
1327
|
-
checkSwapAgain: guard(checkSwapAgain),
|
|
1328
1492
|
walletSats: guard(walletSats),
|
|
1329
1493
|
// liquidity
|
|
1330
1494
|
myPositions: guard(myPositions),
|
|
@@ -1335,6 +1499,8 @@ function makeClient(opts, s) {
|
|
|
1335
1499
|
// your tokens
|
|
1336
1500
|
myTokens: guard(myTokens),
|
|
1337
1501
|
settleSwaps: guard(settleSwaps),
|
|
1502
|
+
swapBasketSats: guard(swapBasketSats),
|
|
1503
|
+
moveBackStagedSats: guard(moveBackStagedSats),
|
|
1338
1504
|
moveBackTokens: guard(moveBackTokens),
|
|
1339
1505
|
sendTokens: guard(sendTokens),
|
|
1340
1506
|
// settings
|
|
@@ -1351,5 +1517,10 @@ function makeClient(opts, s) {
|
|
|
1351
1517
|
clearDefaultMarket: guard(clearDefaultMarket),
|
|
1352
1518
|
discoveryState: guard(discoveryState),
|
|
1353
1519
|
setDiscovery: guard(setDiscovery),
|
|
1520
|
+
setEnablePrice: guard(setEnablePrice),
|
|
1521
|
+
exchangeWallet: guard(exchangeWallet),
|
|
1522
|
+
withdrawAll: guard(withdrawAll),
|
|
1523
|
+
enableTokenPaid: guard(enableTokenPaid),
|
|
1524
|
+
settleWithdrawals,
|
|
1354
1525
|
};
|
|
1355
1526
|
}
|