@dcasegr/amm-client 0.38.2 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,53 +1,32 @@
1
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.
2
+ * The active swap basket (skein-amm 0.12.0, David Case 2026-10-21/22; 0.40.0, agent-env
3
+ * plans/swap-balance-build-2026-11-05.md §0 7, Builder 2 item 2): what a swap stages for its legs,
4
+ * held outside the BRC-207 module in the exchange's own basket `amm-swap` (src/wallet/color.ts).
5
+ * No deposit, no withdraw, no balance the user manages.
6
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`).
7
+ * - **Staging** (`stage`): at Swap, ONE wallet action. Sats only (a buy, or a sell whose tokens
8
+ * the basket holds): a plain createAction, ONE P2PKH output to a fresh key under `SWAP_PROTOCOL`
9
+ * (self, keyID random, `amm-swap-<hex>`), filed in `amm-swap` by the action with its
10
+ * customInstructions (`{sats: true, protocolID, keyID, counterparty}`: no token tag). With
11
+ * tokens: the module transfer plan — ONE token output to a fresh key and the ONE sats output in
12
+ * the same action, the token key's id ALSO in that output's description (`… · amm-swap <hex>`,
13
+ * as the LP key's `amm-lp <hex>`), the action labelled `SWAP_STAGE_LABEL`; the module selects
14
+ * the token inputs, adds change, signs and submits; the page submits it too, then files the
15
+ * token output in `amm-swap` (basket insertion, `{assetId, amount, protocolID, keyID,
16
+ * counterparty}`). A labelled staged token output the basket lacks (the page died before the
17
+ * filing) is filed on load from the action's description (`unfiledStagedTokens`).
18
+ * - **Spent** by the legs (src/market/swapAction.ts), their change kept here between legs.
19
+ * - **Move back** (`moveBack`, as 0.29.3): ONE createAction through the wallet's BRC-207 module,
20
+ * a `p color plan` transfer: every `amm-swap` token row of the token and every sats row as
21
+ * explicit inputs (their source transactions in `inputBEEF`), one output — the whole amount to
22
+ * a NEW module receiving key of the user's own, the plan's owned output 0. The page unlocks its
23
+ * inputs at signAction; the wallet funds the fee and takes the sats into its change. With no
24
+ * token row and sats rows staged: one plain createAction, the sats rows as explicit inputs and
25
+ * no outputs (`moveBackSats`).
47
26
  */
48
27
  import { Beef, P2PKH, PublicKey, Transaction, Utils } from "@bsv/sdk";
49
28
  import { decodeOneColor } from "../pool/index.js";
50
- import { parseOutpoint, parseTokenId, sdkTokenId, tokenIdOfWire, tokenIdText } from "../lib/tokenId.js";
29
+ import { sdkTokenId, tokenIdOfWire } from "../lib/tokenId.js";
51
30
  import { basketBeef, basketRows } from "../lp/wallet.js";
52
31
  import { tokenP2pkhScript } from "../lp/poolDeploy.js";
53
32
  import { steakText, submitToOverlay } from "../lp/close.js";
@@ -57,21 +36,28 @@ import { inputIndexOf, runAppSignedAction } from "../wallet/action.js";
57
36
  import { P2PKH_UNLOCK_LENGTH, satsInputsOf, signP2pkhWithWallet, spendValid, tokenInputsOf } from "./swapAction.js";
58
37
  /** The keyID prefix of an `amm-swap` key (`SWAP_PROTOCOL`, counterparty self). */
59
38
  export const SWAP_KEY_PREFIX = "amm-swap-";
39
+ /** The label of a staging action that carries tokens (0.40.0): the staged token output's key id is in its output description. */
40
+ export const SWAP_STAGE_LABEL = "amm-swap-stage";
41
+ /** The marker a staged token output's description ends with (` · amm-swap <hex>`, 0.40.0): it names the key id. */
42
+ export const SWAP_KEY_MARK = " · amm-swap ";
60
43
  function describe(text) {
61
44
  return text.length <= 50 ? text : `${text.slice(0, 49)}…`;
62
45
  }
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" : ""}`;
46
+ /** A staged row's keyID (0.40.0): `amm-swap-<16 random bytes, hex>`. */
47
+ export function newSwapKeyID() {
48
+ return `${SWAP_KEY_PREFIX}${Utils.toHex(Array.from(crypto.getRandomValues(new Uint8Array(16))))}`;
49
+ }
50
+ /** A staged token output's description: the readable text (at most 50 characters), then `SWAP_KEY_MARK` and the keyID's hex. */
51
+ export function swapKeyDescription(readable, keyID) {
52
+ const hex = keyID.startsWith(SWAP_KEY_PREFIX) ? keyID.slice(SWAP_KEY_PREFIX.length) : "";
53
+ if (!/^[0-9a-f]{2,128}$/.test(hex))
54
+ throw new Error(`not a staged key id: ${keyID}`);
55
+ return `${describe(readable)}${SWAP_KEY_MARK}${hex}`;
56
+ }
57
+ /** The keyID a staged token output's description names (`swapKeyDescription`), or null. */
58
+ export function swapKeyIdOfDescription(description) {
59
+ const m = / · amm-swap ([0-9a-f]{2,128})$/.exec(description ?? "");
60
+ return m && m[1].length % 2 === 0 ? `${SWAP_KEY_PREFIX}${m[1]}` : null;
75
61
  }
