@dcasegr/amm-client 0.39.0 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,52 +1,39 @@
1
1
  /**
2
- * One swap leg through the user's BRC-100 wallet (docs/notes.md, "Swap
3
- * funding and signing"). The page holds no keys: every key and signature
4
- * comes from the wallet (`getPublicKey`, `createAction`, `createSignature`).
2
+ * One swap leg through the user's BRC-100 wallet (docs/notes.md, "Swap funding and signing";
3
+ * 0.40.0, agent-env plans/swap-balance-build-2026-11-05.md §0 7, Builder 2 item 3). The page holds
4
+ * no keys: every key and signature comes from the wallet (`getPublicKey`, `createSignature`).
5
5
  *
6
- * 1. `planSwapLeg` (`PoolTemplate.planSwap`): the outputs the contract
7
- * requires (pool, payout, LP fee, validator fee, commission) and the
8
- * method arguments. The commission goes to the relay's `amm.swap.terms`
9
- * address; when the relay names none and the pool's CommissionBps is
10
- * nonzero, to a key of the taker's own (see "the commission" below). The
11
- * leg's sats are exact: what the contract's outputs take beyond the
12
- * pool's and the token inputs' sats (amount in + 1 sat per 1Color output
13
- * the taker pays for), plus the miner fee of the final swap
14
- * (`swapFunding`). Sized with placeholder addresses: the payout keys
15
- * (and the taker's own commission key) are derived at step 3 from the
16
- * leg's first staged input (`legDerivation`, 0.29.1), not random.
17
- * 2. **The leg's inputs** (0.29.0, David Case 2026-11-02): every one an
18
- * output of the active swap basket `amm-swap` (src/market/activeSwap.ts):
19
- * ONE sats output of exactly that amount (`SatsInput`) and, token → sats,
20
- * the token outputs carrying exactly the amount in (`TokenInput`), staged
21
- * there by one wallet send before the legs (no funding transaction, no
22
- * `1sat-deposit`).
23
- * 3. **Swap transaction**, built here (`prepareSwap`): inputs pool
24
- * (`callUnlock`, the validator's slot `OP_0`, `_changeAmount = 0`), the
25
- * sats output, the token outputs; outputs exactly the contract's, no
26
- * change. The sats and token inputs are signed with `createSignature`
27
- * (SIGHASH_ALL|FORKID over the BIP-143 sighash computed here, under the
28
- * key each input's customInstructions name) and every input but the
29
- * pool's is checked with `Spend`.
30
- * 4. The relay (src/market/relay.ts) carries it to the validator, with the
31
- * sats output's transaction as the swap's funding transaction (its
32
- * AtomicBEEF, from the wallet).
33
- * 5. Accepted: `completeSwap` internalizes the payout (sats: BRC-29 wallet
34
- * payment; tokens, 0.12.0: the output is locked to a BRC-207 module
35
- * receiving key whose sender is the pool's validator, taken in with a
36
- * basket insertion into `p color <assetTag>` and the receipt,
37
- * src/wallet/color.ts), and the commission when it is the taker's own
38
- * (same two forms). No relinquishOutput (0.29.0): the wallet marks the
39
- * `amm-swap` outputs the swap spends spent when it takes the swap in.
40
- * Refused / timed out: nothing to undo; the leg's outputs stay in
41
- * `amm-swap` for a later swap.
6
+ * 1. **The leg's inputs** (`selectLegInputs`): every one an `amm-swap` row (src/market/activeSwap.ts)
7
+ * — the pool, then as many sats rows as cover the leg's sats (largest first), then (a sell) as
8
+ * many token rows as cover `amountIn` (largest first). The leg's sats (`legNeed`): what the
9
+ * contract's outputs take beyond the pool's and the token inputs' sats, plus the miner fee of
10
+ * the final transaction at the rate (`swapTxSize`, the sats change output counted).
11
+ * 2. **The outputs**: the contract's (pool, payout, LP fee, validator fee, commission, then the
12
+ * token change = token inputs − `amountIn` when > 0), then Rúnar's sats change = sats inputs −
13
+ * the need when ≥ 1 (`_changePKH`, `_changeAmount`); under 1, no change output.
14
+ * 3. **The keys** (`prepareSwap`): the payout (and the taker's own commission) under ONE
15
+ * derivation per ORDER (`legDerivation` of the order's key input: the first leg's first token
16
+ * input on a sell, its first sats input on a buy), every leg of the order the same; a token
17
+ * payout to a module receiving key for SELF (sender the user's identity). The change: a leg
18
+ * that is not the order's last → keys under `SWAP_PROTOCOL` (self), keyID
19
+ * `amm-swap-<txid>.<vout>:change` of the leg's first input of that kind (`changeKeyID`), taken
20
+ * into `amm-swap` with the order's key input in its customInstructions; the LAST leg (it takes
21
+ * all that remains) → the WALLET: sats to a BRC-29 key of the user's, tokens to a module
22
+ * receiving key for self, both `legDerivation(order, "change")`.
23
+ * 4. The relay (src/market/relay.ts) carries it to the validator with the first sats input's
24
+ * transaction as the swap's funding transaction: ONE BEEF of every staged input's source,
25
+ * its subject that transaction (0.40.1, `fundingBeef`).
26
+ * 5. Accepted: `completeSwap` takes in, in ONE `internalizeAction`, the payout, the commission
27
+ * when it is the taker's own, and the change. No relinquishOutput: taking the swap in marks the
28
+ * `amm-swap` rows it spends spent.
42
29
  */
