@dcasegr/amm-client 0.0.0-stage → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +107 -2
  2. package/dist/client.d.ts +534 -0
  3. package/dist/client.js +1319 -0
  4. package/dist/errors.d.ts +17 -0
  5. package/dist/errors.js +75 -0
  6. package/dist/index.d.ts +51 -0
  7. package/dist/index.js +53 -0
  8. package/dist/lib/config.d.ts +42 -0
  9. package/dist/lib/config.js +64 -0
  10. package/dist/lib/keys.d.ts +42 -0
  11. package/dist/lib/keys.js +48 -0
  12. package/dist/lib/overlay.d.ts +190 -0
  13. package/dist/lib/overlay.js +302 -0
  14. package/dist/lib/skein.d.ts +82 -0
  15. package/dist/lib/skein.js +142 -0
  16. package/dist/lib/tokenId.d.ts +72 -0
  17. package/dist/lib/tokenId.js +121 -0
  18. package/dist/lp/amounts.d.ts +12 -0
  19. package/dist/lp/amounts.js +38 -0
  20. package/dist/lp/close.d.ts +90 -0
  21. package/dist/lp/close.js +140 -0
  22. package/dist/lp/images.d.ts +11 -0
  23. package/dist/lp/images.js +45 -0
  24. package/dist/lp/inventory.d.ts +68 -0
  25. package/dist/lp/inventory.js +84 -0
  26. package/dist/lp/poolDeploy.d.ts +220 -0
  27. package/dist/lp/poolDeploy.js +291 -0
  28. package/dist/lp/poolRows.d.ts +13 -0
  29. package/dist/lp/poolRows.js +19 -0
  30. package/dist/lp/positions.d.ts +81 -0
  31. package/dist/lp/positions.js +144 -0
  32. package/dist/lp/rebalance.d.ts +101 -0
  33. package/dist/lp/rebalance.js +168 -0
  34. package/dist/lp/wallet.d.ts +12 -0
  35. package/dist/lp/wallet.js +19 -0
  36. package/dist/market/activeSwap.d.ts +158 -0
  37. package/dist/market/activeSwap.js +320 -0
  38. package/dist/market/plan.d.ts +85 -0
  39. package/dist/market/plan.js +149 -0
  40. package/dist/market/prices.d.ts +13 -0
  41. package/dist/market/prices.js +14 -0
  42. package/dist/market/relay.d.ts +84 -0
  43. package/dist/market/relay.js +201 -0
  44. package/dist/market/swapAction.d.ts +306 -0
  45. package/dist/market/swapAction.js +508 -0
  46. package/dist/market/swapFlow.d.ts +58 -0
  47. package/dist/market/swapFlow.js +86 -0
  48. package/dist/market/view.d.ts +58 -0
  49. package/dist/market/view.js +63 -0
  50. package/dist/pool/artifact.d.ts +23 -0
  51. package/dist/pool/artifact.js +38 -0
  52. package/dist/pool/index.d.ts +8 -0
  53. package/dist/pool/index.js +8 -0
  54. package/dist/pool/oneColor.d.ts +19 -0
  55. package/dist/pool/oneColor.js +39 -0
  56. package/dist/pool/pool.artifact.json +5069 -0
  57. package/dist/pool/runar-ir-schema.d.ts +21 -0
  58. package/dist/pool/runar-ir-schema.js +18 -0
  59. package/dist/pool/template.d.ts +295 -0
  60. package/dist/pool/template.js +715 -0
  61. package/dist/vendor/runar/LICENSE +21 -0
  62. package/dist/vendor/runar/NOTICE +15 -0
  63. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
  64. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
  65. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
  66. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
  67. package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
  68. package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
  69. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
  70. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
  71. package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
  72. package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
  73. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
  74. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
  75. package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
  76. package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
  77. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
  78. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
  79. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
  80. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
  81. package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
  82. package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
  83. package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
  84. package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
  85. package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
  86. package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
  87. package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
  88. package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
  89. package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
  90. package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
  91. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
  92. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
  93. package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
  94. package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
  95. package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
  96. package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
  97. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
  98. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
  99. package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
  100. package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
  101. package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
  102. package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
  103. package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
  104. package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
  105. package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
  106. package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
  107. package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
  108. package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
  109. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
  110. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
  111. package/dist/vendor/runar/runar-sdk.d.ts +9 -0
  112. package/dist/vendor/runar/runar-sdk.js +8 -0
  113. package/dist/wallet/action.d.ts +28 -0
  114. package/dist/wallet/action.js +42 -0
  115. package/dist/wallet/brc29.d.ts +83 -0
  116. package/dist/wallet/brc29.js +80 -0
  117. package/dist/wallet/color.d.ts +110 -0
  118. package/dist/wallet/color.js +134 -0
  119. package/dist/wallet/swapPayouts.d.ts +35 -0
  120. package/dist/wallet/swapPayouts.js +146 -0
  121. package/package.json +39 -4
