@dcasegr/amm-client 0.0.0-stage → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +107 -2
  2. package/dist/client.d.ts +534 -0
  3. package/dist/client.js +1319 -0
  4. package/dist/errors.d.ts +17 -0
  5. package/dist/errors.js +75 -0
  6. package/dist/index.d.ts +51 -0
  7. package/dist/index.js +53 -0
  8. package/dist/lib/config.d.ts +42 -0
  9. package/dist/lib/config.js +64 -0
  10. package/dist/lib/keys.d.ts +42 -0
  11. package/dist/lib/keys.js +48 -0
  12. package/dist/lib/overlay.d.ts +190 -0
  13. package/dist/lib/overlay.js +302 -0
  14. package/dist/lib/skein.d.ts +82 -0
  15. package/dist/lib/skein.js +142 -0
  16. package/dist/lib/tokenId.d.ts +72 -0
  17. package/dist/lib/tokenId.js +121 -0
  18. package/dist/lp/amounts.d.ts +12 -0
  19. package/dist/lp/amounts.js +38 -0
  20. package/dist/lp/close.d.ts +90 -0
  21. package/dist/lp/close.js +140 -0
  22. package/dist/lp/images.d.ts +11 -0
  23. package/dist/lp/images.js +45 -0
  24. package/dist/lp/inventory.d.ts +68 -0
  25. package/dist/lp/inventory.js +84 -0
  26. package/dist/lp/poolDeploy.d.ts +220 -0
  27. package/dist/lp/poolDeploy.js +291 -0
  28. package/dist/lp/poolRows.d.ts +13 -0
  29. package/dist/lp/poolRows.js +19 -0
  30. package/dist/lp/positions.d.ts +81 -0
  31. package/dist/lp/positions.js +144 -0
  32. package/dist/lp/rebalance.d.ts +101 -0
  33. package/dist/lp/rebalance.js +168 -0
  34. package/dist/lp/wallet.d.ts +12 -0
  35. package/dist/lp/wallet.js +19 -0
  36. package/dist/market/activeSwap.d.ts +158 -0
  37. package/dist/market/activeSwap.js +320 -0
  38. package/dist/market/plan.d.ts +85 -0
  39. package/dist/market/plan.js +149 -0
  40. package/dist/market/prices.d.ts +13 -0
  41. package/dist/market/prices.js +14 -0
  42. package/dist/market/relay.d.ts +84 -0
  43. package/dist/market/relay.js +201 -0
  44. package/dist/market/swapAction.d.ts +306 -0
  45. package/dist/market/swapAction.js +508 -0
  46. package/dist/market/swapFlow.d.ts +58 -0
  47. package/dist/market/swapFlow.js +86 -0
  48. package/dist/market/view.d.ts +58 -0
  49. package/dist/market/view.js +63 -0
  50. package/dist/pool/artifact.d.ts +23 -0
  51. package/dist/pool/artifact.js +38 -0
  52. package/dist/pool/index.d.ts +8 -0
  53. package/dist/pool/index.js +8 -0
  54. package/dist/pool/oneColor.d.ts +19 -0
  55. package/dist/pool/oneColor.js +39 -0
  56. package/dist/pool/pool.artifact.json +5069 -0
  57. package/dist/pool/runar-ir-schema.d.ts +21 -0
  58. package/dist/pool/runar-ir-schema.js +18 -0
  59. package/dist/pool/template.d.ts +295 -0
  60. package/dist/pool/template.js +715 -0
  61. package/dist/vendor/runar/LICENSE +21 -0
  62. package/dist/vendor/runar/NOTICE +15 -0
  63. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
  64. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
  65. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
  66. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
  67. package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
  68. package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
  69. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
  70. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
  71. package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
  72. package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
  73. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
  74. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
  75. package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
  76. package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
  77. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
  78. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
  79. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
  80. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
  81. package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
  82. package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
  83. package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
  84. package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
  85. package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
  86. package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
  87. package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
  88. package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
  89. package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
  90. package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
  91. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
  92. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
  93. package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
  94. package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
  95. package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
  96. package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
  97. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
  98. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
  99. package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
  100. package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
  101. package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
  102. package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
  103. package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
  104. package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
  105. package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
  106. package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
  107. package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
  108. package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
  109. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
  110. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
  111. package/dist/vendor/runar/runar-sdk.d.ts +9 -0
  112. package/dist/vendor/runar/runar-sdk.js +8 -0
  113. package/dist/wallet/action.d.ts +28 -0
  114. package/dist/wallet/action.js +42 -0
  115. package/dist/wallet/brc29.d.ts +83 -0
  116. package/dist/wallet/brc29.js +80 -0
  117. package/dist/wallet/color.d.ts +110 -0
  118. package/dist/wallet/color.js +134 -0
  119. package/dist/wallet/swapPayouts.d.ts +35 -0
  120. package/dist/wallet/swapPayouts.js +146 -0
  121. package/package.json +39 -4
