@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.
- package/README.md +64 -24
- package/dist/client.d.ts +206 -62
- package/dist/client.js +388 -333
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +6 -3
- package/dist/index.d.ts +16 -4
- package/dist/index.js +16 -4
- package/dist/lib/overlay.d.ts +226 -19
- package/dist/lib/overlay.js +278 -49
- package/dist/lib/skein.d.ts +2 -0
- package/dist/lib/skein.js +1 -1
- package/dist/market/activeSwap.d.ts +77 -96
- package/dist/market/activeSwap.js +146 -166
- package/dist/market/plan.d.ts +2 -1
- package/dist/market/plan.js +1 -1
- package/dist/market/prices.d.ts +4 -4
- package/dist/market/prices.js +4 -4
- package/dist/market/swapAction.d.ts +150 -145
- package/dist/market/swapAction.js +292 -213
- package/dist/market/swapFlow.d.ts +3 -6
- package/dist/market/swapFlow.js +4 -22
- package/dist/pool/pool.artifact.json +760 -528
- package/dist/pool/template.d.ts +10 -2
- package/dist/pool/template.js +16 -6
- package/dist/wallet/color.d.ts +13 -9
- package/dist/wallet/color.js +13 -10
- package/dist/wallet/swapPayouts.d.ts +45 -28
- package/dist/wallet/swapPayouts.js +129 -117
- package/package.json +2 -2
|
@@ -1,52 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One swap leg through the user's BRC-100 wallet (docs/notes.md, "Swap
|
|
3
|
-
*
|
|
4
|
-
* comes from the wallet (`getPublicKey`, `
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
* (
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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:
|
|
106
|
-
keyID:
|
|
107
|
-
counterparty:
|
|
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
|
-
|
|
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:
|
|
166
|
-
keyID:
|
|
167
|
-
counterparty:
|
|
168
|
-
...(
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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
|
|
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
|
|
246
|
-
const
|
|
247
|
-
return {
|
|
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
|
-
|
|
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
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
|
271
|
-
export function
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
274
|
-
throw new LegShapeError(
|
|
275
|
-
return
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
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
|
|
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
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
/**
|
|
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
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
const pay = legDerivation(
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const userPkh =
|
|
346
|
-
let
|
|
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(
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
throw new LegShapeError(`the
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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 =
|
|
370
|
-
? { kind: "brc29", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, remittance:
|
|
371
|
-
: { kind: "color", outputIndex: at, satoshis: Number(out.satoshis), lockingScript: out.script, 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, ...
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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:
|
|
401
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
481
|
-
* relinquishOutput (0.29.0, David Case 2026-11-02: "Anything we
|
|
482
|
-
* should relinquish outputs itself"): taking the swap in marks the wallet's
|
|
483
|
-
*
|
|
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
|
-
|
|
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
|
}
|