43
- import { Hash, LockingScript, PublicKey, Spend, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
30
+ import { Beef, Hash, LockingScript, PublicKey, Spend, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
44
31
  import { PoolTemplate, decodeOneColor } from "../pool/index.js";
45
32
  import { isPoolRow } from "../lp/poolRows.js";
46
33
  import { basketBeef } from "../lp/wallet.js";
47
34
  import { outpointText, parseOutpoint, parseTokenId, sdkTokenId } from "../lib/tokenId.js";
48
- import { WALLET_PAYMENT, identityKeyOf, newBrc29Payout } from "../wallet/brc29.js";
49
- import { SWAP_BASKET, newReceivingKey, receiptOutput } from "../wallet/color.js";
35
+ import { WALLET_PAYMENT, identityKeyOf, newBrc29Payout, p2pkhHexOf } from "../wallet/brc29.js";
36
+ import { SWAP_BASKET, SWAP_PROTOCOL, newReceivingKey, receiptOutput, swapInstructions, swapSatsInstructions } from "../wallet/color.js";
50
37
  /** A P2PKH unlocking script's upper bound: push(DER ≤ 72 + sighash byte) + push(33-byte key). */
51
38
  export const P2PKH_UNLOCK_LENGTH = 108;
52
39
  /**
@@ -68,6 +55,24 @@ export function assetIdOf(tokenId) {
68
55
  throw new Error(`pools exist only for 32-byte ids (<txid>_0), not ${tokenId}`);
69
56
  return toHex(Utils.toArray(r.txid, "hex").reverse());
70
57
  }
58
+ function keyOf(r) {
59
+ let ci = {};
60
+ try {
61
+ ci = JSON.parse(r.customInstructions ?? "{}");
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ if (!Array.isArray(ci.protocolID) || typeof ci.keyID !== "string")
67
+ return null;
68
+ return {
69
+ protocolID: ci.protocolID,
70
+ keyID: ci.keyID,
71
+ counterparty: typeof ci.counterparty === "string" ? ci.counterparty : "self",
72
+ ...(typeof ci.order === "string" ? { order: ci.order } : {}),
73
+ ...(ci.sats === true ? { sats: true } : {}),
74
+ };
75
+ }
71
76
  /**
72
77
  * The basket rows usable as token inputs for `tokenId`: 1Color value outputs
73
78
  * with this 32-byte id (the pool template counts only those) whose
@@ -83,17 +88,9 @@ export function tokenInputsOf(rows, tokenId) {
83
88
  const t = decodeOneColor(Utils.toArray(r.lockingScript, "hex"));
84
89
  if (!t || t.role !== "value" || !t.idBytes || toHex(Array.from(t.idBytes)) !== assetId)
85
90
  continue;
86
- let ci = {};
87
- try {
88
- ci = JSON.parse(r.customInstructions ?? "{}");
89
- }
90
- catch {
91
- continue;
92
- }
93
- if (!Array.isArray(ci.protocolID) || typeof ci.keyID !== "string")
94
- continue;
91
+ const k = keyOf(r);
95
92
  const op = parseOutpoint(r.outpoint);
96
- if (!op)
93
+ if (!k || !op)
97
94
  continue;
98
95
  out.push({
99
96
  outpoint: `${op.txid}.${op.vout}`,
@@ -102,41 +99,14 @@ export function tokenInputsOf(rows, tokenId) {
102
99
  satoshis: r.satoshis,
103
100
  lockingScript: r.lockingScript,
104
101
  amount: t.amount,
105
- protocolID: ci.protocolID,
106
- keyID: ci.keyID,
107
- counterparty: typeof ci.counterparty === "string" ? ci.counterparty : "self",
102
+ protocolID: k.protocolID,
103
+ keyID: k.keyID,
104
+ counterparty: k.counterparty,
105
+ ...(k.order ? { order: k.order } : {}),
108
106
  });
109
107
  }
110
108
  return out;
111
109
  }
112
- /**
113
- * Token outputs summing to exactly `amount`, or null. The pool contract has
114
- * no token change output (Pool.runar.go Swap: the token inputs are the
115
- * amountIn), so an inexact selection cannot be spent into a swap. Fewest
116
- * outputs first; a bounded search.
117
- */
118
- export function selectExactTokenInputs(candidates, amount) {
119
- const sorted = [...candidates].sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0)).slice(0, 24);
120
- for (let size = 1; size <= Math.min(sorted.length, 6); size++) {
121
- const pick = [];
122
- const walk = (from, left) => {
123
- if (pick.length === size)
124
- return left === 0n;
125
- for (let i = from; i < sorted.length; i++) {
126
- if (sorted[i].amount > left)
127
- continue;
128
- pick.push(sorted[i]);
129
- if (walk(i + 1, left - sorted[i].amount))
130
- return true;
131
- pick.pop();
132
- }
133
- return false;
134
- };
135
- if (walk(0, amount))
136
- return pick;
137
- }
138
- return null;
139
- }
140
110
  const P2PKH_RE = /^76a914[0-9a-f]{40}88ac$/i;
141
111
  /** The basket rows usable as a leg's sats: plain P2PKH outputs whose customInstructions say `sats: true` and name the key. */