76
62
  /** The `amm-swap` outputs of `tokenId` the legs can spend. */
77
63
  export async function swapHoldings(wallet, tokenId) {
@@ -81,37 +67,27 @@ export async function swapHoldings(wallet, tokenId) {
81
67
  export async function swapSatsHoldings(wallet) {
82
68
  return satsInputsOf(await basketRows(wallet, SWAP_BASKET));
83
69
  }
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
- */
70
+ /** What `amm-swap` holds, per token id: the token amount and the number of token outputs (0.40.0: the sats rows are no token's, `swapSatsTotal`). */
89
71
  export async function swapTotals(wallet) {
90
72
  const totals = new Map();
91
- const rows = await basketRows(wallet, SWAP_BASKET);
92
- for (const r of rows) {
73
+ for (const r of await basketRows(wallet, SWAP_BASKET)) {
93
74
  if (!r.lockingScript)
94
75
  continue;
95
76
  const t = decodeOneColor(Utils.toArray(r.lockingScript, "hex"));
96
77
  if (!t || t.role !== "value" || !t.idBytes)
97
78
  continue;
98
79
  const id = tokenIdOfWire(t.idBytes);
99
- const s = totals.get(id) ?? { amount: 0n, outputs: 0, sats: 0 };
80
+ const s = totals.get(id) ?? { amount: 0n, outputs: 0 };
100
81
  s.amount += t.amount;
101
82
  s.outputs++;
102
83
  totals.set(id, s);
103
84
  }
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
85
  return totals;
114
86
  }
87
+ /** The sats `amm-swap` holds (0.40.0: shown once, not per token). */
88
+ export async function swapSatsTotal(wallet) {
89
+ return (await swapSatsHoldings(wallet)).reduce((a, r) => a + r.satoshis, 0);
90
+ }
115
91
  /** The source transactions of `amm-swap`'s outputs (a leg's BEEF), with `extra` merged in. */
116
92
  export async function swapHoldingBeef(wallet, extra) {
117
93
  const beef = (await basketBeef(wallet, SWAP_BASKET)) ?? new Beef();
@@ -119,92 +95,78 @@ export async function swapHoldingBeef(wallet, extra) {
119
95
  beef.mergeBeef(Beef.fromBinary(extra));
120
96
  return beef;
121
97
  }
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
98
  /**
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`.
99
+ * Staging (see the module comment): ONE wallet action. Throws when the wallet refuses or fails it;
100
+ * the token row's filing and the page's own submit are reported in `errors`.
164
101
  */
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");
102
+ export async function stage(wallet, req, o = {}) {
103
+ // The sats output: what the basket lacks of the fee sats (with tokens it may lack none).
104
+ if (!Number.isSafeInteger(req.sats) || req.sats < 0 || (req.sats === 0 && !req.tokens))
105
+ throw new Error("nothing to stage in the swap basket");
106
+ const sym = o.sym ?? "token";
107
+ const satsKeyID = newSwapKeyID();
108
+ const satsPub = (await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID: satsKeyID, counterparty: "self", forSelf: true })).publicKey;
109
+ const satsScript = new P2PKH().lock(PublicKey.fromString(satsPub).toAddress()).toHex();
110
+ const satsOutput = {
111
+ lockingScript: satsScript,
112
+ satoshis: req.sats,
113
+ outputDescription: describe(`Sats for a ${sym} swap`),
114
+ basket: SWAP_BASKET,
115
+ customInstructions: swapSatsInstructions({ protocolID: SWAP_PROTOCOL, keyID: satsKeyID, counterparty: "self" }),
116
+ };
117
+ const satsRow = (txid, vout) => ({ outpoint: `${txid}.${vout}`, txid, vout, satoshis: req.sats, lockingScript: satsScript, protocolID: SWAP_PROTOCOL, keyID: satsKeyID, counterparty: "self" });
118
+ if (!req.tokens) {
119
+ const { txid, tx } = await runTokenAction(wallet, {
120
+ description: describe("AMM swap: sats into the swap basket"),
121
+ labels: ["amm-swap"],
122
+ outputs: [satsOutput],
123
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
124
+ });
125
+ const final = Transaction.fromAtomicBEEF(tx);
126
+ if (final.outputs[0]?.lockingScript.toHex() !== satsScript)
127
+ throw new Error("the wallet's action has no output 0 to the swap key");
128
+ return { txid, tx, sats: satsRow(txid, 0), filed: false, submitted: "not submitted (no token moved)", errors: [] };
129
+ }
130
+ const { tokenId, amount } = req.tokens;
131
+ if (amount <= 0n)
132
+ throw new Error("nothing to stage in the swap basket");
169
133
  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);
134
+ const keyID = newSwapKeyID();
135
+ const pub = (await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", forSelf: true })).publicKey;
136
+ const tokenScript = tokenP2pkhScript(assetId, amount, pub);
177
137
  const { txid, tx } = await runTokenAction(wallet, {
178
138
  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)],
139
+ labels: [...transferPlanLabels(assetId), "amm-swap", SWAP_STAGE_LABEL],
140
+ outputs: [{ lockingScript: tokenScript, satoshis: 1, outputDescription: swapKeyDescription(`${sym} for a swap`, keyID) }, ...(req.sats > 0 ? [satsOutput] : [])],
181
141
  options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
182
142
  });
183
- const out = { txid, tx, holdings: [], sats: [], filed: false, submitted: "not submitted", errors: [] };
184
143
  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);
