@dcasegr/amm-client 0.0.0-stage → 0.38.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 +107 -2
- package/dist/client.d.ts +534 -0
- package/dist/client.js +1319 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +53 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.js +64 -0
- package/dist/lib/keys.d.ts +42 -0
- package/dist/lib/keys.js +48 -0
- package/dist/lib/overlay.d.ts +190 -0
- package/dist/lib/overlay.js +302 -0
- package/dist/lib/skein.d.ts +82 -0
- package/dist/lib/skein.js +142 -0
- package/dist/lib/tokenId.d.ts +72 -0
- package/dist/lib/tokenId.js +121 -0
- package/dist/lp/amounts.d.ts +12 -0
- package/dist/lp/amounts.js +38 -0
- package/dist/lp/close.d.ts +90 -0
- package/dist/lp/close.js +140 -0
- package/dist/lp/images.d.ts +11 -0
- package/dist/lp/images.js +45 -0
- package/dist/lp/inventory.d.ts +68 -0
- package/dist/lp/inventory.js +84 -0
- package/dist/lp/poolDeploy.d.ts +220 -0
- package/dist/lp/poolDeploy.js +291 -0
- package/dist/lp/poolRows.d.ts +13 -0
- package/dist/lp/poolRows.js +19 -0
- package/dist/lp/positions.d.ts +81 -0
- package/dist/lp/positions.js +144 -0
- package/dist/lp/rebalance.d.ts +101 -0
- package/dist/lp/rebalance.js +168 -0
- package/dist/lp/wallet.d.ts +12 -0
- package/dist/lp/wallet.js +19 -0
- package/dist/market/activeSwap.d.ts +158 -0
- package/dist/market/activeSwap.js +320 -0
- package/dist/market/plan.d.ts +85 -0
- package/dist/market/plan.js +149 -0
- package/dist/market/prices.d.ts +13 -0
- package/dist/market/prices.js +14 -0
- package/dist/market/relay.d.ts +84 -0
- package/dist/market/relay.js +201 -0
- package/dist/market/swapAction.d.ts +306 -0
- package/dist/market/swapAction.js +508 -0
- package/dist/market/swapFlow.d.ts +58 -0
- package/dist/market/swapFlow.js +86 -0
- package/dist/market/view.d.ts +58 -0
- package/dist/market/view.js +63 -0
- package/dist/pool/artifact.d.ts +23 -0
- package/dist/pool/artifact.js +38 -0
- package/dist/pool/index.d.ts +8 -0
- package/dist/pool/index.js +8 -0
- package/dist/pool/oneColor.d.ts +19 -0
- package/dist/pool/oneColor.js +39 -0
- package/dist/pool/pool.artifact.json +5069 -0
- package/dist/pool/runar-ir-schema.d.ts +21 -0
- package/dist/pool/runar-ir-schema.js +18 -0
- package/dist/pool/template.d.ts +295 -0
- package/dist/pool/template.js +715 -0
- package/dist/vendor/runar/LICENSE +21 -0
- package/dist/vendor/runar/NOTICE +15 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
- package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
- package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
- package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
- package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
- package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
- package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
- package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
- package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
- package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
- package/dist/vendor/runar/runar-sdk.d.ts +9 -0
- package/dist/vendor/runar/runar-sdk.js +8 -0
- package/dist/wallet/action.d.ts +28 -0
- package/dist/wallet/action.js +42 -0
- package/dist/wallet/brc29.d.ts +83 -0
- package/dist/wallet/brc29.js +80 -0
- package/dist/wallet/color.d.ts +110 -0
- package/dist/wallet/color.js +134 -0
- package/dist/wallet/swapPayouts.d.ts +35 -0
- package/dist/wallet/swapPayouts.js +146 -0
- package/package.json +39 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Open Exchange client (skein-amm 0.38.0, D8–D14, agent-env plans/history-api-2026-11-05.md):
|
|
3
|
+
* every read and every action the pages make, bound by `createClient` to one exchange, one miner
|
|
4
|
+
* fee rate, one wallet and one fetch — configuration as arguments (D9): nothing here reads
|
|
5
|
+
* `import.meta.env` or `location`. No React; the pages (src/data/exchange.ts) hold a client and
|
|
6
|
+
* wrap it in their hooks. Published as `@dcasegr/amm-client` (src/index.ts; README "The packages").
|
|
7
|
+
*
|
|
8
|
+
* 0.34.0 (agent-env plans/exchange-api-2026-11-04.md §0, D1–D25; docs/AMM.md "The public read
|
|
9
|
+
* API"): every read is a plain GET of the exchange's public read API (src/lib/overlay.ts), and the
|
|
10
|
+
* client sends `/lookup` nowhere. Where each comes from:
|
|
11
|
+
*
|
|
12
|
+
* - **The token list** (`GET <swap>/tokens`): read ONCE per client (D4; `enabledList`), again
|
|
13
|
+
* only after its own enable or disable changed it.
|
|
14
|
+
* - **Prices** (D6, D22): the host-wide pools route `GET <origin>/amm/pools` (`hostedTokens`,
|
|
15
|
+
* `watchHostedPools`); a token's price is Σsats / Σtokens over its listed pools.
|
|
16
|
+
* - **Swap** (D11–D16): the token's pools route `GET /@<token>/amm/pools` held as an event stream
|
|
17
|
+
* (`watchTokenPools`): the pools whose validator is beating and whose commission meets the
|
|
18
|
+
* market's minimum (the host's filter), the market's commission key hash; a quote is the
|
|
19
|
+
* matching engine's plan over that list (no request); a swap reads ONE `GET
|
|
20
|
+
* /@<token>/amm/pools/beef?lp=` for its legs (freshness, the pools' BEEF), then each leg's relay
|
|
21
|
+
* request (`amm.swap.submit`, held by the host until the relay rests; 15 s expiry); a lost answer
|
|
22
|
+
* is "no answer yet" with "Check again" (`GET /@<token>/amm/swaps?id=`, once).
|
|
23
|
+
* - **Liquidity** (D9, D23): the wallet's pool rows and deploy history, then ONE `GET
|
|
24
|
+
* /@<token>/amm/positions?lp=` per token; a deploy the history has but the basket lacks is
|
|
25
|
+
* filed then; Close and Rebalance read their pool's BEEF by LP key (`/pools/beef?lp=`); the
|
|
26
|
+
* deploy's terms `GET /@<token>/amm/terms`.
|
|
27
|
+
* - **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; a
|
|
29
|
+
* silent pass over the swap basket's staged legs (src/wallet/swapPayouts.ts); Move back submits
|
|
30
|
+
* at the host root.
|
|
31
|
+
* - **Settings** (D20, D21): the claim card (`claimStatus`: swap's identity, the recipient of
|
|
32
|
+
* root's messages), the token list once, discovery's list and ONE `?ids=` read; Enable's deploy
|
|
33
|
+
* BEEF from the wallet's basket else `GET /@discovery/1color/<token>/deploy`.
|
|
34
|
+
*
|
|
35
|
+
* Ids (David, 2026-10-09): every token id is the BRC-207 assetId
|
|
36
|
+
* `<txid>_<vout>`, `_0` included for a 1Color token (src/lib/tokenId.ts);
|
|
37
|
+
* an outpoint is `<txid>.<vout>`. Amounts are bigints in base units; prices
|
|
38
|
+
* are numbers: sats per whole token.
|
|
39
|
+
*/
|
|
40
|
+
import { Beef, Transaction } from "@bsv/sdk";
|
|
41
|
+
import { send1Color } from "@1sat/actions";
|
|
42
|
+
import { computeSwap } from "@dcasegr/amm-engine";
|
|
43
|
+
import { outpointText, parseOutpoint, parseTokenId, sdkTokenId, tokenIdText } from "./lib/tokenId.js";
|
|
44
|
+
import { DEFAULT_FEE_RATE_SATS_PER_KB, appName, exchangeBase, tokenBase } from "./lib/config.js";
|
|
45
|
+
import { ammLookupService, liveOfPools, parseAllPools, parsePools, parseTokenTopic, poolOutputOf, poolsUrl, readDiscoveryDeploy, readDiscoveryIds, readDiscoveryList, readPoolsBeef, readPools, readPositions, readTerms, readTx, watchPools, } from "./lib/overlay.js";
|
|
46
|
+
import { authFetchPerInstance, instanceBase, readDefaults, readDiscovery, readTokens, sendMessage } from "./lib/skein.js";
|
|
47
|
+
import { depthOf } from "./market/prices.js";
|
|
48
|
+
import { buildPlanRequest, goneFromLookup, quote as planQuote } from "./market/plan.js";
|
|
49
|
+
import { validatorStatus } from "./market/view.js";
|
|
50
|
+
import { planSwapLeg, prepareSwap, selectExactSatsInput, selectExactTokenInputs } from "./market/swapAction.js";
|
|
51
|
+
import { moveBack, sendSatsToSwapBasket, sendToSwapBasket, swapHoldingBeef, swapHoldings, swapSatsHoldings, swapTotals } from "./market/activeSwap.js";
|
|
52
|
+
import { checkAgain, relaySwap, swapIdOf } from "./market/swapFlow.js";
|
|
53
|
+
import { settleStagedSwaps } from "./wallet/swapPayouts.js";
|
|
54
|
+
import { formatAmount } from "./lp/amounts.js";
|
|
55
|
+
import { basketRows } from "./lp/wallet.js";
|
|
56
|
+
import { buildInventory, decodeTokenRow, deployIcon } from "./lp/inventory.js";
|
|
57
|
+
import { imageDataUrl } from "./lp/images.js";
|
|
58
|
+
import { deployPool } from "./lp/poolDeploy.js";
|
|
59
|
+
import { rebalancePool } from "./lp/rebalance.js";
|
|
60
|
+
import { historyPoolRows, mergePoolRows, positionRowsOf } from "./lp/positions.js";
|
|
61
|
+
import { POOLS_BASKET, assetBasket, heldOutputs } from "./wallet/color.js";
|
|
62
|
+
import { prepareClose, submitToOverlay } from "./lp/close.js";
|
|
63
|
+
import { ClientError, errorKind, kindOfOutcome, toClientError } from "./errors.js";
|
|
64
|
+
/**
|
|
65
|
+
* `amm.pool.terms`'s refusal when no terms are set (0.15.0, David Case
|
|
66
|
+
* 2026-10-23: terms are a runtime record root sets; unset, a market only).
|
|
67
|
+
*/
|
|
68
|
+
export const NO_VALIDATOR = "no validator here";
|
|
69
|
+
/** What the pages show in place of the terms when none are set. */
|
|
70
|
+
export const NO_TERMS_TEXT = "This exchange validates nothing: no terms are set.";
|
|
71
|
+
/** Whether `t` is a validator's terms (not "none set"). */
|
|
72
|
+
export function hasTerms(t) {
|
|
73
|
+
return !!t && !("none" in t);
|
|
74
|
+
}
|
|
75
|
+
/** The metadata of the deploy output `vout` of an Atomic BEEF's subject; null when that output is no 1Color deploy. */
|
|
76
|
+
function deployMetaOf(beef, vout) {
|
|
77
|
+
const tx = Transaction.fromAtomicBEEF(beef);
|
|
78
|
+
const t = decodeTokenRow({ outpoint: `${tx.id("hex")}.${vout}`, lockingScript: tx.outputs[vout]?.lockingScript.toHex() });
|
|
79
|
+
if (!t || t.role !== "deploy")
|
|
80
|
+
return null;
|
|
81
|
+
const icon = deployIcon(t)?.image;
|
|
82
|
+
return { ...(t.metadata?.sym !== undefined ? { sym: t.metadata.sym } : {}), ...(t.metadata?.dec !== undefined ? { dec: t.metadata.dec } : {}), ...(icon ? { icon: imageDataUrl(icon) } : {}) };
|
|
83
|
+
}
|
|
84
|
+
function symOf(meta, tokenId) {
|
|
85
|
+
return meta.get(tokenId)?.sym ?? tokenId.slice(0, 8);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The hosted tokens (the enabled 1Color tokens at output 0), each priced from its rows of the
|
|
89
|
+
* host-wide pools route (D6, D22: P1, the pools whose validator is beating): Σsats / Σtokens.
|
|
90
|
+
* `rows` null: not read yet (no prices). Pure.
|
|
91
|
+
*/
|
|
92
|
+
export function hostedFrom(list, rows) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const t of list) {
|
|
95
|
+
if (t.status !== "enabled")
|
|
96
|
+
continue;
|
|
97
|
+
const topic = parseTokenTopic(t.topic);
|
|
98
|
+
if (!topic || topic.kind !== "native")
|
|
99
|
+
continue;
|
|
100
|
+
const mine = (rows ?? []).filter((r) => r.tokenId !== undefined && canonicalId(r.tokenId) === t.tokenId);
|
|
101
|
+
const sats = mine.reduce((a, p) => a + p.bsvReserve, 0n);
|
|
102
|
+
const tokens = mine.reduce((a, p) => a + p.tokenReserve, 0n);
|
|
103
|
+
out.push({
|
|
104
|
+
tokenId: t.tokenId,
|
|
105
|
+
sym: t.sym || t.tokenId.slice(0, 8),
|
|
106
|
+
dec: t.dec,
|
|
107
|
+
...(t.icon ? { icon: t.icon } : {}),
|
|
108
|
+
marginalPrice: tokens > 0n ? Number(sats) / (Number(tokens) / 10 ** t.dec) : null,
|
|
109
|
+
depth: depthOf(sats),
|
|
110
|
+
pools: mine.length,
|
|
111
|
+
reserves: { sats, tokens },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** The pools the Swap page lists (D11, D13): the token's pools route's rows, every validator live (the host lists no other). */
|
|
117
|
+
export function poolViews(answer, tokenId) {
|
|
118
|
+
return (answer?.pools ?? []).map((p) => ({
|
|
119
|
+
outpoint: outpointText(p.outpoint),
|
|
120
|
+
tokenId,
|
|
121
|
+
sats: p.bsvReserve,
|
|
122
|
+
tokens: p.tokenReserve,
|
|
123
|
+
validator: { identityKey: p.validatorIdentityKey, live: true },
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
/** The fees a pool's own record names (D13: the Swap page's fee line), basis points. */
|
|
127
|
+
export function poolFees(p) {
|
|
128
|
+
return { lpBps: Number(p.liquidityFeeBps), validatorBps: Number(p.validationFeeBps), commissionBps: Number(p.commissionBps ?? 0n) };
|
|
129
|
+
}
|
|
130
|
+
function swapResultOf(legs) {
|
|
131
|
+
const filled = legs.filter((l) => l.status === "filled").length;
|
|
132
|
+
return { status: filled === legs.length && legs.length > 0 ? "filled" : filled === 0 ? "failed" : "partial", legs };
|
|
133
|
+
}
|
|
134
|
+
/** A swap's result with one leg replaced (after "Check again"). */
|
|
135
|
+
export function withLeg(r, leg) {
|
|
136
|
+
return swapResultOf(r.legs.map((l) => (l.poolOutpoint === leg.poolOutpoint ? leg : l)));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* wallet-toolbox's `specOpWalletBalance` (`sdk.specOpWalletBalance`, @bsv/wallet-toolbox-client): the listOutputs "basket" of its
|
|
140
|
+
* balance special operation. Written out, not imported: the client package exports it only inside its `sdk` namespace, which
|
|
141
|
+
* pulls ~560 kB into the bundle; test/exchange.test.ts checks it equals the toolbox's.
|
|
142
|
+
*/
|
|
143
|
+
export const SPEC_OP_WALLET_BALANCE = "893b7646de0e1c9f741bd6e9169b76a8847ae34adef7bef1e6a285371206d2e8";
|
|
144
|
+
/** A filed row's customInstructions with the token's symbol and decimals in (as a deploy files them), unless they are there. */
|
|
145
|
+
function withMeta(customInstructions, sym, dec) {
|
|
146
|
+
if (!customInstructions)
|
|
147
|
+
return customInstructions;
|
|
148
|
+
try {
|
|
149
|
+
const ci = JSON.parse(customInstructions);
|
|
150
|
+
if (ci.sym !== undefined || (sym === undefined && dec === undefined))
|
|
151
|
+
return customInstructions;
|
|
152
|
+
const { id, op, ...rest } = ci;
|
|
153
|
+
return JSON.stringify({ id, op, ...(sym ? { sym } : {}), ...(dec !== undefined ? { dec: String(dec) } : {}), ...rest });
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return customInstructions;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** The topic a token's pools are admitted under (BRC-207; skein-amm 0.8.0): derived, never typed. */
|
|
160
|
+
export function topicOf(tokenId) {
|
|
161
|
+
return `tm_color_${sdkTokenId(tokenId)}`;
|
|
162
|
+
}
|
|
163
|
+
/** The lookup service of a token (BRC-207): derived, never typed. */
|
|
164
|
+
export function lookupOf(tokenId) {
|
|
165
|
+
return `ls_color_${sdkTokenId(tokenId)}`;
|
|
166
|
+
}
|
|
167
|
+
/** The AMM lookup's service of a token, `ls_amm_<assetId>` (0.21.0): a token instance's config names it, so it beats its prices on `ls_amm_<assetId>-live` (skein-overlay 0.17.0: a configured lookup beats). */
|
|
168
|
+
export function ammLookupOf(tokenId) {
|
|
169
|
+
return ammLookupService(tokenId);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A token deploy's BEEF from the connected wallet (0.17.0), as the 1Color tokens page gets it
|
|
173
|
+
* (skein-1color web/src/overlay.ts `walletDeployBeef`): the deploy output in the module's basket
|
|
174
|
+
* `p color <txid>v<vout>` (then its `authority` basket), its transaction as BEEF (`include: "entire
|
|
175
|
+
* transactions"`), as an Atomic BEEF of the deploy. Undefined when the wallet holds it in neither
|
|
176
|
+
* (not the deployer's, or the output spent).
|
|
177
|
+
*/
|
|
178
|
+
export async function walletDeployBeef(wallet, tokenId) {
|
|
179
|
+
const op = parseOutpoint(sdkTokenId(tokenId).replace("_", "."));
|
|
180
|
+
if (!op)
|
|
181
|
+
throw new Error(`not a token id: ${tokenId}`);
|
|
182
|
+
const assetId = `${op.txid}_${op.vout}`;
|
|
183
|
+
for (const basket of [assetBasket(assetId, "value"), assetBasket(assetId, "authority")]) {
|
|
184
|
+
const r = await wallet.listOutputs({ basket, include: "entire transactions", limit: 100 });
|
|
185
|
+
const held = r.outputs.some((o) => o.outpoint === `${op.txid}.${op.vout}`);
|
|
186
|
+
if (held && r.BEEF && r.BEEF.length > 0)
|
|
187
|
+
return Beef.fromBinary(Array.from(r.BEEF)).toBinaryAtomic(op.txid);
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const canonicalId = (tokenId) => {
|
|
192
|
+
const ref = parseTokenId(tokenId);
|
|
193
|
+
return ref ? tokenIdText(ref) : null;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* The candidates to enable: discovery's token list (its order, newest first, with its metadata),
|
|
197
|
+
* then the tokens only this wallet holds (in the order held); every token already on the swap
|
|
198
|
+
* instance's list (enabled or disabled) left out. Pure: the reads are `tokensToEnable`'s.
|
|
199
|
+
*/
|
|
200
|
+
export function enableCandidates(listed, walletIds, onList) {
|
|
201
|
+
const skip = new Set(onList.map((t) => canonicalId(t) ?? t));
|
|
202
|
+
const held = new Set(walletIds.map((t) => canonicalId(t) ?? t));
|
|
203
|
+
const out = [];
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const t of listed) {
|
|
206
|
+
const id = canonicalId(t.tokenId);
|
|
207
|
+
if (!id || skip.has(id) || seen.has(id))
|
|
208
|
+
continue;
|
|
209
|
+
seen.add(id);
|
|
210
|
+
out.push({ tokenId: id, ...(t.sym ? { sym: t.sym } : {}), ...(t.icon ? { icon: t.icon } : {}), onDiscovery: true, inWallet: held.has(id) });
|
|
211
|
+
}
|
|
212
|
+
for (const w of walletIds) {
|
|
213
|
+
const id = canonicalId(w);
|
|
214
|
+
if (!id || skip.has(id) || seen.has(id))
|
|
215
|
+
continue;
|
|
216
|
+
seen.add(id);
|
|
217
|
+
out.push({ tokenId: id, onDiscovery: false, inWallet: true });
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
const newShared = () => ({ listRead: null, claimRead: null, planned: new WeakMap(), checks: new Map(), loaded: new Map(), listeners: new Set() });
|
|
222
|
+
/**
|
|
223
|
+
* The client of one exchange (D9): its reads over `origin` (src/lib/overlay.ts, src/lib/skein.ts),
|
|
224
|
+
* the swap flow, the pool flows (deploy, close, rebalance, move back, positions) and the wallet
|
|
225
|
+
* pieces, bound to `feeRateSatsPerKb`, `wallet` and `fetch`. Errors of the kinds in
|
|
226
|
+
* `CLIENT_ERROR_KINDS` are `ClientError`s (D13), the message as before.
|
|
227
|
+
*/
|
|
228
|
+
export function createClient(opts) {
|
|
229
|
+
return makeClient(opts, newShared());
|
|
230
|
+
}
|
|
231
|
+
function makeClient(opts, s) {
|
|
232
|
+
const swapBase = exchangeBase(opts.origin);
|
|
233
|
+
const feeRate = opts.feeRateSatsPerKb ?? DEFAULT_FEE_RATE_SATS_PER_KB;
|
|
234
|
+
const f = opts.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
235
|
+
const ff = f;
|
|
236
|
+
/** The submit at the host root of `base` (BRC-22, src/lp/close.ts `submitToOverlay`) over this client's fetch. */
|
|
237
|
+
const submitAt = (base) => (topic, beef) => submitToOverlay(base, topic, beef, f);
|
|
238
|
+
const boundWallet = opts.wallet ?? null;
|
|
239
|
+
const signedFor = boundWallet ? (opts.signedFor ?? authFetchPerInstance(boundWallet, f)) : null;
|
|
240
|
+
const planned = s.planned;
|
|
241
|
+
const checks = s.checks;
|
|
242
|
+
const loaded = s.loaded;
|
|
243
|
+
function needWallet() {
|
|
244
|
+
if (!boundWallet || !signedFor)
|
|
245
|
+
throw new Error("Connect a wallet first");
|
|
246
|
+
return { wallet: boundWallet, signedFor };
|
|
247
|
+
}
|
|
248
|
+
/** The wallet's identity key: as given, else the wallet's (null without a wallet). */
|
|
249
|
+
async function myIdentityKey() {
|
|
250
|
+
if (opts.identityKey !== undefined)
|
|
251
|
+
return opts.identityKey;
|
|
252
|
+
if (!boundWallet)
|
|
253
|
+
return null;
|
|
254
|
+
return (await boundWallet.getPublicKey({ identityKey: true })).publicKey;
|
|
255
|
+
}
|
|
256
|
+
/** `fn` with its error of a named kind thrown as a `ClientError` (D13; the message unchanged). */
|
|
257
|
+
function guard(fn) {
|
|
258
|
+
return async (...a) => {
|
|
259
|
+
try {
|
|
260
|
+
return await fn(...a);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
throw toClientError(e);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function emit() {
|
|
268
|
+
for (const l of s.listeners)
|
|
269
|
+
l();
|
|
270
|
+
}
|
|
271
|
+
/** Calls `fn` after any action changes data (swap, deploy, close, register…). Returns the unsubscribe. */
|
|
272
|
+
function onDataChange(fn) {
|
|
273
|
+
s.listeners.add(fn);
|
|
274
|
+
return () => s.listeners.delete(fn);
|
|
275
|
+
}
|
|
276
|
+
const errText = (e) => (e instanceof Error ? e.message : String(e));
|
|
277
|
+
/** The app's name (its boxes are `<app>/<box>`). */
|
|
278
|
+
function app() {
|
|
279
|
+
return appName(swapBase) ?? "amm";
|
|
280
|
+
}
|
|
281
|
+
/** Forget what was read this page load (a new base; tests). */
|
|
282
|
+
function forgetReads() {
|
|
283
|
+
s.listRead = null;
|
|
284
|
+
s.claimRead = null;
|
|
285
|
+
}
|
|
286
|
+
/** The swap instance's enable list (`GET <swap>/tokens`, P12), every status: read once per page load. */
|
|
287
|
+
function enabledList() {
|
|
288
|
+
if (!s.listRead) {
|
|
289
|
+
const r = readTokens(swapBase, ff).then((ts) => ts.map((t) => {
|
|
290
|
+
const id = parseOutpoint(t.tokenId.replace("_", "."));
|
|
291
|
+
return id ? { ...t, tokenId: tokenIdText(id) } : t;
|
|
292
|
+
}));
|
|
293
|
+
s.listRead = r;
|
|
294
|
+
r.catch(() => {
|
|
295
|
+
if (s.listRead === r)
|
|
296
|
+
s.listRead = null;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return s.listRead;
|
|
300
|
+
}
|
|
301
|
+
/** The tokens up on this exchange: enabled on the swap instance's list. */
|
|
302
|
+
async function enabledTokens() {
|
|
303
|
+
return (await enabledList()).filter((t) => t.status === "enabled");
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A token's own instance's AMM base: `<origin>/@<tokenId>/amm` (Open Exchange: the host makes the
|
|
307
|
+
* instance of every enabled token there). Refused for a token that is not enabled here.
|
|
308
|
+
*/
|
|
309
|
+
async function tokenBaseOf(tokenId) {
|
|
310
|
+
const t = (await enabledTokens()).find((x) => x.tokenId === tokenId);
|
|
311
|
+
if (!t)
|
|
312
|
+
throw new Error(`${tokenId.slice(0, 8)}… is not enabled on this exchange`);
|
|
313
|
+
return tokenBase(t.tokenId, swapBase);
|
|
314
|
+
}
|
|
315
|
+
/** The token list's metadata by token id (every status); empty when the read fails. */
|
|
316
|
+
async function tokenMeta() {
|
|
317
|
+
const m = new Map();
|
|
318
|
+
try {
|
|
319
|
+
for (const t of await enabledList())
|
|
320
|
+
m.set(t.tokenId, { tokenId: t.tokenId, topic: t.topic, sym: t.sym, dec: t.dec, ...(t.icon ? { icon: t.icon } : {}) });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
/* no metadata: ids only */
|
|
324
|
+
}
|
|
325
|
+
return m;
|
|
326
|
+
}
|
|
327
|
+
/** The host root: the pages' origin. */
|
|
328
|
+
function hostRoot() {
|
|
329
|
+
return new URL(swapBase).origin;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* A token's deploy from discovery (0.34.0, D21: `GET /@discovery/1color/<token>/deploy`, P16, the
|
|
333
|
+
* deploy's BEEF): its bytes and metadata; null when discovery does not hold it or the read fails
|
|
334
|
+
* (discovery off).
|
|
335
|
+
*/
|
|
336
|
+
async function discoveryDeploy(tokenId) {
|
|
337
|
+
try {
|
|
338
|
+
const op = parseOutpoint(tokenId.replace("_", "."));
|
|
339
|
+
const beef = op ? await readDiscoveryDeploy(discoveryBase(), tokenId, f) : null;
|
|
340
|
+
if (!op || !beef)
|
|
341
|
+
return null;
|
|
342
|
+
const meta = deployMetaOf(beef, op.vout);
|
|
343
|
+
return meta ? { beef, meta } : null;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* A token's deploy (Settings "Enable a token"): with a wallet, the deploy output in its module
|
|
351
|
+
* basket (`walletDeployBeef`) when it holds it; else discovery's (`GET
|
|
352
|
+
* /@discovery/1color/<token>/deploy`). Refused when neither has it: "no deploy for <id>: not on
|
|
353
|
+
* discovery and not in this wallet".
|
|
354
|
+
*/
|
|
355
|
+
async function deployBeef(tokenId, wallet) {
|
|
356
|
+
const op = parseOutpoint(sdkTokenId(tokenId).replace("_", "."));
|
|
357
|
+
if (!op)
|
|
358
|
+
throw new Error(`not a token id: ${tokenId}`);
|
|
359
|
+
const canonical = tokenIdText(op);
|
|
360
|
+
if (wallet) {
|
|
361
|
+
const beef = await walletDeployBeef(wallet, canonical);
|
|
362
|
+
if (beef)
|
|
363
|
+
return { beef, meta: deployMetaOf(beef, op.vout) ?? {}, from: "wallet" };
|
|
364
|
+
}
|
|
365
|
+
const found = await discoveryDeploy(canonical);
|
|
366
|
+
if (found)
|
|
367
|
+
return { ...found, from: "discovery" };
|
|
368
|
+
throw new Error(`no deploy for ${canonical}: not on discovery and not in this wallet`);
|
|
369
|
+
}
|
|
370
|
+
/** Discovery's rows for `ids` (P13 `?ids=`, ONE read), by token id; empty when the read fails. */
|
|
371
|
+
async function discoveryRows(ids) {
|
|
372
|
+
const m = new Map();
|
|
373
|
+
if (ids.length === 0)
|
|
374
|
+
return m;
|
|
375
|
+
try {
|
|
376
|
+
for (const r of await readDiscoveryIds(discoveryBase(), ids, f)) {
|
|
377
|
+
const id = canonicalId(r.tokenId) ?? r.tokenId;
|
|
378
|
+
if (r.listed)
|
|
379
|
+
m.set(id, r);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* discovery off: none */
|
|
384
|
+
}
|
|
385
|
+
return m;
|
|
386
|
+
}
|
|
387
|
+
/** swap's card (`GET <its origin>/.well-known/skein`) and its claim (`GET <base>/admin`), both unsigned. */
|
|
388
|
+
async function claimStatus() {
|
|
389
|
+
const url = `${instanceBase(swapBase)}/.well-known/skein`;
|
|
390
|
+
const res = await f(url, { headers: { accept: "application/json" } });
|
|
391
|
+
if (!res.ok)
|
|
392
|
+
throw new Error(`GET ${url}: ${res.status}`);
|
|
393
|
+
const v = (await res.json());
|
|
394
|
+
const identity = String(v.identity ?? "").toLowerCase();
|
|
395
|
+
if (!/^0[23][0-9a-f]{64}$/.test(identity))
|
|
396
|
+
throw new Error(`GET ${url}: no identity`);
|
|
397
|
+
const a = await f(`${swapBase}/admin`, { headers: { accept: "application/json" } }).catch(() => undefined);
|
|
398
|
+
let out;
|
|
399
|
+
if (!a?.ok)
|
|
400
|
+
out = { claimed: v.claimed === true, identity };
|
|
401
|
+
else {
|
|
402
|
+
const adm = (await a.json().catch(() => ({})));
|
|
403
|
+
const claimant = typeof adm.claimant === "string" && /^0[23][0-9a-f]{64}$/i.test(adm.claimant) ? adm.claimant.toLowerCase() : undefined;
|
|
404
|
+
out = claimant ? { claimed: true, identity, claimant } : { claimed: false, identity };
|
|
405
|
+
}
|
|
406
|
+
s.claimRead = out;
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* swap's identity, the recipient of root's messages (0.34.0, D21: from the claim card already read
|
|
411
|
+
* on load, `claimStatus`; the signed token-list read for its `x-bsv-auth-identity-key`,
|
|
412
|
+
* `instanceIdentity`, is gone); read now when no card was read yet.
|
|
413
|
+
*/
|
|
414
|
+
async function swapIdentity() {
|
|
415
|
+
return (s.claimRead ?? (await claimStatus())).identity;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Whether this identity may configure the exchange (0.28.0; the name kept from 0.26.0): it is swap's
|
|
419
|
+
* claimant (`GET <base>/admin`, granted amm.admin by the host), or it holds root on swap (the host's
|
|
420
|
+
* `GET <host root>/skeins?key=<identityKey>` names swap's identity: the host's key, or a key the host
|
|
421
|
+
* granted root, as on the rack). False when the reads fail.
|
|
422
|
+
*/
|
|
423
|
+
async function isRoot(identityKey) {
|
|
424
|
+
if (!identityKey)
|
|
425
|
+
return false;
|
|
426
|
+
try {
|
|
427
|
+
const { identity, claimant } = await claimStatus();
|
|
428
|
+
if (claimant && claimant === identityKey.toLowerCase())
|
|
429
|
+
return true;
|
|
430
|
+
const res = await f(`${hostRoot()}/skeins?key=${encodeURIComponent(identityKey)}`, { headers: { accept: "application/json" } });
|
|
431
|
+
if (!res.ok)
|
|
432
|
+
return false;
|
|
433
|
+
const held = (await res.json());
|
|
434
|
+
return Array.isArray(held) && held.some((x) => String(x.identity ?? "").toLowerCase() === identity);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Claims the exchange with the connected wallet (0.28.0): the wallet's message `{fn: "claim"}` in the
|
|
442
|
+
* swap app's box `amm/claim` to swap's identity; the first claim is recorded (swap's `/admin` names
|
|
443
|
+
* its sender) and the host grants it the role amm.admin; a later one is refused. Then reads `isRoot`
|
|
444
|
+
* again (a few reads until it shows) and tells the pages.
|
|
445
|
+
*/
|
|
446
|
+
async function claimExchange() {
|
|
447
|
+
const { signedFor } = needWallet();
|
|
448
|
+
const { identity } = await claimStatus();
|
|
449
|
+
await sendMessage(signedFor(swapBase), swapBase, identity, `${app()}/claim`, { fn: "claim" });
|
|
450
|
+
const me = await myIdentityKey();
|
|
451
|
+
let root = false;
|
|
452
|
+
for (let i = 0; i < 20 && !root; i++) {
|
|
453
|
+
root = await isRoot(me);
|
|
454
|
+
if (!root)
|
|
455
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
456
|
+
}
|
|
457
|
+
emit();
|
|
458
|
+
if (!root)
|
|
459
|
+
throw new Error("The claim was sent, but this wallet is not the admin: someone else may have claimed this exchange first.");
|
|
460
|
+
return root;
|
|
461
|
+
}
|
|
462
|
+
/** The tokens this exchange hosts, priced: the token list (once) and ONE read of the host-wide pools route. No wallet needed. */
|
|
463
|
+
async function hostedTokens() {
|
|
464
|
+
const [list, rows] = await Promise.all([enabledList(), readAllPools().catch(() => null)]);
|
|
465
|
+
return hostedFrom(list, rows);
|
|
466
|
+
}
|
|
467
|
+
async function readAllPools() {
|
|
468
|
+
const res = await f(poolsUrl(swapBase), { headers: { accept: "application/json" } });
|
|
469
|
+
if (!res.ok)
|
|
470
|
+
throw new Error(`GET ${poolsUrl(swapBase)}: ${res.status}`);
|
|
471
|
+
return parseAllPools(await res.json());
|
|
472
|
+
}
|
|
473
|
+
/** The host-wide pools route held as an event stream (D22: one connection): each answer's rows to `onRows`; `onLive` the connection's state. → the unsubscribe. */
|
|
474
|
+
function watchHostedPools(onRows, onLive) {
|
|
475
|
+
return watchPools(poolsUrl(swapBase), parseAllPools, onRows, { onError: () => onLive?.(false), onOpen: () => onLive?.(true), fetch: f });
|
|
476
|
+
}
|
|
477
|
+
/** A token's pools route held as an event stream (D16): each answer to `onAnswer`. → the unsubscribe. */
|
|
478
|
+
function watchTokenPools(tokenId, onAnswer, onError) {
|
|
479
|
+
return watchPools(poolsUrl(tokenBase(tokenId, swapBase)), parsePools, onAnswer, { ...(onError ? { onError } : {}), fetch: f });
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A quote for one token (D12): the matching engine's plan over the pools the Swap page holds
|
|
483
|
+
* (`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); no request. `meta`: the token's symbol and
|
|
485
|
+
* decimals.
|
|
486
|
+
*/
|
|
487
|
+
function quoteSwap(req, held, meta) {
|
|
488
|
+
if (req.amountIn <= 0n)
|
|
489
|
+
throw new Error("Enter an amount");
|
|
490
|
+
const pools = held?.pools ?? [];
|
|
491
|
+
if (pools.length === 0)
|
|
492
|
+
throw new Error("No pool to swap against");
|
|
493
|
+
const live = liveOfPools(pools);
|
|
494
|
+
const dec = meta.dec;
|
|
495
|
+
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), allowPartial: req.allowPartial }, pools, live, dec, feeRate);
|
|
497
|
+
if (!r.ok)
|
|
498
|
+
throw new Error(r.error);
|
|
499
|
+
const { plan, view } = planQuote(r.request, live, dec);
|
|
500
|
+
const used = view.legs.filter((l) => l.amountIn > 0n);
|
|
501
|
+
if (used.length === 0 || view.totalOut === 0n)
|
|
502
|
+
throw new Error("The amount is too small to receive anything");
|
|
503
|
+
const sumSats = pools.reduce((a, p) => a + p.bsvReserve, 0n);
|
|
504
|
+
const sumTokens = pools.reduce((a, p) => a + p.tokenReserve, 0n);
|
|
505
|
+
const marginal = sumTokens > 0n ? Number(sumSats) / (Number(sumTokens) / 10 ** dec) : 0;
|
|
506
|
+
const totalIn = used.reduce((a, l) => a + l.amountIn, 0n);
|
|
507
|
+
const sats = buy ? totalIn : view.totalOut;
|
|
508
|
+
const tokens = buy ? view.totalOut : totalIn;
|
|
509
|
+
const effective = Number(sats) / (Number(tokens) / 10 ** dec);
|
|
510
|
+
const first = used[0].pool;
|
|
511
|
+
const q = {
|
|
512
|
+
request: req,
|
|
513
|
+
legs: used.map((l) => ({
|
|
514
|
+
poolOutpoint: outpointText(l.outpoint),
|
|
515
|
+
amountIn: l.amountIn,
|
|
516
|
+
amountOut: l.amountOut,
|
|
517
|
+
shareBps: Number((l.amountIn * 10000n) / req.amountIn),
|
|
518
|
+
reserves: { sats: l.pool.bsvReserve, tokens: l.pool.tokenReserve },
|
|
519
|
+
validator: { identityKey: l.pool.validatorIdentityKey, live: l.validator.live },
|
|
520
|
+
})),
|
|
521
|
+
amountOut: view.totalOut,
|
|
522
|
+
minAmountOut: view.minAmountOut,
|
|
523
|
+
effectivePrice: effective,
|
|
524
|
+
marginalPrice: marginal,
|
|
525
|
+
impactBps: marginal ? Math.round((Math.abs(effective - marginal) / marginal) * 10_000) : 0,
|
|
526
|
+
fees: { lpBps: Number(first.liquidityFeeBps), validatorBps: Number(first.validationFeeBps), commissionBps: Number(first.commissionBps ?? 0n) },
|
|
527
|
+
};
|
|
528
|
+
planned.set(q, { plan, pools, live, market: held.market, dec, sym: meta.sym });
|
|
529
|
+
return q;
|
|
530
|
+
}
|
|
531
|
+
/** A relay outcome on a leg's result (0.31.1's mapping; 0.34.0: no answer carries its id for "Check again"). */
|
|
532
|
+
function onLeg(result, o, prepared, base, af) {
|
|
533
|
+
delete result.checkId;
|
|
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);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Runs a quoted swap with the connected wallet (D14, D15): ONE `GET /@<token>/amm/pools/beef?lp=`
|
|
557
|
+
* for the legs' LP keys — a leg whose key is missing or whose outpoint moved is gone since the
|
|
558
|
+
* quote (as before 0.34.0) — the commission key hash and each validator's peer ID from the pools
|
|
559
|
+
* the page holds; then one transaction per leg, relayed to the leg's validator (`amm.swap.submit`,
|
|
560
|
+
* its answer the settled record), the payout taken in on acceptance. A planned pool gone stops the
|
|
561
|
+
* swap unless partial fills are allowed.
|
|
562
|
+
*/
|
|
563
|
+
async function executeSwap(quote, o = {}) {
|
|
564
|
+
const progress = (x) => {
|
|
565
|
+
try {
|
|
566
|
+
o.onProgress?.(x);
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
/* the caller's callback: not the swap's failure */
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
let result;
|
|
573
|
+
try {
|
|
574
|
+
result = await runSwap(quote, progress);
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
const err = toClientError(e);
|
|
578
|
+
progress({ phase: "failed", error: err instanceof Error ? err : new Error(String(err)) });
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
progress(result.status === "failed" ? { phase: "failed", result } : { phase: "done", result });
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
/** A leg failed with `e`: its text, and its kind when it has one (D13). */
|
|
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) {
|
|
592
|
+
const { wallet, signedFor } = needWallet();
|
|
593
|
+
const p = planned.get(quote);
|
|
594
|
+
if (!p)
|
|
595
|
+
throw new Error("Quote again: this quote was not planned here");
|
|
596
|
+
const tokenId = quote.request.tokenId;
|
|
597
|
+
const base = tokenBase(tokenId, swapBase);
|
|
598
|
+
const af = signedFor(base);
|
|
599
|
+
const lpOf = (outpoint) => p.pools.find((x) => x.outpoint === outpoint)?.lpPubKey ?? "";
|
|
600
|
+
const keys = [...new Set(p.plan.legs.map((l) => lpOf(l.outpoint)).filter((k) => k !== ""))];
|
|
601
|
+
const fresh = await readPoolsBeef(base, keys, freshFetch);
|
|
602
|
+
const gone = goneFromLookup(p.plan, fresh.pools);
|
|
603
|
+
if (gone.length > 0 && !quote.request.allowPartial)
|
|
604
|
+
throw new ClientError("pool_spent", `${gone.map(outpointText).join(", ")} ${gone.length === 1 ? "was" : "were"} spent since the quote: quote again`);
|
|
605
|
+
const commissionPkh = p.market.commissionPkh;
|
|
606
|
+
const direction = quote.request.side === "buy" ? "bsvToToken" : "tokenToBsv";
|
|
607
|
+
const legs = [];
|
|
608
|
+
const running = [];
|
|
609
|
+
// The legs that can run: not spent since the quote, their validator live.
|
|
610
|
+
const runnable = [];
|
|
611
|
+
for (const leg of p.plan.legs) {
|
|
612
|
+
const poolOutpoint = outpointText(leg.outpoint);
|
|
613
|
+
if (gone.includes(leg.outpoint)) {
|
|
614
|
+
legs.push({ poolOutpoint, status: "failed", error: "spent since the quote", kind: "pool_spent" });
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const pool = p.pools.find((x) => x.outpoint === leg.outpoint);
|
|
618
|
+
const validator = validatorStatus(pool.validatorIdentityKey, p.live);
|
|
619
|
+
if (!validator.live || !validator.peerId) {
|
|
620
|
+
legs.push({ poolOutpoint, status: "failed", error: "the pool's validator is no longer live" });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const result = { poolOutpoint, status: "failed" };
|
|
624
|
+
legs.push(result);
|
|
625
|
+
runnable.push({ leg, result, pool, peerId: validator.peerId });
|
|
626
|
+
}
|
|
627
|
+
// 0.29.0: every leg spends only `amm-swap` outputs — tokens in, outputs carrying exactly its
|
|
628
|
+
// amount in (0.12.0); and one sats output of exactly the leg's sats. What the basket does not
|
|
629
|
+
// cover is staged by ONE send (tokens missing: the module send, the sats with it; else a plain
|
|
630
|
+
// sats send), then each leg is built from the basket.
|
|
631
|
+
const tokensIn = direction === "tokenToBsv";
|
|
632
|
+
if (runnable.length > 0)
|
|
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 });
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
failLeg(r.result, e);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const shortTokens = staged.filter((x) => tokensIn && !x.tokens);
|
|
660
|
+
const shortSats = staged.filter((x) => !x.sats);
|
|
661
|
+
let sent;
|
|
662
|
+
if (shortTokens.length > 0) {
|
|
663
|
+
const s = await sendToSwapBasket(wallet, base, tokenId, { tokens: shortTokens.map((x) => ({ amount: x.r.leg.amountIn, pool: x.r.leg.outpoint })), sats: shortSats.map((x) => ({ satoshis: x.planned.funding.satoshis, pool: x.r.leg.outpoint })) }, { sym: p.sym }, submitAt(base));
|
|
664
|
+
sent = s.tx;
|
|
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;
|
|
716
|
+
}
|
|
717
|
+
let peerId = x.peerId;
|
|
718
|
+
if (pool.validatorIdentityKey !== x.was.validatorIdentityKey) {
|
|
719
|
+
const v = validatorStatus(pool.validatorIdentityKey, p.live);
|
|
720
|
+
if (!v.live || !v.peerId) {
|
|
721
|
+
result.error = "the pool's validator changed";
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
peerId = v.peerId;
|
|
725
|
+
}
|
|
726
|
+
const leg = { outpoint: pool.outpoint, amountIn: x.leg.amountIn, ...priced };
|
|
727
|
+
try {
|
|
728
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [x.was.lpPubKey], freshFetch), pool.outpoint);
|
|
729
|
+
if (!poolOutput)
|
|
730
|
+
throw new Error("the pool moved again: quote again");
|
|
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 };
|
|
737
|
+
}
|
|
738
|
+
catch (e) {
|
|
739
|
+
failLeg(result, e);
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
for (const { r, poolOutput, planned, tokens, sats } of staged) {
|
|
744
|
+
const { leg, result, pool, peerId } = r;
|
|
745
|
+
if (tokensIn && !tokens) {
|
|
746
|
+
result.error ??= `no ${p.sym} in the swap basket for exactly ${formatAmount(leg.amountIn, p.dec)}`;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (!sats) {
|
|
750
|
+
result.error ??= `no sats in the swap basket for this leg`;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const prepared = await prepareSwap({ wallet, tokenId, meta: { sym: p.sym, dec: p.dec }, direction, leg, pool, poolOutput, commissionPkh, ...(tokens ? { tokenInputs: tokens } : {}), satsInput: sats, ...(holdingBeef ? { holdingBeef } : {}), planned, satsPerKb: feeRate });
|
|
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
|
+
})());
|
|
776
|
+
}
|
|
777
|
+
catch (e) {
|
|
778
|
+
failLeg(result, e);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
await Promise.all(running);
|
|
782
|
+
emit();
|
|
783
|
+
return swapResultOf(legs);
|
|
784
|
+
}
|
|
785
|
+
/** 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
|
+
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
|
+
/** 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
|
+
async function walletSats() {
|
|
807
|
+
const { wallet } = needWallet();
|
|
808
|
+
const r = await wallet.listOutputs({ basket: SPEC_OP_WALLET_BALANCE });
|
|
809
|
+
return BigInt(r.totalOutputs);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* The connected wallet's positions (0.34.0, D9): its pool rows (`amm-pools`, and its deploy
|
|
813
|
+
* history's, spent ones too), grouped by token, each token's LP keys asked in ONE `GET
|
|
814
|
+
* /@<token>/amm/positions?lp=k1,k2` (P4): each pool's state (live, rescinded, closed) and the LP fees
|
|
815
|
+
* its swaps paid (`feesEarnedSats`, `feesEarnedTokens`). The token list is read once. D23: a
|
|
816
|
+
* deploy in the history whose pool output has no `amm-pools` row, its pool still at that output,
|
|
817
|
+
* is filed then (`internalizeAction` basket insertion, as the deploy files it; its BEEF `GET
|
|
818
|
+
* /@<token>/amm/tx?id=`). Closed ones are marked `closed`, kept for their fees. `validator.live`
|
|
819
|
+
* (0.34.1, David): the validator is live when it is in the token's pools route's `validators` (the
|
|
820
|
+
* validators beating on the token's topic now): ONE plain `GET /@<token>/amm/pools` per token.
|
|
821
|
+
*/
|
|
822
|
+
async function myPositions() {
|
|
823
|
+
const { wallet } = needWallet();
|
|
824
|
+
const [poolRows, history, list] = await Promise.all([basketRows(wallet, POOLS_BASKET), historyPoolRows(wallet), enabledList().catch(() => [])]);
|
|
825
|
+
const enabled = new Map(list.filter((t) => t.status === "enabled").map((t) => [t.tokenId, t]));
|
|
826
|
+
const filed = new Set(poolRows.map((r) => r.outpoint.replace("_", ".")));
|
|
827
|
+
// The rows by token, one per LP key (the pool's key for its life: a filed row and a history row of one pool are one).
|
|
828
|
+
const byToken = new Map();
|
|
829
|
+
for (const row of positionRowsOf(mergePoolRows(history, poolRows))) {
|
|
830
|
+
if (!row.lpPubKey || !enabled.has(row.tokenId))
|
|
831
|
+
continue;
|
|
832
|
+
const keys = byToken.get(row.tokenId) ?? new Map();
|
|
833
|
+
if (!keys.has(row.lpPubKey))
|
|
834
|
+
keys.set(row.lpPubKey, row);
|
|
835
|
+
byToken.set(row.tokenId, keys);
|
|
836
|
+
}
|
|
837
|
+
const out = [];
|
|
838
|
+
const closed = [];
|
|
839
|
+
loaded.clear();
|
|
840
|
+
for (const [tokenId, keys] of byToken) {
|
|
841
|
+
const base = tokenBase(tokenId, swapBase);
|
|
842
|
+
let pools;
|
|
843
|
+
try {
|
|
844
|
+
pools = await readPositions(base, [...keys.keys()], f);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const t = enabled.get(tokenId);
|
|
850
|
+
const meta = { sym: t?.sym ?? tokenId.slice(0, 8), dec: t?.dec ?? 0, ...(t?.icon ? { icon: t.icon } : {}) };
|
|
851
|
+
const beating = new Set((await readPools(base, f).catch(() => null))?.validators.map((x) => x.identity) ?? []);
|
|
852
|
+
for (const p of pools) {
|
|
853
|
+
const row = keys.get(p.lpPubKey);
|
|
854
|
+
const fees = { feesEarnedSats: p.sats, feesEarnedTokens: p.tokens };
|
|
855
|
+
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 });
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const outpoint = p.outpoint.replace("_", ".");
|
|
860
|
+
loaded.set(outpoint, row);
|
|
861
|
+
// D23: the deploy's row, filed from the wallet's record while the pool is still at the deploy's output.
|
|
862
|
+
if (p.status === "live" && outpoint === row.deployOutpoint && !filed.has(outpoint))
|
|
863
|
+
await fileDeployRow(wallet, base, history, outpoint, meta);
|
|
864
|
+
out.push({
|
|
865
|
+
outpoint,
|
|
866
|
+
tokenId,
|
|
867
|
+
...meta,
|
|
868
|
+
sats: p.current.bsvReserve,
|
|
869
|
+
tokens: p.current.tokenReserve,
|
|
870
|
+
validator: { identityKey: p.current.validatorIdentityKey, live: beating.has(p.current.validatorIdentityKey.toLowerCase()) },
|
|
871
|
+
...(p.status === "rescinded" ? { rescinded: true } : {}),
|
|
872
|
+
...fees,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return [...out, ...closed];
|
|
877
|
+
}
|
|
878
|
+
/** D23: the history's row of the deploy output `outpoint` into `amm-pools` (basket insertion over the deploy's BEEF, `GET /tx?id=`). A failure: nothing, the next load. */
|
|
879
|
+
async function fileDeployRow(wallet, base, history, outpoint, meta) {
|
|
880
|
+
const h = history.find((r) => r.outpoint.replace("_", ".") === outpoint);
|
|
881
|
+
const op = parseOutpoint(outpoint);
|
|
882
|
+
if (!h || !op)
|
|
883
|
+
return;
|
|
884
|
+
try {
|
|
885
|
+
const beef = await readTx(base, op.txid, f);
|
|
886
|
+
if (!beef)
|
|
887
|
+
return;
|
|
888
|
+
const customInstructions = withMeta(h.customInstructions, meta.sym, meta.dec);
|
|
889
|
+
await wallet.internalizeAction({
|
|
890
|
+
tx: beef,
|
|
891
|
+
outputs: [{ outputIndex: op.vout, protocol: "basket insertion", insertionRemittance: { basket: POOLS_BASKET, ...(h.tags ? { tags: h.tags } : {}), ...(customInstructions ? { customInstructions } : {}) } }],
|
|
892
|
+
description: `AMM pool deploy: ${meta.sym}`.slice(0, 50),
|
|
893
|
+
labels: ["amm-pool-deploy"],
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
/* the next load */
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/** A token instance's terms (P7: `GET /@<token>/amm/terms`, unsigned). */
|
|
901
|
+
async function instanceTerms(tokenId) {
|
|
902
|
+
return readTerms(await tokenBaseOf(tokenId), f);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* The validator a new position is deployed with (this skein), and its fees (read-only: the LP
|
|
906
|
+
* accepts them); `{none: true}` when no terms are set. 0.34.0 (D21): `GET /@<token>/amm/terms`
|
|
907
|
+
* (P7), plain.
|
|
908
|
+
*/
|
|
909
|
+
async function validatorTerms(tokenId) {
|
|
910
|
+
const t = (await instanceTerms(tokenId)).validator;
|
|
911
|
+
if (!t)
|
|
912
|
+
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 } };
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Deploys a new position (src/lp/poolDeploy.ts, 0.13.0): this skein's
|
|
917
|
+
* validator affirms the proposed pool at its terms (`amm.pool.affirm`: its
|
|
918
|
+
* key, its nonce, its signature, written into the pool's state), then the
|
|
919
|
+
* wallet's BRC-207 module selects the token inputs, builds, signs and
|
|
920
|
+
* broadcasts the deploy in one createAction; the pool output is filed as a
|
|
921
|
+
* position row (`amm-pools`); the page's own submit at the host root (D19).
|
|
922
|
+
*/
|
|
923
|
+
async function deployPosition(req) {
|
|
924
|
+
const { wallet, signedFor } = needWallet();
|
|
925
|
+
if (req.tokens <= 0n || req.sats <= 0n)
|
|
926
|
+
throw new Error("Both amounts must be more than 0");
|
|
927
|
+
const [terms, held, meta, base] = await Promise.all([instanceTerms(req.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(req.tokenId)]);
|
|
928
|
+
const v = terms.validator;
|
|
929
|
+
if (!v)
|
|
930
|
+
throw new Error(NO_VALIDATOR);
|
|
931
|
+
const m = meta.get(req.tokenId);
|
|
932
|
+
const d = await deployPool({
|
|
933
|
+
wallet,
|
|
934
|
+
af: signedFor(base),
|
|
935
|
+
base,
|
|
936
|
+
tokenId: req.tokenId,
|
|
937
|
+
tokens: req.tokens,
|
|
938
|
+
sats: req.sats,
|
|
939
|
+
fees: { lpFeeBps: BigInt(v.lpFeeBps), validatorFeeBps: BigInt(v.validatorFeeBps), commissionBps: BigInt(v.commissionBps) },
|
|
940
|
+
validator: { identityKey: v.identity },
|
|
941
|
+
held,
|
|
942
|
+
meta: { ...(m?.sym ? { sym: m.sym } : {}), ...(m?.dec !== undefined ? { dec: m.dec } : {}) },
|
|
943
|
+
submit: submitAt(base),
|
|
944
|
+
});
|
|
945
|
+
emit();
|
|
946
|
+
return { outpoint: d.outpoint, txid: d.txid, ...(d.errors.length ? { warnings: d.errors } : {}) };
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Closes a position (src/lp/close.ts): everything in the pool returns to the wallet (its own action
|
|
950
|
+
* pays the miner fee). 0.34.0: the pool's current output and its BEEF by its LP key (`GET
|
|
951
|
+
* /@<token>/amm/pools/beef?lp=<key>`, D2: the pool's id); the page's submit at the host root (D19).
|
|
952
|
+
*/
|
|
953
|
+
async function closePosition(req) {
|
|
954
|
+
const { wallet } = needWallet();
|
|
955
|
+
let row = loaded.get(req.outpoint);
|
|
956
|
+
if (!row) {
|
|
957
|
+
await myPositions();
|
|
958
|
+
row = loaded.get(req.outpoint);
|
|
959
|
+
}
|
|
960
|
+
if (!row || !row.lpPubKey)
|
|
961
|
+
throw new Error("No such position");
|
|
962
|
+
const [meta, base] = await Promise.all([tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
963
|
+
const m = meta.get(row.tokenId);
|
|
964
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.lpPubKey], freshFetch), req.outpoint);
|
|
965
|
+
if (!poolOutput)
|
|
966
|
+
throw new Error(`the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
967
|
+
const prepared = await prepareClose({
|
|
968
|
+
wallet,
|
|
969
|
+
tokenId: row.tokenId,
|
|
970
|
+
meta: { ...(m?.sym ? { sym: m.sym } : {}), ...(m?.dec !== undefined ? { dec: m.dec } : {}) },
|
|
971
|
+
poolOutput,
|
|
972
|
+
lpKey: row.lpKey,
|
|
973
|
+
});
|
|
974
|
+
// The page's own submit (the wallet's module submitted at its overlay too; its failure does not undo the close).
|
|
975
|
+
await submitToOverlay(swapBase, prepared.topic, prepared.beef, f).catch(() => undefined);
|
|
976
|
+
loaded.delete(req.outpoint);
|
|
977
|
+
emit();
|
|
978
|
+
return { txid: prepared.txid, sats: prepared.sats, tokens: prepared.tokenAmount };
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Rebalances a position (D11, src/lp/rebalance.ts): its pool closed and a new pool of `tokens` and
|
|
982
|
+
* `sats` deployed in ONE wallet createAction — the old pool the explicit input (as Close spends
|
|
983
|
+
* it), the new one output 0 affirmed by the validator at its terms now (`amm.pool.affirm`, as a
|
|
984
|
+
* 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 LP key (`GET
|
|
986
|
+
* /@<token>/amm/pools/beef?lp=<key>`); the page's submit at the host root; the new pool's row
|
|
987
|
+
* filed in `amm-pools`.
|
|
988
|
+
*/
|
|
989
|
+
async function rebalancePosition(req) {
|
|
990
|
+
const { wallet, signedFor } = needWallet();
|
|
991
|
+
if (req.tokens <= 0n || req.sats <= 0n)
|
|
992
|
+
throw new Error("Both amounts must be more than 0");
|
|
993
|
+
let row = loaded.get(req.outpoint);
|
|
994
|
+
if (!row) {
|
|
995
|
+
await myPositions();
|
|
996
|
+
row = loaded.get(req.outpoint);
|
|
997
|
+
}
|
|
998
|
+
if (!row || !row.lpPubKey)
|
|
999
|
+
throw new Error("No such position");
|
|
1000
|
+
const [terms, held, meta, base] = await Promise.all([instanceTerms(row.tokenId), heldOutputs(wallet), tokenMeta(), tokenBaseOf(row.tokenId)]);
|
|
1001
|
+
const v = terms.validator;
|
|
1002
|
+
if (!v)
|
|
1003
|
+
throw new Error(NO_VALIDATOR);
|
|
1004
|
+
const m = meta.get(row.tokenId);
|
|
1005
|
+
const poolOutput = poolOutputOf(await readPoolsBeef(base, [row.lpPubKey], freshFetch), req.outpoint);
|
|
1006
|
+
if (!poolOutput)
|
|
1007
|
+
throw new ClientError("stale_pool", `the pool is no longer at ${req.outpoint}: reload your positions`);
|
|
1008
|
+
const r = await rebalancePool({
|
|
1009
|
+
wallet,
|
|
1010
|
+
af: signedFor(base),
|
|
1011
|
+
base,
|
|
1012
|
+
tokenId: row.tokenId,
|
|
1013
|
+
poolOutput,
|
|
1014
|
+
lpKey: row.lpKey,
|
|
1015
|
+
tokens: req.tokens,
|
|
1016
|
+
sats: req.sats,
|
|
1017
|
+
fees: { lpFeeBps: BigInt(v.lpFeeBps), validatorFeeBps: BigInt(v.validatorFeeBps), commissionBps: BigInt(v.commissionBps) },
|
|
1018
|
+
validator: { identityKey: v.identity },
|
|
1019
|
+
held,
|
|
1020
|
+
meta: { ...(m?.sym ? { sym: m.sym } : {}), ...(m?.dec !== undefined ? { dec: m.dec } : {}) },
|
|
1021
|
+
submit: submitAt(base),
|
|
1022
|
+
});
|
|
1023
|
+
loaded.delete(req.outpoint);
|
|
1024
|
+
emit();
|
|
1025
|
+
return { outpoint: r.outpoint, txid: r.txid, closed: r.closed, ...(r.errors.length ? { warnings: r.errors } : {}) };
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* The connected wallet's 1Color tokens (0.12.0: the BRC-207 module's): the assets `p color assets`
|
|
1029
|
+
* lists, each balance the sum of its held value outputs there (0.27.0), and what the active swap
|
|
1030
|
+
* basket (`amm-swap`) holds of each (its tokens and, 0.29.6, the sats staged for its legs).
|
|
1031
|
+
* 0.34.0 (D17): symbol, decimals and icon from the exchange's token list (read once), else ONE
|
|
1032
|
+
* discovery read for every held token the list lacks (`GET /@discovery/1color/tokens?ids=a,b`),
|
|
1033
|
+
* else the wallet's own deploy output. Each marked on this exchange (registered) or not.
|
|
1034
|
+
*/
|
|
1035
|
+
async function myTokens() {
|
|
1036
|
+
const { wallet } = needWallet();
|
|
1037
|
+
const [held, swaps, meta] = await Promise.all([
|
|
1038
|
+
heldOutputs(wallet),
|
|
1039
|
+
swapTotals(wallet).catch(() => new Map()),
|
|
1040
|
+
tokenMeta(),
|
|
1041
|
+
]);
|
|
1042
|
+
const registered = new Set((await enabledTokens().catch(() => [])).map((t) => t.tokenId));
|
|
1043
|
+
const inv = buildInventory(held.filter((h) => h.available));
|
|
1044
|
+
const ids = [...new Set([...inv.tokens.filter((t) => t.valueOutputs > 0).map((t) => t.tokenId), ...swaps.keys()])];
|
|
1045
|
+
const disc = await discoveryRows(ids.filter((id) => !meta.has(id)));
|
|
1046
|
+
const out = ids.map((tokenId) => {
|
|
1047
|
+
const t = inv.tokens.find((x) => x.tokenId === tokenId);
|
|
1048
|
+
const balance = t && t.valueOutputs > 0 ? t.balance : 0n;
|
|
1049
|
+
const d = disc.get(tokenId);
|
|
1050
|
+
const m = meta.get(tokenId) ?? (d ? { ...(d.sym !== undefined ? { sym: d.sym } : {}), ...(d.dec !== undefined ? { dec: d.dec } : {}), ...(d.iconUrl ? { icon: d.iconUrl } : {}) } : {});
|
|
1051
|
+
const icon = m.icon ?? (t?.icon?.image ? imageDataUrl(t.icon.image) : undefined);
|
|
1052
|
+
return {
|
|
1053
|
+
tokenId,
|
|
1054
|
+
sym: m.sym ?? t?.sym ?? tokenId.slice(0, 8),
|
|
1055
|
+
dec: m.dec ?? t?.dec ?? 0,
|
|
1056
|
+
...(icon ? { icon } : {}),
|
|
1057
|
+
balance,
|
|
1058
|
+
outputs: t?.valueOutputs ?? 0,
|
|
1059
|
+
inSwaps: swaps.get(tokenId)?.amount ?? 0n,
|
|
1060
|
+
swapSats: swaps.get(tokenId)?.sats ?? 0,
|
|
1061
|
+
onExchange: registered.has(tokenId),
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1064
|
+
return out.filter((t) => t.balance > 0n || t.inSwaps > 0n || t.swapSats > 0).sort((a, b) => a.sym.localeCompare(b.sym) || a.tokenId.localeCompare(b.tokenId));
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* The silent pass over the swap basket's staged legs (0.34.0, D18, D24; src/wallet/swapPayouts.ts):
|
|
1068
|
+
* per token ONE `GET /spends?outpoint=` over the pools its rows were staged for, the spenders'
|
|
1069
|
+
* transactions, this wallet's payouts taken in. Nothing shown; a payout taken in reloads the
|
|
1070
|
+
* pages' data. Never throws.
|
|
1071
|
+
*/
|
|
1072
|
+
async function settleSwaps() {
|
|
1073
|
+
if (!boundWallet)
|
|
1074
|
+
return [];
|
|
1075
|
+
const taken = await settleStagedSwaps(boundWallet, (t) => tokenBase(t, swapBase), f);
|
|
1076
|
+
if (taken.length > 0)
|
|
1077
|
+
emit();
|
|
1078
|
+
return taken;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* "Move back" (src/market/activeSwap.ts; 0.29.3: a module plan): every `amm-swap` row of the
|
|
1082
|
+
* token (and its staged sats) spent in one action whose one output the wallet's module files
|
|
1083
|
+
* into the token's balance in the same call; 0.34.0 (D19): submitted at the host root (`POST
|
|
1084
|
+
* <origin>/submit` + `X-Topics`), no token-list read for an instance base. The recovery path
|
|
1085
|
+
* for a swap that got stuck. 0.29.6: with only sats staged for the token, one plain createAction
|
|
1086
|
+
* over them with no outputs (the wallet's change takes them).
|
|
1087
|
+
*/
|
|
1088
|
+
async function moveBackTokens(tokenId) {
|
|
1089
|
+
const { wallet } = needWallet();
|
|
1090
|
+
const meta = await tokenMeta();
|
|
1091
|
+
const r = await moveBack({ wallet, base: swapBase, tokenId, meta: { sym: symOf(meta, tokenId) }, submit: submitAt(swapBase) });
|
|
1092
|
+
emit();
|
|
1093
|
+
return r;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* "Send" (0.30.0): gives `amount` (raw units) of a token to a BRC-169 handle (`name@domain` or
|
|
1097
|
+
* `@name@domain`) with @1sat/actions `send1Color`: the handle resolved, one wallet createAction
|
|
1098
|
+
* through the BRC-207 color module (a BRC-177 noSend handoff), delivered (BRC-232) to the handle's
|
|
1099
|
+
* messagebox; the recipient's wallet takes it in. The action's result as is: `{txid, delivered,
|
|
1100
|
+
* messageId}`, or `{error}` (nothing built), or `{error, txid}` (built, not delivered).
|
|
1101
|
+
*/
|
|
1102
|
+
async function sendTokens(tokenId, handle, amount, memo) {
|
|
1103
|
+
const { wallet } = needWallet();
|
|
1104
|
+
const { network } = await wallet.getNetwork({});
|
|
1105
|
+
const r = await send1Color.execute({ wallet, chain: network === "mainnet" ? "main" : "test", isBaseWallet: false }, { assetId: tokenId, amount, destination: { handle }, ...(memo ? { memo } : {}) });
|
|
1106
|
+
emit();
|
|
1107
|
+
return r;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* The tokens on this exchange's list (0.17.0: the config instance's enable list, `GET
|
|
1111
|
+
* <config>/tokens`), every status, newest first.
|
|
1112
|
+
*/
|
|
1113
|
+
async function registeredTokens() {
|
|
1114
|
+
return (await enabledList()).map((t) => ({
|
|
1115
|
+
tokenId: t.tokenId,
|
|
1116
|
+
sym: t.sym || t.tokenId.slice(0, 8),
|
|
1117
|
+
...(t.icon ? { icon: t.icon } : {}),
|
|
1118
|
+
status: t.status,
|
|
1119
|
+
}));
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Enables a token (Open Exchange, shruggr/open-exchange#1): root's message `{fn: "enable", args:
|
|
1123
|
+
* {tokenId, beef}}` to the swap instance's `<app>/admin`. The deploy's BEEF from `deployBeef`: the
|
|
1124
|
+
* connected wallet's module basket, else the discovery lookup at the host root; with neither,
|
|
1125
|
+
* refused here ("no deploy for <id>: not on discovery and not in this wallet"). The swap instance
|
|
1126
|
+
* records the token enabled (a fact) and the host makes its instance at `<origin>/@<tokenId>`. A
|
|
1127
|
+
* token already on the list (disabled) is enabled again with no BEEF (the swap instance keeps its
|
|
1128
|
+
* deploy).
|
|
1129
|
+
*/
|
|
1130
|
+
async function enableToken(tokenId) {
|
|
1131
|
+
const { signedFor, wallet } = needWallet();
|
|
1132
|
+
const id = parseOutpoint(sdkTokenId(tokenId).replace("_", "."));
|
|
1133
|
+
if (!id)
|
|
1134
|
+
throw new Error(`not a token id: ${tokenId}`);
|
|
1135
|
+
const canonical = tokenIdText(id);
|
|
1136
|
+
const row = (await enabledList().catch(() => [])).find((t) => t.tokenId === canonical);
|
|
1137
|
+
let beef;
|
|
1138
|
+
let from = "wallet";
|
|
1139
|
+
if (row?.status !== "disabled") {
|
|
1140
|
+
const d = await deployBeef(canonical, wallet);
|
|
1141
|
+
beef = d.beef;
|
|
1142
|
+
from = d.from;
|
|
1143
|
+
}
|
|
1144
|
+
const me = await swapIdentity();
|
|
1145
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "enable", args: { tokenId: canonical, ...(beef ? { beef: Uint8Array.from(beef) } : {}) } });
|
|
1146
|
+
s.listRead = null; // the list changed: read again
|
|
1147
|
+
emit();
|
|
1148
|
+
return { beefFrom: from };
|
|
1149
|
+
}
|
|
1150
|
+
/** The discovery instance's app base (Open Exchange: the fixed handle `discovery`, its 1Color app `1color`). */
|
|
1151
|
+
function discoveryBase() {
|
|
1152
|
+
return `${hostRoot()}/@discovery/1color`;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* What Settings' "Enable a token" lists (2026-10-31; 0.34.0, D20): discovery's token list (`GET
|
|
1156
|
+
* /@discovery/1color/tokens`, paged; a failed read — discovery off — is an empty list) ∪ the asset
|
|
1157
|
+
* ids of the connected wallet's held token outputs (`p color assets`), minus the tokens on the
|
|
1158
|
+
* swap instance's list (read once). The wallet-only tokens' metadata: ONE `GET
|
|
1159
|
+
* /@discovery/1color/tokens?ids=` for them, else the deploy in the wallet's module basket, else
|
|
1160
|
+
* none (the page shows its short id).
|
|
1161
|
+
*/
|
|
1162
|
+
async function tokensToEnable() {
|
|
1163
|
+
const { wallet } = needWallet();
|
|
1164
|
+
const [listed, held, list] = await Promise.all([readDiscoveryList(discoveryBase(), f).catch(() => []), heldOutputs(wallet), enabledList()]);
|
|
1165
|
+
const walletIds = [...new Set(held.map((h) => decodeTokenRow(h)?.tokenId).filter((t) => typeof t === "string"))];
|
|
1166
|
+
const asListed = listed.map((r) => ({ tokenId: r.tokenId, topic: r.topic ?? `tm_color_${r.tokenId}`, ...(r.sym !== undefined ? { sym: r.sym } : {}), ...(r.dec !== undefined ? { dec: r.dec } : {}), ...(r.iconUrl ? { icon: r.iconUrl } : {}) }));
|
|
1167
|
+
const rows = enableCandidates(asListed, walletIds, list.map((t) => t.tokenId));
|
|
1168
|
+
const disc = await discoveryRows(rows.filter((r) => !r.onDiscovery).map((r) => r.tokenId));
|
|
1169
|
+
return Promise.all(rows.map(async (r) => {
|
|
1170
|
+
if (r.onDiscovery)
|
|
1171
|
+
return r;
|
|
1172
|
+
const d = disc.get(r.tokenId);
|
|
1173
|
+
if (d)
|
|
1174
|
+
return { ...r, ...(d.sym !== undefined ? { sym: d.sym } : {}), ...(d.iconUrl ? { icon: d.iconUrl } : {}) };
|
|
1175
|
+
const beef = await walletDeployBeef(wallet, r.tokenId).catch(() => undefined);
|
|
1176
|
+
const op = parseOutpoint(r.tokenId.replace("_", "."));
|
|
1177
|
+
const m = beef && op ? deployMetaOf(beef, op.vout) : null;
|
|
1178
|
+
return { ...r, ...(m?.sym !== undefined ? { sym: m.sym } : {}), ...(m?.icon ? { icon: m.icon } : {}) };
|
|
1179
|
+
}));
|
|
1180
|
+
}
|
|
1181
|
+
/** Disables a token (0.17.0): root's `{fn: "disable", args: {tokenId}}` to `<app>/admin`; its instance stopped. */
|
|
1182
|
+
async function disableToken(tokenId) {
|
|
1183
|
+
const { signedFor } = needWallet();
|
|
1184
|
+
const me = await swapIdentity();
|
|
1185
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "disable", args: { tokenId: sdkTokenId(tokenId) } });
|
|
1186
|
+
s.listRead = null; // the list changed: read again
|
|
1187
|
+
emit();
|
|
1188
|
+
}
|
|
1189
|
+
/** The defaults a new token instance is created with (0.17.0, `GET <config>/defaults`). */
|
|
1190
|
+
async function defaults() {
|
|
1191
|
+
return readDefaults(swapBase, ff);
|
|
1192
|
+
}
|
|
1193
|
+
async function sendDefaults(args) {
|
|
1194
|
+
const { signedFor } = needWallet();
|
|
1195
|
+
const me = await swapIdentity();
|
|
1196
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "defaults", args });
|
|
1197
|
+
emit();
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* 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 token
|
|
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.
|
|
1204
|
+
*/
|
|
1205
|
+
async function setDefaultTerms(t) {
|
|
1206
|
+
for (const [k, v] of Object.entries(t))
|
|
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 } });
|
|
1210
|
+
}
|
|
1211
|
+
/** 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
|
+
async function tokenTerms(tokenId) {
|
|
1213
|
+
return (await instanceTerms(tokenId)).validator;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* 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
|
|
1218
|
+
* `<app>/admin`; swap emits it as a fact and the host sends it to the token's instance as root's
|
|
1219
|
+
* `terms` (null: a market only). Nothing answers back: the token's `/terms` shows the result.
|
|
1220
|
+
* Refused here unless each fee is an integer 0 to 10 000.
|
|
1221
|
+
*/
|
|
1222
|
+
async function setTokenTerms(tokenId, t) {
|
|
1223
|
+
if (t)
|
|
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`);
|
|
1227
|
+
const { signedFor } = needWallet();
|
|
1228
|
+
const me = await swapIdentity();
|
|
1229
|
+
const terms = t ? { lpFeeBps: t.lpBps, validatorFeeBps: t.validatorBps, commissionBps: t.commissionBps } : null;
|
|
1230
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "terms", args: { tokenId: sdkTokenId(tokenId), terms } });
|
|
1231
|
+
}
|
|
1232
|
+
/** Clears the default validator terms (0.17.0): `{fn: "defaults", args: {terms: null}}`; a new token instance is a market only. */
|
|
1233
|
+
async function clearDefaultTerms() {
|
|
1234
|
+
await sendDefaults({ terms: null });
|
|
1235
|
+
}
|
|
1236
|
+
/** Sets the default market minimum (0.17.0): `{fn: "defaults", args: {market: {minCommissionBps}}}`; a new token instance's initial minimum. */
|
|
1237
|
+
async function setDefaultMarket(minCommissionBps) {
|
|
1238
|
+
if (!Number.isInteger(minCommissionBps) || minCommissionBps < 0 || minCommissionBps > 10_000)
|
|
1239
|
+
throw new Error("minCommissionBps: an integer, 0 to 10000 basis points");
|
|
1240
|
+
await sendDefaults({ market: { minCommissionBps } });
|
|
1241
|
+
}
|
|
1242
|
+
/** Clears the default market minimum (0.17.0): `{fn: "defaults", args: {market: null}}`. */
|
|
1243
|
+
async function clearDefaultMarket() {
|
|
1244
|
+
await sendDefaults({ market: null });
|
|
1245
|
+
}
|
|
1246
|
+
/** The discovery instance's record (0.18.0, shruggr/skein#137; `GET <config>/discovery`): `{on: false}` until root turns it on. */
|
|
1247
|
+
async function discoveryState() {
|
|
1248
|
+
return readDiscovery(swapBase, ff);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Turns the 1Color discovery topic on or off: root's `{fn: "discovery", args: {on}}` to the swap
|
|
1252
|
+
* instance's `<app>/admin` (Open Exchange: a fact; the host starts or stops the instance
|
|
1253
|
+
* `discovery` it made at setup, serving `tm_color` and its lookup `ls_color`); a deploy POSTed to
|
|
1254
|
+
* this origin's `/submit` with `X-Topics: tm_color` is routed to it. Enabling a token does not
|
|
1255
|
+
* submit its deploy there.
|
|
1256
|
+
*/
|
|
1257
|
+
async function setDiscovery(on) {
|
|
1258
|
+
const { signedFor } = needWallet();
|
|
1259
|
+
const me = await swapIdentity();
|
|
1260
|
+
await sendMessage(signedFor(swapBase), swapBase, me, `${app()}/admin`, { fn: "discovery", args: { on } });
|
|
1261
|
+
emit();
|
|
1262
|
+
}
|
|
1263
|
+
/** The same exchange, fee rate and fetch with another wallet (or none); what this client read and planned is kept (its quotes still execute). */
|
|
1264
|
+
function withWallet(wallet, o = {}) {
|
|
1265
|
+
const { signedFor: _s, identityKey: _k, ...rest } = opts;
|
|
1266
|
+
return makeClient({ ...rest, wallet, ...(o.signedFor ? { signedFor: o.signedFor } : {}), ...(o.identityKey !== undefined ? { identityKey: o.identityKey } : {}) }, s);
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
/** The swap app's base URL (`origin` as given, or `<origin>/amm`). */
|
|
1270
|
+
base: swapBase,
|
|
1271
|
+
feeRateSatsPerKb: feeRate,
|
|
1272
|
+
wallet: boundWallet,
|
|
1273
|
+
withWallet,
|
|
1274
|
+
onDataChange,
|
|
1275
|
+
forgetReads,
|
|
1276
|
+
// the exchange
|
|
1277
|
+
enabledList: guard(enabledList),
|
|
1278
|
+
tokenBaseOf: guard(tokenBaseOf),
|
|
1279
|
+
hostRoot,
|
|
1280
|
+
discoveryBase,
|
|
1281
|
+
claimStatus: guard(claimStatus),
|
|
1282
|
+
isRoot,
|
|
1283
|
+
claimExchange: guard(claimExchange),
|
|
1284
|
+
hostedTokens: guard(hostedTokens),
|
|
1285
|
+
watchHostedPools,
|
|
1286
|
+
watchTokenPools,
|
|
1287
|
+
registeredTokens: guard(registeredTokens),
|
|
1288
|
+
// swap
|
|
1289
|
+
quoteSwap,
|
|
1290
|
+
executeSwap,
|
|
1291
|
+
checkSwapAgain: guard(checkSwapAgain),
|
|
1292
|
+
walletSats: guard(walletSats),
|
|
1293
|
+
// liquidity
|
|
1294
|
+
myPositions: guard(myPositions),
|
|
1295
|
+
validatorTerms: guard(validatorTerms),
|
|
1296
|
+
deployPosition: guard(deployPosition),
|
|
1297
|
+
closePosition: guard(closePosition),
|
|
1298
|
+
rebalancePosition: guard(rebalancePosition),
|
|
1299
|
+
// your tokens
|
|
1300
|
+
myTokens: guard(myTokens),
|
|
1301
|
+
settleSwaps: guard(settleSwaps),
|
|
1302
|
+
moveBackTokens: guard(moveBackTokens),
|
|
1303
|
+
sendTokens: guard(sendTokens),
|
|
1304
|
+
// settings
|
|
1305
|
+
deployBeef: guard(deployBeef),
|
|
1306
|
+
enableToken: guard(enableToken),
|
|
1307
|
+
tokensToEnable: guard(tokensToEnable),
|
|
1308
|
+
disableToken: guard(disableToken),
|
|
1309
|
+
defaults: guard(defaults),
|
|
1310
|
+
setDefaultTerms: guard(setDefaultTerms),
|
|
1311
|
+
tokenTerms: guard(tokenTerms),
|
|
1312
|
+
setTokenTerms: guard(setTokenTerms),
|
|
1313
|
+
clearDefaultTerms: guard(clearDefaultTerms),
|
|
1314
|
+
setDefaultMarket: guard(setDefaultMarket),
|
|
1315
|
+
clearDefaultMarket: guard(clearDefaultMarket),
|
|
1316
|
+
discoveryState: guard(discoveryState),
|
|
1317
|
+
setDiscovery: guard(setDiscovery),
|
|
1318
|
+
};
|
|
1319
|
+
}
|