@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.
- package/README.md +107 -2
- package/dist/client.d.ts +534 -0
- package/dist/client.js +1319 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +53 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.js +64 -0
- package/dist/lib/keys.d.ts +42 -0
- package/dist/lib/keys.js +48 -0
- package/dist/lib/overlay.d.ts +190 -0
- package/dist/lib/overlay.js +302 -0
- package/dist/lib/skein.d.ts +82 -0
- package/dist/lib/skein.js +142 -0
- package/dist/lib/tokenId.d.ts +72 -0
- package/dist/lib/tokenId.js +121 -0
- package/dist/lp/amounts.d.ts +12 -0
- package/dist/lp/amounts.js +38 -0
- package/dist/lp/close.d.ts +90 -0
- package/dist/lp/close.js +140 -0
- package/dist/lp/images.d.ts +11 -0
- package/dist/lp/images.js +45 -0
- package/dist/lp/inventory.d.ts +68 -0
- package/dist/lp/inventory.js +84 -0
- package/dist/lp/poolDeploy.d.ts +220 -0
- package/dist/lp/poolDeploy.js +291 -0
- package/dist/lp/poolRows.d.ts +13 -0
- package/dist/lp/poolRows.js +19 -0
- package/dist/lp/positions.d.ts +81 -0
- package/dist/lp/positions.js +144 -0
- package/dist/lp/rebalance.d.ts +101 -0
- package/dist/lp/rebalance.js +168 -0
- package/dist/lp/wallet.d.ts +12 -0
- package/dist/lp/wallet.js +19 -0
- package/dist/market/activeSwap.d.ts +158 -0
- package/dist/market/activeSwap.js +320 -0
- package/dist/market/plan.d.ts +85 -0
- package/dist/market/plan.js +149 -0
- package/dist/market/prices.d.ts +13 -0
- package/dist/market/prices.js +14 -0
- package/dist/market/relay.d.ts +84 -0
- package/dist/market/relay.js +201 -0
- package/dist/market/swapAction.d.ts +306 -0
- package/dist/market/swapAction.js +508 -0
- package/dist/market/swapFlow.d.ts +58 -0
- package/dist/market/swapFlow.js +86 -0
- package/dist/market/view.d.ts +58 -0
- package/dist/market/view.js +63 -0
- package/dist/pool/artifact.d.ts +23 -0
- package/dist/pool/artifact.js +38 -0
- package/dist/pool/index.d.ts +8 -0
- package/dist/pool/index.js +8 -0
- package/dist/pool/oneColor.d.ts +19 -0
- package/dist/pool/oneColor.js +39 -0
- package/dist/pool/pool.artifact.json +5069 -0
- package/dist/pool/runar-ir-schema.d.ts +21 -0
- package/dist/pool/runar-ir-schema.js +18 -0
- package/dist/pool/template.d.ts +295 -0
- package/dist/pool/template.js +715 -0
- package/dist/vendor/runar/LICENSE +21 -0
- package/dist/vendor/runar/NOTICE +15 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
- package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
- package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
- package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
- package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
- package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
- package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
- package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
- package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
- package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
- package/dist/vendor/runar/runar-sdk.d.ts +9 -0
- package/dist/vendor/runar/runar-sdk.js +8 -0
- package/dist/wallet/action.d.ts +28 -0
- package/dist/wallet/action.js +42 -0
- package/dist/wallet/brc29.d.ts +83 -0
- package/dist/wallet/brc29.js +80 -0
- package/dist/wallet/color.d.ts +110 -0
- package/dist/wallet/color.js +134 -0
- package/dist/wallet/swapPayouts.d.ts +35 -0
- package/dist/wallet/swapPayouts.js +146 -0
- 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
|
+
}
|