144
+ if (final.outputs[0]?.lockingScript.toHex() !== tokenScript || (req.sats > 0 && final.outputs[1]?.lockingScript.toHex() !== satsScript))
145
+ throw new Error("the wallet's action does not have the staged outputs at 0 and 1");
146
+ const out = {
147
+ txid,
148
+ tx,
149
+ ...(req.sats > 0 ? { sats: satsRow(txid, 1) } : {}),
150
+ tokens: { outpoint: `${txid}.0`, txid, vout: 0, satoshis: 1, lockingScript: tokenScript, amount, protocolID: SWAP_PROTOCOL, keyID, counterparty: "self" },
151
+ filed: false,
152
+ submitted: "not submitted",
153
+ errors: [],
154
+ };
194
155
  try {
195
- out.submitted = steakText(await submit(`tm_color_${assetId}`, tx));
156
+ if (o.submit)
157
+ out.submitted = steakText(await o.submit(`tm_color_${assetId}`, tx));
196
158
  }
197
159
  catch (err) {
198
160
  out.submitted = "the submit failed";
199
161
  out.errors.push(`submit: ${err instanceof Error ? err.message : String(err)}`);
200
162
  }
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
163
  try {
207
- const r = await wallet.internalizeAction({ tx, outputs: rows, description: describe(`AMM swap: ${sym} into the swap basket`), labels: ["amm-swap"] });
164
+ const r = await wallet.internalizeAction({
165
+ tx,
166
+ outputs: [{ outputIndex: 0, protocol: "basket insertion", insertionRemittance: { basket: SWAP_BASKET, customInstructions: swapInstructions({ assetId, amount, protocolID: SWAP_PROTOCOL, keyID, counterparty: "self" }) } }],
167
+ description: describe(`AMM swap: ${sym} into the swap basket`),
168
+ labels: ["amm-swap"],
169
+ });
208
170
  out.filed = r.accepted;
209
171
  }
