@dcasegr/amm-client 0.0.0-stage → 0.38.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,508 @@
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`).
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.
42
+ */
43
+ import { Hash, LockingScript, PublicKey, Spend, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
44
+ import { PoolTemplate, decodeOneColor } from "../pool/index.js";
45
+ import { isPoolRow } from "../lp/poolRows.js";
46
+ import { basketBeef } from "../lp/wallet.js";
47
+ 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";
50
+ /** A P2PKH unlocking script's upper bound: push(DER ≤ 72 + sighash byte) + push(33-byte key). */
51
+ export const P2PKH_UNLOCK_LENGTH = 108;
52
+ /**
53
+ * How long the relay may take (the swap's `expires`): 15 s (0.34.0, D15, David Case 2026-11-04;
54
+ * was 120 s, a 10-05 builder number): the held submit returns inside it.
55
+ */
56
+ export const SWAP_TTL_MS = 15_000;
57
+ const toHex = (b) => Utils.toHex(b);
58
+ export class LegShapeError extends Error {
59
+ constructor(message) {
60
+ super(message);
61
+ this.name = "LegShapeError";
62
+ }
63
+ }
64
+ /** The token's 32-byte wire id (internal byte order) for a token with a 32-byte id (`<txid>_0`; `<txid>` and `<txid>.0` read the same). */
65
+ export function assetIdOf(tokenId) {
66
+ const r = parseTokenId(tokenId);
67
+ if (!r || r.vout !== 0)
68
+ throw new Error(`pools exist only for 32-byte ids (<txid>_0), not ${tokenId}`);
69
+ return toHex(Utils.toArray(r.txid, "hex").reverse());
70
+ }
71
+ /**
72
+ * The basket rows usable as token inputs for `tokenId`: 1Color value outputs
73
+ * with this 32-byte id (the pool template counts only those) whose
74
+ * customInstructions name the key (protocolID, keyID).
75
+ */
76
+ export function tokenInputsOf(rows, tokenId) {
77
+ const assetId = assetIdOf(tokenId);
78
+ const out = [];
79
+ for (const r of rows) {
80
+ // A pool the wallet holds as LP carries the same 1Color prefix; it is not a token input.
81
+ if (!r.lockingScript || isPoolRow(r))
82
+ continue;
83
+ const t = decodeOneColor(Utils.toArray(r.lockingScript, "hex"));
84
+ if (!t || t.role !== "value" || !t.idBytes || toHex(Array.from(t.idBytes)) !== assetId)
85
+ 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;
95
+ const op = parseOutpoint(r.outpoint);
96
+ if (!op)
97
+ continue;
98
+ out.push({
99
+ outpoint: `${op.txid}.${op.vout}`,
100
+ txid: op.txid,
101
+ vout: op.vout,
102
+ satoshis: r.satoshis,
103
+ lockingScript: r.lockingScript,
104
+ amount: t.amount,
105
+ protocolID: ci.protocolID,
106
+ keyID: ci.keyID,
107
+ counterparty: typeof ci.counterparty === "string" ? ci.counterparty : "self",
108
+ });
109
+ }
110
+ return out;
111
+ }
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
+ const P2PKH_RE = /^76a914[0-9a-f]{40}88ac$/i;
141
+ /** The basket rows usable as a leg's sats: plain P2PKH outputs whose customInstructions say `sats: true` and name the key. */
142
+ export function satsInputsOf(rows) {
143
+ const out = [];
144
+ for (const r of rows) {
145
+ if (!r.lockingScript || !P2PKH_RE.test(r.lockingScript))
146
+ 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;
156
+ const op = parseOutpoint(r.outpoint);
157
+ if (!op)
158
+ continue;
159
+ out.push({
160
+ outpoint: `${op.txid}.${op.vout}`,
161
+ txid: op.txid,
162
+ vout: op.vout,
163
+ satoshis: r.satoshis,
164
+ 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 } : {}),
169
+ });
170
+ }
171
+ return out;
172
+ }
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
+ /** The pool output from the lookup's BEEF, as the template's PoolUtxo. */
178
+ export function poolUtxoFrom(poolOutput) {
179
+ const tx = Transaction.fromAtomicBEEF(poolOutput.beef);
180
+ const out = tx.outputs[poolOutput.outputIndex];
181
+ if (!out)
182
+ throw new LegShapeError(`the lookup's BEEF has no output ${poolOutput.outputIndex}`);
183
+ return { txid: tx.id("hex"), vout: poolOutput.outputIndex, satoshis: out.satoshis, script: out.lockingScript, sourceTransaction: tx };
184
+ }
185
+ /**
186
+ * A P2PKH (or 1Color-on-P2PKH) input signed by the wallet: BIP-143 over
187
+ * the finished transaction under `scope` (ALL|FORKID), `createSignature` with `hashToDirectlySign`
188
+ * (1sat-sdk's `signP2PKHInputWallet`).
189
+ */
190
+ export async function signP2pkhWithWallet(wallet, tx, inputIndex, src, scope = TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID) {
191
+ const input = tx.inputs[inputIndex];
192
+ const preimage = TransactionSignature.format({
193
+ sourceTXID: input.sourceTXID,
194
+ sourceOutputIndex: input.sourceOutputIndex,
195
+ sourceSatoshis: src.satoshis,
196
+ transactionVersion: tx.version,
197
+ otherInputs: tx.inputs.filter((_, n) => n !== inputIndex),
198
+ inputIndex,
199
+ outputs: tx.outputs,
200
+ inputSequence: input.sequence ?? 0xffffffff,
201
+ subscript: LockingScript.fromHex(src.lockingScript),
202
+ lockTime: tx.lockTime,
203
+ scope,
204
+ });
205
+ const sighash = Hash.sha256(Hash.sha256(Array.from(preimage)));
206
+ const args = { protocolID: src.protocolID, keyID: src.keyID, counterparty: src.counterparty };
207
+ const { signature } = await wallet.createSignature({ ...args, hashToDirectlySign: Array.from(sighash) });
208
+ const { publicKey } = await wallet.getPublicKey({ ...args, forSelf: true });
209
+ return new UnlockingScript().writeBin([...signature, scope]).writeBin(Utils.toArray(publicKey, "hex"));
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // The leg's sats
213
+ // ---------------------------------------------------------------------------
214
+ function varIntSize(n) {
215
+ return n < 0xfd ? 1 : n <= 0xffff ? 3 : n <= 0xffffffff ? 5 : 9;
216
+ }
217
+ /**
218
+ * The final swap's size in bytes: the pool input at `maxCallUnlockLength`
219
+ * (validator's signature in), `takerInputs` P2PKH inputs (the sats, tokens)
220
+ * at 108 bytes of unlocking script, the contract's outputs, no change.
221
+ */
222
+ export function swapTxSize(plan, takerInputs) {
223
+ const poolUnlock = PoolTemplate.maxCallUnlockLength(plan);
224
+ let size = 4 + varIntSize(1 + takerInputs);
225
+ size += 36 + varIntSize(poolUnlock) + poolUnlock + 4;
226
+ size += takerInputs * (36 + varIntSize(P2PKH_UNLOCK_LENGTH) + P2PKH_UNLOCK_LENGTH + 4);
227
+ size += varIntSize(plan.outputs.length);
228
+ for (const o of plan.outputs) {
229
+ const len = o.script.length / 2;
230
+ size += 8 + varIntSize(len) + len;
231
+ }
232
+ return size + 4;
233
+ }
234
+ /**
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.
240
+ */
241
+ export function swapFunding(plan, tokenInputs, satsPerKb) {
242
+ const outSum = plan.outputs.reduce((a, o) => a + o.satoshis, 0n);
243
+ const tokenSats = tokenInputs.reduce((a, t) => a + BigInt(t.satoshis), 0n);
244
+ 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 };
248
+ }
249
+ const ZERO_PKH = "00".repeat(20);
250
+ /** BRC-29 / BRC-207 derivationPrefix of a leg's payout (0.29.1): base64 of the UTF-8 text "amm swap". */
251
+ export const LEG_PAYOUT_PREFIX = Utils.toBase64(Utils.toArray("amm swap", "utf8"));
252
+ /** derivationPrefix of the taker's own commission output (0.29.1): base64 of "amm swap commission". */
253
+ export const LEG_COMMISSION_PREFIX = Utils.toBase64(Utils.toArray("amm swap commission", "utf8"));
254
+ /**
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.
263
+ */
264
+ export function legDerivation(keyInput, which) {
265
+ return {
266
+ derivationPrefix: which === "payout" ? LEG_PAYOUT_PREFIX : LEG_COMMISSION_PREFIX,
267
+ derivationSuffix: Utils.toBase64(Hash.sha256(Utils.toArray(keyInput, "utf8"))),
268
+ };
269
+ }
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}`;
276
+ }
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) {
279
+ const { leg, pool } = i;
280
+ return PoolTemplate.planSwap({
281
+ pool: poolUtxo,
282
+ amountIn: leg.amountIn,
283
+ bsvIn: i.direction === "bsvToToken",
284
+ userPkh,
285
+ commissionPkh,
286
+ expect: {
287
+ bsvReserve: pool.bsvReserve,
288
+ tokenReserve: pool.tokenReserve,
289
+ lpFeeBps: pool.liquidityFeeBps,
290
+ validatorFeeBps: pool.validationFeeBps,
291
+ commissionBps: pool.commissionBps,
292
+ lpFee: leg.liquidityFee,
293
+ validatorFee: leg.validationFee,
294
+ commission: leg.commission,
295
+ amountOut: leg.amountOut,
296
+ },
297
+ });
298
+ }
299
+ /**
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`.
304
+ */
305
+ export async function planSwapLeg(i) {
306
+ const { leg } = i;
307
+ const bsvIn = i.direction === "bsvToToken";
308
+ 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)}`);
311
+ }
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}`);
316
+ }
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 };
324
+ }
325
+ /** Steps 1 (unless `planned` is given) and 3 (see the module comment). Throws `PoolBuildError` / `LegShapeError`; the wallet is left as it was. */
326
+ 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;
338
+ const decoded = PoolTemplate.decode(poolUtxo.script);
339
+ const identity = !bsvIn || commissionTo === "own" ? await identityKeyOf(wallet) : "";
340
+ 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;
347
+ 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()) };
352
+ }
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).
365
+ const commission = { amount: commissionAmount, pkh: commissionPkh, to: commissionTo };
366
+ if (ownCommission) {
367
+ const at = plan.outputs.length - 1;
368
+ 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 };
372
+ }
373
+ // The inputs' source transactions: the active swap basket's.
374
+ const beef = i.holdingBeef ?? (await basketBeef(wallet, SWAP_BASKET));
375
+ if (!beef)
376
+ throw new LegShapeError("the wallet returned no BEEF for the swap basket");
377
+ for (const t of [sats, ...tokenInputs])
378
+ if (!beef.findTxid(t.txid))
379
+ throw new LegShapeError(`the wallet's BEEF lacks the swap basket output ${t.outpoint}`);
380
+ const sourceOf = (txid) => beef.findAtomicTransaction(txid) ?? beef.findTxid(txid).tx;
381
+ // Step 3: the swap transaction.
382
+ const swap = new Transaction();
383
+ swap.version = 1;
384
+ swap.lockTime = 0;
385
+ 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)
388
+ swap.addInput({ sourceTXID: t.txid, sourceOutputIndex: t.vout, sourceTransaction: sourceOf(t.txid), unlockingScript: new UnlockingScript(), sequence: 0xffffffff });
389
+ for (const o of plan.outputs)
390
+ 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);
395
+ for (let n = 1; n < swap.inputs.length; n++) {
396
+ if (!spendValid(swap, n))
397
+ throw new LegShapeError(`input ${n} of the swap does not validate`);
398
+ }
399
+ return {
400
+ pool: planned.pool,
401
+ validator: planned.validator,
402
+ expires: (i.now ?? Date.now()) + (i.ttlMs ?? SWAP_TTL_MS),
403
+ plan,
404
+ funding: { ...amounts, input: sats, atomicBeef: beef.toBinaryAtomic(sats.txid), txid: sats.txid, outpoint: sats.outpoint },
405
+ swap,
406
+ tokenInputs,
407
+ payout,
408
+ commission,
409
+ };
410
+ }
411
+ /** @bsv/sdk's `Spend` over input `n` of `tx` (its source output from `sourceTransaction`). */
412
+ export function spendValid(tx, n) {
413
+ const inp = tx.inputs[n];
414
+ const src = inp.sourceTransaction?.outputs[inp.sourceOutputIndex];
415
+ if (!src)
416
+ return false;
417
+ try {
418
+ return new Spend({
419
+ sourceTXID: inp.sourceTXID ?? inp.sourceTransaction.id("hex"),
420
+ sourceOutputIndex: inp.sourceOutputIndex,
421
+ sourceSatoshis: src.satoshis,
422
+ lockingScript: src.lockingScript,
423
+ transactionVersion: tx.version,
424
+ otherInputs: tx.inputs.filter((_, j) => j !== n),
425
+ outputs: tx.outputs,
426
+ inputIndex: n,
427
+ unlockingScript: inp.unlockingScript,
428
+ inputSequence: inp.sequence ?? 0xffffffff,
429
+ lockTime: tx.lockTime,
430
+ }).validate();
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ }
436
+ // ---------------------------------------------------------------------------
437
+ // After the relay's answer
438
+ // ---------------------------------------------------------------------------
439
+ /**
440
+ * The validator's final transaction, checked against ours: same inputs (in
441
+ * order, the taker's unlocking scripts unchanged), same outputs; only input
442
+ * 0's unlocking script (the validator's slot) differs. Its inputs get our
443
+ * source transactions, so it serializes as AtomicBEEF.
444
+ */
445
+ export function finalSwapOf(p, raw, txid) {
446
+ return finalCallOf(p.swap, raw, txid, "the swap we built");
447
+ }
448
+ /**
449
+ * A validator-signed pool call checked against the one we built (`ours`):
450
+ * same inputs in order with our unlocking scripts on every input but the
451
+ * pool's (input 0), same outputs. Shared by the swap and AddLiquidity.
452
+ */
453
+ export function finalCallOf(ours, raw, txid, what) {
454
+ let tx;
455
+ try {
456
+ tx = Transaction.fromBinary(raw);
457
+ }
458
+ catch {
459
+ tx = Transaction.fromBEEF(raw);
460
+ }
461
+ if (txid && tx.id("hex") !== txid)
462
+ throw new LegShapeError(`the relay's transaction is ${tx.id("hex")}, not ${txid}`);
463
+ const same = tx.inputs.length === ours.inputs.length &&
464
+ tx.outputs.length === ours.outputs.length &&
465
+ tx.inputs.every((inp, n) => {
466
+ const o = ours.inputs[n];
467
+ const src = inp.sourceTXID ?? inp.sourceTransaction?.id("hex");
468
+ return src === o.sourceTXID && inp.sourceOutputIndex === o.sourceOutputIndex && (n === 0 || inp.unlockingScript?.toHex() === o.unlockingScript.toHex());
469
+ }) &&
470
+ tx.outputs.every((out, n) => out.satoshis === ours.outputs[n].satoshis && out.lockingScript.toHex() === ours.outputs[n].lockingScript.toHex());
471
+ if (!same)
472
+ throw new LegShapeError(`the relay's transaction is not ${what}`);
473
+ tx.inputs.forEach((inp, n) => {
474
+ inp.sourceTransaction = ours.inputs[n].sourceTransaction;
475
+ inp.sourceTXID = ours.inputs[n].sourceTXID;
476
+ });
477
+ return tx;
478
+ }
479
+ /**
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`).
485
+ */
486
+ export async function completeSwap(wallet, p, raw, txid) {
487
+ const final = finalSwapOf(p, raw, txid);
488
+ 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
+ try {
493
+ const r = await wallet.internalizeAction({
494
+ tx: final.toAtomicBEEF(true),
495
+ outputs,
496
+ description: p.payout.kind === "brc29" ? "AMM swap payout" : "AMM swap tokens",
497
+ labels: ["amm-swap"],
498
+ });
499
+ out.internalized = r.accepted;
500
+ }
501
+ catch (err) {
502
+ out.errors.push(`internalizeAction: ${err instanceof Error ? err.message : String(err)}`);
503
+ }
504
+ return out;
505
+ }
506
+ function internalizeOutputOf(o) {
507
+ return o.kind === "brc29" ? { outputIndex: o.outputIndex, protocol: WALLET_PAYMENT, paymentRemittance: { ...o.remittance } } : receiptOutput(o.outputIndex, o.receipt);
508
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A prepared swap through the relay to its end (pure over the wallet and
3
+ * AuthFetch, tested with fakes). 0.34.0 (D15, David Case 2026-11-04): the submit's answer is
4
+ * the settled record — the host holds the `/call` request until the relay thread rests (the
5
+ * validator's answer; a dial failure settles `failed` at once) — so nothing polls:
6
+ *
7
+ * submit (amm.swap.submit) →
8
+ * accepted: completeSwap (internalizeAction; 0.29.0: no relinquishOutput)
9
+ * refused / timeout / an error answer to submit (nothing recorded):
10
+ * nothing to undo: the leg's `amm-swap` outputs stay there
11
+ * no answer (a network failure, the host's 503 past its bound, a `pending` record, the
12
+ * relay's transport `failed`): "no answer yet", left alone — the relay may still carry it;
13
+ * "Check again" reads the record ONCE (`GET <base>/swaps?id=`, plain) and acts on it.
14
+ *
15
+ * The swap's id is the relay's (amm-p2p relay.zig `idOf`): sha256 of the swap as submitted (the
16
+ * validator's slot empty), hex — known to the page, so a lost answer can be checked again.
17
+ */
18
+ import { type WalletInterface } from "@bsv/sdk";
19
+ import type { PoolState } from "@dcasegr/amm-engine";
20
+ import { type AuthFetchLike } from "./relay.js";
21
+ import { type CompletedSwap, type PreparedSwap } from "./swapAction.js";
22
+ export type SwapOutcome = {
23
+ status: "accepted";
24
+ id: string;
25
+ txid: string;
26
+ completed: CompletedSwap;
27
+ } | {
28
+ status: "refused";
29
+ id: string;
30
+ reason: string;
31
+ pool?: PoolState;
32
+ } | {
33
+ status: "timeout";
34
+ id: string;
35
+ } | {
36
+ status: "failed";
37
+ reason: string;
38
+ } | {
39
+ status: "unknown";
40
+ id: string;
41
+ reason: string;
42
+ };
43
+ export interface RelayContext {
44
+ wallet: WalletInterface;
45
+ authFetch: AuthFetchLike;
46
+ /** The token instance's AMM base. */
47
+ base: string;
48
+ /** The plain fetch "Check again" reads the record with (default: the browser's). */
49
+ fetch?: (url: string, init?: RequestInit) => Promise<Response>;
50
+ }
51
+ /** The relay's id of a prepared swap: sha256 of the swap's bytes as submitted, hex. */
52
+ export declare function swapIdOf(p: PreparedSwap): string;
53
+ /** "no answer yet": what a held request that came back without the settled record means. */
54
+ export declare const NO_ANSWER_YET = "no answer yet";
55
+ /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the relay dials it). */
56
+ export declare function relaySwap(c: RelayContext, p: PreparedSwap, peerId: string): Promise<SwapOutcome>;
57
+ /** "Check again" for an unknown outcome: the record read ONCE, plain (`GET <base>/swaps?id=`, P10), and acted on. */
58
+ export declare function checkAgain(c: RelayContext, p: PreparedSwap, id: string): Promise<SwapOutcome>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * A prepared swap through the relay to its end (pure over the wallet and
3
+ * AuthFetch, tested with fakes). 0.34.0 (D15, David Case 2026-11-04): the submit's answer is
4
+ * the settled record — the host holds the `/call` request until the relay thread rests (the
5
+ * validator's answer; a dial failure settles `failed` at once) — so nothing polls:
6
+ *
7
+ * submit (amm.swap.submit) →
8
+ * accepted: completeSwap (internalizeAction; 0.29.0: no relinquishOutput)
9
+ * refused / timeout / an error answer to submit (nothing recorded):
10
+ * nothing to undo: the leg's `amm-swap` outputs stay there
11
+ * no answer (a network failure, the host's 503 past its bound, a `pending` record, the
12
+ * relay's transport `failed`): "no answer yet", left alone — the relay may still carry it;
13
+ * "Check again" reads the record ONCE (`GET <base>/swaps?id=`, plain) and acts on it.
14
+ *
15
+ * The swap's id is the relay's (amm-p2p relay.zig `idOf`): sha256 of the swap as submitted (the
16
+ * validator's slot empty), hex — known to the page, so a lost answer can be checked again.
17
+ */
18
+ import { Hash, Utils } from "@bsv/sdk";
19
+ import { RelayError, parseSwapRecord, submitSwap } from "./relay.js";
20
+ import { completeSwap } from "./swapAction.js";
21
+ import { readSwap } from "../lib/overlay.js";
22
+ const errText = (e) => (e instanceof Error ? e.message : String(e));
23
+ /** The relay's id of a prepared swap: sha256 of the swap's bytes as submitted, hex. */
24
+ export function swapIdOf(p) {
25
+ return Utils.toHex(Hash.sha256(p.swap.toBinary()));
26
+ }
27
+ /** "no answer yet": what a held request that came back without the settled record means. */
28
+ export const NO_ANSWER_YET = "no answer yet";
29
+ /** `peerId`: the chosen validator's libp2p peer ID, from the pools route's row (the relay dials it). */
30
+ export async function relaySwap(c, p, peerId) {
31
+ const id = swapIdOf(p);
32
+ let record;
33
+ try {
34
+ record = await submitSwap(c.authFetch, c.base, {
35
+ funding: p.funding.atomicBeef,
36
+ swap: p.swap.toBinary(),
37
+ pool: p.pool,
38
+ validator: p.validator,
39
+ peerId,
40
+ expires: p.expires,
41
+ });
42
+ }
43
+ catch (err) {
44
+ // The host's bound passed with the relay still out: no answer yet (the swap stands).
45
+ if (err instanceof RelayError && err.httpStatus === 503)
46
+ return { status: "unknown", id, reason: NO_ANSWER_YET };
47
+ // The relay answered with an error: it recorded nothing and sent nothing on.
48
+ if (err instanceof RelayError)
49
+ return { status: "failed", reason: `amm.swap.submit: ${err.message}` };
50
+ return { status: "unknown", id, reason: `${NO_ANSWER_YET}: amm.swap.submit: ${errText(err)}` };
51
+ }
52
+ return act(c, p, record);
53
+ }
54
+ /** A record's outcome, acted on (accepted: the wallet takes the payout in). */
55
+ async function act(c, p, r) {
56
+ switch (r.status) {
57
+ case "pending":
58
+ return { status: "unknown", id: r.id, reason: NO_ANSWER_YET };
59
+ case "accepted":
60
+ return { status: "accepted", id: r.id, txid: r.txid, completed: await completeSwap(c.wallet, p, r.tx, r.txid) };
61
+ case "refused":
62
+ return r.pool ? { status: "refused", id: r.id, reason: r.reason, pool: r.pool } : { status: "refused", id: r.id, reason: r.reason };
63
+ case "timeout":
64
+ return { status: "timeout", id: r.id };
65
+ case "failed":
66
+ return { status: "unknown", id: r.id, reason: `the relay could not reach the validator (${r.reason})` };
67
+ }
68
+ }
69
+ /** "Check again" for an unknown outcome: the record read ONCE, plain (`GET <base>/swaps?id=`, P10), and acted on. */
70
+ export async function checkAgain(c, p, id) {
71
+ let raw;
72
+ try {
73
+ raw = await readSwap(c.base, id, c.fetch);
74
+ }
75
+ catch (err) {
76
+ return { status: "unknown", id, reason: `${NO_ANSWER_YET}: ${errText(err)}` };
77
+ }
78
+ if (raw === null)
79
+ return { status: "unknown", id, reason: `${NO_ANSWER_YET}: the relay has no record of it` };
80
+ try {
81
+ return await act(c, p, parseSwapRecord(raw));
82
+ }
83
+ catch (err) {
84
+ return { status: "unknown", id, reason: errText(err) };
85
+ }
86
+ }