142
112
  export function satsInputsOf(rows) {
@@ -144,17 +114,9 @@ export function satsInputsOf(rows) {
144
114
  for (const r of rows) {
145
115
  if (!r.lockingScript || !P2PKH_RE.test(r.lockingScript))
146
116
  continue;
147
- let ci = {};
148
- try {
149
- ci = JSON.parse(r.customInstructions ?? "{}");
150
- }
151
- catch {
152
- continue;
153
- }
154
- if (ci.sats !== true || !Array.isArray(ci.protocolID) || typeof ci.keyID !== "string")
155
- continue;
117
+ const k = keyOf(r);
156
118
  const op = parseOutpoint(r.outpoint);
157
- if (!op)
119
+ if (!k || !k.sats || !op)
158
120
  continue;
159
121
  out.push({
160
122
  outpoint: `${op.txid}.${op.vout}`,
@@ -162,18 +124,14 @@ export function satsInputsOf(rows) {
162
124
  vout: op.vout,
163
125
  satoshis: r.satoshis,
164
126
  lockingScript: r.lockingScript,
165
- protocolID: ci.protocolID,
166
- keyID: ci.keyID,
167
- counterparty: typeof ci.counterparty === "string" ? ci.counterparty : "self",
168
- ...(typeof ci.assetId === "string" ? { assetId: ci.assetId } : {}),
127
+ protocolID: k.protocolID,
128
+ keyID: k.keyID,
129
+ counterparty: k.counterparty,
130
+ ...(k.order ? { order: k.order } : {}),
169
131
  });
170
132
  }
171
133
  return out;
172
134
  }
173
- /** The sats twin of `selectExactTokenInputs`: one output of exactly `satoshis` (the leg has no change), or null. */
174
- export function selectExactSatsInput(candidates, satoshis) {
175
- return candidates.find((c) => c.satoshis === satoshis) ?? null;
176
- }
177
135
  /** The pool output from the lookup's BEEF, as the template's PoolUtxo. */
178
136
  export function poolUtxoFrom(poolOutput) {
179
137
  const tx = Transaction.fromAtomicBEEF(poolOutput.beef);
@@ -214,68 +172,89 @@ export async function signP2pkhWithWallet(wallet, tx, inputIndex, src, scope = T
214
172
  function varIntSize(n) {
215
173
  return n < 0xfd ? 1 : n <= 0xffff ? 3 : n <= 0xffffffff ? 5 : 9;
216
174
  }
175
+ /** A P2PKH output's size: 8 (sats) + 1 (length) + 25. */
176
+ const P2PKH_OUTPUT_SIZE = 34;
217
177
  /**
218
178
  * The final swap's size in bytes: the pool input at `maxCallUnlockLength`
219
179
  * (validator's signature in), `takerInputs` P2PKH inputs (the sats, tokens)
220
- * at 108 bytes of unlocking script, the contract's outputs, no change.
180
+ * at 108 bytes of unlocking script, the contract's outputs, and (0.40.0) the
181
+ * sats change output when `withChange`.
221
182
  */
222
- export function swapTxSize(plan, takerInputs) {
183
+ export function swapTxSize(plan, takerInputs, withChange = false) {
223
184
  const poolUnlock = PoolTemplate.maxCallUnlockLength(plan);
224
185
  let size = 4 + varIntSize(1 + takerInputs);
225
186
  size += 36 + varIntSize(poolUnlock) + poolUnlock + 4;
226
187
  size += takerInputs * (36 + varIntSize(P2PKH_UNLOCK_LENGTH) + P2PKH_UNLOCK_LENGTH + 4);
227
- size += varIntSize(plan.outputs.length);
188
+ const outs = plan.outputs.length + (withChange ? 1 : 0);
189
+ size += varIntSize(outs);
228
190
  for (const o of plan.outputs) {
229
191
  const len = o.script.length / 2;
230
192
  size += 8 + varIntSize(len) + len;
231
193
  }
194
+ if (withChange)
195
+ size += P2PKH_OUTPUT_SIZE;
232
196
  return size + 4;
233
197
  }
234
198
  /**
235
- * The leg's exact sats: Σ contract outputs − pool sats − token inputs' sats
236
- * (sats in: amount in + 1 for the token payout, the fees and the commission
237
- * being part of amount in; tokens in: 1 per 1Color fee or commission output
238
- * − the token inputs' sats), plus
239
- * ceil(size × rate / 1000) for the final swap.
199
+ * A leg's sats need: Σ contract outputs − pool sats − token inputs' sats, plus ceil(size × rate /
200
+ * 1000) for the final swap with `inputs` taker inputs (sats and tokens), without and with the sats
201
+ * change output.
240
202
  */
241
- export function swapFunding(plan, tokenInputs, satsPerKb) {
203
+ export function legNeed(plan, tokenInputs, inputs, satsPerKb) {
242
204
  const outSum = plan.outputs.reduce((a, o) => a + o.satoshis, 0n);
243
205
  const tokenSats = tokenInputs.reduce((a, t) => a + BigInt(t.satoshis), 0n);
244
206
  const outputs = Number(outSum - BigInt(plan.poolUtxo.satoshis) - tokenSats);
245
- const size = swapTxSize(plan, 1 + tokenInputs.length);
246
- const fee = Math.ceil((size * satsPerKb) / 1000);
247
- return { satoshis: Math.max(1, outputs + fee), outputs, fee, size, satsPerKb };
207
+ const fee = Math.ceil((swapTxSize(plan, inputs) * satsPerKb) / 1000);
208
+ const feeWithChange = Math.ceil((swapTxSize(plan, inputs, true) * satsPerKb) / 1000);
209
+ return { outputs, fee, need: outputs + fee, feeWithChange, needWithChange: outputs + feeWithChange, satsPerKb };
248
210
  }
249
- const ZERO_PKH = "00".repeat(20);
211
+ // ---------------------------------------------------------------------------
212
+ // The keys
213
+ // ---------------------------------------------------------------------------
250
214
  /** BRC-29 / BRC-207 derivationPrefix of a leg's payout (0.29.1): base64 of the UTF-8 text "amm swap". */
251
215
  export const LEG_PAYOUT_PREFIX = Utils.toBase64(Utils.toArray("amm swap", "utf8"));
252
216
  /** derivationPrefix of the taker's own commission output (0.29.1): base64 of "amm swap commission". */
253
217
  export const LEG_COMMISSION_PREFIX = Utils.toBase64(Utils.toArray("amm swap commission", "utf8"));
218
+ /** derivationPrefix of an order's last leg's change to the wallet (0.40.0): base64 of "amm swap change". */
219
+ export const LEG_CHANGE_PREFIX = Utils.toBase64(Utils.toArray("amm swap change", "utf8"));
254
220
  /**
255
- * The derivation of a leg's payout keys (0.29.1, David Case 2026-11-02: deterministic from what
256
- * the leg spends). `keyInput` is the leg's first staged `amm-swap` input, `<txid>.<vout>` (txid in
257
- * display hex, lowercase): tokens in, its first token input (the swap's input 2); sats in, its sats
258
- * input (input 1). derivationSuffix = base64(sha256(UTF-8 of that text)); derivationPrefix
259
- * `LEG_PAYOUT_PREFIX` for the payout, `LEG_COMMISSION_PREFIX` for the taker's own commission. The
260
- * same pair keys the BRC-29 payment (keyID "<prefix> <suffix>", sender the user) and the BRC-207
261
- * receipt (`p color <assetTag> <prefixHex> <suffixHex>`, sender the pool's validator). A staged
262
- * output is spent by one transaction only, so the keys are the leg's alone.
221
+ * The derivation of an order's payout keys (0.29.1; 0.40.0: ONE per ORDER). `keyInput` is the
222
+ * order's key input, `<txid>.<vout>` (txid in display hex, lowercase): the first leg's first token
223
+ * input on a sell, its first sats input on a buy. derivationSuffix = base64(sha256(UTF-8 of that
224
+ * text)); derivationPrefix `LEG_PAYOUT_PREFIX` for the payout, `LEG_COMMISSION_PREFIX` for the
225
+ * taker's own commission, `LEG_CHANGE_PREFIX` for the last leg's change to the wallet. The same
226
+ * pair keys the BRC-29 payment (keyID "<prefix> <suffix>", sender the user) and the BRC-207 receipt
227
+ * (`p color <assetTag> <prefixHex> <suffixHex>`, sender the user: a receipt for self).
263
228
  */
264
229
  export function legDerivation(keyInput, which) {
265
230
  return {
266
- derivationPrefix: which === "payout" ? LEG_PAYOUT_PREFIX : LEG_COMMISSION_PREFIX,
231
+ derivationPrefix: which === "payout" ? LEG_PAYOUT_PREFIX : which === "commission" ? LEG_COMMISSION_PREFIX : LEG_CHANGE_PREFIX,
267
232
  derivationSuffix: Utils.toBase64(Hash.sha256(Utils.toArray(keyInput, "utf8"))),
268
233
  };
269
234
  }
270
- /** The leg's key input in a swap transaction (see `legDerivation`): input 2 when it spends tokens, else input 1. */
271
- export function legKeyInputOf(swap, bsvIn) {
272
- const inp = swap.inputs[bsvIn ? 1 : 2];
273
- if (!inp)
274
- throw new LegShapeError("the swap has no staged input to key its payout by");
275
- return `${inp.sourceTXID ?? inp.sourceTransaction.id("hex")}.${inp.sourceOutputIndex}`;
235
+ /** The keyID of a leg's change kept in `amm-swap` (0.40.0): `amm-swap-<txid>.<vout>:change`, the leg's first input of that kind. */
236
+ export function changeKeyID(firstInput) {
237
+ const op = parseOutpoint(firstInput);
238
+ if (!op)
239
+ throw new LegShapeError(`not an outpoint: ${firstInput}`);
240
+ return `amm-swap-${op.txid}.${op.vout}:change`;
241
+ }
242
+ /** A swap transaction's taker inputs (every input after the pool's), each told sats or tokens by its source output (0.40.0). */
243
+ export function legInputsOf(tx) {
244
+ const sats = [];
245
+ const tokens = [];
246
+ for (const inp of tx.inputs.slice(1)) {
247
+ const txid = inp.sourceTXID ?? inp.sourceTransaction?.id("hex");
248
+ const src = inp.sourceTransaction?.outputs[inp.sourceOutputIndex];
249
+ if (!txid || !src)
250
+ continue;
251
+ (decodeOneColor(src.lockingScript.toBinary()) ? tokens : sats).push(`${txid}.${inp.sourceOutputIndex}`);
252
+ }
253
+ return { sats, tokens };
276
254
  }
277
- /** The plan with the given payout and commission addresses (`planSwap`, the leg's quote as the expectation). */
278
- function legPlan(i, poolUtxo, userPkh, commissionPkh) {
255
+ const ZERO_PKH = "00".repeat(20);
256
+ /** The plan with the given payout, commission and token change addresses (`planSwap`, the leg's quote as the expectation). */
257
+ function legPlan(i, poolUtxo, userPkh, commissionPkh, tokenChange, tokenChangePkh) {
279
258
  const { leg, pool } = i;
280
259
  return PoolTemplate.planSwap({
281
260
  pool: poolUtxo,
@@ -283,6 +262,8 @@ function legPlan(i, poolUtxo, userPkh, commissionPkh) {
283
262
  bsvIn: i.direction === "bsvToToken",
284
263
  userPkh,
285
264
  commissionPkh,
265
+ tokenChange,
266
+ tokenChangePkh,
286
267
  expect: {
287
268
  bsvReserve: pool.bsvReserve,
288
269
  tokenReserve: pool.tokenReserve,
@@ -296,116 +277,203 @@ function legPlan(i, poolUtxo, userPkh, commissionPkh) {
296
277
  },
297
278
  });
298
279
  }
280
+ const largestFirst = (xs, size) => [...xs].sort((a, b) => {
281
+ const d = size(b) - size(a);
282
+ return d > 0n ? 1 : d < 0n ? -1 : a.outpoint.localeCompare(b.outpoint);
283
+ });
299
284
  /**
300
- * Step 1 (see the module comment): the leg's plan and exact sats, sized with placeholder payout
301
- * and commission addresses (20 bytes each: the sizes are the same). No wallet call: the payout
302
- * keys are derived at step 3 from the leg's staged input (`legDerivation`). Throws
303
- * `PoolBuildError` / `LegShapeError`.
285
+ * Item 3's inputs: (a sell) as many token rows as cover `leg.amountIn`, largest first; then as many
286
+ * sats rows as cover the leg's sats need, largest first (each sats row added grows the size). Null
287
+ * when the rows cannot cover the leg (the staged sats or tokens ran out). The need is planned with
288
+ * placeholder addresses (20 bytes each: the sizes are the same).
304
289
  */
305
- export async function planSwapLeg(i) {
306
- const { leg } = i;
290
+ export function selectLegInputs(i, sats, tokens) {
307
291
  const bsvIn = i.direction === "bsvToToken";
308
292
  const poolUtxo = poolUtxoFrom(i.poolOutput);
309
- if (`${poolUtxo.txid}_${poolUtxo.vout}` !== leg.outpoint) {
310
- throw new LegShapeError(`the pool moved: the lookup's tip is ${poolUtxo.txid}.${poolUtxo.vout}, the plan priced ${outpointText(leg.outpoint)}`);
293
+ if (`${poolUtxo.txid}_${poolUtxo.vout}` !== i.leg.outpoint) {
294
+ throw new LegShapeError(`the pool moved: the read's tip is ${poolUtxo.txid}.${poolUtxo.vout}, the plan priced ${outpointText(i.leg.outpoint)}`);
311
295
  }
312
- const tokenInputs = i.tokenInputs ?? [];
313
- const tokensIn = tokenInputs.reduce((a, t) => a + t.amount, 0n);
314
- if (bsvIn ? tokenInputs.length > 0 : tokensIn !== leg.amountIn) {
315
- throw new LegShapeError(`token inputs carry ${tokensIn}, the leg needs exactly ${bsvIn ? 0n : leg.amountIn}`);
296
+ const pickedTokens = [];
297
+ if (!bsvIn) {
298
+ let held = 0n;
299
+ for (const t of largestFirst(tokens, (x) => x.amount)) {
300
+ if (held >= i.leg.amountIn)
301
+ break;
302
+ pickedTokens.push(t);
303
+ held += t.amount;
304
+ }
305
+ if (held < i.leg.amountIn)
306
+ return null;
316
307
  }
317
- const decoded = PoolTemplate.decode(poolUtxo.script);
318
- if (!decoded)
319
- throw new LegShapeError(`${outpointText(leg.outpoint)} is not a pool output`);
320
- const commissionAmount = (leg.amountIn * decoded.args.commissionBps + 9999n) / 10000n;
321
- const commissionTo = commissionAmount === 0n ? "none" : i.commissionPkh ? "relay" : "own";
322
- const plan = legPlan(i, poolUtxo, ZERO_PKH, i.commissionPkh ?? ZERO_PKH);
323
- return { pool: leg.outpoint, validator: i.pool.validatorIdentityKey, bsvIn, poolUtxo, plan, funding: swapFunding(plan, tokenInputs, i.satsPerKb), commissionAmount, commissionTo };
308
+ const tokenChange = pickedTokens.reduce((a, t) => a + t.amount, 0n) - (bsvIn ? 0n : i.leg.amountIn);
309
+ const plan = legPlan(i, poolUtxo, ZERO_PKH, i.commissionPkh ?? ZERO_PKH, tokenChange, ZERO_PKH);
310
+ const pickedSats = [];
311
+ let held = 0;
312
+ for (const s of largestFirst(sats, (x) => BigInt(x.satoshis))) {
313
+ pickedSats.push(s);
314
+ held += s.satoshis;
315
+ const need = legNeed(plan, pickedTokens, pickedSats.length + pickedTokens.length, i.satsPerKb);
316
+ if (held >= need.need)
317
+ return { sats: pickedSats, tokens: pickedTokens, need };
318
+ }
319
+ return null;
320
+ }
321
+ const pkhOfKey = (publicKey) => toHex(PublicKey.fromString(publicKey).toHash());
322
+ /** The leg's own key input: its first token input on a sell, its first sats input on a buy. */
323
+ export function keyInputOf(inputs, bsvIn) {
324
+ const k = bsvIn ? inputs.sats[0] : inputs.tokens[0];
325
+ if (!k)
326
+ throw new LegShapeError("the leg has no staged input to key its payout by");
327
+ return k.outpoint;
328
+ }
329
+ /** BRC-95's Atomic BEEF prefix (the sdk's `ATOMIC_BEEF`), little-endian. */
330
+ const ATOMIC_BEEF_PREFIX = [0x01, 0x01, 0x01, 0x01];
331
+ /**
332
+ * The relay's `funding` (0.40.1): ONE BEEF holding the source transaction of every staged input
333
+ * the leg spends (sats and token rows alike, each with its ancestry as the wallet gives it), its
334
+ * subject the funding transaction (the first sats input's). The form: an Atomic BEEF envelope
335
+ * (the prefix and the subject's txid, as the sdk writes one) around the sdk's V2 BEEF of them all
336
+ * — the relay's `given()` reads the envelope's subject and keeps every transaction in it; the
337
+ * sdk's own `toBinaryAtomic` would drop the sources the subject does not descend from. The
338
+ * relay's request BEEF to the validator is its own V2 (relay.zig `requestBeef`).
339
+ */
340
+ export function fundingBeef(beef, fundingTxid, sources) {
341
+ const all = new Beef();
342
+ for (const txid of new Set([fundingTxid, ...sources]))
343
+ all.mergeBeef(beef.toBinaryAtomic(txid));
344
+ return [...ATOMIC_BEEF_PREFIX, ...Utils.toArray(fundingTxid, "hex").reverse(), ...all.toBinary()];
324
345
  }
325
- /** Steps 1 (unless `planned` is given) and 3 (see the module comment). Throws `PoolBuildError` / `LegShapeError`; the wallet is left as it was. */
346
+ /** Builds and signs the leg (see the module comment). Throws `PoolBuildError` / `LegShapeError`; the wallet is left as it was. */
326
347
  export async function prepareSwap(i) {
327
- const { wallet } = i;
328
- const planned = i.planned ?? (await planSwapLeg(i));
329
- const { poolUtxo, bsvIn, commissionTo, commissionAmount } = planned;
330
- const tokenInputs = i.tokenInputs ?? [];
331
- const tokensIn = tokenInputs.reduce((a, t) => a + t.amount, 0n);
332
- if (bsvIn ? tokenInputs.length > 0 : tokensIn !== i.leg.amountIn) {
333
- throw new LegShapeError(`token inputs carry ${tokensIn}, the leg needs exactly ${bsvIn ? 0n : i.leg.amountIn}`);
334
- }
335
- const sats = i.satsInput;
336
- // The payout keys (0.29.1): derived from the leg's first staged input.
337
- const keyInput = bsvIn ? sats.outpoint : tokenInputs[0].outpoint;
348
+ const { wallet, inputs } = i;
349
+ const bsvIn = i.direction === "bsvToToken";
350
+ const poolUtxo = poolUtxoFrom(i.poolOutput);
351
+ if (`${poolUtxo.txid}_${poolUtxo.vout}` !== i.leg.outpoint)
352
+ throw new LegShapeError(`the pool moved: the read's tip is ${poolUtxo.txid}.${poolUtxo.vout}`);
353
+ const tokensIn = inputs.tokens.reduce((a, t) => a + t.amount, 0n);
354
+ if (bsvIn ? inputs.tokens.length > 0 : tokensIn < i.leg.amountIn)
355
+ throw new LegShapeError(`token inputs carry ${tokensIn}, the leg needs ${bsvIn ? 0n : i.leg.amountIn}`);
356
+ if (inputs.sats.length === 0)
357
+ throw new LegShapeError("the leg has no sats row");
358
+ const tokenChange = tokensIn - (bsvIn ? 0n : i.leg.amountIn);
359
+ const order = i.order ?? keyInputOf(inputs, bsvIn);
338
360
  const decoded = PoolTemplate.decode(poolUtxo.script);
339
- const identity = !bsvIn || commissionTo === "own" ? await identityKeyOf(wallet) : "";
361
+ if (!decoded)
362
+ throw new LegShapeError(`${outpointText(i.leg.outpoint)} is not a pool output`);
363
+ const commissionAmount = (i.leg.amountIn * decoded.args.commissionBps + 9999n) / 10000n;
364
+ const commissionTo = commissionAmount === 0n ? "none" : i.commissionPkh ? "relay" : "own";
365
+ const identity = await identityKeyOf(wallet);
340
366
  const sdkId = sdkTokenId(i.tokenId);
341
- const validatorIdentity = decoded.state.validatorIdentity;
342
- const pay = legDerivation(keyInput, "payout");
343
- const brc29 = bsvIn ? null : await newBrc29Payout(wallet, identity, pay);
344
- const tokenKey = bsvIn ? await newReceivingKey(wallet, sdkId, validatorIdentity, pay) : null;
345
- const userPkh = toHex(PublicKey.fromString(brc29 ? brc29.publicKey : tokenKey.publicKey).toHash());
346
- let ownCommission = null;
367
+ // The payout (one derivation per order; a token payout is a receipt for self).
368
+ const pay = legDerivation(order, "payout");
369
+ const payBrc29 = bsvIn ? null : await newBrc29Payout(wallet, identity, pay);
370
+ const payToken = bsvIn ? await newReceivingKey(wallet, sdkId, identity, pay) : null;
371
+ const userPkh = pkhOfKey(payBrc29 ? payBrc29.publicKey : payToken.publicKey);
372
+ let own = null;
347
373
  if (commissionTo === "own") {
348
- const com = legDerivation(keyInput, "commission");
349
- const own = bsvIn ? await newBrc29Payout(wallet, identity, com) : null;
350
- const token = bsvIn ? null : await newReceivingKey(wallet, sdkId, validatorIdentity, com);
351
- ownCommission = { brc29: own, token, pkh: toHex(PublicKey.fromString(own ? own.publicKey : token.publicKey).toHash()) };
374
+ const com = legDerivation(order, "commission");
375
+ const b = bsvIn ? await newBrc29Payout(wallet, identity, com) : null;
376
+ const t = bsvIn ? null : await newReceivingKey(wallet, sdkId, identity, com);
377
+ own = { brc29: b, token: t, pkh: pkhOfKey(b ? b.publicKey : t.publicKey) };
378
+ }
379
+ const commissionPkh = own?.pkh ?? i.commissionPkh ?? ZERO_PKH;
380
+ // The change keys: kept in amm-swap (not the last leg) or the wallet's (the last).
381
+ const change = legDerivation(order, "change");
382
+ const swapKey = async (keyID) => (await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", forSelf: true })).publicKey;
383
+ let tokenChangeKey = null;
384
+ if (tokenChange > 0n) {
385
+ if (i.last) {
386
+ const k = await newReceivingKey(wallet, sdkId, identity, change);
387
+ tokenChangeKey = { pkh: k.pkh, out: (outputIndex, lockingScript) => ({ kind: "color", outputIndex, satoshis: 1, lockingScript, receipt: k.receipt }) };
388
+ }
389
+ else {
390
+ const keyID = changeKeyID(inputs.tokens[0].outpoint);
391
+ const pub = await swapKey(keyID);
392
+ const ci = swapInstructions({ assetId: sdkId, amount: tokenChange, protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", order });
393
+ tokenChangeKey = { pkh: pkhOfKey(pub), out: (outputIndex, lockingScript) => ({ kind: "basket", outputIndex, satoshis: 1, lockingScript, customInstructions: ci }) };
394
+ }
352
395
  }
353
- const commissionPkh = ownCommission?.pkh ?? i.commissionPkh ?? ZERO_PKH;
354
- const plan = legPlan(i, poolUtxo, userPkh, commissionPkh);
355
- // The leg's sats were sized for these token inputs: the same count and sats, or the fee is off.
356
- const amounts = swapFunding(plan, tokenInputs, i.satsPerKb);
357
- if (amounts.satoshis !== planned.funding.satoshis)
358
- throw new LegShapeError(`the token inputs need ${amounts.satoshis} sats with the leg, it was planned with ${planned.funding.satoshis}`);
359
- if (sats.satoshis !== amounts.satoshis)
360
- throw new LegShapeError(`the swap basket's sats output ${sats.outpoint} holds ${sats.satoshis} sats, the leg needs exactly ${amounts.satoshis}`);
361
- const payout = brc29
362
- ? { kind: "brc29", outputIndex: 1, satoshis: Number(plan.outputs[1].satoshis), lockingScript: plan.outputs[1].script, remittance: brc29.remittance }
363
- : { kind: "color", outputIndex: 1, satoshis: Number(plan.outputs[1].satoshis), lockingScript: plan.outputs[1].script, receipt: tokenKey.receipt };
364
- // The commission is the last output when there is one (Pool.runar.go Swap).
396
+ const plan = legPlan(i, poolUtxo, userPkh, commissionPkh, tokenChange, tokenChangeKey?.pkh ?? ZERO_PKH);
397
+ // The sats change: sats inputs − the need with the change output counted, when ≥ 1.
398
+ const held = inputs.sats.reduce((a, s) => a + s.satoshis, 0);
399
+ const need = legNeed(plan, inputs.tokens, inputs.sats.length + inputs.tokens.length, i.satsPerKb);
400
+ if (held < need.need)
401
+ throw new LegShapeError(`the swap basket's sats rows hold ${held} sats, the leg needs ${need.need}`);
402
+ const changeSats = held - need.needWithChange;
403
+ let satsChange = null;
404
+ if (changeSats >= 1) {
405
+ const at = plan.outputs.length;
406
+ if (i.last) {
407
+ const b = await newBrc29Payout(wallet, identity, change);
408
+ satsChange = { pkh: pkhOfKey(b.publicKey), satoshis: changeSats, payout: { kind: "brc29", outputIndex: at, satoshis: changeSats, lockingScript: b.lockingScript, remittance: b.remittance } };
409
+ }
410
+ else {
411
+ const keyID = changeKeyID(inputs.sats[0].outpoint);
412
+ const pub = await swapKey(keyID);
413
+ satsChange = {
414
+ pkh: pkhOfKey(pub),
415
+ satoshis: changeSats,
416
+ payout: { kind: "basket", outputIndex: at, satoshis: changeSats, lockingScript: p2pkhHexOf(pub), customInstructions: swapSatsInstructions({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", order }) },
417
+ };
418
+ }
419
+ }
420
+ const payout = payBrc29
421
+ ? { kind: "brc29", outputIndex: 1, satoshis: Number(plan.outputs[1].satoshis), lockingScript: plan.outputs[1].script, remittance: payBrc29.remittance }
422
+ : { kind: "color", outputIndex: 1, satoshis: Number(plan.outputs[1].satoshis), lockingScript: plan.outputs[1].script, receipt: payToken.receipt };
365
423
  const commission = { amount: commissionAmount, pkh: commissionPkh, to: commissionTo };
366
- if (ownCommission) {
367
- const at = plan.outputs.length - 1;
424
+ // The commission is the last fee output when there is one (Pool.runar.go Swap), before the token change.
425
+ if (own) {
426
+ const at = plan.outputs.length - 1 - (tokenChange > 0n ? 1 : 0);
368
427
  const out = plan.outputs[at];
369
- commission.payout = ownCommission.brc29
370
- ? { kind: "brc29", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, remittance: ownCommission.brc29.remittance }
371
- : { kind: "color", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, receipt: ownCommission.token.receipt };
428
+ commission.payout = own.brc29
429
+ ? { kind: "brc29", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, remittance: own.brc29.remittance }
430
+ : { kind: "color", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, receipt: own.token.receipt };
372
431
  }
432
+ const tokenChangeOut = tokenChangeKey ? tokenChangeKey.out(plan.outputs.length - 1, plan.outputs[plan.outputs.length - 1].script) : undefined;
373
433
  // The inputs' source transactions: the active swap basket's.
374
434
  const beef = i.holdingBeef ?? (await basketBeef(wallet, SWAP_BASKET));
375
435
  if (!beef)
376
436
  throw new LegShapeError("the wallet returned no BEEF for the swap basket");
377
- for (const t of [sats, ...tokenInputs])
437
+ for (const t of [...inputs.sats, ...inputs.tokens])
378
438
  if (!beef.findTxid(t.txid))
379
439
  throw new LegShapeError(`the wallet's BEEF lacks the swap basket output ${t.outpoint}`);
380
440
  const sourceOf = (txid) => beef.findAtomicTransaction(txid) ?? beef.findTxid(txid).tx;
381
- // Step 3: the swap transaction.
441
+ // The swap transaction.
382
442
  const swap = new Transaction();
383
443
  swap.version = 1;
384
444
  swap.lockTime = 0;
385
445
  swap.addInput({ sourceTXID: poolUtxo.txid, sourceOutputIndex: poolUtxo.vout, sourceTransaction: poolUtxo.sourceTransaction, unlockingScript: new UnlockingScript(), sequence: 0xffffffff });
386
- swap.addInput({ sourceTXID: sats.txid, sourceOutputIndex: sats.vout, sourceTransaction: sourceOf(sats.txid), unlockingScript: new UnlockingScript(), sequence: 0xffffffff });
387
- for (const t of tokenInputs)
446
+ for (const t of [...inputs.sats, ...inputs.tokens])
388
447
  swap.addInput({ sourceTXID: t.txid, sourceOutputIndex: t.vout, sourceTransaction: sourceOf(t.txid), unlockingScript: new UnlockingScript(), sequence: 0xffffffff });
389
448
  for (const o of plan.outputs)
390
449
  swap.addOutput({ satoshis: Number(o.satoshis), lockingScript: LockingScript.fromHex(o.script) });
391
- swap.inputs[0].unlockingScript = PoolTemplate.callUnlock(plan, swap, null).unlockingScript;
392
- swap.inputs[1].unlockingScript = await signP2pkhWithWallet(wallet, swap, 1, sats);
393
- for (const [n, t] of tokenInputs.entries())
394
- swap.inputs[n + 2].unlockingScript = await signP2pkhWithWallet(wallet, swap, n + 2, t);
450
+ if (satsChange)
451
+ swap.addOutput({ satoshis: satsChange.satoshis, lockingScript: LockingScript.fromHex(`76a914${satsChange.pkh}88ac`) });
452
+ swap.inputs[0].unlockingScript = PoolTemplate.callUnlock(plan, swap, satsChange ? { pkh: satsChange.pkh, satoshis: satsChange.satoshis } : null).unlockingScript;
453
+ const rows = [...inputs.sats, ...inputs.tokens];
454
+ for (const [n, t] of rows.entries())
455
+ swap.inputs[n + 1].unlockingScript = await signP2pkhWithWallet(wallet, swap, n + 1, t);
395
456
  for (let n = 1; n < swap.inputs.length; n++) {
396
457
  if (!spendValid(swap, n))
397
458
  throw new LegShapeError(`input ${n} of the swap does not validate`);
398
459
  }
460
+ const funding = inputs.sats[0];
399
461
  return {
400
- pool: planned.pool,
401
- validator: planned.validator,
462
+ pool: i.leg.outpoint,
463
+ validator: i.pool.validatorIdentityKey,
464
+ bsvIn,
402
465
  expires: (i.now ?? Date.now()) + (i.ttlMs ?? SWAP_TTL_MS),
403
466
  plan,
404
- funding: { ...amounts, input: sats, atomicBeef: beef.toBinaryAtomic(sats.txid), txid: sats.txid, outpoint: sats.outpoint },
467
+ order,
468
+ last: i.last,
469
+ funding: { atomicBeef: fundingBeef(beef, funding.txid, [...inputs.sats, ...inputs.tokens].map((r) => r.txid)), txid: funding.txid, fee: satsChange ? need.feeWithChange : need.fee, need: need.need },
405
470
  swap,
406
- tokenInputs,
471
+ satsInputs: inputs.sats,
472
+ tokenInputs: inputs.tokens,
407
473
  payout,
408
474
  commission,
475
+ ...(tokenChangeOut ? { tokenChange: tokenChangeOut } : {}),
476
+ ...(satsChange ? { satsChange: satsChange.payout } : {}),
409
477
  };
410
478
  }
411
479
  /** @bsv/sdk's `Spend` over input `n` of `tx` (its source output from `sourceTransaction`). */
@@ -476,23 +544,30 @@ export function finalCallOf(ours, raw, txid, what) {
476
544
  });
477
545
  return tx;
478
546
  }
547
+ /** The leg's outputs the wallet takes in (0.40.0): the payout, the taker's own commission, the token change, the sats change. */
548
+ export function takeInOutputs(p) {
549
+ const outs = [internalizeOutputOf(p.payout)];
550
+ if (p.commission.payout)
551
+ outs.push(internalizeOutputOf(p.commission.payout));
552
+ if (p.tokenChange)
553
+ outs.push(internalizeOutputOf(p.tokenChange));
554
+ if (p.satsChange)
555
+ outs.push(internalizeOutputOf(p.satsChange));
556
+ return outs;
557
+ }
479
558
  /**
480
- * Accepted: the wallet takes the payout in (`internalizeAction`). No
481
- * relinquishOutput (0.29.0, David Case 2026-11-02: "Anything we internalize
482
- * should relinquish outputs itself"): taking the swap in marks the wallet's
483
- * outputs it spends — the `amm-swap` sats and token inputs — spent
484
- * (wallet-toolbox `markUserInputsSpent`).
559
+ * Accepted: the wallet takes the leg in, ONE `internalizeAction` (0.40.0: the payout, the taker's
560
+ * own commission, the change). No relinquishOutput (0.29.0, David Case 2026-11-02: "Anything we
561
+ * internalize should relinquish outputs itself"): taking the swap in marks the wallet's outputs
562
+ * it spends — the `amm-swap` sats and token rows — spent (wallet-toolbox `markUserInputsSpent`).
485
563
  */
486
564
  export async function completeSwap(wallet, p, raw, txid) {
487
565
  const final = finalSwapOf(p, raw, txid);
488
566
  const out = { txid: final.id("hex"), internalized: false, errors: [] };
489
- const outputs = [internalizeOutputOf(p.payout)];
490
- if (p.commission.payout)
491
- outputs.push(internalizeOutputOf(p.commission.payout));
492
567
  try {
493
568
  const r = await wallet.internalizeAction({
494
569
  tx: final.toAtomicBEEF(true),
495
- outputs,
570
+ outputs: takeInOutputs(p),
496
571
  description: p.payout.kind === "brc29" ? "AMM swap payout" : "AMM swap tokens",
497
572
  labels: ["amm-swap"],
498
573
  });
@@ -503,6 +578,10 @@ export async function completeSwap(wallet, p, raw, txid) {
503
578
  }
504
579
  return out;
505
580
  }
506
- function internalizeOutputOf(o) {
507
- return o.kind === "brc29" ? { outputIndex: o.outputIndex, protocol: WALLET_PAYMENT, paymentRemittance: { ...o.remittance } } : receiptOutput(o.outputIndex, o.receipt);
581
+ export function internalizeOutputOf(o) {
582
+ if (o.kind === "brc29")
583
+ return { outputIndex: o.outputIndex, protocol: WALLET_PAYMENT, paymentRemittance: { ...o.remittance } };
584
+ if (o.kind === "basket")
585
+ return { outputIndex: o.outputIndex, protocol: "basket insertion", insertionRemittance: { basket: SWAP_BASKET, customInstructions: o.customInstructions } };
586
+ return receiptOutput(o.outputIndex, o.receipt);
508
587
  }