210
172
  catch (err) {
@@ -213,23 +175,38 @@ export async function sendToSwapBasket(wallet, base, tokenId, stage, meta = {},
213
175
  return out;
214
176
  }
215
177
  /**
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.
178
+ * The labelled staging actions' token outputs not in `amm-swap` (0.40.0): every output of an action
179
+ * labelled `SWAP_STAGE_LABEL` whose description names a staged key and whose script is a 1Color
180
+ * value output, its basket not `amm-swap`. A wallet that refuses the read: none.
219
181
  */
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`),
182
+ export async function unfiledStagedTokens(wallet) {
183
+ const out = [];
184
+ try {
185
+ const r = await wallet.listActions({ labels: [SWAP_STAGE_LABEL], includeOutputs: true, includeOutputLockingScripts: true, limit: 1000 });
186
+ for (const a of r.actions ?? []) {
187
+ for (const o of a.outputs ?? []) {
188
+ const keyID = swapKeyIdOfDescription(o.outputDescription);
189
+ const t = keyID && o.lockingScript ? decodeOneColor(Utils.toArray(o.lockingScript, "hex")) : null;
190
+ if (!keyID || !t || t.role !== "value" || !t.idBytes || o.basket === SWAP_BASKET)
191
+ continue;
192
+ out.push({ txid: a.txid, vout: o.outputIndex, tokenId: tokenIdOfWire(t.idBytes), amount: t.amount, keyID });
193
+ }
194
+ }
195
+ }
196
+ catch {
197
+ /* no history read: none */
198
+ }
199
+ return out;
200
+ }
201
+ /** One unfiled staged token output filed in `amm-swap` from its transaction's BEEF (basket insertion, as `stage` files it). */
202
+ export async function fileStagedToken(wallet, u, beef) {
203
+ const r = await wallet.internalizeAction({
204
+ tx: beef,
205
+ outputs: [{ outputIndex: u.vout, protocol: "basket insertion", insertionRemittance: { basket: SWAP_BASKET, customInstructions: swapInstructions({ assetId: sdkTokenId(u.tokenId), amount: u.amount, protocolID: SWAP_PROTOCOL, keyID: u.keyID, counterparty: "self" }) } }],
206
+ description: "AMM swap: a staged token filed",
227
207
  labels: ["amm-swap"],
228
- outputs: keys.map((k) => k.output),
229
- options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
230
208
  });
231
- const errors = [];
232
- return { txid, tx, sats: satsHeld(Transaction.fromAtomicBEEF(tx), txid, keys, 0, errors), errors };
209
+ return r.accepted;
233
210
  }
234
211
  /** The page's unlocking scripts for the `amm-swap` outputs `holdings` in the wallet's transaction `tx`, each checked with `Spend`. */
235
212
  export async function swapBasketSpends(wallet, tx, holdings, what) {
@@ -259,8 +236,8 @@ export async function moveBack(i) {
259
236
  const topic = `tm_color_${assetId}`;
260
237
  const rows = await basketRows(wallet, SWAP_BASKET);
261
238
  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);
239
+ // 0.40.0: every sats row goes in too (they are no token's); the wallet's change takes their sats.
240
+ const satsRows = satsInputsOf(rows);
264
241
  if (holdings.length === 0 && satsRows.length > 0)
265
242
  return await moveBackSats(wallet, sym, satsRows);
266
243
  if (holdings.length === 0)
@@ -279,7 +256,7 @@ export async function moveBack(i) {
279
256
  inputBEEF: sources.toBinary(),
280
257
  inputs: [
281
258
  ...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`) })),
259
+ ...satsRows.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe("Sats in the swap basket") })),
283
260
  ],
284
261
  outputs: [{ lockingScript, satoshis: 1, outputDescription: describe(`${sym} back to your wallet`) }],
285
262
  labels: [...transferPlanLabels(assetId, { ownedOutputs: [{ outputIndex: 0, ...key.receipt }] }), "amm-swap"],