@@ -0,0 +1,320 @@
1
+ /**
2
+ * The active swap basket (skein-amm 0.12.0, David Case 2026-10-21/22; 0.29.0,
3
+ * 2026-11-02): everything the swap legs spend, held outside the BRC-207 module
4
+ * in the exchange's own basket `amm-swap` (src/wallet/color.ts): the token
5
+ * outputs and each leg's sats.
6
+ *
7
+ * - **In** (`sendToSwapBasket`): before the legs of a tokens-in swap, ONE
8
+ * wallet-built send through the module: one 1Color value output per leg
9
+ * not covered by what `amm-swap` already holds, each exactly the leg's
10
+ * amount in (the pool takes no token change, so a leg needs inputs that
11
+ * sum to its amount exactly), locked to a key derived from the leg's pool outpoint (`swapKeyID`) under `[2, "amm
12
+ * swap"]` (counterparty self), the plan naming them a transfer out; and
13
+ * (0.29.0) one P2PKH sats output per leg not covered (the leg's exact
14
+ * sats, `swapFunding`), to that outpoint's `:sats` key, filed in
15
+ * `amm-swap` by the createAction itself (customInstructions `{sats: true,
16
+ * assetId, protocolID, keyID, counterparty}`). The module passes the sats outputs
17
+ * through, selects the token inputs, adds change, asks once, signs and
18
+ * submits to `tm_color_<assetId>`; the wallet funds the sats; the page
19
+ * submits it to this skein too (a repeat is harmless), then files each
20
+ * token output in `amm-swap` (basket insertion, customInstructions
21
+ * `{assetId, amount, protocolID, keyID, counterparty}`). A sats-in swap,
22
+ * or a tokens-in swap whose tokens are all staged already, sends only the
23
+ * sats (`sendSatsToSwapBasket`: a plain createAction, no `p color` labels).
24
+ * - **Spent** by the legs (src/market/swapAction.ts): app-built,
25
+ * `createSignature` under the key the customInstructions name; the
26
+ * wallet marks them spent when it takes the accepted swap in (0.29.0: no
27
+ * relinquishOutput). An output a failed leg did not spend stays for a
28
+ * later swap.
29
+ * - **Move back** (`moveBack`, the recovery path; 0.29.3: a module plan,
30
+ * David Case 2026-11-02): ONE createAction through the wallet's BRC-207
31
+ * module, a `p color plan` transfer: every `amm-swap` token output of the
32
+ * token and (0.29.1) the sats rows staged for its legs (their `assetId`)
33
+ * as explicit inputs (their source transactions in `inputBEEF`) — to the
34
+ * module FOREIGN inputs (not in a `p color` basket): the token rows
35
+ * checked admitted and unspent on the token's overlay, the sats rows
36
+ * adding nothing; one output, the whole amount to a NEW module receiving
37
+ * key of the user's own, the plan's owned output 0 (the module checks it
38
+ * pays the remittance's key and files it in `p color <assetTag>` with its
39
+ * custody in the same call). The page unlocks its inputs at signAction;
40
+ * the wallet funds the fee and takes the sats rows into its change; the
41
+ * module approves and submits, the wallet broadcasts. The page submits it
42
+ * to this skein's token topic too. Nothing is taken in afterwards and
43
+ * nothing is kept in the browser. * (0.29.6) With no token row and only sats rows staged for the token
44
+ * (a sats-in leg that failed): one plain createAction (no `p color`
45
+ * labels), the sats rows as explicit inputs and no outputs — the wallet
46
+ * keeps them as its change and pays the fee (`moveBackSats`).
47
+ */
48
+ import { Beef, P2PKH, PublicKey, Transaction, Utils } from "@bsv/sdk";
49
+ import { decodeOneColor } from "../pool/index.js";
50
+ import { parseOutpoint, parseTokenId, sdkTokenId, tokenIdOfWire, tokenIdText } from "../lib/tokenId.js";
51
+ import { basketBeef, basketRows } from "../lp/wallet.js";
52
+ import { tokenP2pkhScript } from "../lp/poolDeploy.js";
53
+ import { steakText, submitToOverlay } from "../lp/close.js";
54
+ import { SWAP_BASKET, SWAP_PROTOCOL, newReceivingKey, swapInstructions, swapSatsInstructions, runTokenAction, transferPlanLabels } from "../wallet/color.js";
55
+ import { identityKeyOf } from "../wallet/brc29.js";
56
+ import { inputIndexOf, runAppSignedAction } from "../wallet/action.js";
57
+ import { P2PKH_UNLOCK_LENGTH, satsInputsOf, signP2pkhWithWallet, spendValid, tokenInputsOf } from "./swapAction.js";
58
+ /** The keyID prefix of an `amm-swap` key (`SWAP_PROTOCOL`, counterparty self). */
59
+ export const SWAP_KEY_PREFIX = "amm-swap-";
60
+ function describe(text) {
61
+ return text.length <= 50 ? text : `${text.slice(0, 49)}…`;
62
+ }
63
+ /**
64
+ * The keyID of a staged `amm-swap` output (0.29.2, David Case 2026-11-02: derived, not random):
65
+ * `amm-swap-<txid>.<vout>` for a leg's token row and `amm-swap-<txid>.<vout>:sats` for its sats
66
+ * row, `<txid>.<vout>` the leg's pool outpoint as quoted (`leg.outpoint`, `_` written `.`).
67
+ * Protocol `[2, "amm swap"]`, counterparty self, as before. A leg refused and retried against the
68
+ * same pool outpoint stages to the same key again, which is fine.
69
+ */
70
+ export function swapKeyID(poolOutpoint, kind) {
71
+ const op = parseOutpoint(poolOutpoint);
72
+ if (!op)
73
+ throw new Error(`not a pool outpoint: ${poolOutpoint}`);
74
+ return `${SWAP_KEY_PREFIX}${op.txid}.${op.vout}${kind === "sats" ? ":sats" : ""}`;
75
+ }
76
+ /** The `amm-swap` outputs of `tokenId` the legs can spend. */
77
+ export async function swapHoldings(wallet, tokenId) {
78
+ return tokenInputsOf(await basketRows(wallet, SWAP_BASKET), tokenId);
79
+ }
80
+ /** The `amm-swap` sats outputs the legs can spend (0.29.0). */
81
+ export async function swapSatsHoldings(wallet) {
82
+ return satsInputsOf(await basketRows(wallet, SWAP_BASKET));
83
+ }
84
+ /**
85
+ * What `amm-swap` holds, per token id: the token amount and the number of token outputs, and
86
+ * (0.29.6) the sats staged for the token's legs (the sats rows whose customInstructions name its
87
+ * `assetId`). A token with only sats staged has amount 0n and outputs 0.
88
+ */
89
+ export async function swapTotals(wallet) {
90
+ const totals = new Map();
91
+ const rows = await basketRows(wallet, SWAP_BASKET);
92
+ for (const r of rows) {
93
+ if (!r.lockingScript)
94
+ continue;
95
+ const t = decodeOneColor(Utils.toArray(r.lockingScript, "hex"));
96
+ if (!t || t.role !== "value" || !t.idBytes)
97
+ continue;
98
+ const id = tokenIdOfWire(t.idBytes);
99
+ const s = totals.get(id) ?? { amount: 0n, outputs: 0, sats: 0 };
100
+ s.amount += t.amount;
101
+ s.outputs++;
102
+ totals.set(id, s);
103
+ }
104
+ for (const r of satsInputsOf(rows)) {
105
+ const ref = r.assetId ? parseTokenId(r.assetId) : null;
106
+ if (!ref)
107
+ continue;
108
+ const id = tokenIdText(ref);
109
+ const s = totals.get(id) ?? { amount: 0n, outputs: 0, sats: 0 };
110
+ s.sats += r.satoshis;
111
+ totals.set(id, s);
112
+ }
113
+ return totals;
114
+ }
115
+ /** The source transactions of `amm-swap`'s outputs (a leg's BEEF), with `extra` merged in. */
116
+ export async function swapHoldingBeef(wallet, extra) {
117
+ const beef = (await basketBeef(wallet, SWAP_BASKET)) ?? new Beef();
118
+ if (extra)
119
+ beef.mergeBeef(Beef.fromBinary(extra));
120
+ return beef;
121
+ }
122
+ /** The "amm swap" keys (`swapKeyID`) for `sats` and their outputs, filed in `amm-swap` by the createAction itself. */
123
+ async function satsOutputs(wallet, sats, sym, assetId) {
124
+ return await Promise.all(sats.map(async ({ satoshis, pool }) => {
125
+ if (!Number.isSafeInteger(satoshis) || satoshis <= 0)
126
+ throw new Error("nothing to send to the swap basket");
127
+ const keyID = swapKeyID(pool, "sats");
128
+ const { publicKey } = await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", forSelf: true });
129
+ const lockingScript = new P2PKH().lock(PublicKey.fromString(publicKey).toAddress()).toHex();
130
+ return {
131
+ keyID,
132
+ assetId,
133
+ satoshis,
134
+ lockingScript,
135
+ output: {
136
+ lockingScript,
137
+ satoshis,
138
+ outputDescription: describe(`Sats for a ${sym} swap leg`),
139
+ basket: SWAP_BASKET,
140
+ customInstructions: swapSatsInstructions({ assetId, protocolID: SWAP_PROTOCOL, keyID, counterparty: "self" }),
141
+ },
142
+ };
143
+ }));
144
+ }
145
+ /** The send's sats outputs from `first` on, as `SatsInput`s (an output the wallet moved is an error). */
146
+ function satsHeld(final, txid, keys, first, errors) {
147
+ const out = [];
148
+ keys.forEach((k, n) => {
149
+ const vout = first + n;
150
+ const o = final.outputs[vout];
151
+ if (!o || o.lockingScript.toHex() !== k.lockingScript || o.satoshis !== k.satoshis) {
152
+ errors.push(`the wallet's send has no output ${vout} to the swap key`);
153
+ return;
154
+ }
155
+ out.push({ outpoint: `${txid}.${vout}`, txid, vout, satoshis: k.satoshis, lockingScript: k.lockingScript, protocolID: SWAP_PROTOCOL, keyID: k.keyID, counterparty: "self", assetId: k.assetId });
156
+ });
157
+ return out;
158
+ }
159
+ /**
160
+ * "In" (see the module comment): one module send of `stage.tokens` (each one
161
+ * output) to `amm-swap` keys derived from each leg's pool (`swapKeyID`), filed in `amm-swap`, and of `stage.sats`
162
+ * (each one output) after them. Throws when the wallet refuses or fails the
163
+ * send; filing and the page's own submit are reported in `errors`.
164
+ */
165
+ export async function sendToSwapBasket(wallet, base, tokenId, stage, meta = {}, submit = (topic, beef) => submitToOverlay(base, topic, beef)) {
166
+ const amounts = stage.tokens;
167
+ if (amounts.length === 0 || amounts.some((a) => a.amount <= 0n))
168
+ throw new Error("nothing to send to the swap basket");
169
+ const assetId = sdkTokenId(tokenId);
170
+ const sym = meta.sym ?? "token";
171
+ const keys = await Promise.all(amounts.map(async ({ amount, pool }) => {
172
+ const keyID = swapKeyID(pool, "token");
173
+ const { publicKey } = await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", forSelf: true });
174
+ return { amount, keyID, publicKey, lockingScript: tokenP2pkhScript(assetId, amount, publicKey) };
175
+ }));
176
+ const satsKeys = await satsOutputs(wallet, stage.sats, sym, assetId);
177
+ const { txid, tx } = await runTokenAction(wallet, {
178
+ description: describe(`AMM swap: ${sym} into the swap basket`),
179
+ labels: [...transferPlanLabels(assetId), "amm-swap"],
180
+ outputs: [...keys.map((k) => ({ lockingScript: k.lockingScript, satoshis: 1, outputDescription: describe(`${sym} for a swap leg`) })), ...satsKeys.map((k) => k.output)],
181
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
182
+ });
183
+ const out = { txid, tx, holdings: [], sats: [], filed: false, submitted: "not submitted", errors: [] };
184
+ const final = Transaction.fromAtomicBEEF(tx);
185
+ keys.forEach((k, vout) => {
186
+ const o = final.outputs[vout];
187
+ if (!o || o.lockingScript.toHex() !== k.lockingScript) {
188
+ out.errors.push(`the wallet's send has no output ${vout} to the swap key`);
189
+ return;
190
+ }
191
+ out.holdings.push({ outpoint: `${txid}.${vout}`, txid, vout, satoshis: o.satoshis, lockingScript: k.lockingScript, amount: k.amount, protocolID: SWAP_PROTOCOL, keyID: k.keyID, counterparty: "self" });
192
+ });
193
+ out.sats = satsHeld(final, txid, satsKeys, keys.length, out.errors);
194
+ try {
195
+ out.submitted = steakText(await submit(`tm_color_${assetId}`, tx));
196
+ }
197
+ catch (err) {
198
+ out.submitted = "the submit failed";
199
+ out.errors.push(`submit: ${err instanceof Error ? err.message : String(err)}`);
200
+ }
201
+ const rows = out.holdings.map((h) => ({
202
+ outputIndex: h.vout,
203
+ protocol: "basket insertion",
204
+ insertionRemittance: { basket: SWAP_BASKET, customInstructions: swapInstructions({ assetId, amount: h.amount, protocolID: SWAP_PROTOCOL, keyID: h.keyID, counterparty: "self" }) },
205
+ }));
206
+ try {
207
+ const r = await wallet.internalizeAction({ tx, outputs: rows, description: describe(`AMM swap: ${sym} into the swap basket`), labels: ["amm-swap"] });
208
+ out.filed = r.accepted;
209
+ }
210
+ catch (err) {
211
+ out.errors.push(`internalizeAction: ${err instanceof Error ? err.message : String(err)}`);
212
+ }
213
+ return out;
214
+ }
215
+ /**
216
+ * The sats-only send (0.29.0): one plain createAction (no `p color` labels: the module is not in
217
+ * the path) with one P2PKH output per amount to the leg's "amm swap" `:sats` key (`swapKeyID`), filed in `amm-swap`; the
218
+ * wallet funds it, adds change and broadcasts. Throws when the wallet refuses or fails it.
219
+ */
220
+ export async function sendSatsToSwapBasket(wallet, tokenId, sats, meta = {}) {
221
+ if (sats.length === 0)
222
+ throw new Error("nothing to send to the swap basket");
223
+ const sym = meta.sym ?? "token";
224
+ const keys = await satsOutputs(wallet, sats, sym, sdkTokenId(tokenId));
225
+ const { txid, tx } = await runTokenAction(wallet, {
226
+ description: describe(`AMM swap: sats into the swap basket`),
227
+ labels: ["amm-swap"],
228
+ outputs: keys.map((k) => k.output),
229
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
230
+ });
231
+ const errors = [];
232
+ return { txid, tx, sats: satsHeld(Transaction.fromAtomicBEEF(tx), txid, keys, 0, errors), errors };
233
+ }
234
+ /** The page's unlocking scripts for the `amm-swap` outputs `holdings` in the wallet's transaction `tx`, each checked with `Spend`. */
235
+ export async function swapBasketSpends(wallet, tx, holdings, what) {
236
+ const spends = {};
237
+ for (const h of holdings) {
238
+ const n = inputIndexOf(tx, h.txid, h.vout);
239
+ if (n < 0)
240
+ throw new Error(`the wallet's ${what} does not spend the swap basket output ${h.outpoint}`);
241
+ tx.inputs[n].unlockingScript = await signP2pkhWithWallet(wallet, tx, n, h);
242
+ if (!spendValid(tx, n))
243
+ throw new Error(`input ${n} of the ${what} does not validate`);
244
+ spends[n] = { unlockingScript: tx.inputs[n].unlockingScript.toHex() };
245
+ }
246
+ return spends;
247
+ }
248
+ /**
249
+ * "Move back" (see the module comment; 0.29.3: a module plan). Throws before anything is
250
+ * broadcast (no outputs, a wallet or module refusal, an input that does not validate: the action
251
+ * is aborted). The module files the output in `p color <assetTag>` with its custody in the same
252
+ * createAction: no take-in afterwards, nothing kept in the browser. The page's own submit to this
253
+ * skein's token topic (the module submitted at the wallet's overlay too) is reported in `errors`.
254
+ */
255
+ export async function moveBack(i) {
256
+ const { wallet } = i;
257
+ const assetId = sdkTokenId(i.tokenId);
258
+ const sym = i.meta?.sym ?? "token";
259
+ const topic = `tm_color_${assetId}`;
260
+ const rows = await basketRows(wallet, SWAP_BASKET);
261
+ const holdings = tokenInputsOf(rows, assetId);
262
+ // 0.29.1 (David): the token's sats rows (staged for its legs) go in too; the wallet's change takes their sats.
263
+ const satsRows = satsInputsOf(rows).filter((r) => r.assetId === assetId);
264
+ if (holdings.length === 0 && satsRows.length > 0)
265
+ return await moveBackSats(wallet, sym, satsRows);
266
+ if (holdings.length === 0)
267
+ throw new Error(`the swap basket holds no ${sym}`);
268
+ const amount = holdings.reduce((a, h) => a + h.amount, 0n);
269
+ const sources = await swapHoldingBeef(wallet);
270
+ for (const h of [...holdings, ...satsRows])
271
+ if (!sources.findTxid(h.txid))
272
+ throw new Error(`the wallet's BEEF lacks the swap basket output ${h.outpoint}`);
273
+ // The output: the whole amount to a module receiving key of the user's own, the plan's owned output 0.
274
+ const me = await identityKeyOf(wallet);
275
+ const key = await newReceivingKey(wallet, assetId, me);
276
+ const lockingScript = tokenP2pkhScript(assetId, amount, key.publicKey);
277
+ const { txid, tx: beef } = await runAppSignedAction(wallet, {
278
+ description: describe(`AMM move back: ${sym}`),
279
+ inputBEEF: sources.toBinary(),
280
+ inputs: [
281
+ ...holdings.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe(`${sym} in the swap basket`) })),
282
+ ...satsRows.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe(`Sats staged for a ${sym} swap`) })),
283
+ ],
284
+ outputs: [{ lockingScript, satoshis: 1, outputDescription: describe(`${sym} back to your wallet`) }],
285
+ labels: [...transferPlanLabels(assetId, { ownedOutputs: [{ outputIndex: 0, ...key.receipt }] }), "amm-swap"],
286
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
287
+ }, (tx) => swapBasketSpends(wallet, tx, [...holdings, ...satsRows], "move back"));
288
+ const out = { txid, amount, beef, submitted: "not submitted", errors: [] };
289
+ try {
290
+ out.submitted = steakText(await (i.submit ?? ((t, b) => submitToOverlay(i.base, t, b)))(topic, beef));
291
+ }
292
+ catch (err) {
293
+ out.submitted = "the submit failed";
294
+ out.errors.push(`submit: ${err instanceof Error ? err.message : String(err)}`);
295
+ }
296
+ return out;
297
+ }
298
+ /**
299
+ * Move back with only sats staged for the token (0.29.6, David Case 2026-11-03: a sats-in leg that
300
+ * failed leaves sats and no token): ONE plain wallet createAction (no `p color` labels, the module
301
+ * is not in the path) with the token's `amm-swap` sats rows as explicit inputs (their sources in
302
+ * `inputBEEF`) and NO outputs: the wallet keeps everything as its change and pays the fee from it.
303
+ * The page unlocks the rows at signAction; the wallet broadcasts and marks the rows spent itself
304
+ * (no relinquish, no take-in, nothing kept). Nothing is submitted to an overlay (no token moves).
305
+ */
306
+ async function moveBackSats(wallet, sym, satsRows) {
307
+ const sources = await swapHoldingBeef(wallet);
308
+ for (const h of satsRows)
309
+ if (!sources.findTxid(h.txid))
310
+ throw new Error(`the wallet's BEEF lacks the swap basket output ${h.outpoint}`);
311
+ const { txid, tx: beef } = await runAppSignedAction(wallet, {
312
+ description: describe(`AMM move back: ${sym} sats`),
313
+ inputBEEF: sources.toBinary(),
314
+ inputs: satsRows.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe(`Sats staged for a ${sym} swap`) })),
315
+ outputs: [],
316
+ labels: ["amm-swap"],
317
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
318
+ }, (tx) => swapBasketSpends(wallet, tx, satsRows, "move back"));
319
+ return { txid, amount: 0n, beef, submitted: "not submitted (no token moved)", errors: [] };
320
+ }
@@ -0,0 +1,85 @@
1
+ import type { Direction, Plan, PlanRequest, PoolState } from "@dcasegr/amm-engine";
2
+ import type { LiveAnswer } from "../lib/overlay.js";
3
+ import { type ValidatorStatus } from "./view.js";
4
+ /**
5
+ * Estimated miner fee per leg, in sats, for the engine's per-leg cost
6
+ * heuristic: a swap is ~13 KB (docs/notes.md, "Publishing is part of
7
+ * submit") at the client's fee rate (`createClient`'s `feeRateSatsPerKb`; the pages':
8
+ * `VITE_FEE_RATE`). The exact fee of a built swap is `swapFunding` (src/market/swapAction.ts).
9
+ */
10
+ export declare function legMinerFeeSats(satsPerKb: number): bigint;
11
+ /** `legMinerFeeSats` at the default rate (100 sats/kB). */
12
+ export declare const LEG_MINER_FEE_SATS: bigint;
13
+ export interface SwapForm {
14
+ direction: Direction;
15
+ /** Sats (bsvToToken) or tokens in display units (tokenToBsv). */
16
+ amount: string;
17
+ slippageBps: string;
18
+ allowPartial: boolean;
19
+ }
20
+ export type PlanInput = {
21
+ ok: true;
22
+ request: PlanRequest;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ };
27
+ /**
28
+ * The pools the planner may use (0.4.0, shruggr/skein#120): those whose validator is in the token's
29
+ * liveness (0.34.0: the pools route's rows, src/lib/overlay.ts `liveOfPools`: the host listed only the pools whose validator beats; before, the `.live` read), which holds only the beats within the
30
+ * instance's own window. No read, none.
31
+ */
32
+ export declare function livePools(pools: PoolState[], live: LiveAnswer | null): PoolState[];
33
+ /**
34
+ * Parses the form against one token's pools; only the pools whose validator is live (`livePools`)
35
+ * are planned. `dec` scales a token amount (base units when unknown).
36
+ */
37
+ export declare function buildPlanRequest(tokenId: string, form: SwapForm, pools: PoolState[], live: LiveAnswer | null, dec: number | undefined,
38
+ /** The miner fee rate the per-leg cost is estimated at, sats per 1000 bytes (default 100). */
39
+ satsPerKb?: number): PlanInput;
40
+ export interface LegView {
41
+ outpoint: string;
42
+ pool: PoolState;
43
+ amountIn: bigint;
44
+ amountOut: bigint;
45
+ lpFee: bigint;
46
+ validatorFee: bigint;
47
+ /** The pool's commission (CommissionBps of amountIn), in the input asset; 0 for a pool with none. */
48
+ commission: bigint;
49
+ validator: ValidatorStatus;
50
+ }
51
+ export interface PlanView {
52
+ direction: Direction;
53
+ legs: LegView[];
54
+ totalIn: bigint;
55
+ totalOut: bigint;
56
+ /** In the input asset (LP and validator fees and the commission are taken from amountIn). */
57
+ lpFees: bigint;
58
+ validatorFees: bigint;
59
+ commissions: bigint;
60
+ /** Sats per display token paid (bsvToToken) or received (tokenToBsv). */
61
+ effectivePrice: string;
62
+ /** The best pool's marginal price, same unit. */
63
+ midPrice: string;
64
+ /** What the order would get at the mid price, minus what the plan gets, in bps of the former (fees included). */
65
+ slippageVsMidBps: bigint;
66
+ /** The engine's absolute bound (from toleranceBps against its quoted mid). */
67
+ minAmountOut: bigint;
68
+ meetsSlippageBound: boolean;
69
+ filledCompletely: boolean;
70
+ /** amountIn the plan leaves unfilled. */
71
+ unfilled: bigint;
72
+ }
73
+ export declare function planView(p: Plan, inventory: PoolState[], live: LiveAnswer | null, dec: number | undefined): PlanView;
74
+ /** Plans with the engine and shapes the result. */
75
+ export declare function quote(request: PlanRequest, live: LiveAnswer | null, dec: number | undefined): {
76
+ plan: Plan;
77
+ view: PlanView;
78
+ };
79
+ /**
80
+ * Races (docs/notes.md "Market UI and matching engine"): the plan's pools
81
+ * that are no longer in a fresh lookup answer (spent by someone else's swap
82
+ * since the plan was made). A pool's outpoint changes on every spend, so
83
+ * "gone" is exactly "not listed".
84
+ */
85
+ export declare function goneFromLookup(planned: Pick<Plan, "legs">, fresh: PoolState[]): string[];
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The swap form → the engine's `PlanRequest`, and the engine's `Plan` → what
3
+ * the page shows (pure). The planning itself is the engine's (`plan`, partial
4
+ * fills across several pools when allowed); nothing is re-derived here except
5
+ * display numbers: per-leg fees (LP, validator, commission), effective price
6
+ * and slippage against the mid price.
7
+ */
8
+ import { plan as enginePlan } from "@dcasegr/amm-engine";
9
+ import { DEFAULT_FEE_RATE_SATS_PER_KB } from "../lib/config.js";
10
+ import { parseAmount } from "../lp/amounts.js";
11
+ import { priceOf, validatorStatus, withLastSeen } from "./view.js";
12
+ /**
13
+ * Estimated miner fee per leg, in sats, for the engine's per-leg cost
14
+ * heuristic: a swap is ~13 KB (docs/notes.md, "Publishing is part of
15
+ * submit") at the client's fee rate (`createClient`'s `feeRateSatsPerKb`; the pages':
16
+ * `VITE_FEE_RATE`). The exact fee of a built swap is `swapFunding` (src/market/swapAction.ts).
17
+ */
18
+ export function legMinerFeeSats(satsPerKb) {
19
+ return BigInt(Math.ceil(13 * satsPerKb));
20
+ }
21
+ /** `legMinerFeeSats` at the default rate (100 sats/kB). */
22
+ export const LEG_MINER_FEE_SATS = legMinerFeeSats(DEFAULT_FEE_RATE_SATS_PER_KB);
23
+ /**
24
+ * The pools the planner may use (0.4.0, shruggr/skein#120): those whose validator is in the token's
25
+ * liveness (0.34.0: the pools route's rows, src/lib/overlay.ts `liveOfPools`: the host listed only the pools whose validator beats; before, the `.live` read), which holds only the beats within the
26
+ * instance's own window. No read, none.
27
+ */
28
+ export function livePools(pools, live) {
29
+ if (!live)
30
+ return [];
31
+ return pools.filter((p) => validatorStatus(p.validatorIdentityKey, live).live);
32
+ }
33
+ /**
34
+ * Parses the form against one token's pools; only the pools whose validator is live (`livePools`)
35
+ * are planned. `dec` scales a token amount (base units when unknown).
36
+ */
37
+ export function buildPlanRequest(tokenId, form, pools, live, dec,
38
+ /** The miner fee rate the per-leg cost is estimated at, sats per 1000 bytes (default 100). */
39
+ satsPerKb = DEFAULT_FEE_RATE_SATS_PER_KB) {
40
+ if (!form.amount.trim())
41
+ return { ok: false, error: "enter an amount" };
42
+ let amountIn;
43
+ try {
44
+ amountIn = parseAmount(form.amount, form.direction === "bsvToToken" ? 0 : (dec ?? 0));
45
+ }
46
+ catch (err) {
47
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
48
+ }
49
+ if (amountIn <= 0n)
50
+ return { ok: false, error: "the amount must be positive" };
51
+ let toleranceBps;
52
+ try {
53
+ toleranceBps = BigInt(form.slippageBps.trim() || "0");
54
+ }
55
+ catch {
56
+ return { ok: false, error: "slippage: whole basis points" };
57
+ }
58
+ if (toleranceBps < 0n || toleranceBps > 10000n)
59
+ return { ok: false, error: "slippage: 0-10000 bps" };
60
+ if (pools.length === 0)
61
+ return { ok: false, error: "no pools for this token" };
62
+ const usable = livePools(pools, live);
63
+ if (usable.length === 0) {
64
+ return {
65
+ ok: false,
66
+ error: !live
67
+ ? "no liveness read for this token: no pool can be planned"
68
+ : !live.kept
69
+ ? "this instance keeps no liveness for this token (its market role is off for it): no validator is known live"
70
+ : "no pool's validator is live on this token",
71
+ };
72
+ }
73
+ const fixedCost = { minerFeeSats: legMinerFeeSats(satsPerKb) };
74
+ return {
75
+ ok: true,
76
+ request: {
77
+ tokenId,
78
+ direction: form.direction,
79
+ amountIn,
80
+ slippage: { toleranceBps },
81
+ allowPartial: form.allowPartial,
82
+ fixedCost,
83
+ inventory: withLastSeen(usable, live),
84
+ },
85
+ };
86
+ }
87
+ export function planView(p, inventory, live, dec) {
88
+ const byOutpoint = new Map(inventory.map((x) => [x.outpoint, x]));
89
+ const legs = p.legs.map((l) => {
90
+ const pool = byOutpoint.get(l.outpoint);
91
+ return {
92
+ outpoint: l.outpoint,
93
+ pool,
94
+ amountIn: l.amountIn,
95
+ amountOut: l.amountOut,
96
+ lpFee: l.liquidityFee,
97
+ validatorFee: l.validationFee,
98
+ commission: l.commission,
99
+ validator: validatorStatus(pool.validatorIdentityKey, live),
100
+ };
101
+ });
102
+ const sum = (f) => legs.reduce((a, l) => a + f(l), 0n);
103
+ const totalIn = sum((l) => l.amountIn);
104
+ const totalOut = sum((l) => l.amountOut);
105
+ const bsvToToken = p.direction === "bsvToToken";
106
+ // Mid: the best pool's marginal price (most output per unit in), pre-fee.
107
+ let best;
108
+ for (const x of inventory) {
109
+ if (!best)
110
+ best = x;
111
+ else if (bsvToToken ? x.tokenReserve * best.bsvReserve > best.tokenReserve * x.bsvReserve : x.bsvReserve * best.tokenReserve > best.bsvReserve * x.tokenReserve)
112
+ best = x;
113
+ }
114
+ let midOut = 0n;
115
+ if (best && totalIn > 0n)
116
+ midOut = bsvToToken ? (totalIn * best.tokenReserve) / best.bsvReserve : (totalIn * best.bsvReserve) / best.tokenReserve;
117
+ const slippageVsMidBps = midOut > 0n ? ((midOut - totalOut) * 10000n) / midOut : 0n;
118
+ return {
119
+ direction: p.direction,
120
+ legs,
121
+ totalIn,
122
+ totalOut,
123
+ lpFees: sum((l) => l.lpFee),
124
+ validatorFees: sum((l) => l.validatorFee),
125
+ commissions: sum((l) => l.commission),
126
+ effectivePrice: bsvToToken ? priceOf(totalIn, totalOut, dec) : priceOf(totalOut, totalIn, dec),
127
+ midPrice: best ? priceOf(best.bsvReserve, best.tokenReserve, dec) : "—",
128
+ slippageVsMidBps,
129
+ minAmountOut: p.resolvedMinAmountOut,
130
+ meetsSlippageBound: p.meetsSlippageBound,
131
+ filledCompletely: p.filledCompletely,
132
+ unfilled: p.originalAmountIn - totalIn,
133
+ };
134
+ }
135
+ /** Plans with the engine and shapes the result. */
136
+ export function quote(request, live, dec) {
137
+ const p = enginePlan(request);
138
+ return { plan: p, view: planView(p, request.inventory, live, dec) };
139
+ }
140
+ /**
141
+ * Races (docs/notes.md "Market UI and matching engine"): the plan's pools
142
+ * that are no longer in a fresh lookup answer (spent by someone else's swap
143
+ * since the plan was made). A pool's outpoint changes on every spend, so
144
+ * "gone" is exactly "not listed".
145
+ */
146
+ export function goneFromLookup(planned, fresh) {
147
+ const live = new Set(fresh.map((p) => p.outpoint));
148
+ return planned.legs.map((l) => l.outpoint).filter((op) => !live.has(op));
149
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Prices (0.34.0, D6, David Case 2026-11-04: one source for prices): a token's price comes from
3
+ * its pool list — the pools route the host answers (`GET <origin>/amm/pools`, held as an event
4
+ * stream on every page) — Σsats / Σtokens over the pools listed there (the host lists only pools
5
+ * whose validator is beating). No price is derived from beats (the 0.9.0 beat prices are gone);
6
+ * the depth is the sats a buy needs to move the price up by 2%, x·(√1.02 − 1), over the summed
7
+ * sats.
8
+ */
9
+ /** The sats a buy needs to move the marginal price up by `bps`, over `sats` summed reserves; null with none. */
10
+ export declare function depthOf(sats: bigint, bps?: number): {
11
+ bps: number;
12
+ sats: bigint;
13
+ } | null;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prices (0.34.0, D6, David Case 2026-11-04: one source for prices): a token's price comes from
3
+ * its pool list — the pools route the host answers (`GET <origin>/amm/pools`, held as an event
4
+ * stream on every page) — Σsats / Σtokens over the pools listed there (the host lists only pools
5
+ * whose validator is beating). No price is derived from beats (the 0.9.0 beat prices are gone);
6
+ * the depth is the sats a buy needs to move the price up by 2%, x·(√1.02 − 1), over the summed
7
+ * sats.
8
+ */
9
+ /** The sats a buy needs to move the marginal price up by `bps`, over `sats` summed reserves; null with none. */
10
+ export function depthOf(sats, bps = 200) {
11
+ if (sats === 0n)
12
+ return null;
13
+ return { bps, sats: BigInt(Math.round(Number(sats) * (Math.sqrt(1 + bps / 10_000) - 1))) };
14
+ }
@@ -0,0 +1,84 @@
1
+ import type { PoolState } from "@dcasegr/amm-engine";
2
+ export declare const SWAP_SUBMIT_FN = "amm.swap.submit";
3
+ export declare const SWAP_STATUS_FN = "amm.swap.status";
4
+ export declare const SWAP_TERMS_FN = "amm.swap.terms";
5
+ /** The relay's terms: where it takes the commission (hash160 hex), or null when it names none; this market's minimum commission (bps, 0 when unset). */
6
+ export interface SwapTerms {
7
+ commissionPkh: string | null;
8
+ minCommissionBps: number;
9
+ }
10
+ /** What the relay needs of `AuthFetch` (and what the tests fake). */
11
+ export interface AuthFetchLike {
12
+ fetch(url: string, config?: {
13
+ method?: string;
14
+ headers?: Record<string, string>;
15
+ body?: string;
16
+ }): Promise<Pick<Response, "status" | "text">>;
17
+ }
18
+ export interface SwapSubmit {
19
+ /** The funding transaction as AtomicBEEF (the swap's unproven parent, with its ancestry). */
20
+ funding: number[];
21
+ /** Raw swap transaction, the validator's slot empty. */
22
+ swap: number[];
23
+ /** The pool outpoint the swap spends, `<txid>_<vout>`. */
24
+ pool: string;
25
+ /** The pool's validator identity key (hex; sent as 33 bytes). */
26
+ validator: string;
27
+ /** The validator's libp2p peer ID (text), as the liveness read gave it: what the relay dials. */
28
+ peerId: string;
29
+ /** Unix ms after which the relay gives up. */
30
+ expires: number;
31
+ }
32
+ export type SwapRecord = {
33
+ id: string;
34
+ status: "pending";
35
+ } | {
36
+ id: string;
37
+ status: "accepted";
38
+ tx: number[];
39
+ txid: string;
40
+ } | {
41
+ id: string;
42
+ status: "refused";
43
+ reason: string;
44
+ pool?: PoolState;
45
+ } | {
46
+ id: string;
47
+ status: "timeout";
48
+ } | {
49
+ id: string;
50
+ status: "failed";
51
+ reason: string;
52
+ };
53
+ export declare class RelayError extends Error {
54
+ readonly code: string;
55
+ readonly httpStatus: number;
56
+ constructor(code: string, message: string, httpStatus: number);
57
+ }
58
+ export declare function dagBytes(bytes: number[]): {
59
+ "/": {
60
+ bytes: string;
61
+ };
62
+ };
63
+ /** DAG-JSON bytes, hex, or a byte array → bytes; null otherwise. */
64
+ export declare function readBytes(x: unknown): number[] | null;
65
+ export declare function callUrl(base: string): string;
66
+ export declare function submitBody(s: SwapSubmit): {
67
+ fn: string;
68
+ args: Record<string, unknown>;
69
+ };
70
+ /** One `{fn, args}` call on the app's `/call` route. Throws `RelayError` on an error answer. */
71
+ export declare function callApp(af: AuthFetchLike, base: string, fn: string, args: unknown): Promise<unknown>;
72
+ /**
73
+ * A refusal's pool, as the validator writes it (messages.zig poolValue:
74
+ * `lpFeeBps`, `validatorFeeBps`, `validatorIdentity`) or as the lookup does
75
+ * (the engine's names), into the engine's PoolState. A closed pool, or one
76
+ * missing a field, is undefined: nothing left to re-price.
77
+ */
78
+ export declare function toPoolState(raw: unknown): PoolState | undefined;
79
+ export declare function parseSwapRecord(x: unknown): SwapRecord;
80
+ /** `amm.swap.terms`'s answer: `commissionPkh` as DAG-JSON bytes (or hex, or a byte array) of 20 bytes, or null / absent; `minCommissionBps` an integer 0 to 10 000, absent 0 (0.16.0). */
81
+ export declare function parseSwapTerms(x: unknown): SwapTerms;
82
+ export declare function swapTerms(af: AuthFetchLike, base: string): Promise<SwapTerms>;
83
+ export declare function submitSwap(af: AuthFetchLike, base: string, s: SwapSubmit): Promise<SwapRecord>;
84
+ export declare function swapStatus(af: AuthFetchLike, base: string, id: string): Promise<SwapRecord>;