@@ -0,0 +1,715 @@
1
+ /**
2
+ * The AMM Pool script template (pool/Pool.runar.go) over runar-sdk.
3
+ *
4
+ * On chain a pool output is a BRC-162 (1Color) value output wrapped around the
5
+ * Rúnar contract:
6
+ *
7
+ * 0x20 <assetId:32> <push tokenReserve> OP_2DROP || code || OP_RETURN || state
8
+ *
9
+ * One layout (artifact.ts `PoolVersion`; 0.29.1: layout 1, 0.9.0-0.12.x, dropped). Layout 2 (skein-amm 0.13.0): `state` is
10
+ * TokenReserve (8 bytes LE sign-magnitude) ‖ LpPubKey ‖ ValidatorPubKey ‖ ValidatorIdentity
11
+ * (33 each) ‖ Nonce (32) ‖ Affirmation (its length byte, then the validator's DER signature
12
+ * over sha256 of `affirmationMessage`, docs/AMM.md "The deploy and the affirmation"); no
13
+ * payload; methods Swap, Close (the LP's, outputs free) and Rescind (the validator's: the
14
+ * ValidatorPubKey cleared to 33 zero bytes). A layout-1 script (the 0.9.0-0.12.x contract)
15
+ * decodes as no pool.
16
+ *
17
+ * `code` is the artifact's template with the readonly constructor args
18
+ * (assetId, lpFeeBps, validatorFeeBps, commissionBps) spliced in.
19
+ * runar-sdk knows nothing of the prefix, so this module splits it off and hands
20
+ * runar-sdk only `code || OP_RETURN || state`.
21
+ *
22
+ * A method call (input 0's unlocking script) is pushes only, in the order the
23
+ * compiled contract reads them:
24
+ *
25
+ * _codePart arg0 .. argN-1 _changePKH _changeAmount txPreimage methodIndex
26
+ *
27
+ * The preimage is BIP-143 (ALL|FORKID) over the pool input with the scriptCode
28
+ * after Rúnar's leading `OP_NOP OP_CODESEPARATOR`; every signature a method
29
+ * checks (validator's, LP's) is over sha256d of that preimage. BIP-143 does not
30
+ * cover any unlocking script, so the builder (taker or LP) finishes the whole
31
+ * transaction, signs its own inputs, and leaves the validator's slot empty
32
+ * (`OP_0`); the validator then fills its slot with `signPoolInput` without
33
+ * invalidating anything else. That is the sequence pool/pool_test.go and
34
+ * programs/amm-topic/gen use.
35
+ *
36
+ * Uses from runar-sdk: artifact matching, constructor-arg and state extraction,
37
+ * state serialization, the code part, the BIP-143 preimage at the code
38
+ * separator (`computeOpPushTxWithCodeSep`), and its push encoders.
39
+ * `RunarContract.prepareCall` is not used: it needs a Provider to select
40
+ * funding and sets the fee from a fee rate, it builds the continuation from
41
+ * the bare artifact script (no BRC-162 prefix), and this contract's outputs
42
+ * come from `addRawOutput` with a prefix the SDK cannot predict.
43
+ */
44
+ import { EMPTY_SIG, RunarContract, encodeArg, encodePushData, encodeScriptNumber, extractConstructorArgs, extractStateFromScript, findLastOpReturn, matchesArtifact, serializeState, } from "../vendor/runar/runar-sdk.js";
45
+ import { Hash, LockingScript, P2PKH, PrivateKey, PublicKey, Signature, Transaction, UnlockingScript, Utils, } from "@bsv/sdk";
46
+ import { METHOD_INDEX, artifactOf, poolArtifact } from "./artifact.js";
47
+ import { decodeOneColor, oneColorValuePrefix } from "./oneColor.js";
48
+ import { deriveFirstValidatorPubKey, deriveValidatorPubKey } from "../lib/keys.js";
49
+ /** A cleared ValidatorPubKey (Rescind, 0.13.0): 33 zero bytes. */
50
+ export const RESCINDED_KEY = "00".repeat(33);
51
+ /** A refusal by the builder's own checks: nothing was signed. */
52
+ export class PoolBuildError extends Error {
53
+ code;
54
+ constructor(code, message) {
55
+ super(`${code}: ${message}`);
56
+ this.code = code;
57
+ this.name = "PoolBuildError";
58
+ }
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Script helpers
62
+ // ---------------------------------------------------------------------------
63
+ const toHex = (b) => Utils.toHex(Array.from(b));
64
+ const fromHex = (h) => Utils.toArray(h, "hex");
65
+ const scriptHex = (s) => (typeof s === "string" ? s.toLowerCase() : s.toHex());
66
+ const p2pkhHex = (pkh) => `76a914${pkh}88ac`;
67
+ const pkhOf = (pubKeyHex) => toHex(Hash.hash160(fromHex(pubKeyHex)));
68
+ function tokenPrefixHex(assetId, amount) {
69
+ return toHex(oneColorValuePrefix(fromHex(assetId), amount));
70
+ }
71
+ /** `tokenP2pkh` in Pool.runar.go: a 1Color value output of `amount` to `pkh`. */
72
+ function tokenP2pkhHex(assetId, amount, pkh) {
73
+ return tokenPrefixHex(assetId, amount) + p2pkhHex(pkh);
74
+ }
75
+ /** The constructor args in the artifact's order (its ABI's names). */
76
+ function ctorArgs(version, args, state) {
77
+ const v = {
78
+ tokenReserve: state.tokenReserve,
79
+ lpPubKey: state.lpPubKey,
80
+ validatorPubKey: state.validatorPubKey,
81
+ validatorIdentity: state.validatorIdentity,
82
+ nonce: state.nonce ?? "00".repeat(32),
83
+ affirmation: state.affirmation ?? "",
84
+ assetId: args.assetId,
85
+ lpFeeBps: args.lpFeeBps,
86
+ validatorFeeBps: args.validatorFeeBps,
87
+ commissionBps: args.commissionBps,
88
+ };
89
+ return artifactOf(version).abi.constructor.params.map((p) => v[p.name]);
90
+ }
91
+ function stateHex(version, state) {
92
+ const fields = {
93
+ tokenReserve: state.tokenReserve,
94
+ lpPubKey: state.lpPubKey,
95
+ validatorPubKey: state.validatorPubKey,
96
+ validatorIdentity: state.validatorIdentity,
97
+ nonce: state.nonce ?? "00".repeat(32),
98
+ affirmation: state.affirmation ?? "",
99
+ };
100
+ return serializeState(artifactOf(version).stateFields, fields);
101
+ }
102
+ /** Whether `method` takes Rúnar's change parameters (`_changePKH`, `_changeAmount`): a method that writes outputs. Close writes none. */
103
+ function takesChange(version, method) {
104
+ const m = artifactOf(version).abi.methods.find((x) => x.name === method);
105
+ return Boolean(m?.params.some((p) => p.name === "_changePKH"));
106
+ }
107
+ /** The affirmation message's tag (0.13.0: message v2). */
108
+ const AFFIRM_TAG = "skein-amm affirm 2";
109
+ /** `n` as 8 bytes, little-endian two's complement (an int64 or a uint64 below 2^63). */
110
+ function le64(n) {
111
+ let v = BigInt.asUintN(64, n);
112
+ const out = [];
113
+ for (let i = 0; i < 8; i++) {
114
+ out.push(Number(v & 0xffn));
115
+ v >>= 8n;
116
+ }
117
+ return out;
118
+ }
119
+ function isHex(s, bytes) {
120
+ return typeof s === "string" && s.length === bytes * 2 && /^[0-9a-f]*$/.test(s);
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // The template
124
+ // ---------------------------------------------------------------------------
125
+ export class PoolTemplate {
126
+ /**
127
+ * Decodes a pool locking script: a 1Color value prefix with a 32-byte id (no payload), then the Pool artifact
128
+ * (template bytes between the constructor slots), then the state. Also requires
129
+ * prefix id == assetId and prefix amount == TokenReserve (the admission check of
130
+ * Pool.runar.go's doc). Returns null for anything else.
131
+ */
132
+ static decode(script) {
133
+ const bytes = Array.isArray(script) ? script : fromHex(scriptHex(script));
134
+ const prefix = decodeOneColor(bytes);
135
+ if (!prefix || prefix.role !== "value")
136
+ return null;
137
+ if (prefix.idBytes?.length !== 32)
138
+ return null;
139
+ const contractScript = toHex(bytes.slice(prefix.length));
140
+ for (const version of [2]) {
141
+ const artifact = artifactOf(version);
142
+ if (!matchesArtifact(artifact, contractScript))
143
+ continue;
144
+ const ctor = extractConstructorArgs(artifact, contractScript);
145
+ let raw;
146
+ try {
147
+ raw = extractStateFromScript(artifact, contractScript);
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ if (!raw)
153
+ return null;
154
+ const { assetId, lpFeeBps, validatorFeeBps, commissionBps } = ctor;
155
+ const { tokenReserve, lpPubKey, validatorPubKey, validatorIdentity, nonce, affirmation } = raw;
156
+ if (!isHex(assetId, 32) || typeof lpFeeBps !== "bigint" || typeof validatorFeeBps !== "bigint" || typeof commissionBps !== "bigint")
157
+ return null;
158
+ if (typeof tokenReserve !== "bigint" || !isHex(lpPubKey, 33) || !isHex(validatorPubKey, 33) || !isHex(validatorIdentity, 33))
159
+ return null;
160
+ if (!isHex(nonce, 32) || typeof affirmation !== "string" || !/^[0-9a-f]*$/.test(affirmation))
161
+ return null;
162
+ if (prefix.payload !== undefined)
163
+ return null; // a pool writes no payload
164
+ if (assetId !== toHex(prefix.idBytes) || tokenReserve !== prefix.amount)
165
+ return null;
166
+ const opReturn = findLastOpReturn(contractScript);
167
+ return {
168
+ version,
169
+ args: { assetId, lpFeeBps, validatorFeeBps, commissionBps },
170
+ state: { tokenReserve, lpPubKey, validatorPubKey, validatorIdentity, nonce: nonce, affirmation: affirmation },
171
+ code: contractScript.slice(0, opReturn),
172
+ contractScript,
173
+ };
174
+ }
175
+ return null;
176
+ }
177
+ /** Whether the pool was rescinded by its validator (layout 2: ValidatorPubKey 33 zero bytes). */
178
+ static rescinded(pool) {
179
+ return pool.state.validatorPubKey === RESCINDED_KEY;
180
+ }
181
+ /**
182
+ * A new pool's locking script (layout 2, 0.13.0): the 1Color prefix for TokenReserve (which
183
+ * the contract writes itself on every continuation, but nothing on chain writes for a deploy)
184
+ * in front of runar-sdk's locking script for the constructor args. `state` may leave the
185
+ * ValidatorPubKey, Nonce and Affirmation to come (33 zero bytes, absent, absent: the pool as
186
+ * `amm.pool.affirm` is asked about it); the deploy carries the validator's three.
187
+ */
188
+ static lockDeploy(args, state) {
189
+ if (!isHex(args.assetId, 32))
190
+ throw new Error("lockDeploy: assetId must be 32 bytes of hex");
191
+ for (const k of ["lpPubKey", "validatorPubKey", "validatorIdentity"]) {
192
+ if (k === "validatorPubKey" && state[k] === RESCINDED_KEY)
193
+ continue; // still to come
194
+ PublicKey.fromString(state[k]); // throws on a malformed key
195
+ }
196
+ if (state.nonce !== undefined && !isHex(state.nonce, 32))
197
+ throw new Error("lockDeploy: the nonce must be 32 bytes of hex");
198
+ const af = (state.affirmation ?? "").toLowerCase();
199
+ if (!/^[0-9a-f]*$/.test(af) || af.length % 2 !== 0 || af.length > 0x4b * 2)
200
+ throw new Error("lockDeploy: the affirmation must be a DER signature, hex");
201
+ const contract = new RunarContract(poolArtifact, ctorArgs(2, args, { ...state, affirmation: af }));
202
+ return LockingScript.fromHex(tokenPrefixHex(args.assetId, state.tokenReserve) + contract.getLockingScript());
203
+ }
204
+ /**
205
+ * The bytes the validator's affirmation signs (0.13.0, message v2, docs/AMM.md "The deploy
206
+ * and the affirmation"; src/pool.zig `affirmMessage`): "skein-amm affirm 2" ‖ AssetId (32,
207
+ * wire order) ‖ LpFeeBps ‖ ValidatorFeeBps ‖ CommissionBps (int64 LE) ‖ TokenReserve ‖ the
208
+ * pool output's satoshis (uint64 LE) ‖ LpPubKey ‖ ValidatorPubKey ‖ ValidatorIdentity (33
209
+ * each) ‖ Nonce (32). 221 bytes.
210
+ */
211
+ static affirmationMessage(pool, satoshis) {
212
+ const { args, state } = pool;
213
+ if (!isHex(state.nonce, 32))
214
+ throw new Error("affirmationMessage: the pool has no nonce");
215
+ return [
216
+ ...Utils.toArray(AFFIRM_TAG, "utf8"),
217
+ ...fromHex(args.assetId),
218
+ ...le64(args.lpFeeBps),
219
+ ...le64(args.validatorFeeBps),
220
+ ...le64(args.commissionBps),
221
+ ...le64(state.tokenReserve),
222
+ ...le64(satoshis),
223
+ ...fromHex(state.lpPubKey),
224
+ ...fromHex(state.validatorPubKey),
225
+ ...fromHex(state.validatorIdentity),
226
+ ...fromHex(state.nonce),
227
+ ];
228
+ }
229
+ /**
230
+ * Whether the pool output (`script`, holding `satoshis`) attests itself (0.13.0, amm-lookup's
231
+ * deploy rule, src/pool.zig `affirmed`): layout 2, not rescinded, its ValidatorPubKey the
232
+ * ValidatorIdentity's child keyed by its Nonce (derived here), and its Affirmation that key's
233
+ * signature of `affirmationMessage`.
234
+ */
235
+ static affirmed(script, satoshis) {
236
+ const pool = PoolTemplate.decode(script);
237
+ if (!pool || pool.version !== 2 || PoolTemplate.rescinded(pool) || !pool.state.nonce || !pool.state.affirmation)
238
+ return false;
239
+ try {
240
+ if (deriveFirstValidatorPubKey(pool.state.validatorIdentity, pool.state.nonce) !== pool.state.validatorPubKey)
241
+ return false;
242
+ const message = PoolTemplate.affirmationMessage(pool, satoshis); // PublicKey.verify hashes it (sha256)
243
+ return PublicKey.fromString(pool.state.validatorPubKey).verify(message, Signature.fromDER(fromHex(pool.state.affirmation)));
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ }
249
+ /** The continuation script: `code` kept byte for byte, prefix and state rewritten. */
250
+ static lockContinuation(pool, state) {
251
+ return LockingScript.fromHex(tokenPrefixHex(pool.args.assetId, state.tokenReserve) + pool.code + "6a" + stateHex(pool.version, state));
252
+ }
253
+ // -------------------------------------------------------------------------
254
+ // Methods
255
+ // -------------------------------------------------------------------------
256
+ /**
257
+ * Swap (method 0). Outputs: 0 pool, 1 the taker's payout, then the LP fee,
258
+ * the validator fee and the commission (each only when nonzero, each in
259
+ * the input asset), then change. Token payouts and token fees are 1-sat
260
+ * 1Color outputs; BSV ones are P2PKH. The validator's slot is left empty.
261
+ */
262
+ static async swap(p) {
263
+ const plan = PoolTemplate.planSwap(p);
264
+ // Tokens in: the token inputs must carry exactly amountIn (no token change
265
+ // output exists). BSV in: no token of this asset may be spent (it would burn).
266
+ checkTokenInputs(p.inputs, plan.pool.args.assetId, p.bsvIn ? 0n : p.amountIn);
267
+ const call = await buildCall({
268
+ common: p,
269
+ pool: plan.pool,
270
+ method: "swap",
271
+ outputs: plan.outputs,
272
+ args: plan.args,
273
+ sigSlots: plan.sigSlots,
274
+ });
275
+ call.next = plan.next;
276
+ call.next.script = call.tx.outputs[0].lockingScript;
277
+ call.swap = plan.swap;
278
+ return call;
279
+ }
280
+ /**
281
+ * The Swap call without its funding: the outputs the contract requires (in
282
+ * order: pool, payout, LP fee, validator fee, commission; no change) and
283
+ * the method arguments, with the validator's slot empty. For a builder that
284
+ * does not pick the funding itself (a BRC-100 wallet's `createAction` picks
285
+ * inputs and change): hand these outputs over, then fill input 0 with
286
+ * `callUnlock` over the transaction the wallet produced. Same checks and
287
+ * refusals as `swap` except the funding ones.
288
+ */
289
+ static planSwap(p) {
290
+ const pool = decodeUtxo(p.pool);
291
+ const { args, state } = pool;
292
+ const bsvReserve = BigInt(p.pool.satoshis);
293
+ const e = p.expect ?? {};
294
+ if (e.bsvReserve !== undefined && e.bsvReserve !== bsvReserve) {
295
+ throw new PoolBuildError("stale_pool", `BSV reserve is ${bsvReserve}, priced against ${e.bsvReserve}`);
296
+ }
297
+ if (e.tokenReserve !== undefined && e.tokenReserve !== state.tokenReserve) {
298
+ throw new PoolBuildError("stale_pool", `token reserve is ${state.tokenReserve}, priced against ${e.tokenReserve}`);
299
+ }
300
+ if (e.lpFeeBps !== undefined && e.lpFeeBps !== args.lpFeeBps) {
301
+ throw new PoolBuildError("fee_mismatch", `pool LP fee is ${args.lpFeeBps} bps, priced at ${e.lpFeeBps}`);
302
+ }
303
+ if (e.validatorFeeBps !== undefined && e.validatorFeeBps !== args.validatorFeeBps) {
304
+ throw new PoolBuildError("fee_mismatch", `pool validator fee is ${args.validatorFeeBps} bps, priced at ${e.validatorFeeBps}`);
305
+ }
306
+ if (e.commissionBps !== undefined && e.commissionBps !== args.commissionBps) {
307
+ throw new PoolBuildError("fee_mismatch", `pool commission is ${args.commissionBps} bps, priced at ${e.commissionBps}`);
308
+ }
309
+ if (!isHex(p.commissionPkh, 20))
310
+ throw new PoolBuildError("invalid", "commissionPkh must be 20 bytes of hex");
311
+ if (!isHex(p.userPkh, 20))
312
+ throw new PoolBuildError("invalid", "userPkh must be 20 bytes of hex");
313
+ // Pool.runar.go Swap, line for line.
314
+ const amountIn = p.amountIn;
315
+ if (amountIn <= 0n)
316
+ throw new PoolBuildError("invalid", "amountIn must be > 0");
317
+ const lpFee = (amountIn * args.lpFeeBps + 9999n) / 10000n;
318
+ const validatorFee = (amountIn * args.validatorFeeBps + 9999n) / 10000n;
319
+ const commission = (amountIn * args.commissionBps + 9999n) / 10000n;
320
+ const net = amountIn - lpFee - validatorFee - commission;
321
+ if (net <= 0n)
322
+ throw new PoolBuildError("invalid", "amountIn does not cover the fees");
323
+ let out = (net * bsvReserve) / (state.tokenReserve + net);
324
+ let newBsv = bsvReserve - out;
325
+ let newTokens = state.tokenReserve + net;
326
+ if (p.bsvIn) {
327
+ out = (net * state.tokenReserve) / (bsvReserve + net);
328
+ newBsv = bsvReserve + net;
329
+ newTokens = state.tokenReserve - out;
330
+ }
331
+ if (out <= 0n)
332
+ throw new PoolBuildError("invalid", "amountIn too small: nothing out");
333
+ if (newBsv <= 0n || newTokens <= 0n)
334
+ throw new PoolBuildError("invalid", "the swap would empty the pool");
335
+ if (e.lpFee !== undefined && e.lpFee !== lpFee) {
336
+ throw new PoolBuildError("fee_mismatch", `LP fee is ${lpFee}, priced at ${e.lpFee}`);
337
+ }
338
+ if (e.validatorFee !== undefined && e.validatorFee !== validatorFee) {
339
+ throw new PoolBuildError("fee_mismatch", `validator fee is ${validatorFee}, priced at ${e.validatorFee}`);
340
+ }
341
+ if (e.commission !== undefined && e.commission !== commission) {
342
+ throw new PoolBuildError("fee_mismatch", `commission is ${commission}, priced at ${e.commission}`);
343
+ }
344
+ if (e.amountOut !== undefined && e.amountOut !== out) {
345
+ throw new PoolBuildError("quote_mismatch", `amount out is ${out}, quoted ${e.amountOut}`);
346
+ }
347
+ if (e.minAmountOut !== undefined && out < e.minAmountOut) {
348
+ throw new PoolBuildError("slippage", `amount out ${out} is below the minimum ${e.minAmountOut}`);
349
+ }
350
+ const nextValidatorPubKey = p.nextValidatorPubKey ?? deriveValidatorPubKey(state.validatorIdentity, { txid: p.pool.txid, vout: p.pool.vout });
351
+ PublicKey.fromString(nextValidatorPubKey);
352
+ const nextState = { ...state, tokenReserve: newTokens, validatorPubKey: nextValidatorPubKey };
353
+ const lpPkh = pkhOf(state.lpPubKey);
354
+ const validatorPkh = pkhOf(state.validatorPubKey);
355
+ const payout = (amount, pkh, isBsv) => isBsv ? { satoshis: amount, script: p2pkhHex(pkh) } : { satoshis: 1n, script: tokenP2pkhHex(args.assetId, amount, pkh) };
356
+ const continuation = PoolTemplate.lockContinuation(pool, nextState);
357
+ const outputs = [
358
+ { satoshis: newBsv, script: continuation.toHex() },
359
+ payout(out, p.userPkh, !p.bsvIn),
360
+ ];
361
+ if (lpFee > 0n)
362
+ outputs.push(payout(lpFee, lpPkh, p.bsvIn));
363
+ if (validatorFee > 0n)
364
+ outputs.push(payout(validatorFee, validatorPkh, p.bsvIn));
365
+ if (commission > 0n)
366
+ outputs.push(payout(commission, p.commissionPkh, p.bsvIn));
367
+ return {
368
+ pool,
369
+ poolUtxo: p.pool,
370
+ method: "swap",
371
+ outputs,
372
+ args: [EMPTY_SIG, nextValidatorPubKey, amountIn, p.bsvIn, p.userPkh, p.commissionPkh],
373
+ sigSlots: { validator: 0 },
374
+ next: { pool: { ...pool, state: nextState, contractScript: pool.code + "6a" + stateHex(pool.version, nextState) }, satoshis: Number(newBsv), script: continuation },
375
+ swap: { amountOut: out, lpFee, validatorFee, commission },
376
+ };
377
+ }
378
+ /**
379
+ * The pool input's unlocking script for `plan` over a finished transaction
380
+ * `tx` whose input 0 spends `plan.poolUtxo` and whose outputs are
381
+ * `plan.outputs`, then (optionally) one P2PKH change output: `change` names
382
+ * it (`{pkh, satoshis}`), or `null` when there is none. Signature slots stay
383
+ * empty (`OP_0`), to be filled with `signPoolInput`. Throws when `tx` does
384
+ * not have that shape (the contract would refuse it).
385
+ */
386
+ static callUnlock(plan, tx, change) {
387
+ const in0 = tx.inputs[0];
388
+ if (!in0 || in0.sourceTXID !== plan.poolUtxo.txid || in0.sourceOutputIndex !== plan.poolUtxo.vout) {
389
+ throw new PoolBuildError("invalid", "input 0 does not spend the pool");
390
+ }
391
+ const want = plan.outputs.length + (change ? 1 : 0);
392
+ if (tx.outputs.length !== want)
393
+ throw new PoolBuildError("invalid", `the transaction has ${tx.outputs.length} outputs, the call needs ${want}`);
394
+ plan.outputs.forEach((o, i) => {
395
+ const got = tx.outputs[i];
396
+ if (BigInt(got.satoshis ?? -1) !== o.satoshis || got.lockingScript.toHex() !== o.script) {
397
+ throw new PoolBuildError("invalid", `output ${i} is not the one the contract requires`);
398
+ }
399
+ });
400
+ if (change) {
401
+ if (!isHex(change.pkh, 20))
402
+ throw new PoolBuildError("invalid", "change pkh must be 20 bytes of hex");
403
+ const got = tx.outputs[plan.outputs.length];
404
+ if (got.satoshis !== change.satoshis || got.lockingScript.toHex() !== p2pkhHex(change.pkh)) {
405
+ throw new PoolBuildError("invalid", "the last output is not the named P2PKH change");
406
+ }
407
+ }
408
+ const methodIndex = METHOD_INDEX[plan.method];
409
+ const preimage = contractPreimage(tx, plan.pool, plan.poolUtxo.satoshis, methodIndex);
410
+ const hex = callUnlockHex(plan.pool, plan.method, plan.args, change?.pkh ?? "00".repeat(20), BigInt(change?.satoshis ?? 0), preimage, methodIndex);
411
+ return { unlockingScript: UnlockingScript.fromHex(hex), preimage, sighash: toHex(Hash.hash256(fromHex(preimage))) };
412
+ }
413
+ /**
414
+ * An upper bound on the length of `plan`'s pool unlocking script once every
415
+ * signature slot is filled: what a wallet's `createAction` is told as the
416
+ * pool input's `unlockingScriptLength` before the transaction exists (the
417
+ * fee is sized from it). The preimage's length depends only on the code, so
418
+ * it is exact; the change amount is bounded by 2^53.
419
+ */
420
+ static maxCallUnlockLength(plan) {
421
+ const tx = new Transaction();
422
+ tx.addInput({ sourceTXID: plan.poolUtxo.txid, sourceOutputIndex: plan.poolUtxo.vout, unlockingScript: new UnlockingScript(), sequence: 0xffffffff });
423
+ for (const o of plan.outputs)
424
+ tx.addOutput({ satoshis: Number(o.satoshis), lockingScript: LockingScript.fromHex(o.script) });
425
+ const methodIndex = METHOD_INDEX[plan.method];
426
+ const preimage = contractPreimage(tx, plan.pool, plan.poolUtxo.satoshis, methodIndex);
427
+ const hex = callUnlockHex(plan.pool, plan.method, plan.args, "00".repeat(20), BigInt(Number.MAX_SAFE_INTEGER), preimage, methodIndex);
428
+ return hex.length / 2 + Object.keys(plan.sigSlots).length * (SIG_PUSH_LEN - 1);
429
+ }
430
+ /**
431
+ * Close (method 1; skein-amm 0.9.0, David Case 2026-10-09: a position is deployed or closed,
432
+ * never resized in place), the LP's, built here: the outputs are the LP's (`outputs`), then
433
+ * change. No continuation.
434
+ */
435
+ static async close(p) {
436
+ const plan = PoolTemplate.planClose(p);
437
+ checkTokenInputs(p.inputs, plan.pool.args.assetId, 0n);
438
+ const call = await buildCall({
439
+ common: p,
440
+ pool: plan.pool,
441
+ method: "close",
442
+ outputs: plan.outputs,
443
+ args: plan.args,
444
+ sigSlots: plan.sigSlots,
445
+ });
446
+ call.next = null;
447
+ if (p.lpKey)
448
+ await PoolTemplate.signPoolInput(call, "lp", p.lpKey);
449
+ return call;
450
+ }
451
+ /**
452
+ * The Close call without its funding (as `planSwap` for Swap), the LP's slot empty
453
+ * (`Close(lpSig)`, outputs free): `outputs` are the LP's choice, passed through (at least one).
454
+ */
455
+ static planClose(p) {
456
+ const pool = decodeUtxo(p.pool);
457
+ const next = { pool, satoshis: 0, script: new LockingScript() };
458
+ if (p.outputs.length === 0)
459
+ throw new PoolBuildError("invalid", "a Close needs the LP's outputs");
460
+ return { pool, poolUtxo: p.pool, method: "close", outputs: p.outputs, args: [EMPTY_SIG], sigSlots: { lp: 0 }, next };
461
+ }
462
+ // -------------------------------------------------------------------------
463
+ // The pool input's signatures
464
+ // -------------------------------------------------------------------------
465
+ /**
466
+ * The BIP-143 preimage of `tx`'s pool input (input 0), computed from the
467
+ * transaction itself. Needs the pool output being spent: `source`, or input
468
+ * 0's `sourceTransaction`.
469
+ */
470
+ static preimage(tx, source) {
471
+ const in0 = tx.inputs[0];
472
+ if (!in0)
473
+ throw new Error("preimage: no input 0");
474
+ let satoshis;
475
+ let lock;
476
+ if (source) {
477
+ satoshis = source.satoshis;
478
+ lock = source.script;
479
+ }
480
+ else {
481
+ const out = in0.sourceTransaction?.outputs[in0.sourceOutputIndex];
482
+ satoshis = out?.satoshis;
483
+ lock = out?.lockingScript;
484
+ }
485
+ if (satoshis === undefined || lock === undefined)
486
+ throw new Error("preimage: the spent pool output is unknown");
487
+ const pool = PoolTemplate.decode(lock);
488
+ if (!pool)
489
+ throw new Error("preimage: input 0 does not spend a pool");
490
+ const call = parseCall(tx.inputs[0].unlockingScript?.toHex() ?? "");
491
+ const methodIndex = call ? Number(call.methodIndex) : 0;
492
+ return contractPreimage(tx, pool, satoshis, methodIndex);
493
+ }
494
+ /**
495
+ * Fills signature slot `slot` of the pool input with `signer`'s signature
496
+ * over the preimage recomputed from `call.tx` (which must agree with the
497
+ * preimage pushed in the call). Every other push is kept byte for byte;
498
+ * no other input's signature is affected.
499
+ */
500
+ static async signPoolInput(call, slot, signer) {
501
+ const tx = call.tx;
502
+ const unlock = tx.inputs[0].unlockingScript?.toHex() ?? "";
503
+ const parsed = parseCall(unlock);
504
+ if (!parsed)
505
+ throw new Error("signPoolInput: input 0 is not a pool call");
506
+ const methodIndex = METHOD_INDEX[call.method];
507
+ if (parsed.methodIndex !== BigInt(methodIndex))
508
+ throw new Error("signPoolInput: method index differs");
509
+ const satoshis = call.sourceSatoshis ?? sourceSatoshisOf(tx);
510
+ const preimage = contractPreimage(tx, call.pool, satoshis, methodIndex);
511
+ if (preimage !== parsed.preimage)
512
+ throw new Error("signPoolInput: the pushed preimage does not match the transaction");
513
+ const argIndex = SIG_SLOTS[call.method][slot];
514
+ if (argIndex === undefined)
515
+ throw new Error(`signPoolInput: ${call.method} has no ${slot} signature`);
516
+ const expected = slot === "validator" ? call.pool.state.validatorPubKey : call.pool.state.lpPubKey;
517
+ if (signer instanceof PrivateKey && signer.toPublicKey().toString() !== expected) {
518
+ throw new Error(`signPoolInput: that key is not the pool's ${slot} key (${expected})`);
519
+ }
520
+ const sighash = Hash.hash256(fromHex(preimage));
521
+ const der = signer instanceof PrivateKey ? signer.sign(Hash.sha256(fromHex(preimage))).toDER() : await signer(sighash);
522
+ const sigHex = toHex(der) + "41";
523
+ tx.inputs[0].unlockingScript = UnlockingScript.fromHex(replacePush(unlock, parsed.pushes, 1 + argIndex, encodePushData(sigHex)));
524
+ call.unsigned = call.unsigned.filter((s) => s !== slot);
525
+ }
526
+ }
527
+ /** Arg index of each signature, per method (Pool.runar.go's parameter order). */
528
+ const SIG_SLOTS = {
529
+ swap: { validator: 0 },
530
+ close: { lp: 0 },
531
+ rescind: { validator: 0 },
532
+ };
533
+ // ---------------------------------------------------------------------------
534
+ // Internals
535
+ // ---------------------------------------------------------------------------
536
+ function decodeUtxo(utxo) {
537
+ const pool = PoolTemplate.decode(utxo.script);
538
+ if (!pool)
539
+ throw new PoolBuildError("not_a_pool", `${utxo.txid}:${utxo.vout} is not a pool output`);
540
+ if (!Number.isSafeInteger(utxo.satoshis) || utxo.satoshis <= 0)
541
+ throw new PoolBuildError("invalid", "pool satoshis");
542
+ return pool;
543
+ }
544
+ function checkTokenInputs(inputs, assetId, expected) {
545
+ let sum = 0n;
546
+ for (const i of inputs) {
547
+ const t = decodeOneColor(fromHex(scriptHex(i.lockingScript)));
548
+ if (t && t.idBytes && toHex(t.idBytes) === assetId)
549
+ sum += t.amount;
550
+ }
551
+ if (sum !== expected) {
552
+ throw new PoolBuildError("token_mismatch", `token inputs carry ${sum} of the pool's token, the call needs exactly ${expected}`);
553
+ }
554
+ }
555
+ function sourceSatoshisOf(tx) {
556
+ const in0 = tx.inputs[0];
557
+ const sats = in0.sourceTransaction?.outputs[in0.sourceOutputIndex]?.satoshis;
558
+ if (sats === undefined)
559
+ throw new Error("the pool input's source satoshis are unknown");
560
+ return sats;
561
+ }
562
+ function contractPreimage(tx, pool, satoshis, methodIndex) {
563
+ if (!tx.inputs[0].sourceTXID) {
564
+ throw new Error("the pool input needs its sourceTXID");
565
+ }
566
+ const contract = RunarContract.fromUtxo(artifactOf(pool.version), {
567
+ txid: tx.inputs[0].sourceTXID,
568
+ outputIndex: tx.inputs[0].sourceOutputIndex,
569
+ satoshis,
570
+ script: pool.contractScript,
571
+ });
572
+ return contract.computeOpPushTxWithCodeSep(tx, 0, pool.contractScript, satoshis, methodIndex).preimageHex;
573
+ }
574
+ /** Splits a pushes-only unlocking script; null when it is not one. */
575
+ function parseCall(hex) {
576
+ const pushes = [];
577
+ let pos = 0;
578
+ while (pos < hex.length) {
579
+ const op = parseInt(hex.slice(pos, pos + 2), 16);
580
+ let len;
581
+ let at = pos + 2;
582
+ if (op === 0)
583
+ len = 0;
584
+ else if (op <= 0x4b)
585
+ len = op;
586
+ else if (op === 0x4c) {
587
+ len = parseInt(hex.slice(at, at + 2), 16);
588
+ at += 2;
589
+ }
590
+ else if (op === 0x4d) {
591
+ len = parseInt(hex.slice(at + 2, at + 4) + hex.slice(at, at + 2), 16);
592
+ at += 4;
593
+ }
594
+ else if (op === 0x4e) {
595
+ len = parseInt(hex.slice(at + 6, at + 8) + hex.slice(at + 4, at + 6) + hex.slice(at + 2, at + 4) + hex.slice(at, at + 2), 16);
596
+ at += 8;
597
+ }
598
+ else if (op === 0x4f || (op >= 0x51 && op <= 0x60))
599
+ len = 0;
600
+ else
601
+ return null;
602
+ const end = at + len * 2;
603
+ if (end > hex.length)
604
+ return null;
605
+ pushes.push({ start: pos, end, data: hex.slice(at, end), op });
606
+ pos = end;
607
+ }
608
+ if (pushes.length < 3)
609
+ return null;
610
+ const sel = pushes[pushes.length - 1];
611
+ const methodIndex = sel.op === 0 ? 0n : sel.op >= 0x51 && sel.op <= 0x60 ? BigInt(sel.op - 0x50) : -1n;
612
+ return { pushes, preimage: pushes[pushes.length - 2].data, methodIndex };
613
+ }
614
+ function replacePush(hex, pushes, i, push) {
615
+ const p = pushes[i];
616
+ return hex.slice(0, p.start) + push + hex.slice(p.end);
617
+ }
618
+ /** `_codePart args… [_changePKH _changeAmount] txPreimage methodIndex`, as pushes (layout 2's Close takes no change). */
619
+ function callUnlockHex(pool, method, args, changePkh, change, preimage, methodIndex) {
620
+ let hex = encodePushData(pool.code);
621
+ for (const a of args)
622
+ hex += encodeArg(a);
623
+ if (takesChange(pool.version, method))
624
+ hex += encodePushData(changePkh) + encodeArg(change);
625
+ return hex + encodePushData(preimage) + encodeScriptNumber(BigInt(methodIndex));
626
+ }
627
+ const P2PKH_UNLOCK_LEN = 107;
628
+ const SIG_PUSH_LEN = 74; // length byte + a DER signature (<= 72) + the sighash byte
629
+ async function buildCall(o) {
630
+ const { common, pool, method } = o;
631
+ if (!isHex(common.changePkh, 20))
632
+ throw new PoolBuildError("invalid", "changePkh must be 20 bytes of hex");
633
+ const methodIndex = METHOD_INDEX[method];
634
+ const totalIn = BigInt(common.pool.satoshis) + common.inputs.reduce((s, i) => s + BigInt(i.satoshis), 0n);
635
+ const totalOut = o.outputs.reduce((s, x) => s + x.satoshis, 0n);
636
+ const assemble = (change) => {
637
+ const tx = new Transaction();
638
+ tx.version = 1;
639
+ tx.lockTime = 0;
640
+ tx.addInput({
641
+ sourceTXID: common.pool.txid,
642
+ sourceOutputIndex: common.pool.vout,
643
+ sourceTransaction: common.pool.sourceTransaction,
644
+ unlockingScript: new UnlockingScript(),
645
+ sequence: 0xffffffff,
646
+ });
647
+ for (const i of common.inputs) {
648
+ tx.addInput({
649
+ sourceTXID: i.txid,
650
+ sourceOutputIndex: i.vout,
651
+ sourceTransaction: i.sourceTransaction,
652
+ unlockingScript: new UnlockingScript(),
653
+ sequence: 0xffffffff,
654
+ });
655
+ }
656
+ for (const x of o.outputs)
657
+ tx.addOutput({ satoshis: Number(x.satoshis), lockingScript: LockingScript.fromHex(x.script) });
658
+ if (change > 0n)
659
+ tx.addOutput({ satoshis: Number(change), lockingScript: LockingScript.fromHex(p2pkhHex(common.changePkh)) });
660
+ return tx;
661
+ };
662
+ const unlockFor = (tx, change) => {
663
+ const preimage = contractPreimage(tx, pool, common.pool.satoshis, methodIndex);
664
+ return { hex: callUnlockHex(pool, method, o.args, common.changePkh, change, preimage, methodIndex), preimage };
665
+ };
666
+ // The fee: fixed, or iterated to a fixed point over the finished size (the
667
+ // change push and output depend on the fee).
668
+ let fee;
669
+ if ("sats" in common.fee) {
670
+ fee = BigInt(common.fee.sats);
671
+ }
672
+ else {
673
+ const rate = common.fee.satsPerKb;
674
+ const otherUnlocks = await Promise.all(common.inputs.map(async (i) => (i.unlock ? await i.unlock.estimateLength() : (i.estimatedUnlockLength ?? P2PKH_UNLOCK_LEN))));
675
+ const emptySigs = Object.keys(o.sigSlots).length;
676
+ fee = 0n;
677
+ for (let round = 0; round < 4; round++) {
678
+ const change = totalIn - totalOut - fee;
679
+ const tx = assemble(change > 0n ? change : 0n);
680
+ const { hex } = unlockFor(tx, change > 0n ? change : 0n);
681
+ tx.inputs[0].unlockingScript = UnlockingScript.fromHex(hex);
682
+ const size = tx.toBinary().length + emptySigs * (SIG_PUSH_LEN - 1) + otherUnlocks.reduce((s, n) => s + n + (n < 0xfd ? 0 : 2), 0);
683
+ const next = BigInt(Math.ceil((size * rate) / 1000));
684
+ if (next === fee)
685
+ break;
686
+ fee = next;
687
+ }
688
+ }
689
+ const change = totalIn - totalOut - fee;
690
+ if (change < 0n) {
691
+ throw new PoolBuildError("insufficient_funds", `inputs ${totalIn} sats, outputs ${totalOut} + fee ${fee}`);
692
+ }
693
+ const tx = assemble(change);
694
+ const { hex, preimage } = unlockFor(tx, change);
695
+ tx.inputs[0].unlockingScript = UnlockingScript.fromHex(hex);
696
+ for (const [n, i] of common.inputs.entries()) {
697
+ if (i.unlock)
698
+ tx.inputs[n + 1].unlockingScript = await i.unlock.sign(tx, n + 1);
699
+ }
700
+ return {
701
+ tx,
702
+ method,
703
+ pool,
704
+ next: null,
705
+ preimage,
706
+ sighash: toHex(Hash.hash256(fromHex(preimage))),
707
+ unsigned: Object.keys(o.sigSlots),
708
+ change: Number(change),
709
+ fee: Number(fee),
710
+ };
711
+ }
712
+ /** A P2PKH unlocker for a builder input (also a 1Color token held on P2PKH). */
713
+ export function p2pkhUnlock(key, satoshis, lockingScript) {
714
+ return new P2PKH().unlock(key, "all", false, satoshis, typeof lockingScript === "string" ? LockingScript.fromHex(lockingScript) : lockingScript);
715
+ }