@@ -296,14 +273,17 @@ export async function moveBack(i) {
296
273
  return out;
297
274
  }
298
275
  /**
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
276
+ * Move back with only sats in the swap basket (0.29.6, David Case 2026-11-03; 0.40.0: every sats
277
+ * row, `satsRows` default the basket's): ONE plain wallet createAction (no `p color` labels, the module
301
278
  * is not in the path) with the token's `amm-swap` sats rows as explicit inputs (their sources in
302
279
  * `inputBEEF`) and NO outputs: the wallet keeps everything as its change and pays the fee from it.
303
280
  * The page unlocks the rows at signAction; the wallet broadcasts and marks the rows spent itself
304
281
  * (no relinquish, no take-in, nothing kept). Nothing is submitted to an overlay (no token moves).
305
282
  */
306
- async function moveBackSats(wallet, sym, satsRows) {
283
+ export async function moveBackSats(wallet, sym = "the swap", satsRows) {
284
+ satsRows ??= await swapSatsHoldings(wallet);
285
+ if (satsRows.length === 0)
286
+ throw new Error("the swap basket holds no sats");
307
287
  const sources = await swapHoldingBeef(wallet);
308
288
  for (const h of satsRows)
309
289
  if (!sources.findTxid(h.txid))
@@ -311,7 +291,7 @@ async function moveBackSats(wallet, sym, satsRows) {
311
291
  const { txid, tx: beef } = await runAppSignedAction(wallet, {
312
292
  description: describe(`AMM move back: ${sym} sats`),
313
293
  inputBEEF: sources.toBinary(),
314
- inputs: satsRows.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe(`Sats staged for a ${sym} swap`) })),
294
+ inputs: satsRows.map((h) => ({ outpoint: h.outpoint, unlockingScriptLength: P2PKH_UNLOCK_LENGTH, inputDescription: describe("Sats in the swap basket") })),
315
295
  outputs: [],
316
296
  labels: ["amm-swap"],
317
297
  options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
@@ -15,7 +15,8 @@ export interface SwapForm {
15
15
  /** Sats (bsvToToken) or tokens in display units (tokenToBsv). */
16
16
  amount: string;
17
17
  slippageBps: string;
18
- allowPartial: boolean;
18
+ /** 0.40.2: the most pools the order may draw on, a positive integer the caller gives. */
19
+ maxSources: number;
19
20
  }
20
21
  export type PlanInput = {
21
22
  ok: true;
@@ -78,7 +78,7 @@ satsPerKb = DEFAULT_FEE_RATE_SATS_PER_KB) {
78
78
  direction: form.direction,
79
79
  amountIn,
80
80
  slippage: { toleranceBps },
81
- allowPartial: form.allowPartial,
81
+ maxSources: form.maxSources,
82
82
  fixedCost,
83
83
  inventory: withLastSeen(usable, live),
84
84
  },
@@ -1,8 +1,8 @@
1
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);
2
+ * Prices (0.34.0, D6, David Case 2026-11-04: one source for prices; 0.39.0, D3, D7): a token's
3
+ * listed price comes from the host-wide stats (`GET <origin>/amm/stats`, read once per load and
4
+ * every 30 s while the tab is visible; no host-wide stream), the Swap page's quote from the pools
5
+ * its token's pools route lists. No price is derived from beats (the 0.9.0 beat prices are gone);
6
6
  * the depth is the sats a buy needs to move the price up by 2%, x·(√1.02 − 1), over the summed
7
7
  * sats.
8
8
  */
@@ -1,8 +1,8 @@
1
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);
2
+ * Prices (0.34.0, D6, David Case 2026-11-04: one source for prices; 0.39.0, D3, D7): a token's
3
+ * listed price comes from the host-wide stats (`GET <origin>/amm/stats`, read once per load and
4
+ * every 30 s while the tab is visible; no host-wide stream), the Swap page's quote from the pools
5
+ * its token's pools route lists. No price is derived from beats (the 0.9.0 beat prices are gone);
6
6
  * the depth is the sats a buy needs to move the price up by 2%, x·(√1.02 − 1), over the summed
7
7
  * sats.
8
8
  */