@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.
- 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,2516 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// runar-sdk/contract.ts — Main contract runtime wrapper
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { InputLimits } from '../../../../pool/runar-ir-schema.js';
|
|
5
|
+
import { assertScriptHexUnderLimit, WitnessValueMissingError } from './errors.js';
|
|
6
|
+
import { txToTransactionData, warnNonFatal } from './providers/provider.js';
|
|
7
|
+
import { buildDeployTransaction, selectUtxos } from './deployment.js';
|
|
8
|
+
import { buildCallTransaction, resolveInputSequence } from './calling.js';
|
|
9
|
+
import { serializeState, extractStateFromScript, findLastOpReturn } from './state.js';
|
|
10
|
+
import { computeOpPushTx } from './oppushtx.js';
|
|
11
|
+
import { buildP2PKHScript, extractConstructorArgs } from './script-utils.js';
|
|
12
|
+
import { computeNewStateAndDataOutputs } from './anf-interpreter.js';
|
|
13
|
+
import { buildInscriptionEnvelope, parseInscriptionEnvelope } from './ordinals/envelope.js';
|
|
14
|
+
import { Utils, Hash, Transaction as BsvTransaction, LockingScript, UnlockingScript, Spend } from '@bsv/sdk';
|
|
15
|
+
import { WalletProvider } from './providers/wallet-provider.js';
|
|
16
|
+
import { detachUnlockingScript } from './spend-safety.js';
|
|
17
|
+
import { assertUnsoundPrimitivesAcknowledged } from './unsound-primitives.js';
|
|
18
|
+
/**
|
|
19
|
+
* Producer-side marker (issue #106) for the deliberately-empty branch of an
|
|
20
|
+
* OR-CHECKSIG method — `checkSig(sigA, pkA) || checkSig(sigB, pkB)`, where
|
|
21
|
+
* `||` short-circuits (NEW-014), so the second `OP_CHECKSIG` runs only when the
|
|
22
|
+
* first branch FAILS — and when it does, the first ran with a non-empty
|
|
23
|
+
* signature. Only the
|
|
24
|
+
* matching branch supplies a real signature; the failing branch MUST push an
|
|
25
|
+
* empty signature (OP_0) or BIP146 NULLFAIL rejects the whole spend.
|
|
26
|
+
*
|
|
27
|
+
* Pass `EMPTY_SIG` as the call arg for the non-matching `Sig` slot: the SDK
|
|
28
|
+
* pushes OP_0 for it and never signs it, distinct from `null` (auto-sign) and
|
|
29
|
+
* an explicit hex-bytes value. Coexists with `null` at the same call —
|
|
30
|
+
* `call('execute', [null, EMPTY_SIG])` signs only slot 0.
|
|
31
|
+
*
|
|
32
|
+
* Uses the global symbol registry so identity holds across duplicate module
|
|
33
|
+
* instances (bundlers).
|
|
34
|
+
*/
|
|
35
|
+
export const EMPTY_SIG = Symbol.for('runar.sdk.emptySig');
|
|
36
|
+
/**
|
|
37
|
+
* Deep-review finding C29: `PreparedCall` hands the caller a live, mutable
|
|
38
|
+
* `Transaction` (`prepared.tx`) with no consume guard. `finalizeCall`
|
|
39
|
+
* mutates `prepared.tx.inputs[0].unlockingScript` in place and broadcasts —
|
|
40
|
+
* nothing stops the SAME `PreparedCall` from being finalized a second time
|
|
41
|
+
* against a tx that has already been consumed (and, on a real network,
|
|
42
|
+
* already spent), producing signatures that no longer correspond to a
|
|
43
|
+
* fresh, still-live UTXO.
|
|
44
|
+
*
|
|
45
|
+
* Tracked by object identity (not a field on `PreparedCall` itself) so the
|
|
46
|
+
* public shape stays untouched — a `WeakSet` also lets consumed entries be
|
|
47
|
+
* garbage-collected once the caller drops its reference. Full
|
|
48
|
+
* freezing/defensive-copying of `prepared.tx` was considered but rejected:
|
|
49
|
+
* `finalizeCall` itself must mutate `prepared.tx` in place to insert the
|
|
50
|
+
* final unlocking script, so freezing the object would break the one
|
|
51
|
+
* caller that legitimately needs to write to it.
|
|
52
|
+
*/
|
|
53
|
+
const consumedPreparedCalls = new WeakSet();
|
|
54
|
+
/** Type guard: is this call arg the {@link EMPTY_SIG} marker (issue #106)? */
|
|
55
|
+
export function isEmptySig(value) {
|
|
56
|
+
return value === EMPTY_SIG;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* True when the locking script looks like OR-CHECKSIG (OP_BOOLOR + OP_CHECKSIG)
|
|
60
|
+
* rather than multi-sig (OP_CHECKMULTISIG). Used to scope the multi-null-Sig
|
|
61
|
+
* soft warning so genuine checkMultiSig unlocks are not spammed.
|
|
62
|
+
*
|
|
63
|
+
* Exported for unit tests.
|
|
64
|
+
*/
|
|
65
|
+
export function isLikelyOrCheckSigMethod(artifact) {
|
|
66
|
+
const asm = (artifact.asm ?? '').toUpperCase();
|
|
67
|
+
if (asm.includes('OP_CHECKMULTISIG')) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (asm.includes('OP_BOOLOR') && asm.includes('OP_CHECKSIG')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// NEW-014: `||` no longer lowers to OP_BOOLOR — it lowers to real
|
|
74
|
+
// OP_IF / OP_ELSE / OP_ENDIF control flow. The NULLFAIL hazard SURVIVES that
|
|
75
|
+
// change: when the FIRST branch fails, its OP_CHECKSIG has already run with a
|
|
76
|
+
// non-empty signature, which is exactly what BIP146 rejects. Short-circuiting
|
|
77
|
+
// only removes the hazard when the first branch succeeds, so this warning
|
|
78
|
+
// must still fire for the branch-shaped form.
|
|
79
|
+
if (asm.includes('OP_IF') && asm.includes('OP_CHECKSIG')) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Fall back to hex when ASM is missing: OP_CHECKMULTISIG=0xae, OP_BOOLOR=0x9a
|
|
83
|
+
const hex = (artifact.script ?? artifact.scriptHex ?? '').toLowerCase();
|
|
84
|
+
if (hex.includes('ae') || hex.includes('af')) {
|
|
85
|
+
// May false-positive inside push data; prefer ASM. If ASM empty and we see
|
|
86
|
+
// ae, treat as multi-sig (safe: suppresses warning for MultiSig contracts).
|
|
87
|
+
if (!asm)
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Invalidate the @bsv/sdk Transaction's serialization caches after
|
|
94
|
+
* directly modifying inputs/outputs. The SDK caches toHex()/toBinary()
|
|
95
|
+
* results and only invalidates them through addInput/addOutput.
|
|
96
|
+
*/
|
|
97
|
+
function invalidateTxCache(tx) {
|
|
98
|
+
const t = tx;
|
|
99
|
+
t.hexCache = undefined;
|
|
100
|
+
t.rawBytesCache = undefined;
|
|
101
|
+
t.cachedHash = undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The one ByteString parameter name whose `null` argument is auto-resolved to
|
|
105
|
+
* the serialized outpoints of every input (deep-review finding G6). Must match
|
|
106
|
+
* Go's `AutoPrevoutsParamName` and the Rust/Python/Ruby/Zig/Java equivalents —
|
|
107
|
+
* this name is a cross-tier calling convention, not a per-tier ergonomic.
|
|
108
|
+
*/
|
|
109
|
+
const AUTO_PREVOUTS_PARAM_NAME = 'allPrevouts';
|
|
110
|
+
/**
|
|
111
|
+
* Decode the value of every EQUALITY `verify_code_part_len` pin in a compiled
|
|
112
|
+
* script.
|
|
113
|
+
*
|
|
114
|
+
* The compiler emits the pin as a fixed-width, unambiguous nine-byte run:
|
|
115
|
+
*
|
|
116
|
+
* 76 | 04 LL LL LL LL | 81 | (9c | a2) | 69
|
|
117
|
+
* OP_DUP <len LE32> OP_BIN2NUM cmp OP_VERIFY
|
|
118
|
+
*
|
|
119
|
+
* `9c` is OP_NUMEQUAL — an exact pin, the only variant a longer code part can
|
|
120
|
+
* violate. `a2` is OP_GREATERTHANOREQUAL, a lower bound that extra bytes
|
|
121
|
+
* satisfy, so it is deliberately not returned here.
|
|
122
|
+
*
|
|
123
|
+
* Read from the emitted TEMPLATE rather than from a built code script: the
|
|
124
|
+
* template holds OP_0 placeholders where constructor args go, so no
|
|
125
|
+
* caller-supplied byte string can be mistaken for a pin.
|
|
126
|
+
*/
|
|
127
|
+
function decodeExactCodePartLenPins(scriptHex) {
|
|
128
|
+
const values = [];
|
|
129
|
+
for (let i = 0; i + 18 <= scriptHex.length; i += 2) {
|
|
130
|
+
const seq = scriptHex.slice(i, i + 18);
|
|
131
|
+
if (!seq.startsWith('7604'))
|
|
132
|
+
continue;
|
|
133
|
+
if (seq.slice(12, 14) !== '81')
|
|
134
|
+
continue;
|
|
135
|
+
if (seq.slice(14, 16) !== '9c')
|
|
136
|
+
continue;
|
|
137
|
+
if (seq.slice(16, 18) !== '69')
|
|
138
|
+
continue;
|
|
139
|
+
values.push(parseInt(seq.slice(4, 12).match(/../g).reverse().join(''), 16));
|
|
140
|
+
}
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Deep-review finding C8: offline dry-run of ONE input of a fully-assembled
|
|
145
|
+
* tx through @bsv/sdk's production `Spend` interpreter (real `OP_CHECKSIG`,
|
|
146
|
+
* real BIP-143). Used by `finalizeCall` to fail closed on a script-invalid
|
|
147
|
+
* (or, via its caller, underfunded) tx BEFORE it reaches `provider.broadcast`.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately self-contained rather than imported from `multi-contract.ts`
|
|
150
|
+
* (which already has an equivalent `dryRunMultiContractInput`): that module
|
|
151
|
+
* imports `encodeArg`/`encodePushData`/`encodeScriptNumber` FROM this file,
|
|
152
|
+
* so importing back from it here would create a contract.ts <-> multi-contract.ts
|
|
153
|
+
* import cycle. Keep both thin wrappers in sync if `Spend`'s constructor shape
|
|
154
|
+
* ever changes.
|
|
155
|
+
*/
|
|
156
|
+
function dryRunContractInput(tx, inputIndex, utxoScriptHex, utxoSatoshis) {
|
|
157
|
+
const input = tx.inputs[inputIndex];
|
|
158
|
+
if (!input)
|
|
159
|
+
return { valid: false, error: `no input at index ${inputIndex}` };
|
|
160
|
+
// `otherInputs` feeds BIP-143's hashPrevouts/hashSequence, so what it
|
|
161
|
+
// contains matters: it must be exactly the OTHER inputs (current one
|
|
162
|
+
// excluded), in their original relative order, or the input's
|
|
163
|
+
// OP_CHECKSIG(VERIFY) fails for a perfectly valid transaction — a false
|
|
164
|
+
// rejection, which for a fail-closed pre-broadcast gate is worse than the
|
|
165
|
+
// hole C8 closes. Normalize to exactly the projection the proven harnesses
|
|
166
|
+
// use (`validateSpend` in the SDK spend tests and `runStatefulSpend` in
|
|
167
|
+
// runar-testing's real-crypto oracle, both of which agree with the
|
|
168
|
+
// network).
|
|
169
|
+
//
|
|
170
|
+
// P2 (testing-gap remediation): the per-entry stubbing below
|
|
171
|
+
// (`unlockingScript`/`sourceSatoshis`/`lockingScript`, and each entry's
|
|
172
|
+
// own re-indexed `inputIndex`) is belt-and-braces, not load-bearing for
|
|
173
|
+
// the sighash — `TransactionSignature.formatBip143` (the function `Spend`
|
|
174
|
+
// delegates to) only ever reads `sourceTXID`, `sourceOutputIndex`, and
|
|
175
|
+
// `sequence` off each `otherInputs` entry. Verified equivalent to
|
|
176
|
+
// `providers/mock.ts`'s `validateBroadcastTx`, which passes the raw
|
|
177
|
+
// filtered `tx.inputs` slice straight through with none of this stubbing —
|
|
178
|
+
// both produce an identical sighash as long as the filter excludes exactly
|
|
179
|
+
// the current input and preserves order, which both do. Keep both thin
|
|
180
|
+
// wrappers in sync if `TransactionSignature`'s field usage ever changes.
|
|
181
|
+
const otherInputs = tx.inputs
|
|
182
|
+
.filter((_, i) => i !== inputIndex)
|
|
183
|
+
.map((inp, idx) => ({
|
|
184
|
+
inputIndex: idx >= inputIndex ? idx + 1 : idx,
|
|
185
|
+
sourceOutputIndex: inp.sourceOutputIndex,
|
|
186
|
+
sourceTXID: inp.sourceTXID,
|
|
187
|
+
sequence: inp.sequence,
|
|
188
|
+
unlockingScript: undefined,
|
|
189
|
+
sourceSatoshis: 0,
|
|
190
|
+
lockingScript: LockingScript.fromHex(''),
|
|
191
|
+
}));
|
|
192
|
+
try {
|
|
193
|
+
const spend = new Spend({
|
|
194
|
+
sourceTXID: input.sourceTXID,
|
|
195
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
196
|
+
sourceSatoshis: utxoSatoshis,
|
|
197
|
+
lockingScript: LockingScript.fromHex(utxoScriptHex),
|
|
198
|
+
transactionVersion: tx.version,
|
|
199
|
+
otherInputs: otherInputs,
|
|
200
|
+
outputs: tx.outputs,
|
|
201
|
+
inputIndex,
|
|
202
|
+
// NEW-005: `Spend` mutates the script it executes in place, so it must
|
|
203
|
+
// never be handed the live in-flight input's own object — the dry-run
|
|
204
|
+
// would corrupt every evaluation that follows it (including
|
|
205
|
+
// `MockProvider.validateBroadcastTx`, one line later in `finalizeCall`).
|
|
206
|
+
// See spend-safety.ts.
|
|
207
|
+
unlockingScript: detachUnlockingScript(input.unlockingScript),
|
|
208
|
+
inputSequence: input.sequence ?? 0xffffffff,
|
|
209
|
+
lockTime: tx.lockTime,
|
|
210
|
+
});
|
|
211
|
+
// P2: @bsv/sdk's `Spend.validate()` never actually returns `false` —
|
|
212
|
+
// every failure path throws `ScriptEvaluationError` instead, so this
|
|
213
|
+
// line always returns `{ valid: true }` when reached; every rejection
|
|
214
|
+
// exits through the `catch` below. The `!!` is defensive belt-and-
|
|
215
|
+
// braces in case a future @bsv/sdk version reverts to a boolean.
|
|
216
|
+
return { valid: !!spend.validate() };
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
return { valid: false, error: e instanceof Error ? e.message : String(e) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Walk a hex-encoded script and return the byte offsets of every
|
|
224
|
+
* OP_CODESEPARATOR (0xab) that sits at a real opcode boundary (i.e. not inside
|
|
225
|
+
* push-data). Correctly skips all BSV push opcodes (0x01..0x4b,
|
|
226
|
+
* OP_PUSHDATA1/2/4).
|
|
227
|
+
*
|
|
228
|
+
* Used by getSubscriptForSigning to recover the true on-chain byte offsets when
|
|
229
|
+
* the in-memory constructor args don't reflect what was actually baked into the
|
|
230
|
+
* locking script (e.g. after fromTxid populates dummy placeholders).
|
|
231
|
+
*/
|
|
232
|
+
export function findCodesepOffsets(scriptHex) {
|
|
233
|
+
const out = [];
|
|
234
|
+
let off = 0;
|
|
235
|
+
const n = scriptHex.length;
|
|
236
|
+
const b = (i) => {
|
|
237
|
+
const v = parseInt(scriptHex.slice(i, i + 2), 16);
|
|
238
|
+
return Number.isNaN(v) ? 0 : v;
|
|
239
|
+
};
|
|
240
|
+
while (off + 2 <= n) {
|
|
241
|
+
const op = b(off);
|
|
242
|
+
const bytePos = off / 2;
|
|
243
|
+
if (op === 0xab) {
|
|
244
|
+
out.push(bytePos);
|
|
245
|
+
off += 2;
|
|
246
|
+
}
|
|
247
|
+
else if (op >= 0x01 && op <= 0x4b) {
|
|
248
|
+
off += 2 + op * 2;
|
|
249
|
+
}
|
|
250
|
+
else if (op === 0x4c) {
|
|
251
|
+
if (off + 4 > n)
|
|
252
|
+
break;
|
|
253
|
+
const pushLen = b(off + 2);
|
|
254
|
+
off += 4 + pushLen * 2;
|
|
255
|
+
}
|
|
256
|
+
else if (op === 0x4d) {
|
|
257
|
+
if (off + 6 > n)
|
|
258
|
+
break;
|
|
259
|
+
const lo = b(off + 2);
|
|
260
|
+
const hi = b(off + 4);
|
|
261
|
+
const pushLen = lo | (hi << 8);
|
|
262
|
+
off += 6 + pushLen * 2;
|
|
263
|
+
}
|
|
264
|
+
else if (op === 0x4e) {
|
|
265
|
+
if (off + 10 > n)
|
|
266
|
+
break;
|
|
267
|
+
const b0 = b(off + 2);
|
|
268
|
+
const b1 = b(off + 4);
|
|
269
|
+
const b2 = b(off + 6);
|
|
270
|
+
const b3 = b(off + 8);
|
|
271
|
+
// >>> 0 forces uint32: with b3 >= 0x80 the signed OR result is negative,
|
|
272
|
+
// which would walk the cursor backwards and never terminate the scan.
|
|
273
|
+
const pushLen = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
|
|
274
|
+
// A declared push length past the script end means a malformed script;
|
|
275
|
+
// stop scanning rather than skipping into nothing.
|
|
276
|
+
if (pushLen > (n - off - 10) / 2)
|
|
277
|
+
break;
|
|
278
|
+
off += 10 + pushLen * 2;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
off += 2;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Runtime wrapper for a compiled Runar contract.
|
|
288
|
+
*
|
|
289
|
+
* Handles deployment, method invocation, state tracking, and script
|
|
290
|
+
* construction. Works with any Provider and Signer implementation.
|
|
291
|
+
*
|
|
292
|
+
* ```ts
|
|
293
|
+
* const artifact = JSON.parse(fs.readFileSync('./artifacts/Counter.json', 'utf8'));
|
|
294
|
+
* const contract = new RunarContract(artifact, [0n]); // constructor args
|
|
295
|
+
* const { txid } = await contract.deploy(provider, signer, { satoshis: 10000 });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export class RunarContract {
|
|
299
|
+
artifact;
|
|
300
|
+
/**
|
|
301
|
+
* Constructor arguments for the contract, typed as `unknown[]` because
|
|
302
|
+
* they can be any of the Runar primitive types: `bigint`, `boolean`,
|
|
303
|
+
* `ByteString` (hex string), `PubKey` (hex string), etc. TypeScript
|
|
304
|
+
* generics are not practical here since the types depend on the specific
|
|
305
|
+
* contract being used and are only known at runtime from the ABI.
|
|
306
|
+
*/
|
|
307
|
+
constructorArgs;
|
|
308
|
+
_state = {};
|
|
309
|
+
_codeScript = null;
|
|
310
|
+
_inscription = null;
|
|
311
|
+
currentUtxo = null;
|
|
312
|
+
/** Returns the current UTXO tracked by this contract, if any. */
|
|
313
|
+
getUtxo() { return this.currentUtxo; }
|
|
314
|
+
_provider = null;
|
|
315
|
+
_signer = null;
|
|
316
|
+
/**
|
|
317
|
+
* Witness values for intent-covenant intrinsic auto-injected params.
|
|
318
|
+
* `_prevOutScript_<i>` values are stored per-input-index in `_prevOutScripts`;
|
|
319
|
+
* `_serialisedOutputs` is stored in `_serialisedOutputs`. Both are hex strings
|
|
320
|
+
* (normalized in the setters). Read by the call-builder when assembling the
|
|
321
|
+
* unlocking script for methods that use `extractPrevOutputScript` /
|
|
322
|
+
* `requireOutputP2PKH`.
|
|
323
|
+
*/
|
|
324
|
+
_prevOutScripts = new Map();
|
|
325
|
+
_serialisedOutputs = null;
|
|
326
|
+
constructor(artifact, constructorArgs) {
|
|
327
|
+
this.artifact = artifact;
|
|
328
|
+
this.constructorArgs = constructorArgs;
|
|
329
|
+
// Validate constructor args match ABI
|
|
330
|
+
const expected = artifact.abi.constructor.params.length;
|
|
331
|
+
if (constructorArgs.length !== expected) {
|
|
332
|
+
throw new Error(`RunarContract: expected ${expected} constructor args for ${artifact.contractName}, got ${constructorArgs.length}`);
|
|
333
|
+
}
|
|
334
|
+
// Initialize state from constructor args for stateful contracts.
|
|
335
|
+
// Properties with initialValue use their default; others are matched
|
|
336
|
+
// to constructor args by name lookup in the ABI constructor params.
|
|
337
|
+
if (artifact.stateFields && artifact.stateFields.length > 0) {
|
|
338
|
+
for (const field of artifact.stateFields) {
|
|
339
|
+
const fa = field.fixedArray;
|
|
340
|
+
if (fa) {
|
|
341
|
+
// FixedArray state field. The assembler stores `initialValue`
|
|
342
|
+
// (when every element has a compile-time default) as a real
|
|
343
|
+
// JS array of element values — no more stringified-tuple
|
|
344
|
+
// parsing. For nested arrays the stored value is a nested
|
|
345
|
+
// JS array mirroring the declared shape. Leaf values may
|
|
346
|
+
// still be bigint-as-string when the artifact was loaded
|
|
347
|
+
// via a plain JSON import without the custom reviver, so
|
|
348
|
+
// walk the tree and revive each leaf.
|
|
349
|
+
const rawInit = field.initialValue;
|
|
350
|
+
if (Array.isArray(rawInit)) {
|
|
351
|
+
this._state[field.name] = reviveNestedValue(rawInit, field.type);
|
|
352
|
+
}
|
|
353
|
+
else if (rawInit !== undefined) {
|
|
354
|
+
// Defensive: we shouldn't hit this path anymore, but if a
|
|
355
|
+
// third-party producer emits a scalar where we expect an
|
|
356
|
+
// array, keep the value as-is instead of crashing.
|
|
357
|
+
this._state[field.name] = rawInit;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const paramIdx = artifact.abi.constructor.params.findIndex(p => p.name === field.name);
|
|
361
|
+
if (paramIdx >= 0 && paramIdx < constructorArgs.length) {
|
|
362
|
+
this._state[field.name] = constructorArgs[paramIdx];
|
|
363
|
+
}
|
|
364
|
+
else if (field.index < constructorArgs.length) {
|
|
365
|
+
this._state[field.name] = constructorArgs[field.index];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (field.initialValue !== undefined) {
|
|
371
|
+
// Property has a compile-time default value.
|
|
372
|
+
// Revive BigInt strings ("0n") that occur when artifacts are loaded
|
|
373
|
+
// via plain JSON import (without the bigintReviver).
|
|
374
|
+
this._state[field.name] = reviveJsonValue(field.initialValue, field.type);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Match by name to constructor params
|
|
378
|
+
const paramIdx = artifact.abi.constructor.params.findIndex(p => p.name === field.name);
|
|
379
|
+
if (paramIdx >= 0 && paramIdx < constructorArgs.length) {
|
|
380
|
+
this._state[field.name] = constructorArgs[paramIdx];
|
|
381
|
+
}
|
|
382
|
+
else if (field.index < constructorArgs.length) {
|
|
383
|
+
// Fallback: use declaration index for backward compatibility
|
|
384
|
+
this._state[field.name] = constructorArgs[field.index];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
// Connection
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
/**
|
|
394
|
+
* Store a provider and signer on this contract so they don't need to be
|
|
395
|
+
* passed to every `deploy()` and `call()` invocation.
|
|
396
|
+
*/
|
|
397
|
+
connect(provider, signer) {
|
|
398
|
+
this._provider = provider;
|
|
399
|
+
this._signer = signer;
|
|
400
|
+
}
|
|
401
|
+
// -------------------------------------------------------------------------
|
|
402
|
+
// Ordinals
|
|
403
|
+
// -------------------------------------------------------------------------
|
|
404
|
+
/**
|
|
405
|
+
* Attach a 1sat ordinals inscription to this contract. The inscription
|
|
406
|
+
* envelope is injected into the locking script between the compiled code
|
|
407
|
+
* and the state section (if any). Once deployed, the inscription is
|
|
408
|
+
* immutable — it persists identically across all state transitions.
|
|
409
|
+
*
|
|
410
|
+
* ```ts
|
|
411
|
+
* contract.withInscription({ contentType: 'image/png', data: pngHex });
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* N-043 — THROWS when the envelope would break the contract's own
|
|
415
|
+
* `SIZE(_codePart)` pin. A stateful contract with a variable-length state
|
|
416
|
+
* section carries an equality pin on the deployed code-part length, and the
|
|
417
|
+
* envelope lands INSIDE the code part (see `getCodePartHex`). The compiler
|
|
418
|
+
* bakes that number before any inscription exists, so the pinned length and
|
|
419
|
+
* the real one differ by the envelope's size and every honest spend aborts
|
|
420
|
+
* at OP_VERIFY — with the funds already committed. Refusing here turns a
|
|
421
|
+
* permanent, silent lock into a loud error before a single satoshi moves.
|
|
422
|
+
*/
|
|
423
|
+
withInscription(inscription) {
|
|
424
|
+
const previous = this._inscription;
|
|
425
|
+
this._inscription = inscription;
|
|
426
|
+
try {
|
|
427
|
+
this.assertCodePartLengthPinHonoured();
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
this._inscription = previous;
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Verify that every equality `verify_code_part_len` pin the compiler baked
|
|
437
|
+
* into this artifact still describes the code part this contract produces.
|
|
438
|
+
*
|
|
439
|
+
* The check is the invariant itself, not a restatement of the compiler's
|
|
440
|
+
* derivation: it decodes the pinned number straight out of the emitted
|
|
441
|
+
* template and compares it to `getCodePartHex()`. So it permits every
|
|
442
|
+
* combination that actually works — a stateless contract or a fixed-size
|
|
443
|
+
* state layout carries no pin at all, and a lower-bound pin is satisfied by
|
|
444
|
+
* a longer code part — and rejects only the shape that would lock funds.
|
|
445
|
+
*/
|
|
446
|
+
assertCodePartLengthPinHonoured() {
|
|
447
|
+
const pinned = decodeExactCodePartLenPins(this.artifact.script);
|
|
448
|
+
if (pinned.length === 0)
|
|
449
|
+
return;
|
|
450
|
+
const actual = this.getCodePartHex().length / 2;
|
|
451
|
+
for (const value of pinned) {
|
|
452
|
+
if (value === actual)
|
|
453
|
+
continue;
|
|
454
|
+
throw new Error(`RunarContract.withInscription: ${this.artifact.contractName} pins ` +
|
|
455
|
+
`SIZE(_codePart) == ${value}, but with this inscription attached the ` +
|
|
456
|
+
`code part is ${actual} bytes. Deploying it would make every spend ` +
|
|
457
|
+
`fail OP_VERIFY and lock the contract's funds permanently. An ` +
|
|
458
|
+
`inscription cannot be attached to a stateful contract with a ` +
|
|
459
|
+
`variable-length state section: the envelope is part of the code ` +
|
|
460
|
+
`part, and its length is not known when the pin is compiled.`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Returns the current inscription, if any. */
|
|
464
|
+
get inscription() {
|
|
465
|
+
return this._inscription;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Resolve provider/signer: explicit args win, then connected, then error.
|
|
469
|
+
*/
|
|
470
|
+
resolveProviderSigner(provider, signer) {
|
|
471
|
+
const p = provider ?? this._provider;
|
|
472
|
+
const s = signer ?? this._signer;
|
|
473
|
+
if (!p || !s) {
|
|
474
|
+
throw new Error('No provider/signer available. Call connect() or pass them explicitly.');
|
|
475
|
+
}
|
|
476
|
+
return { provider: p, signer: s };
|
|
477
|
+
}
|
|
478
|
+
async deploy(providerOrOptions, maybeSigner, maybeOptions) {
|
|
479
|
+
let provider;
|
|
480
|
+
let signer;
|
|
481
|
+
let options;
|
|
482
|
+
if (maybeSigner !== undefined && maybeOptions !== undefined) {
|
|
483
|
+
// Explicit: deploy(provider, signer, options)
|
|
484
|
+
provider = providerOrOptions;
|
|
485
|
+
signer = maybeSigner;
|
|
486
|
+
options = maybeOptions;
|
|
487
|
+
}
|
|
488
|
+
else if (typeof providerOrOptions === 'object' &&
|
|
489
|
+
!('getUtxos' in providerOrOptions)) {
|
|
490
|
+
// Connected: deploy(options)
|
|
491
|
+
const resolved = this.resolveProviderSigner();
|
|
492
|
+
provider = resolved.provider;
|
|
493
|
+
signer = resolved.signer;
|
|
494
|
+
options = providerOrOptions;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
throw new Error('RunarContract.deploy: invalid arguments. Pass (options) or (provider, signer, options).');
|
|
498
|
+
}
|
|
499
|
+
const address = await signer.getAddress();
|
|
500
|
+
const changeAddress = options.changeAddress ?? address;
|
|
501
|
+
const deploySatoshis = options.satoshis ?? 1;
|
|
502
|
+
const lockingScript = this.getLockingScript();
|
|
503
|
+
// DoS-bound: reject pathological scripts BEFORE any signing / broadcast.
|
|
504
|
+
assertScriptHexUnderLimit(lockingScript, InputLimits.MAX_SCRIPT_BYTES, `${this.artifact.contractName}.deploy`);
|
|
505
|
+
// R-062: and refuse to fund a script reaching a builtin the compiler does
|
|
506
|
+
// not claim is sound unless the caller says so here, in the same breath as
|
|
507
|
+
// the money. Same position as the guard above — before any signing or
|
|
508
|
+
// broadcast.
|
|
509
|
+
assertUnsoundPrimitivesAcknowledged(this.artifact, options.acknowledgeUnsound, `${this.artifact.contractName}.deploy`);
|
|
510
|
+
// Fetch fee rate and funding UTXOs
|
|
511
|
+
const feeRate = await provider.getFeeRate();
|
|
512
|
+
const allUtxos = await provider.getUtxos(address);
|
|
513
|
+
if (allUtxos.length === 0) {
|
|
514
|
+
throw new Error(`RunarContract.deploy: no UTXOs found for address ${address}`);
|
|
515
|
+
}
|
|
516
|
+
const utxos = selectUtxos(allUtxos, deploySatoshis, lockingScript.length / 2, feeRate);
|
|
517
|
+
// Build the deploy transaction
|
|
518
|
+
const changeScript = buildP2PKHScript(changeAddress);
|
|
519
|
+
const { tx, inputCount } = buildDeployTransaction(lockingScript, utxos, deploySatoshis, changeAddress, changeScript, feeRate);
|
|
520
|
+
// Sign all inputs — need unsigned hex for signer. Funding inputs are
|
|
521
|
+
// signed by fundingSigner when set (issue #134): the deploy signer may not
|
|
522
|
+
// own the funding coins. Defaults to the connected signer.
|
|
523
|
+
const fundingSigner = options.fundingSigner ?? signer;
|
|
524
|
+
const unsignedHex = tx.toHex();
|
|
525
|
+
for (let i = 0; i < inputCount; i++) {
|
|
526
|
+
const utxo = utxos[i];
|
|
527
|
+
const sig = await fundingSigner.sign(unsignedHex, i, utxo.script, utxo.satoshis);
|
|
528
|
+
const pubKey = await fundingSigner.getPublicKey();
|
|
529
|
+
// Build P2PKH unlocking script: <sig> <pubkey>
|
|
530
|
+
const unlockScript = encodePushData(sig) + encodePushData(pubKey);
|
|
531
|
+
tx.inputs[i].unlockingScript = UnlockingScript.fromHex(unlockScript);
|
|
532
|
+
}
|
|
533
|
+
invalidateTxCache(tx);
|
|
534
|
+
// Broadcast
|
|
535
|
+
const txid = await provider.broadcast(tx);
|
|
536
|
+
// Track the deployed UTXO
|
|
537
|
+
this.currentUtxo = {
|
|
538
|
+
txid,
|
|
539
|
+
outputIndex: 0,
|
|
540
|
+
satoshis: deploySatoshis,
|
|
541
|
+
script: lockingScript,
|
|
542
|
+
};
|
|
543
|
+
const txData = await provider.getTransaction(txid).catch((err) => {
|
|
544
|
+
// Audit finding C4: the fallback reports the transaction this SDK
|
|
545
|
+
// actually broadcast, not an empty `inputs: []` / `outputs: []` shell
|
|
546
|
+
// that reads as a real confirmed tx. The warn stays — this is
|
|
547
|
+
// locally-derived, unconfirmed data.
|
|
548
|
+
console.warn('Failed to fetch transaction after broadcast:', err);
|
|
549
|
+
return txToTransactionData(txid, tx);
|
|
550
|
+
});
|
|
551
|
+
return { txid, tx: txData };
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Deploy the contract using a BRC-100 wallet. The wallet owns the coins
|
|
555
|
+
* and creates the transaction itself via `createAction()`.
|
|
556
|
+
*
|
|
557
|
+
* Requires the contract to be connected to a `WalletProvider` (via `connect()`).
|
|
558
|
+
*
|
|
559
|
+
* @param options.satoshis - Satoshis to lock in the contract output (default: 1).
|
|
560
|
+
* @param options.description - Description for the wallet action.
|
|
561
|
+
*/
|
|
562
|
+
async deployWithWallet(options = {}) {
|
|
563
|
+
if (!(this._provider instanceof WalletProvider)) {
|
|
564
|
+
throw new Error('deployWithWallet requires a connected WalletProvider. Call connect(walletProvider, signer) first.');
|
|
565
|
+
}
|
|
566
|
+
const walletProvider = this._provider;
|
|
567
|
+
const wallet = walletProvider.walletClient;
|
|
568
|
+
const basket = walletProvider.basketName;
|
|
569
|
+
const lockingScript = this.getLockingScript();
|
|
570
|
+
const satoshis = options.satoshis ?? 1;
|
|
571
|
+
// DoS-bound: reject pathological scripts BEFORE involving the wallet.
|
|
572
|
+
assertScriptHexUnderLimit(lockingScript, InputLimits.MAX_SCRIPT_BYTES, `${this.artifact.contractName}.deployWithWallet`);
|
|
573
|
+
// R-062: the wallet is a SECOND funding path, and it must make the same
|
|
574
|
+
// decision `deploy()` makes — refuse to fund a script reaching a builtin
|
|
575
|
+
// the compiler does not claim is sound unless the caller says so here.
|
|
576
|
+
// Before `createAction`, so no wallet is ever asked for the coins.
|
|
577
|
+
assertUnsoundPrimitivesAcknowledged(this.artifact, options.acknowledgeUnsound, `${this.artifact.contractName}.deployWithWallet`);
|
|
578
|
+
const result = await wallet.createAction({
|
|
579
|
+
description: options.description ?? 'Runar contract deployment',
|
|
580
|
+
outputs: [{
|
|
581
|
+
lockingScript,
|
|
582
|
+
satoshis,
|
|
583
|
+
outputDescription: `Deploy ${this.artifact.contractName}`,
|
|
584
|
+
basket,
|
|
585
|
+
}],
|
|
586
|
+
});
|
|
587
|
+
// Parse BEEF to find the correct vout for our locking script
|
|
588
|
+
let outputIndex = 0;
|
|
589
|
+
let actualSatoshis = satoshis;
|
|
590
|
+
if (result.tx) {
|
|
591
|
+
try {
|
|
592
|
+
const tx = BsvTransaction.fromAtomicBEEF(result.tx);
|
|
593
|
+
for (let i = 0; i < tx.outputs.length; i++) {
|
|
594
|
+
const out = tx.outputs[i];
|
|
595
|
+
if (out.lockingScript?.toHex() === lockingScript) {
|
|
596
|
+
outputIndex = i;
|
|
597
|
+
actualSatoshis = out.satoshis != null ? out.satoshis : satoshis;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Cache raw hex for EF child tx builds
|
|
602
|
+
const txid = result.txid || '';
|
|
603
|
+
if (txid) {
|
|
604
|
+
walletProvider.cacheTx(txid, tx.toHex());
|
|
605
|
+
}
|
|
606
|
+
// Broadcast to ARC (may already be known — non-fatal)
|
|
607
|
+
await walletProvider.broadcast(tx).catch((e) => warnNonFatal('deploy-tx broadcast', e));
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
warnNonFatal('deploy-tx BEEF parse', e);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const txid = result.txid || '';
|
|
614
|
+
// Track the deployed UTXO
|
|
615
|
+
this.currentUtxo = {
|
|
616
|
+
txid,
|
|
617
|
+
outputIndex,
|
|
618
|
+
satoshis: actualSatoshis,
|
|
619
|
+
script: lockingScript,
|
|
620
|
+
};
|
|
621
|
+
return { txid, outputIndex };
|
|
622
|
+
}
|
|
623
|
+
async call(methodName, args, providerOrOptions, maybeSigner, maybeOptions) {
|
|
624
|
+
// If explicit provider/signer passed, temporarily connect them for
|
|
625
|
+
// prepareCall / finalizeCall which use the connected references.
|
|
626
|
+
if (maybeSigner !== undefined) {
|
|
627
|
+
const prevProvider = this._provider;
|
|
628
|
+
const prevSigner = this._signer;
|
|
629
|
+
this._provider = providerOrOptions;
|
|
630
|
+
this._signer = maybeSigner;
|
|
631
|
+
try {
|
|
632
|
+
const result = await this.call(methodName, args, maybeOptions);
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
this._provider = prevProvider;
|
|
637
|
+
this._signer = prevSigner;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
let options;
|
|
641
|
+
if (providerOrOptions === undefined ||
|
|
642
|
+
(typeof providerOrOptions === 'object' &&
|
|
643
|
+
!('getUtxos' in providerOrOptions))) {
|
|
644
|
+
options = providerOrOptions;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// providerOrOptions looks like a Provider but no signer — try connected
|
|
648
|
+
const prevProvider = this._provider;
|
|
649
|
+
this._provider = providerOrOptions;
|
|
650
|
+
try {
|
|
651
|
+
const result = await this.call(methodName, args, undefined);
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
this._provider = prevProvider;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const prepared = await this.prepareCall(methodName, args, options);
|
|
659
|
+
const signer = this._signer;
|
|
660
|
+
// Stateful contracts: checkPreimage is auto-injected at method entry, so
|
|
661
|
+
// the user checkSig executes AFTER the OP_CODESEPARATOR — the sighash must
|
|
662
|
+
// be computed over the subscript trimmed at that separator (issue #42: the
|
|
663
|
+
// trim must land at the *real* on-chain codesep byte position, recovered by
|
|
664
|
+
// getSubscriptForSigning's byte-walker).
|
|
665
|
+
// Stateless contracts: the user controls statement order and may place
|
|
666
|
+
// checkSig BEFORE the codesep (e.g. CovenantVault) — those must use the
|
|
667
|
+
// FULL script, so the trim stays gated on the parent class. A stateful
|
|
668
|
+
// contract with ZERO mutable fields (empty stateFields) still injects
|
|
669
|
+
// checkPreimage at entry, so the trim is gated on `_parentStateful`
|
|
670
|
+
// (artifact.parentClass), NOT `_isStateful` (issue #44).
|
|
671
|
+
let mIdx = 0;
|
|
672
|
+
if (prepared._parentStateful) {
|
|
673
|
+
const pubMethods = this.artifact.abi.methods.filter((m) => m.isPublic);
|
|
674
|
+
if (pubMethods.length > 1) {
|
|
675
|
+
const idx = pubMethods.findIndex((m) => m.name === methodName);
|
|
676
|
+
if (idx >= 0)
|
|
677
|
+
mIdx = idx;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const sigSubscript = prepared._parentStateful
|
|
681
|
+
? this.getSubscriptForSigning(prepared._contractUtxo.script, mIdx)
|
|
682
|
+
: prepared._contractUtxo.script;
|
|
683
|
+
const signatures = {};
|
|
684
|
+
const txHex = prepared.tx.toHex();
|
|
685
|
+
for (const idx of prepared.sigIndices) {
|
|
686
|
+
signatures[idx] = await signer.sign(txHex, 0, sigSubscript, prepared._contractUtxo.satoshis);
|
|
687
|
+
}
|
|
688
|
+
return this.finalizeCall(prepared, signatures);
|
|
689
|
+
}
|
|
690
|
+
// -------------------------------------------------------------------------
|
|
691
|
+
// prepareCall / finalizeCall — multi-signer support
|
|
692
|
+
// -------------------------------------------------------------------------
|
|
693
|
+
/**
|
|
694
|
+
* Build the transaction for a method call without signing the primary
|
|
695
|
+
* contract input's Sig params. Returns a `PreparedCall` containing the
|
|
696
|
+
* BIP-143 sighash that external signers need, plus opaque internals for
|
|
697
|
+
* `finalizeCall()`.
|
|
698
|
+
*
|
|
699
|
+
* P2PKH funding inputs and additional contract inputs ARE signed with the
|
|
700
|
+
* connected signer. Only the primary contract input's Sig params are left
|
|
701
|
+
* as 72-byte placeholders.
|
|
702
|
+
*/
|
|
703
|
+
async prepareCall(methodName, args, options) {
|
|
704
|
+
const { provider, signer } = this.resolveProviderSigner();
|
|
705
|
+
// Funding (and terminal fee) inputs are signed by fundingSigner when set
|
|
706
|
+
// (issue #134). The method's own Sig args stay with the connected signer.
|
|
707
|
+
const fundingSigner = options?.fundingSigner ?? signer;
|
|
708
|
+
const method = this.findMethod(methodName);
|
|
709
|
+
if (!method) {
|
|
710
|
+
throw new Error(`RunarContract.prepareCall: method '${methodName}' not found in ${this.artifact.contractName}`);
|
|
711
|
+
}
|
|
712
|
+
const isStateful = this.artifact.stateFields !== undefined &&
|
|
713
|
+
this.artifact.stateFields.length > 0;
|
|
714
|
+
// `isStateful` (derived from non-empty stateFields) drives continuation
|
|
715
|
+
// params, op_push_tx, change outputs, etc. But a StatefulSmartContract
|
|
716
|
+
// with ZERO mutable fields has empty stateFields yet STILL auto-injects
|
|
717
|
+
// checkPreimage at method entry — so its user checkSig runs AFTER the
|
|
718
|
+
// OP_CODESEPARATOR and needs the issue-#42 subscript trim. The parentClass
|
|
719
|
+
// is the authoritative signal for the trim gate (issue #44). Fall back to
|
|
720
|
+
// `isStateful` for older artifacts that predate the parentClass field.
|
|
721
|
+
const parentStateful = this.artifact.parentClass !== undefined
|
|
722
|
+
? this.artifact.parentClass === 'StatefulSmartContract'
|
|
723
|
+
: isStateful;
|
|
724
|
+
const methodNeedsChange = method.params.some((p) => p.name === '_changePKH');
|
|
725
|
+
const methodNeedsNewAmount = method.params.some((p) => p.name === '_newAmount');
|
|
726
|
+
// Whether the unlocking script is prefixed with `_codePart`. New artifacts
|
|
727
|
+
// carry the authoritative `usesCodePart` flag (true for continuation
|
|
728
|
+
// builders AND terminal var-length-state readers — issue #100). Older
|
|
729
|
+
// artifacts lack it; fall back to the legacy rule (codePart iff continuation).
|
|
730
|
+
const methodUsesCodePart = method.usesCodePart ?? methodNeedsChange;
|
|
731
|
+
// Drop auto-injected continuation params AND intent-intrinsic witness
|
|
732
|
+
// params (`_prevOutScript_<i>`, `_serialisedOutputs`) from the
|
|
733
|
+
// user-facing arg count check. Witness values come from
|
|
734
|
+
// setPrevOutScript / setSerialisedOutputs, not from the args array.
|
|
735
|
+
const isAutoInjectedWitnessParam = (name) => name.startsWith('_prevOutScript_') || name === '_serialisedOutputs';
|
|
736
|
+
const userParams = isStateful
|
|
737
|
+
? method.params.filter((p) => p.type !== 'SigHashPreimage' &&
|
|
738
|
+
p.name !== '_changePKH' &&
|
|
739
|
+
p.name !== '_changeAmount' &&
|
|
740
|
+
p.name !== '_newAmount' &&
|
|
741
|
+
!isAutoInjectedWitnessParam(p.name))
|
|
742
|
+
: method.params.filter((p) => !isAutoInjectedWitnessParam(p.name));
|
|
743
|
+
if (userParams.length !== args.length) {
|
|
744
|
+
throw new Error(`RunarContract.prepareCall: method '${methodName}' expects ${userParams.length} args, got ${args.length}`);
|
|
745
|
+
}
|
|
746
|
+
if (!this.currentUtxo) {
|
|
747
|
+
throw new Error('RunarContract.prepareCall: contract is not deployed. Call deploy() or fromTxId() first.');
|
|
748
|
+
}
|
|
749
|
+
// DoS-bound: reject pathological scripts BEFORE any signing / broadcast.
|
|
750
|
+
// Guards the current locking script (existing UTXO) AND the new locking
|
|
751
|
+
// script if this is a stateful continuation, since both cross the wire.
|
|
752
|
+
assertScriptHexUnderLimit(this.currentUtxo.script, InputLimits.MAX_SCRIPT_BYTES, `${this.artifact.contractName}.call(${methodName})`);
|
|
753
|
+
const contractUtxo = { ...this.currentUtxo };
|
|
754
|
+
const address = await signer.getAddress();
|
|
755
|
+
const changeAddress = options?.changeAddress ?? address;
|
|
756
|
+
// Detect auto-compute params (user passed null)
|
|
757
|
+
const sigIndices = [];
|
|
758
|
+
const prevoutsIndices = [];
|
|
759
|
+
let preimageIndex = -1;
|
|
760
|
+
const resolvedArgs = [...args];
|
|
761
|
+
for (let i = 0; i < userParams.length; i++) {
|
|
762
|
+
if (userParams[i].type === 'Sig' && args[i] === null) {
|
|
763
|
+
sigIndices.push(i);
|
|
764
|
+
resolvedArgs[i] = '00'.repeat(72); // placeholder
|
|
765
|
+
}
|
|
766
|
+
if (userParams[i].type === 'PubKey' && args[i] === null) {
|
|
767
|
+
resolvedArgs[i] = await signer.getPublicKey();
|
|
768
|
+
}
|
|
769
|
+
if (userParams[i].type === 'SigHashPreimage' && args[i] === null) {
|
|
770
|
+
preimageIndex = i;
|
|
771
|
+
resolvedArgs[i] = '00'.repeat(181);
|
|
772
|
+
}
|
|
773
|
+
if (userParams[i].type === 'ByteString' && args[i] === null) {
|
|
774
|
+
// Finding G6: this stub used to be TYPE-blind — ANY null ByteString got
|
|
775
|
+
// the allPrevouts outpoint stub, so a caller who passed null for an
|
|
776
|
+
// ordinary ByteString param (say `memo`) silently got real outpoint
|
|
777
|
+
// bytes spliced into their own parameter. That builds a tx which
|
|
778
|
+
// broadcasts and then fails at script execution with an opaque error.
|
|
779
|
+
// Gate on the documented parameter NAME instead: `allPrevouts` keeps
|
|
780
|
+
// its auto-compute convention (used by the token-ft integration tests
|
|
781
|
+
// across tiers), everything else fails loudly at build time naming the
|
|
782
|
+
// offending parameter. Matches Go `isAutoPrevoutsParam`/`errNilNonSigArg`
|
|
783
|
+
// and the Rust/Python/Ruby/Zig/Java equivalents.
|
|
784
|
+
if (userParams[i].name !== AUTO_PREVOUTS_PARAM_NAME) {
|
|
785
|
+
throw new Error(`RunarContract.prepareCall: null arg for ByteString param ` +
|
|
786
|
+
`'${userParams[i].name}' (index ${i}): null is only auto-resolved for ` +
|
|
787
|
+
`Sig (auto-signed), PubKey (taken from the signer), SigHashPreimage, ` +
|
|
788
|
+
`and the '${AUTO_PREVOUTS_PARAM_NAME}' outpoint slot. Pass an explicit ` +
|
|
789
|
+
`value (hex string, or '' for an empty ByteString).`);
|
|
790
|
+
}
|
|
791
|
+
prevoutsIndices.push(i);
|
|
792
|
+
const estimatedInputs = 1 + (options?.additionalContractInputs?.length ?? 0) + 1;
|
|
793
|
+
resolvedArgs[i] = '00'.repeat(36 * estimatedInputs);
|
|
794
|
+
}
|
|
795
|
+
// EMPTY_SIG (issue #106) is intentionally NOT handled here: it is not
|
|
796
|
+
// `null`, so it is never added to `sigIndices` and never signed. It stays
|
|
797
|
+
// in `resolvedArgs` and `encodeArg` emits OP_0 (empty sig) for it.
|
|
798
|
+
}
|
|
799
|
+
// Soft heuristic (issue #106): more than one auto-signed Sig slot on an
|
|
800
|
+
// OR-CHECKSIG method (`checkSig || checkSig` → OP_BOOLOR) usually means
|
|
801
|
+
// the non-matching branch should use EMPTY_SIG — otherwise every branch
|
|
802
|
+
// gets the same real signature and the failing CHECKSIG trips BIP146
|
|
803
|
+
// NULLFAIL on broadcast.
|
|
804
|
+
//
|
|
805
|
+
// Do NOT warn for genuine multi-sig (OP_CHECKMULTISIG / OP_CHECKMULTISIGVERIFY):
|
|
806
|
+
// those require multiple real signatures (AND-style), not EMPTY_SIG.
|
|
807
|
+
if (sigIndices.length >= 2 && isLikelyOrCheckSigMethod(this.artifact)) {
|
|
808
|
+
console.warn(`runar-sdk: ${this.artifact.contractName}.call('${methodName}') has ` +
|
|
809
|
+
`${sigIndices.length} auto-signed Sig slots. If this is an OR-CHECKSIG ` +
|
|
810
|
+
`method, pass EMPTY_SIG for the non-matching branch(es) to satisfy ` +
|
|
811
|
+
`BIP146 NULLFAIL (issue #106).`);
|
|
812
|
+
}
|
|
813
|
+
const needsOpPushTx = preimageIndex >= 0 || isStateful;
|
|
814
|
+
// Compute method selector and method index (needed for both terminal and non-terminal)
|
|
815
|
+
let methodSelectorHex = '';
|
|
816
|
+
let methodIndex = 0;
|
|
817
|
+
if (isStateful) {
|
|
818
|
+
const publicMethods = this.artifact.abi.methods.filter((m) => m.isPublic);
|
|
819
|
+
if (publicMethods.length > 1) {
|
|
820
|
+
const idx = publicMethods.findIndex((m) => m.name === methodName);
|
|
821
|
+
if (idx >= 0) {
|
|
822
|
+
methodSelectorHex = encodeScriptNumber(BigInt(idx));
|
|
823
|
+
methodIndex = idx;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Compute change PKH for stateful methods that need it
|
|
828
|
+
let changePKHHex = '';
|
|
829
|
+
if (isStateful && methodNeedsChange) {
|
|
830
|
+
const changePubKeyHex = options?.changePubKey ?? await signer.getPublicKey();
|
|
831
|
+
const pubKeyBytes = Utils.toArray(changePubKeyHex, 'hex');
|
|
832
|
+
const hash160Bytes = Hash.hash160(pubKeyBytes);
|
|
833
|
+
changePKHHex = Utils.toHex(hash160Bytes);
|
|
834
|
+
}
|
|
835
|
+
// Pre-resolve intent-intrinsic witness hex (throws WitnessValueMissingError
|
|
836
|
+
// if a `_prevOutScript_<i>` or `_serialisedOutputs` param wasn't set on the
|
|
837
|
+
// contract). Resolving up-front means the error is raised BEFORE any
|
|
838
|
+
// signing / broadcast work, mirroring the deploy/call script-size guard.
|
|
839
|
+
const witnessHex = this.buildIntentWitnessHex(method);
|
|
840
|
+
// -------------------------------------------------------------------
|
|
841
|
+
// Terminal method path
|
|
842
|
+
// -------------------------------------------------------------------
|
|
843
|
+
if (options?.terminalOutputs) {
|
|
844
|
+
const terminalOutputs = options.terminalOutputs;
|
|
845
|
+
let termUnlockScript;
|
|
846
|
+
if (needsOpPushTx) {
|
|
847
|
+
termUnlockScript = encodePushData('00'.repeat(72)) +
|
|
848
|
+
this.buildUnlockingScript(methodName, resolvedArgs);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
termUnlockScript = this.buildUnlockingScript(methodName, resolvedArgs);
|
|
852
|
+
}
|
|
853
|
+
// Witness values are appended to the size-estimation script too so
|
|
854
|
+
// the BIP-143 preimage sees the same input weight that the final
|
|
855
|
+
// stateful unlock will produce (the real final unlock is built by
|
|
856
|
+
// `buildUnlock` below which inserts witnessHex in the correct ABI slot).
|
|
857
|
+
termUnlockScript += witnessHex;
|
|
858
|
+
// Sequence (issue #131): all-final inputs make nLockTime a consensus
|
|
859
|
+
// no-op — when a non-zero locktime is set, default to 0xfffffffe so the
|
|
860
|
+
// terminal method's extractLocktime assertion is actually enforced.
|
|
861
|
+
const termSequence = resolveInputSequence(options?.locktime, options?.sequence);
|
|
862
|
+
const buildTerminalTx = (unlock) => {
|
|
863
|
+
const ttx = new BsvTransaction();
|
|
864
|
+
// Terminal calls (auction close/claim/withdraw) typically assert
|
|
865
|
+
// `extractLocktime(preimage) >= deadline`. Default 0 preserves legacy
|
|
866
|
+
// behavior for contracts that don't check locktime.
|
|
867
|
+
ttx.lockTime = options?.locktime ?? 0;
|
|
868
|
+
ttx.addInput({
|
|
869
|
+
sourceTXID: contractUtxo.txid,
|
|
870
|
+
sourceOutputIndex: contractUtxo.outputIndex,
|
|
871
|
+
unlockingScript: UnlockingScript.fromHex(unlock),
|
|
872
|
+
sequence: termSequence,
|
|
873
|
+
});
|
|
874
|
+
// Fee input (issue #118): a plain P2PKH input added BEFORE the
|
|
875
|
+
// OP_PUSH_TX preimage is computed so hashPrevouts covers it. Consumed
|
|
876
|
+
// entirely as fee (no change output) — the covenant's terminal output
|
|
877
|
+
// assertions are untouched; only the input side grows. Its P2PKH sig is
|
|
878
|
+
// filled in after the tx structure is final (below).
|
|
879
|
+
if (options?.feeUtxo) {
|
|
880
|
+
ttx.addInput({
|
|
881
|
+
sourceTXID: options.feeUtxo.txid,
|
|
882
|
+
sourceOutputIndex: options.feeUtxo.outputIndex,
|
|
883
|
+
unlockingScript: new UnlockingScript(),
|
|
884
|
+
sequence: termSequence,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
for (const out of terminalOutputs) {
|
|
888
|
+
ttx.addOutput({
|
|
889
|
+
satoshis: out.satoshis,
|
|
890
|
+
lockingScript: LockingScript.fromHex(out.scriptHex),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return ttx;
|
|
894
|
+
};
|
|
895
|
+
let termTx = buildTerminalTx(termUnlockScript);
|
|
896
|
+
let finalOpPushTxSig = '';
|
|
897
|
+
let finalPreimage = '';
|
|
898
|
+
if (isStateful) {
|
|
899
|
+
// Build stateful terminal unlock with PLACEHOLDER user sigs
|
|
900
|
+
const buildUnlock = (tx) => {
|
|
901
|
+
const { sigHex: opSig, preimageHex: preimage } = this.computeOpPushTxWithCodeSep(tx, 0, contractUtxo.script, contractUtxo.satoshis, methodIndex);
|
|
902
|
+
let argsHex = '';
|
|
903
|
+
for (const arg of resolvedArgs)
|
|
904
|
+
argsHex += encodeArg(arg);
|
|
905
|
+
let changeHex = '';
|
|
906
|
+
if (methodNeedsChange && changePKHHex) {
|
|
907
|
+
changeHex = encodePushData(changePKHHex) + encodeArg(0n);
|
|
908
|
+
}
|
|
909
|
+
let newAmountHex = '';
|
|
910
|
+
if (methodNeedsNewAmount) {
|
|
911
|
+
newAmountHex = encodeArg(BigInt(contractUtxo.satoshis));
|
|
912
|
+
}
|
|
913
|
+
const unlock = this.buildStatefulPrefix(opSig, methodUsesCodePart) + argsHex + changeHex + newAmountHex + encodePushData(preimage) + witnessHex + methodSelectorHex;
|
|
914
|
+
return { unlock, opSig, preimage };
|
|
915
|
+
};
|
|
916
|
+
// First pass
|
|
917
|
+
const first = buildUnlock(termTx);
|
|
918
|
+
termTx = buildTerminalTx(first.unlock);
|
|
919
|
+
// Second pass
|
|
920
|
+
const second = buildUnlock(termTx);
|
|
921
|
+
termTx.inputs[0].unlockingScript = UnlockingScript.fromHex(second.unlock);
|
|
922
|
+
invalidateTxCache(termTx);
|
|
923
|
+
finalOpPushTxSig = second.opSig;
|
|
924
|
+
finalPreimage = second.preimage;
|
|
925
|
+
}
|
|
926
|
+
else if (needsOpPushTx || sigIndices.length > 0) {
|
|
927
|
+
// Stateless terminal — keep placeholder sigs
|
|
928
|
+
if (needsOpPushTx) {
|
|
929
|
+
const { sigHex, preimageHex } = this.computeOpPushTxWithCodeSep(termTx, 0, contractUtxo.script, contractUtxo.satoshis, methodIndex);
|
|
930
|
+
finalOpPushTxSig = sigHex;
|
|
931
|
+
resolvedArgs[preimageIndex] = preimageHex;
|
|
932
|
+
}
|
|
933
|
+
// Don't sign Sig params — keep 72-byte placeholders
|
|
934
|
+
let realUnlock = this.buildUnlockingScript(methodName, resolvedArgs);
|
|
935
|
+
if (needsOpPushTx && finalOpPushTxSig) {
|
|
936
|
+
realUnlock = this.buildStatefulPrefix(finalOpPushTxSig) + realUnlock;
|
|
937
|
+
termTx.inputs[0].unlockingScript = UnlockingScript.fromHex(realUnlock);
|
|
938
|
+
invalidateTxCache(termTx);
|
|
939
|
+
const { sigHex: finalSig, preimageHex: finalPre } = this.computeOpPushTxWithCodeSep(termTx, 0, contractUtxo.script, contractUtxo.satoshis, methodIndex);
|
|
940
|
+
resolvedArgs[preimageIndex] = finalPre;
|
|
941
|
+
finalOpPushTxSig = finalSig;
|
|
942
|
+
finalPreimage = finalPre;
|
|
943
|
+
realUnlock = this.buildStatefulPrefix(finalSig) +
|
|
944
|
+
this.buildUnlockingScript(methodName, resolvedArgs);
|
|
945
|
+
}
|
|
946
|
+
termTx.inputs[0].unlockingScript = UnlockingScript.fromHex(realUnlock);
|
|
947
|
+
invalidateTxCache(termTx);
|
|
948
|
+
if (!finalPreimage && needsOpPushTx) {
|
|
949
|
+
finalPreimage = resolvedArgs[preimageIndex];
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Sign the fee input (issue #118). Its BIP-143 P2PKH sighash covers only
|
|
953
|
+
// hashPrevouts / hashOutputs / its own outpoint — NOT input 0's scriptSig
|
|
954
|
+
// — so it stays valid even after finalizeCall rewrites input 0. Owned by
|
|
955
|
+
// fundingSigner ?? signer (composes with #134). The fee input sits at
|
|
956
|
+
// index 1 (right after the primary contract input).
|
|
957
|
+
if (options?.feeUtxo) {
|
|
958
|
+
const feeInputIdx = 1;
|
|
959
|
+
const feeTxHex = termTx.toHex();
|
|
960
|
+
const feeSig = await fundingSigner.sign(feeTxHex, feeInputIdx, options.feeUtxo.script, options.feeUtxo.satoshis);
|
|
961
|
+
const feePubKey = await fundingSigner.getPublicKey();
|
|
962
|
+
termTx.inputs[feeInputIdx].unlockingScript = UnlockingScript.fromHex(encodePushData(feeSig) + encodePushData(feePubKey));
|
|
963
|
+
invalidateTxCache(termTx);
|
|
964
|
+
// #118: a feeUtxo is consumed ENTIRELY as fee — there is no change
|
|
965
|
+
// output, because the covenant binds the exact terminal output set.
|
|
966
|
+
// An oversized feeUtxo therefore silently BURNS the excess. Warn (but
|
|
967
|
+
// never block) when it dwarfs the terminal tx's estimated fee.
|
|
968
|
+
// Heuristic: excess is burned if the feeUtxo is > 5x the estimated fee
|
|
969
|
+
// AND at least ~1000 sats of excess would be burned (an absolute floor
|
|
970
|
+
// so a slightly-generous fee on a tiny tx does not nag). Best-effort:
|
|
971
|
+
// any failure fetching the fee rate skips the advisory silently.
|
|
972
|
+
try {
|
|
973
|
+
const feeRate = await provider.getFeeRate(); // sat/KB
|
|
974
|
+
const termTxSizeBytes = termTx.toHex().length / 2;
|
|
975
|
+
const estimatedFee = Math.max(1, Math.ceil((termTxSizeBytes * feeRate) / 1000));
|
|
976
|
+
const excess = options.feeUtxo.satoshis - estimatedFee;
|
|
977
|
+
const OVERSIZE_FEE_MULTIPLE = 5;
|
|
978
|
+
const OVERSIZE_MIN_EXCESS_SATS = 1000;
|
|
979
|
+
if (options.feeUtxo.satoshis > estimatedFee * OVERSIZE_FEE_MULTIPLE &&
|
|
980
|
+
excess > OVERSIZE_MIN_EXCESS_SATS) {
|
|
981
|
+
console.warn(`runar-sdk: ${this.artifact.contractName}.call('${methodName}'): feeUtxo is ` +
|
|
982
|
+
`${options.feeUtxo.satoshis} sats but the terminal tx needs only ~${estimatedFee} sats ` +
|
|
983
|
+
`of fee. A feeUtxo is consumed ENTIRELY as fee (no change output — the covenant binds ` +
|
|
984
|
+
`the exact terminal outputs), so ~${excess} sats will be BURNED. Size the feeUtxo close ` +
|
|
985
|
+
`to the intended fee (issue #118).`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// Advisory only — never fail a call because the fee-rate lookup threw.
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// Compute sighash from preimage (C19: the true BIP-143 digest external
|
|
993
|
+
// signers must ECDSA-sign — hash256(preimage), not sha256(preimage)).
|
|
994
|
+
const sighash = finalPreimage ? computeBip143Sighash(finalPreimage) : '';
|
|
995
|
+
return {
|
|
996
|
+
sighash,
|
|
997
|
+
preimage: finalPreimage,
|
|
998
|
+
opPushTxSig: finalOpPushTxSig,
|
|
999
|
+
tx: termTx,
|
|
1000
|
+
sigIndices,
|
|
1001
|
+
_methodName: methodName,
|
|
1002
|
+
_resolvedArgs: resolvedArgs,
|
|
1003
|
+
_methodSelectorHex: methodSelectorHex,
|
|
1004
|
+
_isStateful: isStateful,
|
|
1005
|
+
_parentStateful: parentStateful,
|
|
1006
|
+
_isTerminal: true,
|
|
1007
|
+
_needsOpPushTx: needsOpPushTx,
|
|
1008
|
+
_methodNeedsChange: methodNeedsChange,
|
|
1009
|
+
_methodUsesCodePart: methodUsesCodePart,
|
|
1010
|
+
_changePKHHex: changePKHHex,
|
|
1011
|
+
_changeAmount: 0,
|
|
1012
|
+
_methodNeedsNewAmount: false,
|
|
1013
|
+
_newAmount: 0,
|
|
1014
|
+
_preimageIndex: preimageIndex,
|
|
1015
|
+
_contractUtxo: contractUtxo,
|
|
1016
|
+
_newLockingScript: '',
|
|
1017
|
+
_newSatoshis: 0,
|
|
1018
|
+
_hasMultiOutput: false,
|
|
1019
|
+
_contractOutputs: [],
|
|
1020
|
+
_intentWitnessHex: witnessHex,
|
|
1021
|
+
_dryRun: options?.dryRun ?? false,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
// -------------------------------------------------------------------
|
|
1025
|
+
// Non-terminal path
|
|
1026
|
+
// -------------------------------------------------------------------
|
|
1027
|
+
// Build the initial unlocking script (with placeholders). Intent-witness
|
|
1028
|
+
// hex is suffixed so size estimation (and any downstream
|
|
1029
|
+
// assertScriptHexUnderLimit / fee math) accounts for the witness pushes.
|
|
1030
|
+
// The real ABI-correct unlock is rebuilt by buildStatefulUnlock below for
|
|
1031
|
+
// stateful methods.
|
|
1032
|
+
let unlockingScript;
|
|
1033
|
+
if (needsOpPushTx) {
|
|
1034
|
+
unlockingScript = encodePushData('00'.repeat(72)) +
|
|
1035
|
+
this.buildUnlockingScript(methodName, resolvedArgs) + witnessHex;
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
unlockingScript = this.buildUnlockingScript(methodName, resolvedArgs) + witnessHex;
|
|
1039
|
+
}
|
|
1040
|
+
let newLockingScript;
|
|
1041
|
+
let newSatoshis;
|
|
1042
|
+
let contractOutputs;
|
|
1043
|
+
const extraContractUtxos = options?.additionalContractInputs ?? [];
|
|
1044
|
+
const hasMultiOutput = options?.outputs && options.outputs.length > 0;
|
|
1045
|
+
// Data outputs declared via this.addDataOutput(...). Explicit
|
|
1046
|
+
// options.dataOutputs wins; otherwise populated by the ANF
|
|
1047
|
+
// interpreter pass below.
|
|
1048
|
+
let resolvedDataOutputs = options?.dataOutputs
|
|
1049
|
+
? options.dataOutputs.map((d) => ({ script: d.script, satoshis: Number(d.satoshis) }))
|
|
1050
|
+
: [];
|
|
1051
|
+
// Always run the ANF interpreter on stateful artifacts so addDataOutput
|
|
1052
|
+
// payloads are extracted even when the caller pre-supplied newState.
|
|
1053
|
+
// newState only overrides the state continuation; data outputs are
|
|
1054
|
+
// method-body behaviour and the on-chain continuation hash check fails
|
|
1055
|
+
// at spend time if they're omitted. Mirrors Go SDK + Ruby SDK.
|
|
1056
|
+
let autoComputedState;
|
|
1057
|
+
let autoFlatState;
|
|
1058
|
+
// State-class outputs (state continuation + raw) in source order, from the
|
|
1059
|
+
// ANF interpreter. Empty for methods that emit no `this.addRawOutput(...)`
|
|
1060
|
+
// (finding G1) — the raw-output branch below is a no-op in that case.
|
|
1061
|
+
let anfOrderedOutputs = [];
|
|
1062
|
+
if (isStateful && this.artifact.anf) {
|
|
1063
|
+
const namedArgs = buildNamedArgs(userParams, resolvedArgs);
|
|
1064
|
+
const flatState = flattenFixedArrayState(this._state, this.artifact.stateFields);
|
|
1065
|
+
const flatCtorArgs = flattenFixedArrayArgs(this.constructorArgs, this.artifact.abi.constructor.params);
|
|
1066
|
+
try {
|
|
1067
|
+
const { state: computed, dataOutputs: anfDataOutputs, outputs: anfOutputs } = computeNewStateAndDataOutputs(this.artifact.anf, methodName, flatState, namedArgs, flatCtorArgs);
|
|
1068
|
+
autoComputedState = computed;
|
|
1069
|
+
autoFlatState = flatState;
|
|
1070
|
+
anfOrderedOutputs = anfOutputs;
|
|
1071
|
+
if (anfDataOutputs.length > 0 && resolvedDataOutputs.length === 0) {
|
|
1072
|
+
resolvedDataOutputs = anfDataOutputs.map((d) => ({
|
|
1073
|
+
script: d.script,
|
|
1074
|
+
satoshis: Number(d.satoshis),
|
|
1075
|
+
}));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
// FAIL CLOSED (NEW-006). The legacy behaviour was to swallow this and
|
|
1080
|
+
// build the continuation from the CURRENT state, which the covenant's
|
|
1081
|
+
// hashOutputs binding then rejects — a silent "your call cannot be
|
|
1082
|
+
// broadcast", plus silent loss of the method's data / raw outputs.
|
|
1083
|
+
// The interpreter is the only thing that knows this method's post-state
|
|
1084
|
+
// and its addDataOutput/addRawOutput payloads, so there is nothing to
|
|
1085
|
+
// fall back TO: an explicit `newState` covers only the state field and
|
|
1086
|
+
// still leaves the outputs missing.
|
|
1087
|
+
throw new Error(`RunarContract.call('${methodName}'): the ANF interpreter could not evaluate ` +
|
|
1088
|
+
`the method body, so the state continuation and data outputs this call would ` +
|
|
1089
|
+
`commit cannot be derived. Refusing to broadcast a transaction built from the ` +
|
|
1090
|
+
`pre-call state. Cause: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (isStateful && hasMultiOutput) {
|
|
1094
|
+
const codeScript = this._codeScript ?? this.buildCodeScript();
|
|
1095
|
+
contractOutputs = options.outputs.map((out) => {
|
|
1096
|
+
const stateHex = serializeState(this.artifact.stateFields, out.state);
|
|
1097
|
+
return { script: codeScript + '6a' + stateHex, satoshis: out.satoshis ?? 1 };
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
else if (isStateful) {
|
|
1101
|
+
// Honor an explicit `this.addOutput(<sats>, ...)` state continuation: the
|
|
1102
|
+
// ANF interpreter records that amount in `anfOrderedOutputs` (one
|
|
1103
|
+
// `kind:'state'` entry per addOutput). A method with a single explicit
|
|
1104
|
+
// addOutput and no raw output must build its continuation at that amount,
|
|
1105
|
+
// not default to the spent input's value — otherwise the covenant's
|
|
1106
|
+
// hashOutputs binding rejects the spend and funds are stranded. Finding G1
|
|
1107
|
+
// reads the same satoshis but only on the raw-output-present branch below;
|
|
1108
|
+
// this generalizes it to the no-raw single-continuation path. With no
|
|
1109
|
+
// explicit addOutput (the auto-injected continuation) `anfOrderedOutputs`
|
|
1110
|
+
// is empty and the input-value default is kept.
|
|
1111
|
+
const anfStateEntries = anfOrderedOutputs.filter((o) => o.kind === 'state');
|
|
1112
|
+
const singleExplicitStateSats = anfOrderedOutputs.length === 1 && anfStateEntries.length === 1
|
|
1113
|
+
? Number(anfStateEntries[0].satoshis)
|
|
1114
|
+
: undefined;
|
|
1115
|
+
newSatoshis = options?.satoshis ?? singleExplicitStateSats ?? this.currentUtxo.satoshis;
|
|
1116
|
+
if (options?.newState) {
|
|
1117
|
+
// Explicit newState takes priority (backward compat)
|
|
1118
|
+
this._state = { ...this._state, ...options.newState };
|
|
1119
|
+
}
|
|
1120
|
+
else if (methodNeedsChange && autoComputedState && autoFlatState) {
|
|
1121
|
+
const merged = { ...autoFlatState, ...autoComputedState };
|
|
1122
|
+
const regrouped = regroupFixedArrayState(merged, this.artifact.stateFields);
|
|
1123
|
+
this._state = { ...this._state, ...regrouped };
|
|
1124
|
+
}
|
|
1125
|
+
newLockingScript = this.getLockingScript();
|
|
1126
|
+
// DoS-bound: also reject pathological continuation scripts BEFORE broadcast.
|
|
1127
|
+
assertScriptHexUnderLimit(newLockingScript, InputLimits.MAX_SCRIPT_BYTES, `${this.artifact.contractName}.call(${methodName}).continuation`);
|
|
1128
|
+
}
|
|
1129
|
+
// Finding G1: a method that calls `this.addRawOutput(...)` folds the raw
|
|
1130
|
+
// output(s) into the covenant's continuation hashOutputs IN SOURCE ORDER,
|
|
1131
|
+
// interleaved with the state continuation `this.addOutput(...)`. The
|
|
1132
|
+
// single-stateful branch above only builds the state continuation, so the
|
|
1133
|
+
// built tx's outputs would mismatch hashOutputs and input 0's OP_VERIFY
|
|
1134
|
+
// would reject. Rebuild an ORDERED `contractOutputs` from the interpreter's
|
|
1135
|
+
// source-ordered output list. Purely additive: absent raw outputs this is a
|
|
1136
|
+
// no-op and existing behaviour is untouched.
|
|
1137
|
+
if (isStateful && anfOrderedOutputs.some((o) => o.kind === 'raw')) {
|
|
1138
|
+
const stateEntries = anfOrderedOutputs.filter((o) => o.kind === 'state');
|
|
1139
|
+
// Fail closed. The current SDK only builds raw outputs alongside a SINGLE
|
|
1140
|
+
// state continuation (the covenant machinery below threads exactly one
|
|
1141
|
+
// newLockingScript/newAmount). Multi-output calls, multiple continuations,
|
|
1142
|
+
// or a missing continuation script are not representable — throw rather
|
|
1143
|
+
// than silently drop outputs and strand the funds.
|
|
1144
|
+
if (hasMultiOutput || stateEntries.length >= 2 || (stateEntries.length === 1 && !newLockingScript)) {
|
|
1145
|
+
throw new Error(`RunarContract.call('${methodName}'): cannot build a transaction that interleaves ` +
|
|
1146
|
+
`raw outputs with ${stateEntries.length} state continuations; the SDK currently ` +
|
|
1147
|
+
`supports raw outputs alongside a single state continuation only (finding G1).`);
|
|
1148
|
+
}
|
|
1149
|
+
contractOutputs = anfOrderedOutputs.map((o) => o.kind === 'raw'
|
|
1150
|
+
? { script: o.script, satoshis: Number(o.satoshis) }
|
|
1151
|
+
: { script: newLockingScript, satoshis: Number(o.satoshis) });
|
|
1152
|
+
// Keep the preimage's newAmount (buildStatefulUnlock) in step with the
|
|
1153
|
+
// continuation output's sats — `this.addOutput(0n, ...)` makes it 0, not
|
|
1154
|
+
// the input value the single-stateful branch defaulted to.
|
|
1155
|
+
newSatoshis = stateEntries.length === 1 ? Number(stateEntries[0].satoshis) : newSatoshis;
|
|
1156
|
+
}
|
|
1157
|
+
const feeRate = await provider.getFeeRate();
|
|
1158
|
+
const changeScript = buildP2PKHScript(changeAddress);
|
|
1159
|
+
const allFundingUtxos = await provider.getUtxos(address);
|
|
1160
|
+
const candidateFundingUtxos = allFundingUtxos.filter((u) => !(u.txid === this.currentUtxo.txid && u.outputIndex === this.currentUtxo.outputIndex));
|
|
1161
|
+
// Resolve per-input args for additional contract inputs. Hoisted ABOVE the
|
|
1162
|
+
// funding coin-selection (was below it) so the merge unlock placeholders —
|
|
1163
|
+
// and therefore the contract-input byte size the funding fee must cover —
|
|
1164
|
+
// are known before we size the funding. Consumed again later when the real
|
|
1165
|
+
// merge unlocks are built.
|
|
1166
|
+
const resolvedPerInputArgs = options?.additionalContractInputArgs
|
|
1167
|
+
? options.additionalContractInputArgs.map((inputArgs) => {
|
|
1168
|
+
const resolved = [...inputArgs];
|
|
1169
|
+
for (let i = 0; i < userParams.length; i++) {
|
|
1170
|
+
if (userParams[i].type === 'Sig' && resolved[i] === null) {
|
|
1171
|
+
resolved[i] = '00'.repeat(72);
|
|
1172
|
+
}
|
|
1173
|
+
if (userParams[i].type === 'PubKey' && resolved[i] === null) {
|
|
1174
|
+
resolved[i] = resolvedArgs[userParams.findIndex((p) => p.type === 'PubKey')];
|
|
1175
|
+
}
|
|
1176
|
+
if (userParams[i].type === 'ByteString' && resolved[i] === null) {
|
|
1177
|
+
const estimatedInputs = 1 + (options?.additionalContractInputs?.length ?? 0) + 1;
|
|
1178
|
+
resolved[i] = '00'.repeat(36 * estimatedInputs);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return resolved;
|
|
1182
|
+
})
|
|
1183
|
+
: undefined;
|
|
1184
|
+
// Build placeholder unlocking scripts for merge inputs (witnessHex
|
|
1185
|
+
// suffixed for sizing — buildStatefulUnlock builds the real scripts).
|
|
1186
|
+
const extraUnlockPlaceholders = extraContractUtxos.map((_, i) => {
|
|
1187
|
+
const argsForPlaceholder = resolvedPerInputArgs?.[i] ?? resolvedArgs;
|
|
1188
|
+
return encodePushData('00'.repeat(72)) + this.buildUnlockingScript(methodName, argsForPlaceholder) + witnessHex;
|
|
1189
|
+
});
|
|
1190
|
+
// Coin selection for funding inputs (issue #133): don't sweep the whole
|
|
1191
|
+
// wallet. Compute how much the funding must cover — the contract's own
|
|
1192
|
+
// input value already offsets the contract/data outputs — and pick the
|
|
1193
|
+
// smallest largest-first set via selectUtxos (the strategy deploy uses).
|
|
1194
|
+
const contractOutputSats = (contractOutputs
|
|
1195
|
+
? contractOutputs.reduce((sum, o) => sum + o.satoshis, 0)
|
|
1196
|
+
: (newSatoshis ?? 0))
|
|
1197
|
+
+ resolvedDataOutputs.reduce((sum, o) => sum + o.satoshis, 0);
|
|
1198
|
+
const contractInputSats = this.currentUtxo.satoshis
|
|
1199
|
+
+ extraContractUtxos.reduce((sum, u) => sum + u.satoshis, 0);
|
|
1200
|
+
const fundingTarget = Math.max(0, contractOutputSats - contractInputSats);
|
|
1201
|
+
// Fee sizing hint for selectUtxos: the continuation script length (falls
|
|
1202
|
+
// back to the first multi-output script, else 0 for stateless calls).
|
|
1203
|
+
const fundingLockLen = (newLockingScript?.length ?? contractOutputs?.[0]?.script.length ?? 0) / 2;
|
|
1204
|
+
// Contract-input unlock bytes. selectUtxos/estimateDeployFee otherwise model
|
|
1205
|
+
// ONLY the funding inputs (148-byte P2PKH each) + continuation + change —
|
|
1206
|
+
// they are blind to the contract input(s) being spent. For a MERGE each
|
|
1207
|
+
// covenant input embeds both parent txs as method args (tens of KB), so
|
|
1208
|
+
// ignoring them under-provisions the funding; buildCallTransaction then sees
|
|
1209
|
+
// change <= 0 and DROPS the change output, and the merge covenant — which
|
|
1210
|
+
// reconstructs [continuation][P2PKH change] UNCONDITIONALLY — fails its
|
|
1211
|
+
// hashOutputs OP_VERIFY. Size the funding fee against the SAME serialized
|
|
1212
|
+
// per-input bytes buildCallTransaction uses (32 outpoint + 4 index + varint +
|
|
1213
|
+
// script + 4 sequence) for the primary contract input plus every extra one.
|
|
1214
|
+
// Over-estimating is safe (a little more funding / higher change); under-
|
|
1215
|
+
// estimating is the bug. Small ops (single covenant input, tiny unlock) add
|
|
1216
|
+
// a correspondingly small term and are unaffected.
|
|
1217
|
+
const perInputBytes = (unlockHex) => {
|
|
1218
|
+
const len = unlockHex.length / 2;
|
|
1219
|
+
const vi = len < 0xfd ? 1 : len <= 0xffff ? 3 : len <= 0xffffffff ? 5 : 9;
|
|
1220
|
+
return 32 + 4 + vi + len + 4;
|
|
1221
|
+
};
|
|
1222
|
+
// The `unlockingScript` / `extraUnlockPlaceholders` above are SIZING
|
|
1223
|
+
// placeholders: `encodePushData(sig72) + buildUnlockingScript(args) +
|
|
1224
|
+
// witnessHex`. For a stateful call the REAL unlock buildStatefulUnlock emits
|
|
1225
|
+
// is larger — it prepends the opSig codePart prefix and appends the BIP-143
|
|
1226
|
+
// preimage (whose scriptCode ≈ the locking script), the change + new-amount
|
|
1227
|
+
// pushes, and the method selector. That gap (~6.5 KB per input for the EAC
|
|
1228
|
+
// merge) is exactly what would make selectUtxos stop one UTXO short, leaving
|
|
1229
|
+
// change <= 0 so the covenant's [continuation][change] reconstruction fails.
|
|
1230
|
+
// Add a per-contract-input overestimate covering those omitted components:
|
|
1231
|
+
// codePart (≈ locking script) + preimage scriptCode (≈ locking script) + a
|
|
1232
|
+
// fixed buffer for sig/amount/selector/varint framing. Over-estimating only
|
|
1233
|
+
// pulls slightly more funding (bigger change) — always safe.
|
|
1234
|
+
const numContractInputs = 1 + extraContractUtxos.length;
|
|
1235
|
+
const perContractInputOverhead = 2 * Math.ceil(fundingLockLen) + 512;
|
|
1236
|
+
const contractInputBytes = perInputBytes(unlockingScript)
|
|
1237
|
+
+ extraUnlockPlaceholders.reduce((sum, u) => sum + perInputBytes(u), 0)
|
|
1238
|
+
+ numContractInputs * perContractInputOverhead;
|
|
1239
|
+
// C15: size the funding fee against ALL outputs, not just the single
|
|
1240
|
+
// continuation that `fundingLockLen` already covers. estimateDeployFee counts
|
|
1241
|
+
// one output of `fundingLockLen` bytes; add the framing (8 + varint +
|
|
1242
|
+
// scriptLen) of every OTHER contract output (extra multi-outputs + raw
|
|
1243
|
+
// outputs, finding G1) and every data output so selection does not stop one
|
|
1244
|
+
// UTXO short on multi-output / large-dataOutput calls. Single-output calls
|
|
1245
|
+
// net 0 (estimate unchanged). Over-estimating only pulls slightly more
|
|
1246
|
+
// funding (bigger change) — always safe.
|
|
1247
|
+
const outputFraming = (byteLen) => {
|
|
1248
|
+
const vi = byteLen < 0xfd ? 1 : byteLen <= 0xffff ? 3 : byteLen <= 0xffffffff ? 5 : 9;
|
|
1249
|
+
return 8 + vi + byteLen;
|
|
1250
|
+
};
|
|
1251
|
+
const allOutputByteLens = [
|
|
1252
|
+
...(contractOutputs
|
|
1253
|
+
? contractOutputs.map((o) => o.script.length / 2)
|
|
1254
|
+
: newLockingScript ? [newLockingScript.length / 2] : []),
|
|
1255
|
+
...resolvedDataOutputs.map((o) => o.script.length / 2),
|
|
1256
|
+
];
|
|
1257
|
+
const totalOutputFraming = allOutputByteLens.reduce((s, n) => s + outputFraming(n), 0);
|
|
1258
|
+
const extraOutputBytes = Math.max(0, totalOutputFraming - outputFraming(Math.ceil(fundingLockLen)));
|
|
1259
|
+
const additionalUtxos = candidateFundingUtxos.length > 0
|
|
1260
|
+
? selectUtxos(candidateFundingUtxos, fundingTarget, fundingLockLen, feeRate, contractInputBytes, extraOutputBytes)
|
|
1261
|
+
: [];
|
|
1262
|
+
// Cap funding inputs when the caller sets maxFundingInputs. selectUtxos
|
|
1263
|
+
// returns the minimal largest-first set; if that still exceeds the cap the
|
|
1264
|
+
// funding can't cover outputs + fee within the budget, so fail loudly
|
|
1265
|
+
// instead of broadcasting an underfunded tx.
|
|
1266
|
+
if (options?.maxFundingInputs !== undefined &&
|
|
1267
|
+
additionalUtxos.length > options.maxFundingInputs) {
|
|
1268
|
+
throw new Error(`RunarContract.call(${methodName}): funding requires ${additionalUtxos.length} input(s) ` +
|
|
1269
|
+
`but maxFundingInputs=${options.maxFundingInputs}. Increase maxFundingInputs, ` +
|
|
1270
|
+
`use larger UTXOs, or consolidate.`);
|
|
1271
|
+
}
|
|
1272
|
+
// (resolvedPerInputArgs + extraUnlockPlaceholders are computed above, before
|
|
1273
|
+
// funding coin-selection, so their byte sizes feed the funding fee estimate.)
|
|
1274
|
+
let { tx, inputCount, changeAmount } = buildCallTransaction(this.currentUtxo, unlockingScript, newLockingScript, newSatoshis, changeAddress, changeScript, additionalUtxos.length > 0 ? additionalUtxos : undefined, feeRate, {
|
|
1275
|
+
contractOutputs,
|
|
1276
|
+
additionalContractInputs: extraContractUtxos.length > 0
|
|
1277
|
+
? extraContractUtxos.map((utxo, i) => ({ utxo, unlockingScript: extraUnlockPlaceholders[i] }))
|
|
1278
|
+
: undefined,
|
|
1279
|
+
dataOutputs: resolvedDataOutputs.length > 0 ? resolvedDataOutputs : undefined,
|
|
1280
|
+
// Thread CallOptions.locktime so contracts asserting
|
|
1281
|
+
// extractLocktime(preimage) can succeed. Default unset → 0.
|
|
1282
|
+
locktime: options?.locktime,
|
|
1283
|
+
// Thread CallOptions.sequence (issue #131): a non-zero locktime needs
|
|
1284
|
+
// non-final input sequences or consensus ignores nLockTime.
|
|
1285
|
+
sequence: options?.sequence,
|
|
1286
|
+
});
|
|
1287
|
+
// Sign P2PKH funding inputs (with fundingSigner — issue #134)
|
|
1288
|
+
let txHex = tx.toHex();
|
|
1289
|
+
const p2pkhStartIdx = 1 + extraContractUtxos.length;
|
|
1290
|
+
for (let i = p2pkhStartIdx; i < inputCount; i++) {
|
|
1291
|
+
const utxo = additionalUtxos[i - p2pkhStartIdx];
|
|
1292
|
+
if (utxo) {
|
|
1293
|
+
const sig = await fundingSigner.sign(txHex, i, utxo.script, utxo.satoshis);
|
|
1294
|
+
const pubKey = await fundingSigner.getPublicKey();
|
|
1295
|
+
const unlockScript = encodePushData(sig) + encodePushData(pubKey);
|
|
1296
|
+
tx.inputs[i].unlockingScript = UnlockingScript.fromHex(unlockScript);
|
|
1297
|
+
invalidateTxCache(tx);
|
|
1298
|
+
txHex = tx.toHex();
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
let finalOpPushTxSig = '';
|
|
1302
|
+
let finalPreimage = '';
|
|
1303
|
+
if (isStateful) {
|
|
1304
|
+
const perInputArgs = options?.additionalContractInputArgs;
|
|
1305
|
+
// Helper: build a stateful unlock. For inputIdx===0 (primary), keeps
|
|
1306
|
+
// placeholder Sig params. For inputIdx>0 (extra), signs with signer.
|
|
1307
|
+
const buildStatefulUnlock = async (currentTx, inputIdx, subscript, sats, argsOverride, txChangeAmount) => {
|
|
1308
|
+
const { sigHex: opSig, preimageHex: preimage } = this.computeOpPushTxWithCodeSep(currentTx, inputIdx, subscript, sats, methodIndex);
|
|
1309
|
+
const baseArgs = argsOverride ?? resolvedArgs;
|
|
1310
|
+
const inputArgs = [...baseArgs];
|
|
1311
|
+
// Only sign Sig params for extra inputs, not the primary
|
|
1312
|
+
if (inputIdx > 0) {
|
|
1313
|
+
// Stateful: user checkSig is AFTER OP_CODESEPARATOR — use trimmed script.
|
|
1314
|
+
const trimmedSubscript = this.getSubscriptForSigning(subscript, methodIndex);
|
|
1315
|
+
const currentHex = currentTx.toHex();
|
|
1316
|
+
for (const idx of sigIndices) {
|
|
1317
|
+
inputArgs[idx] = await signer.sign(currentHex, inputIdx, trimmedSubscript, sats);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (prevoutsIndices.length > 0) {
|
|
1321
|
+
let allPrevoutsHex = '';
|
|
1322
|
+
for (const inp of currentTx.inputs) {
|
|
1323
|
+
const txidLE = inp.sourceTXID.match(/.{2}/g).reverse().join('');
|
|
1324
|
+
const voutLE = inp.sourceOutputIndex.toString(16).padStart(8, '0')
|
|
1325
|
+
.match(/.{2}/g).reverse().join('');
|
|
1326
|
+
allPrevoutsHex += txidLE + voutLE;
|
|
1327
|
+
}
|
|
1328
|
+
for (const idx of prevoutsIndices) {
|
|
1329
|
+
inputArgs[idx] = allPrevoutsHex;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
let argsHex = '';
|
|
1333
|
+
for (const arg of inputArgs)
|
|
1334
|
+
argsHex += encodeArg(arg);
|
|
1335
|
+
let changeHex = '';
|
|
1336
|
+
if (methodNeedsChange && changePKHHex) {
|
|
1337
|
+
changeHex = encodePushData(changePKHHex) + encodeArg(BigInt(txChangeAmount ?? 0));
|
|
1338
|
+
}
|
|
1339
|
+
let newAmountHex = '';
|
|
1340
|
+
if (methodNeedsNewAmount) {
|
|
1341
|
+
newAmountHex = encodeArg(BigInt(newSatoshis ?? this.currentUtxo.satoshis));
|
|
1342
|
+
}
|
|
1343
|
+
const unlock = this.buildStatefulPrefix(opSig, methodUsesCodePart) + argsHex + changeHex + newAmountHex + encodePushData(preimage) + witnessHex + methodSelectorHex;
|
|
1344
|
+
return { unlock, opSig, preimage };
|
|
1345
|
+
};
|
|
1346
|
+
// First pass
|
|
1347
|
+
const { unlock: input0Unlock } = await buildStatefulUnlock(tx, 0, contractUtxo.script, contractUtxo.satoshis, undefined, changeAmount);
|
|
1348
|
+
const extraUnlocks = [];
|
|
1349
|
+
for (let i = 0; i < extraContractUtxos.length; i++) {
|
|
1350
|
+
const mu = extraContractUtxos[i];
|
|
1351
|
+
const extraArgs = perInputArgs?.[i] ? resolvedPerInputArgs?.[i] : undefined;
|
|
1352
|
+
const { unlock } = await buildStatefulUnlock(tx, i + 1, mu.script, mu.satoshis, extraArgs, changeAmount);
|
|
1353
|
+
extraUnlocks.push(unlock);
|
|
1354
|
+
}
|
|
1355
|
+
// Rebuild TX with real unlocking scripts.
|
|
1356
|
+
//
|
|
1357
|
+
// Deep-review finding C13: the funding coin-selection above ran against
|
|
1358
|
+
// an ESTIMATE (placeholder unlock + the `perContractInputOverhead`
|
|
1359
|
+
// heuristic), not this REAL unlock — and there is no re-selection pass
|
|
1360
|
+
// here if the real size turns out bigger than estimated. Investigated:
|
|
1361
|
+
// this IS reachable (e.g. a stateful method whose current — spent —
|
|
1362
|
+
// locking script is far larger than its new/continuation one, since the
|
|
1363
|
+
// heuristic only has the NEW script's length to go on; see
|
|
1364
|
+
// `c13-call-funding-fee-bound.test.ts`). It can NEVER produce an
|
|
1365
|
+
// invalid or overspending tx, though: `buildCallTransaction` always
|
|
1366
|
+
// computes fee/change from these REAL bytes (not the estimate), and
|
|
1367
|
+
// finding C3's fail-closed guard guarantees `totalInput >=
|
|
1368
|
+
// contractOutputSats`, so the worst case is a lower-than-intended fee
|
|
1369
|
+
// down to (and never below) 0 — the same zero-fee "exact cover" floor
|
|
1370
|
+
// issue #116 already established as valid/intentional. No re-selection
|
|
1371
|
+
// loop is added here; see the linked test for the invariant this bound
|
|
1372
|
+
// relies on.
|
|
1373
|
+
({ tx, inputCount, changeAmount } = buildCallTransaction(this.currentUtxo, input0Unlock, newLockingScript, newSatoshis, changeAddress, changeScript, additionalUtxos.length > 0 ? additionalUtxos : undefined, feeRate, {
|
|
1374
|
+
contractOutputs,
|
|
1375
|
+
additionalContractInputs: extraContractUtxos.length > 0
|
|
1376
|
+
? extraContractUtxos.map((utxo, i) => ({ utxo, unlockingScript: extraUnlocks[i] }))
|
|
1377
|
+
: undefined,
|
|
1378
|
+
dataOutputs: resolvedDataOutputs.length > 0 ? resolvedDataOutputs : undefined,
|
|
1379
|
+
// Rebuild path must honor the override too: a preimage computed on a
|
|
1380
|
+
// rebuilt tx with locktime 0 would mismatch the final on-chain tx.
|
|
1381
|
+
locktime: options?.locktime,
|
|
1382
|
+
// Same for sequence — the second-pass preimage must see the final
|
|
1383
|
+
// input sequences (issue #131).
|
|
1384
|
+
sequence: options?.sequence,
|
|
1385
|
+
}));
|
|
1386
|
+
// Second pass: recompute with final tx
|
|
1387
|
+
const { unlock: finalInput0Unlock, opSig, preimage } = await buildStatefulUnlock(tx, 0, contractUtxo.script, contractUtxo.satoshis, undefined, changeAmount);
|
|
1388
|
+
finalOpPushTxSig = opSig;
|
|
1389
|
+
finalPreimage = preimage;
|
|
1390
|
+
tx.inputs[0].unlockingScript = UnlockingScript.fromHex(finalInput0Unlock);
|
|
1391
|
+
invalidateTxCache(tx);
|
|
1392
|
+
for (let i = 0; i < extraContractUtxos.length; i++) {
|
|
1393
|
+
const mu = extraContractUtxos[i];
|
|
1394
|
+
const extraArgs = perInputArgs?.[i] ? resolvedPerInputArgs?.[i] : undefined;
|
|
1395
|
+
const { unlock: finalMergeUnlock } = await buildStatefulUnlock(tx, i + 1, mu.script, mu.satoshis, extraArgs, changeAmount);
|
|
1396
|
+
tx.inputs[i + 1].unlockingScript = UnlockingScript.fromHex(finalMergeUnlock);
|
|
1397
|
+
invalidateTxCache(tx);
|
|
1398
|
+
}
|
|
1399
|
+
// Re-sign P2PKH funding inputs (with fundingSigner — issue #134)
|
|
1400
|
+
txHex = tx.toHex();
|
|
1401
|
+
for (let i = p2pkhStartIdx; i < inputCount; i++) {
|
|
1402
|
+
const utxo = additionalUtxos[i - p2pkhStartIdx];
|
|
1403
|
+
if (utxo) {
|
|
1404
|
+
const sig = await fundingSigner.sign(txHex, i, utxo.script, utxo.satoshis);
|
|
1405
|
+
const pubKey = await fundingSigner.getPublicKey();
|
|
1406
|
+
const unlockScript = encodePushData(sig) + encodePushData(pubKey);
|
|
1407
|
+
tx.inputs[i].unlockingScript = UnlockingScript.fromHex(unlockScript);
|
|
1408
|
+
invalidateTxCache(tx);
|
|
1409
|
+
txHex = tx.toHex();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
// Update resolvedArgs with real prevouts so finalizeCall can
|
|
1413
|
+
// rebuild the primary unlock with correct allPrevouts values.
|
|
1414
|
+
if (prevoutsIndices.length > 0) {
|
|
1415
|
+
let allPrevoutsHex = '';
|
|
1416
|
+
for (const inp of tx.inputs) {
|
|
1417
|
+
const txidLE = inp.sourceTXID.match(/.{2}/g).reverse().join('');
|
|
1418
|
+
const voutLE = inp.sourceOutputIndex.toString(16).padStart(8, '0')
|
|
1419
|
+
.match(/.{2}/g).reverse().join('');
|
|
1420
|
+
allPrevoutsHex += txidLE + voutLE;
|
|
1421
|
+
}
|
|
1422
|
+
for (const idx of prevoutsIndices) {
|
|
1423
|
+
resolvedArgs[idx] = allPrevoutsHex;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else if (needsOpPushTx || sigIndices.length > 0) {
|
|
1428
|
+
// Stateless: keep placeholder sigs, compute OP_PUSH_TX
|
|
1429
|
+
if (needsOpPushTx) {
|
|
1430
|
+
const { sigHex, preimageHex } = this.computeOpPushTxWithCodeSep(tx, 0, contractUtxo.script, contractUtxo.satoshis, methodIndex);
|
|
1431
|
+
finalOpPushTxSig = sigHex;
|
|
1432
|
+
resolvedArgs[preimageIndex] = preimageHex;
|
|
1433
|
+
}
|
|
1434
|
+
// Don't sign Sig params — keep placeholders
|
|
1435
|
+
let realUnlockingScript = this.buildUnlockingScript(methodName, resolvedArgs);
|
|
1436
|
+
if (needsOpPushTx && finalOpPushTxSig) {
|
|
1437
|
+
realUnlockingScript = this.buildStatefulPrefix(finalOpPushTxSig) + realUnlockingScript;
|
|
1438
|
+
tx.inputs[0].unlockingScript = UnlockingScript.fromHex(realUnlockingScript);
|
|
1439
|
+
invalidateTxCache(tx);
|
|
1440
|
+
const { sigHex: finalSig, preimageHex: finalPre } = this.computeOpPushTxWithCodeSep(tx, 0, contractUtxo.script, contractUtxo.satoshis, methodIndex);
|
|
1441
|
+
resolvedArgs[preimageIndex] = finalPre;
|
|
1442
|
+
finalOpPushTxSig = finalSig;
|
|
1443
|
+
finalPreimage = finalPre;
|
|
1444
|
+
realUnlockingScript = this.buildStatefulPrefix(finalSig) +
|
|
1445
|
+
this.buildUnlockingScript(methodName, resolvedArgs);
|
|
1446
|
+
}
|
|
1447
|
+
tx.inputs[0].unlockingScript = UnlockingScript.fromHex(realUnlockingScript);
|
|
1448
|
+
invalidateTxCache(tx);
|
|
1449
|
+
if (!finalPreimage && needsOpPushTx) {
|
|
1450
|
+
finalPreimage = resolvedArgs[preimageIndex];
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
// Compute sighash from preimage (C19: the true BIP-143 digest external
|
|
1454
|
+
// signers must ECDSA-sign — hash256(preimage), not sha256(preimage)).
|
|
1455
|
+
const sighash = finalPreimage ? computeBip143Sighash(finalPreimage) : '';
|
|
1456
|
+
return {
|
|
1457
|
+
sighash,
|
|
1458
|
+
preimage: finalPreimage,
|
|
1459
|
+
opPushTxSig: finalOpPushTxSig,
|
|
1460
|
+
tx,
|
|
1461
|
+
sigIndices,
|
|
1462
|
+
_methodName: methodName,
|
|
1463
|
+
_resolvedArgs: resolvedArgs,
|
|
1464
|
+
_methodSelectorHex: methodSelectorHex,
|
|
1465
|
+
_isStateful: isStateful,
|
|
1466
|
+
_parentStateful: parentStateful,
|
|
1467
|
+
_isTerminal: false,
|
|
1468
|
+
_needsOpPushTx: needsOpPushTx,
|
|
1469
|
+
_methodNeedsChange: methodNeedsChange,
|
|
1470
|
+
_methodUsesCodePart: methodUsesCodePart,
|
|
1471
|
+
_changePKHHex: changePKHHex,
|
|
1472
|
+
_changeAmount: changeAmount,
|
|
1473
|
+
_methodNeedsNewAmount: methodNeedsNewAmount,
|
|
1474
|
+
_newAmount: newSatoshis ?? this.currentUtxo.satoshis,
|
|
1475
|
+
_preimageIndex: preimageIndex,
|
|
1476
|
+
_contractUtxo: contractUtxo,
|
|
1477
|
+
_newLockingScript: newLockingScript ?? '',
|
|
1478
|
+
_newSatoshis: newSatoshis ?? 0,
|
|
1479
|
+
_hasMultiOutput: !!hasMultiOutput,
|
|
1480
|
+
_contractOutputs: contractOutputs ?? [],
|
|
1481
|
+
_intentWitnessHex: witnessHex,
|
|
1482
|
+
_dryRun: options?.dryRun ?? false,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Complete a prepared call by injecting external signatures and broadcasting.
|
|
1487
|
+
*
|
|
1488
|
+
* @param prepared — The `PreparedCall` returned by `prepareCall()`.
|
|
1489
|
+
* @param signatures — Map from arg index to DER signature hex (with sighash byte).
|
|
1490
|
+
* Each key must be one of `prepared.sigIndices`.
|
|
1491
|
+
*/
|
|
1492
|
+
async finalizeCall(prepared, signatures) {
|
|
1493
|
+
// C29: one-shot guard. Mark consumed BEFORE any mutation/broadcast work
|
|
1494
|
+
// so a second call fails fast regardless of whether the first attempt
|
|
1495
|
+
// succeeded, was rejected by the C8 dry-run, or threw during broadcast —
|
|
1496
|
+
// `prepared.tx` may already have been mutated by that first attempt in
|
|
1497
|
+
// every one of those cases.
|
|
1498
|
+
if (consumedPreparedCalls.has(prepared)) {
|
|
1499
|
+
throw new Error('RunarContract.finalizeCall: this PreparedCall has already been finalized (deep-review C29). ' +
|
|
1500
|
+
'A PreparedCall is one-shot — call prepareCall() again to build a fresh one.');
|
|
1501
|
+
}
|
|
1502
|
+
consumedPreparedCalls.add(prepared);
|
|
1503
|
+
const { provider } = this.resolveProviderSigner();
|
|
1504
|
+
// Replace placeholder sigs with real signatures
|
|
1505
|
+
const resolvedArgs = [...prepared._resolvedArgs];
|
|
1506
|
+
for (const idx of prepared.sigIndices) {
|
|
1507
|
+
if (signatures[idx] !== undefined) {
|
|
1508
|
+
resolvedArgs[idx] = signatures[idx];
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
// Assemble the primary unlocking script
|
|
1512
|
+
let primaryUnlock;
|
|
1513
|
+
if (prepared._isStateful) {
|
|
1514
|
+
let argsHex = '';
|
|
1515
|
+
for (const arg of resolvedArgs)
|
|
1516
|
+
argsHex += encodeArg(arg);
|
|
1517
|
+
let changeHex = '';
|
|
1518
|
+
if (prepared._methodNeedsChange && prepared._changePKHHex) {
|
|
1519
|
+
changeHex = encodePushData(prepared._changePKHHex) +
|
|
1520
|
+
encodeArg(BigInt(prepared._changeAmount));
|
|
1521
|
+
}
|
|
1522
|
+
let newAmountHex = '';
|
|
1523
|
+
if (prepared._methodNeedsNewAmount) {
|
|
1524
|
+
newAmountHex = encodeArg(BigInt(prepared._newAmount));
|
|
1525
|
+
}
|
|
1526
|
+
primaryUnlock =
|
|
1527
|
+
this.buildStatefulPrefix(prepared.opPushTxSig, prepared._methodUsesCodePart ?? prepared._methodNeedsChange) +
|
|
1528
|
+
argsHex +
|
|
1529
|
+
changeHex +
|
|
1530
|
+
newAmountHex +
|
|
1531
|
+
encodePushData(prepared.preimage) +
|
|
1532
|
+
prepared._intentWitnessHex +
|
|
1533
|
+
prepared._methodSelectorHex;
|
|
1534
|
+
}
|
|
1535
|
+
else if (prepared._needsOpPushTx) {
|
|
1536
|
+
// Stateless with SigHashPreimage: put preimage into resolvedArgs
|
|
1537
|
+
if (prepared._preimageIndex >= 0) {
|
|
1538
|
+
resolvedArgs[prepared._preimageIndex] = prepared.preimage;
|
|
1539
|
+
}
|
|
1540
|
+
primaryUnlock = this.buildStatefulPrefix(prepared.opPushTxSig) +
|
|
1541
|
+
this.buildUnlockingScript(prepared._methodName, resolvedArgs);
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
primaryUnlock = this.buildUnlockingScript(prepared._methodName, resolvedArgs);
|
|
1545
|
+
}
|
|
1546
|
+
// Insert primary unlock into the transaction
|
|
1547
|
+
const finalTx = prepared.tx;
|
|
1548
|
+
finalTx.inputs[0].unlockingScript = UnlockingScript.fromHex(primaryUnlock);
|
|
1549
|
+
invalidateTxCache(finalTx);
|
|
1550
|
+
// C8: local pre-broadcast dry-run. Replay the fully-signed primary
|
|
1551
|
+
// contract input through @bsv/sdk's production Spend interpreter and fail
|
|
1552
|
+
// closed — instead of broadcasting a tx the network would reject — unless
|
|
1553
|
+
// the caller opted IN (CallOptions.dryRun, threaded via prepareCall).
|
|
1554
|
+
// Opt-in rather than default-on: see the CallOptions.dryRun docstring for
|
|
1555
|
+
// the false-rejection evidence that forced this polarity.
|
|
1556
|
+
if (prepared._dryRun) {
|
|
1557
|
+
const dryRun = dryRunContractInput(finalTx, 0, prepared._contractUtxo.script, prepared._contractUtxo.satoshis);
|
|
1558
|
+
if (!dryRun.valid) {
|
|
1559
|
+
throw new Error(`RunarContract.finalizeCall: local pre-broadcast dry-run rejected the primary ` +
|
|
1560
|
+
`contract input (deep-review C8)${dryRun.error ? `: ${dryRun.error}` : ' (script evaluated to false)'}. ` +
|
|
1561
|
+
`Refusing to broadcast a tx the network would reject. Omit { dryRun: true } in ` +
|
|
1562
|
+
`CallOptions to bypass (see PreparedCall docs).`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// Broadcast
|
|
1566
|
+
const txid = await provider.broadcast(finalTx);
|
|
1567
|
+
// Update tracked UTXO
|
|
1568
|
+
if (prepared._isStateful && prepared._hasMultiOutput && prepared._contractOutputs.length > 0) {
|
|
1569
|
+
this.currentUtxo = {
|
|
1570
|
+
txid,
|
|
1571
|
+
outputIndex: 0,
|
|
1572
|
+
satoshis: prepared._contractOutputs[0].satoshis,
|
|
1573
|
+
script: prepared._contractOutputs[0].script,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
else if (prepared._isStateful && prepared._newLockingScript) {
|
|
1577
|
+
// The state continuation is normally output 0, but a method that also
|
|
1578
|
+
// calls `this.addRawOutput(...)` (finding G1) can push raw outputs ahead
|
|
1579
|
+
// of it. `_contractOutputs`, when populated (the raw-output path),
|
|
1580
|
+
// records the real source order and each output's satoshis — so track the
|
|
1581
|
+
// continuation at its actual index and value (which may legitimately be
|
|
1582
|
+
// 0). Empty `_contractOutputs` (the common case) keeps the legacy
|
|
1583
|
+
// index-0 / `_newSatoshis`-fallback behaviour byte-for-byte unchanged.
|
|
1584
|
+
const contIdx = prepared._contractOutputs.length > 0
|
|
1585
|
+
? prepared._contractOutputs.findIndex((o) => o.script === prepared._newLockingScript)
|
|
1586
|
+
: -1;
|
|
1587
|
+
this.currentUtxo = contIdx >= 0
|
|
1588
|
+
? {
|
|
1589
|
+
txid,
|
|
1590
|
+
outputIndex: contIdx,
|
|
1591
|
+
satoshis: prepared._contractOutputs[contIdx].satoshis,
|
|
1592
|
+
script: prepared._newLockingScript,
|
|
1593
|
+
}
|
|
1594
|
+
: {
|
|
1595
|
+
txid,
|
|
1596
|
+
outputIndex: 0,
|
|
1597
|
+
satoshis: prepared._newSatoshis || prepared._contractUtxo.satoshis,
|
|
1598
|
+
script: prepared._newLockingScript,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
else if (prepared._isTerminal) {
|
|
1602
|
+
this.currentUtxo = null;
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
this.currentUtxo = null;
|
|
1606
|
+
}
|
|
1607
|
+
const txData = await provider.getTransaction(txid).catch((err) => {
|
|
1608
|
+
// Audit finding C4 — see the peer fallback in `deploy()`.
|
|
1609
|
+
console.warn('Failed to fetch transaction after broadcast:', err);
|
|
1610
|
+
return txToTransactionData(txid, finalTx);
|
|
1611
|
+
});
|
|
1612
|
+
return { txid, tx: txData };
|
|
1613
|
+
}
|
|
1614
|
+
// -------------------------------------------------------------------------
|
|
1615
|
+
// State access
|
|
1616
|
+
// -------------------------------------------------------------------------
|
|
1617
|
+
/** Get the current contract state (for stateful contracts). */
|
|
1618
|
+
get state() {
|
|
1619
|
+
return { ...this._state };
|
|
1620
|
+
}
|
|
1621
|
+
/** Update state values directly (for stateful contracts). */
|
|
1622
|
+
setState(newState) {
|
|
1623
|
+
this._state = { ...this._state, ...newState };
|
|
1624
|
+
}
|
|
1625
|
+
// -------------------------------------------------------------------------
|
|
1626
|
+
// Intent-intrinsic witness values
|
|
1627
|
+
// -------------------------------------------------------------------------
|
|
1628
|
+
/**
|
|
1629
|
+
* Supply the prev-output locking-script witness for input `inputIndex`.
|
|
1630
|
+
* Required for methods that call `extractPrevOutputScript(inputIndex)`,
|
|
1631
|
+
* which the compiler lowers into an auto-injected
|
|
1632
|
+
* `_prevOutScript_<inputIndex>` ABI param.
|
|
1633
|
+
*
|
|
1634
|
+
* @param inputIndex the literal input index passed to extractPrevOutputScript
|
|
1635
|
+
* @param bytes hex string (with or without 0x prefix) or raw bytes
|
|
1636
|
+
*/
|
|
1637
|
+
setPrevOutScript(inputIndex, bytes) {
|
|
1638
|
+
this._prevOutScripts.set(inputIndex, normalizeWitnessBytes(bytes));
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Supply the serialised-outputs witness for the current call.
|
|
1642
|
+
* Required for methods that call `requireOutputP2PKH(...)`, which the
|
|
1643
|
+
* compiler lowers into an auto-injected `_serialisedOutputs` ABI param.
|
|
1644
|
+
*
|
|
1645
|
+
* @param bytes hex string (with or without 0x prefix) or raw bytes
|
|
1646
|
+
*/
|
|
1647
|
+
setSerialisedOutputs(bytes) {
|
|
1648
|
+
this._serialisedOutputs = normalizeWitnessBytes(bytes);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Build the trailing witness-hex for the auto-injected intent-intrinsic
|
|
1652
|
+
* params of a method, in ABI order (`_prevOutScript_*` first, then
|
|
1653
|
+
* `_serialisedOutputs`). Each value is pushed via PUSHDATA so that the
|
|
1654
|
+
* on-chain method body's `load_param` lifts the exact bytes the caller set.
|
|
1655
|
+
*
|
|
1656
|
+
* Throws {@link WitnessValueMissingError} for any auto-injected param the
|
|
1657
|
+
* caller hasn't supplied via setPrevOutScript / setSerialisedOutputs.
|
|
1658
|
+
*/
|
|
1659
|
+
buildIntentWitnessHex(method) {
|
|
1660
|
+
let hex = '';
|
|
1661
|
+
for (const p of method.params) {
|
|
1662
|
+
if (p.name.startsWith('_prevOutScript_')) {
|
|
1663
|
+
const idxStr = p.name.slice('_prevOutScript_'.length);
|
|
1664
|
+
const idx = Number(idxStr);
|
|
1665
|
+
const val = this._prevOutScripts.get(idx);
|
|
1666
|
+
if (val === undefined) {
|
|
1667
|
+
throw new WitnessValueMissingError({
|
|
1668
|
+
paramName: p.name,
|
|
1669
|
+
methodName: method.name,
|
|
1670
|
+
contractName: this.artifact.contractName,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
hex += encodePushData(val);
|
|
1674
|
+
}
|
|
1675
|
+
else if (p.name === '_serialisedOutputs') {
|
|
1676
|
+
if (this._serialisedOutputs === null) {
|
|
1677
|
+
throw new WitnessValueMissingError({
|
|
1678
|
+
paramName: p.name,
|
|
1679
|
+
methodName: method.name,
|
|
1680
|
+
contractName: this.artifact.contractName,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
hex += encodePushData(this._serialisedOutputs);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return hex;
|
|
1687
|
+
}
|
|
1688
|
+
// -------------------------------------------------------------------------
|
|
1689
|
+
// Script construction
|
|
1690
|
+
// -------------------------------------------------------------------------
|
|
1691
|
+
/**
|
|
1692
|
+
* Get the full locking script hex for the contract.
|
|
1693
|
+
*
|
|
1694
|
+
* For stateful contracts this includes the code followed by OP_RETURN and
|
|
1695
|
+
* the serialized state fields.
|
|
1696
|
+
*/
|
|
1697
|
+
getLockingScript() {
|
|
1698
|
+
// Use stored code script from chain if available (reconnected contract).
|
|
1699
|
+
// When loaded from chain, _codeScript already contains the inscription
|
|
1700
|
+
// envelope (if any). When built from the template, we splice it in.
|
|
1701
|
+
const builtFromTemplate = this._codeScript === null;
|
|
1702
|
+
let script = this._codeScript ?? this.buildCodeScript();
|
|
1703
|
+
// Inject inscription envelope between code and state (template-built only;
|
|
1704
|
+
// chain-loaded _codeScript already includes it).
|
|
1705
|
+
if (builtFromTemplate && this._inscription) {
|
|
1706
|
+
script += buildInscriptionEnvelope(this._inscription.contentType, this._inscription.data);
|
|
1707
|
+
}
|
|
1708
|
+
// Append state section for stateful contracts
|
|
1709
|
+
if (this.artifact.stateFields && this.artifact.stateFields.length > 0) {
|
|
1710
|
+
const stateHex = serializeState(this.artifact.stateFields, this._state);
|
|
1711
|
+
if (stateHex.length > 0) {
|
|
1712
|
+
script += '6a'; // OP_RETURN
|
|
1713
|
+
script += stateHex;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return script;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Build the code portion of the locking script from the artifact and
|
|
1720
|
+
* constructor args. This is the script without any state suffix.
|
|
1721
|
+
*/
|
|
1722
|
+
buildCodeScript() {
|
|
1723
|
+
let script = this.artifact.script;
|
|
1724
|
+
const hasConstructorSlots = this.artifact.constructorSlots && this.artifact.constructorSlots.length > 0;
|
|
1725
|
+
const hasCodeSepSlots = this.artifact.codeSepIndexSlots && this.artifact.codeSepIndexSlots.length > 0;
|
|
1726
|
+
if (hasConstructorSlots || hasCodeSepSlots) {
|
|
1727
|
+
const subs = [];
|
|
1728
|
+
// Constructor arg slots: replace OP_0 placeholder with encoded arg
|
|
1729
|
+
if (hasConstructorSlots) {
|
|
1730
|
+
for (const slot of this.artifact.constructorSlots) {
|
|
1731
|
+
subs.push({
|
|
1732
|
+
byteOffset: slot.byteOffset,
|
|
1733
|
+
encoded: encodeArg(this.constructorArgs[slot.paramIndex]),
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
// CodeSepIndex slots: replace OP_0 placeholder with encoded adjusted
|
|
1738
|
+
// codeSeparatorIndex. The adjusted value accounts for constructor arg
|
|
1739
|
+
// expansion AND earlier codeSepIndex slot expansions that shift
|
|
1740
|
+
// OP_CODESEPARATOR positions in the substituted script.
|
|
1741
|
+
if (hasCodeSepSlots) {
|
|
1742
|
+
const resolved = this._resolvedCodeSepSlotValues();
|
|
1743
|
+
for (const rs of resolved) {
|
|
1744
|
+
subs.push({
|
|
1745
|
+
byteOffset: rs.templateByteOffset,
|
|
1746
|
+
encoded: encodeScriptNumber(BigInt(rs.adjustedValue)),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
// Sort descending by byte offset and apply
|
|
1751
|
+
subs.sort((a, b) => b.byteOffset - a.byteOffset);
|
|
1752
|
+
for (const sub of subs) {
|
|
1753
|
+
const hexOffset = sub.byteOffset * 2;
|
|
1754
|
+
// Replace the 1-byte OP_0 placeholder (2 hex chars) with the encoded value
|
|
1755
|
+
script = script.slice(0, hexOffset) + sub.encoded + script.slice(hexOffset + 2);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
else if (!this.artifact.stateFields || this.artifact.stateFields.length === 0) {
|
|
1759
|
+
// Backward compatibility: old stateless artifacts without constructorSlots.
|
|
1760
|
+
// For stateful contracts, constructor args initialize the state section
|
|
1761
|
+
// (after OP_RETURN), not the code portion.
|
|
1762
|
+
for (const arg of this.constructorArgs) {
|
|
1763
|
+
script += encodeArg(arg);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return script;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Build the unlocking script for a method call.
|
|
1770
|
+
*
|
|
1771
|
+
* The unlocking script pushes the method arguments onto the stack in
|
|
1772
|
+
* order, followed by a method selector (the method index as a Script
|
|
1773
|
+
* number) if the contract has multiple public methods.
|
|
1774
|
+
*/
|
|
1775
|
+
buildUnlockingScript(methodName, args) {
|
|
1776
|
+
let script = '';
|
|
1777
|
+
// Push each argument
|
|
1778
|
+
for (const arg of args) {
|
|
1779
|
+
script += encodeArg(arg);
|
|
1780
|
+
}
|
|
1781
|
+
// If there are multiple public methods, push the method selector
|
|
1782
|
+
const publicMethods = this.artifact.abi.methods.filter((m) => m.isPublic);
|
|
1783
|
+
if (publicMethods.length > 1) {
|
|
1784
|
+
const methodIndex = publicMethods.findIndex((m) => m.name === methodName);
|
|
1785
|
+
if (methodIndex < 0) {
|
|
1786
|
+
throw new Error(`buildUnlockingScript: public method '${methodName}' not found`);
|
|
1787
|
+
}
|
|
1788
|
+
script += encodeScriptNumber(BigInt(methodIndex));
|
|
1789
|
+
}
|
|
1790
|
+
return script;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Get the code script hex (locking script without state) for use as _codePart.
|
|
1794
|
+
* Returns the code portion that the on-chain contract uses for output reconstruction.
|
|
1795
|
+
* Includes the inscription envelope if one is attached — this is required for
|
|
1796
|
+
* stateful contracts where the on-chain hashOutputs verification includes
|
|
1797
|
+
* the envelope as part of the codePart.
|
|
1798
|
+
*
|
|
1799
|
+
* Public low-level assembly surface: needed for cross-artifact transaction
|
|
1800
|
+
* assembly (see `assembleMultiContractCall`), where a spending tx mixes
|
|
1801
|
+
* covenant inputs from DIFFERENT artifacts and each input's `_codePart` /
|
|
1802
|
+
* continuation script must be built outside `call()`.
|
|
1803
|
+
*/
|
|
1804
|
+
getCodePartHex() {
|
|
1805
|
+
if (this._codeScript)
|
|
1806
|
+
return this._codeScript;
|
|
1807
|
+
let code = this.buildCodeScript();
|
|
1808
|
+
if (this._inscription) {
|
|
1809
|
+
code += buildInscriptionEnvelope(this._inscription.contentType, this._inscription.data);
|
|
1810
|
+
}
|
|
1811
|
+
return code;
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Adjust a code separator byte offset from the base (template) script to
|
|
1815
|
+
* the fully-substituted script. Both constructor arg slots and codeSepIndex
|
|
1816
|
+
* slots replace OP_0 (1 byte) with encoded push data, shifting subsequent
|
|
1817
|
+
* byte offsets.
|
|
1818
|
+
*/
|
|
1819
|
+
adjustCodeSepOffset(baseOffset) {
|
|
1820
|
+
let shift = 0;
|
|
1821
|
+
if (this.artifact.constructorSlots) {
|
|
1822
|
+
for (const slot of this.artifact.constructorSlots) {
|
|
1823
|
+
if (slot.byteOffset < baseOffset) {
|
|
1824
|
+
const encoded = encodeArg(this.constructorArgs[slot.paramIndex]);
|
|
1825
|
+
shift += encoded.length / 2 - 1; // encoded bytes minus the 1-byte OP_0 placeholder
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Account for codeSepIndex slot expansions. Each slot's encoded value
|
|
1830
|
+
// is the fully-adjusted codeSep index, computed by resolveCodeSepSlotValues.
|
|
1831
|
+
const resolvedSlots = this._resolvedCodeSepSlotValues();
|
|
1832
|
+
for (const rs of resolvedSlots) {
|
|
1833
|
+
if (rs.templateByteOffset < baseOffset) {
|
|
1834
|
+
const encoded = encodeScriptNumber(BigInt(rs.adjustedValue));
|
|
1835
|
+
shift += encoded.length / 2 - 1;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return baseOffset + shift;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Resolve the adjusted codeSep index values for all codeSepIndex slots,
|
|
1842
|
+
* processing them in ascending template byte-offset order so that each
|
|
1843
|
+
* slot's value correctly accounts for earlier slots' expansions.
|
|
1844
|
+
*/
|
|
1845
|
+
_resolvedCodeSepSlotValues() {
|
|
1846
|
+
if (!this.artifact.codeSepIndexSlots || this.artifact.codeSepIndexSlots.length === 0) {
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
// Sort by template byte offset ascending (left-to-right in the script)
|
|
1850
|
+
const sorted = [...this.artifact.codeSepIndexSlots].sort((a, b) => a.byteOffset - b.byteOffset);
|
|
1851
|
+
const result = [];
|
|
1852
|
+
for (const slot of sorted) {
|
|
1853
|
+
// Compute the fully-adjusted codeSep index: constructor expansion +
|
|
1854
|
+
// expansion from earlier codeSepIndex slots that precede this slot's codeSepIndex.
|
|
1855
|
+
let shift = 0;
|
|
1856
|
+
if (this.artifact.constructorSlots) {
|
|
1857
|
+
for (const cs of this.artifact.constructorSlots) {
|
|
1858
|
+
if (cs.byteOffset < slot.codeSepIndex) {
|
|
1859
|
+
const encoded = encodeArg(this.constructorArgs[cs.paramIndex]);
|
|
1860
|
+
shift += encoded.length / 2 - 1;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
for (const prev of result) {
|
|
1865
|
+
if (prev.templateByteOffset < slot.codeSepIndex) {
|
|
1866
|
+
const prevEncoded = encodeScriptNumber(BigInt(prev.adjustedValue));
|
|
1867
|
+
shift += prevEncoded.length / 2 - 1;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
result.push({ templateByteOffset: slot.byteOffset, adjustedValue: slot.codeSepIndex + shift });
|
|
1871
|
+
}
|
|
1872
|
+
return result;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Get the subscript trimmed at the OP_CODESEPARATOR for a given method.
|
|
1876
|
+
* Used for BIP-143 sighash computation for the user CHECKSIG whenever the
|
|
1877
|
+
* script contains an OP_CODESEPARATOR before it (per BIP-143 / BSV consensus,
|
|
1878
|
+
* for stateful AND terminal methods alike). Returns the full script unchanged
|
|
1879
|
+
* when no OP_CODESEPARATOR is present.
|
|
1880
|
+
*
|
|
1881
|
+
* When `_codeScript` is set (the contract is loaded from chain, or the deploy
|
|
1882
|
+
* script has already been built from real constructor args), walk the actual
|
|
1883
|
+
* script and trim at the true on-chain byte position. This is required
|
|
1884
|
+
* because `fromTxid` populates constructorArgs with dummy placeholders — the
|
|
1885
|
+
* real arg bytes are already baked into the on-chain locking script — so
|
|
1886
|
+
* `adjustCodeSepOffset` computes a shift of zero and returns the wrong offset
|
|
1887
|
+
* whenever the OP_CODESEPARATOR sits after constructor slots that expand at
|
|
1888
|
+
* deploy time. The symptom of using the wrong offset is NULLFAIL at
|
|
1889
|
+
* OP_CHECKSIG for terminal methods.
|
|
1890
|
+
*
|
|
1891
|
+
* Public low-level assembly surface: multi-contract tx assembly must sign
|
|
1892
|
+
* each covenant input's user `Sig` over ITS artifact's codesep-trimmed
|
|
1893
|
+
* subscript, which `call()` only does for its own single primary input.
|
|
1894
|
+
*/
|
|
1895
|
+
getSubscriptForSigning(fullScript, methodIndex) {
|
|
1896
|
+
if (this._codeScript !== null) {
|
|
1897
|
+
const realOffsets = findCodesepOffsets(this._codeScript);
|
|
1898
|
+
if (realOffsets.length > 0) {
|
|
1899
|
+
const indices = this.artifact.codeSeparatorIndices;
|
|
1900
|
+
let off;
|
|
1901
|
+
if (indices && methodIndex !== undefined && methodIndex < indices.length
|
|
1902
|
+
&& methodIndex < realOffsets.length) {
|
|
1903
|
+
off = realOffsets[methodIndex];
|
|
1904
|
+
}
|
|
1905
|
+
else if (this.artifact.codeSeparatorIndex !== undefined) {
|
|
1906
|
+
off = realOffsets[0];
|
|
1907
|
+
}
|
|
1908
|
+
if (off !== undefined) {
|
|
1909
|
+
return fullScript.slice((off + 1) * 2);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const indices = this.artifact.codeSeparatorIndices;
|
|
1914
|
+
let codeSepIdx;
|
|
1915
|
+
if (indices && methodIndex !== undefined && methodIndex < indices.length) {
|
|
1916
|
+
codeSepIdx = indices[methodIndex];
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
codeSepIdx = this.artifact.codeSeparatorIndex;
|
|
1920
|
+
}
|
|
1921
|
+
if (codeSepIdx !== undefined) {
|
|
1922
|
+
codeSepIdx = this.adjustCodeSepOffset(codeSepIdx);
|
|
1923
|
+
// Skip past the separator byte (+1 byte = +2 hex chars)
|
|
1924
|
+
return fullScript.slice((codeSepIdx + 1) * 2);
|
|
1925
|
+
}
|
|
1926
|
+
return fullScript;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Wrap computeOpPushTx to automatically pass the correct codeSeparatorIndex.
|
|
1930
|
+
* For multi-method contracts, each method has its own separator at a different
|
|
1931
|
+
* byte offset. Uses codeSeparatorIndices[methodIndex] if available, otherwise
|
|
1932
|
+
* falls back to the single codeSeparatorIndex.
|
|
1933
|
+
*
|
|
1934
|
+
* When `_codeScript` is set (chain-loaded, or a deploy script already built
|
|
1935
|
+
* from real constructor args), byte-walk the actual script for the true
|
|
1936
|
+
* on-chain OP_CODESEPARATOR offset — mirroring `getSubscriptForSigning`.
|
|
1937
|
+
* `adjustCodeSepOffset` derives the shift from the in-memory
|
|
1938
|
+
* `constructorArgs`, which are placeholders on the restore path (issue #132);
|
|
1939
|
+
* the byte-walk is independent of those args. The template `adjustCodeSepOffset`
|
|
1940
|
+
* path remains only for template-built (deploy-time) contracts.
|
|
1941
|
+
*
|
|
1942
|
+
* Public low-level assembly surface: each covenant input of a
|
|
1943
|
+
* multi-contract tx needs its own OP_PUSH_TX signature + BIP-143 preimage
|
|
1944
|
+
* computed against ITS artifact's code separator layout.
|
|
1945
|
+
*/
|
|
1946
|
+
computeOpPushTxWithCodeSep(tx, inputIndex, subscript, satoshis, methodIndex) {
|
|
1947
|
+
let codeSepIdx;
|
|
1948
|
+
if (this._codeScript !== null) {
|
|
1949
|
+
const realOffsets = findCodesepOffsets(this._codeScript);
|
|
1950
|
+
if (realOffsets.length > 0) {
|
|
1951
|
+
const indices = this.artifact.codeSeparatorIndices;
|
|
1952
|
+
if (indices && methodIndex !== undefined && methodIndex < indices.length
|
|
1953
|
+
&& methodIndex < realOffsets.length) {
|
|
1954
|
+
codeSepIdx = realOffsets[methodIndex];
|
|
1955
|
+
}
|
|
1956
|
+
else if (this.artifact.codeSeparatorIndex !== undefined) {
|
|
1957
|
+
codeSepIdx = realOffsets[0];
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (codeSepIdx === undefined) {
|
|
1962
|
+
// Template (deploy-time) path: derive the shifted offset from the args.
|
|
1963
|
+
codeSepIdx = this.artifact.codeSeparatorIndex;
|
|
1964
|
+
const indices = this.artifact.codeSeparatorIndices;
|
|
1965
|
+
if (indices && methodIndex !== undefined && methodIndex < indices.length) {
|
|
1966
|
+
codeSepIdx = indices[methodIndex];
|
|
1967
|
+
}
|
|
1968
|
+
if (codeSepIdx !== undefined) {
|
|
1969
|
+
codeSepIdx = this.adjustCodeSepOffset(codeSepIdx);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
// Issue #123: build the preimage under the method's declared @sighash mode.
|
|
1973
|
+
// `methodIndex` indexes the public-methods list (same convention as the
|
|
1974
|
+
// codeSeparatorIndices lookup above); a method with no directive carries no
|
|
1975
|
+
// `sigHashType` and falls back to 0x41 (ALL|FORKID), unchanged behaviour.
|
|
1976
|
+
const publicMethods = this.artifact.abi.methods.filter((m) => m.isPublic);
|
|
1977
|
+
const sigHashType = (methodIndex !== undefined ? publicMethods[methodIndex]?.sigHashType : undefined) ?? 0x41;
|
|
1978
|
+
return computeOpPushTx(tx, inputIndex, subscript, satoshis, codeSepIdx, sigHashType);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Build the prefix for an unlocking script: optionally _codePart.
|
|
1982
|
+
* needsCodePart should be true only when the method constructs continuation outputs
|
|
1983
|
+
* (non-terminal stateful calls). Terminal and stateless methods don't use _codePart.
|
|
1984
|
+
*
|
|
1985
|
+
* Public low-level assembly surface. BUG-100: the OP_PUSH_TX signature is
|
|
1986
|
+
* derived on-chain from the preimage (see codegen emitCheckPreimageBinding),
|
|
1987
|
+
* so NO signature is pushed here — the stateful unlock layout is
|
|
1988
|
+
* `[_codePart?] args... [_changePKH _changeAmount] [_newAmount] txPreimage
|
|
1989
|
+
* [selector]` (multi-contract assembly rebuilds it per input). `opSig` is
|
|
1990
|
+
* accepted for call-site compatibility but ignored.
|
|
1991
|
+
*/
|
|
1992
|
+
buildStatefulPrefix(_opSig, needsCodePart = false) {
|
|
1993
|
+
let prefix = '';
|
|
1994
|
+
if (needsCodePart && this.artifact.codeSeparatorIndex !== undefined) {
|
|
1995
|
+
prefix += encodePushData(this.getCodePartHex());
|
|
1996
|
+
}
|
|
1997
|
+
return prefix;
|
|
1998
|
+
}
|
|
1999
|
+
// -------------------------------------------------------------------------
|
|
2000
|
+
// Reconnection
|
|
2001
|
+
// -------------------------------------------------------------------------
|
|
2002
|
+
/**
|
|
2003
|
+
* Reconnect to an existing deployed contract from a known UTXO.
|
|
2004
|
+
*
|
|
2005
|
+
* This is the synchronous equivalent of `fromTxId()` — use it when the
|
|
2006
|
+
* UTXO data is already available (e.g. from an overlay service or cache)
|
|
2007
|
+
* without needing a Provider to fetch the transaction.
|
|
2008
|
+
*
|
|
2009
|
+
* @param artifact - The compiled artifact describing the contract.
|
|
2010
|
+
* @param utxo - The UTXO containing the contract output.
|
|
2011
|
+
* @returns A RunarContract instance connected to the existing UTXO.
|
|
2012
|
+
*/
|
|
2013
|
+
static fromUtxo(artifact, utxo) {
|
|
2014
|
+
const contract = new RunarContract(artifact, restoreConstructorArgs(artifact, utxo.script));
|
|
2015
|
+
if (artifact.stateFields && artifact.stateFields.length > 0) {
|
|
2016
|
+
const lastOpReturn = findLastOpReturn(utxo.script);
|
|
2017
|
+
contract._codeScript = lastOpReturn !== -1
|
|
2018
|
+
? utxo.script.slice(0, lastOpReturn)
|
|
2019
|
+
: utxo.script;
|
|
2020
|
+
}
|
|
2021
|
+
else {
|
|
2022
|
+
contract._codeScript = utxo.script;
|
|
2023
|
+
}
|
|
2024
|
+
// Detect inscription envelope in the code portion. Keep it in _codeScript
|
|
2025
|
+
// (do NOT strip) so that stateful continuation outputs preserve it.
|
|
2026
|
+
if (contract._codeScript) {
|
|
2027
|
+
const inscription = parseInscriptionEnvelope(contract._codeScript);
|
|
2028
|
+
if (inscription) {
|
|
2029
|
+
contract._inscription = inscription;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
contract.currentUtxo = {
|
|
2033
|
+
txid: utxo.txid,
|
|
2034
|
+
outputIndex: utxo.outputIndex,
|
|
2035
|
+
satoshis: utxo.satoshis,
|
|
2036
|
+
script: utxo.script,
|
|
2037
|
+
};
|
|
2038
|
+
if (artifact.stateFields && artifact.stateFields.length > 0) {
|
|
2039
|
+
const state = extractStateFromScript(artifact, utxo.script);
|
|
2040
|
+
// Deep-review finding C10: `extractStateFromScript` returns null ONLY
|
|
2041
|
+
// when the script has no recognisable state section at all (no
|
|
2042
|
+
// OP_RETURN found) — i.e. state extraction failed ENTIRELY, distinct
|
|
2043
|
+
// from "this one field has no on-chain slot" (issue #119's deliberate
|
|
2044
|
+
// zero-fill of unslotted CONSTRUCTOR args in `restoreConstructorArgs`,
|
|
2045
|
+
// untouched here). Silently keeping the constructor-initial `_state`
|
|
2046
|
+
// set by the `RunarContract` constructor above would present stale
|
|
2047
|
+
// deploy-time defaults as if they were live on-chain state. Fail
|
|
2048
|
+
// loudly instead of fabricating plausible-looking state.
|
|
2049
|
+
if (state === null) {
|
|
2050
|
+
throw new Error(`RunarContract.fromUtxo: could not extract state for ${artifact.contractName} — ` +
|
|
2051
|
+
`no state section (OP_RETURN) found in the UTXO script, but the artifact declares ` +
|
|
2052
|
+
`${artifact.stateFields.length} state field(s). Refusing to silently present ` +
|
|
2053
|
+
`constructor-initial state as live on-chain state.`);
|
|
2054
|
+
}
|
|
2055
|
+
contract._state = state;
|
|
2056
|
+
}
|
|
2057
|
+
return contract;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Reconnect to an existing deployed contract from its deployment transaction.
|
|
2061
|
+
*
|
|
2062
|
+
* @param artifact - The compiled artifact describing the contract.
|
|
2063
|
+
* @param txid - The transaction ID containing the contract UTXO.
|
|
2064
|
+
* @param outputIndex - The output index of the contract UTXO.
|
|
2065
|
+
* @param provider - Blockchain provider.
|
|
2066
|
+
* @returns A RunarContract instance connected to the existing UTXO.
|
|
2067
|
+
*/
|
|
2068
|
+
static async fromTxId(artifact, txid, outputIndex, provider) {
|
|
2069
|
+
const tx = await provider.getTransaction(txid);
|
|
2070
|
+
if (outputIndex >= tx.outputs.length) {
|
|
2071
|
+
throw new Error(`RunarContract.fromTxId: output index ${outputIndex} out of range (tx has ${tx.outputs.length} outputs)`);
|
|
2072
|
+
}
|
|
2073
|
+
const output = tx.outputs[outputIndex];
|
|
2074
|
+
return RunarContract.fromUtxo(artifact, {
|
|
2075
|
+
txid,
|
|
2076
|
+
outputIndex,
|
|
2077
|
+
satoshis: output.satoshis,
|
|
2078
|
+
script: output.script,
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
// -------------------------------------------------------------------------
|
|
2082
|
+
// Private helpers
|
|
2083
|
+
// -------------------------------------------------------------------------
|
|
2084
|
+
findMethod(name) {
|
|
2085
|
+
return this.artifact.abi.methods.find((m) => m.name === name && m.isPublic);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
// ---------------------------------------------------------------------------
|
|
2089
|
+
// JSON BigInt revival
|
|
2090
|
+
// ---------------------------------------------------------------------------
|
|
2091
|
+
/**
|
|
2092
|
+
* Revive a value that may have been serialized as a BigInt string ("0n")
|
|
2093
|
+
* when the artifact JSON was loaded without the bigintReviver (e.g. via
|
|
2094
|
+
* Vite's `import artifact from './artifact.json'`).
|
|
2095
|
+
*/
|
|
2096
|
+
function reviveJsonValue(value, type) {
|
|
2097
|
+
if (typeof value === 'string' && (type === 'bigint' || type === 'int')) {
|
|
2098
|
+
if (value.endsWith('n'))
|
|
2099
|
+
return BigInt(value.slice(0, -1));
|
|
2100
|
+
return BigInt(value);
|
|
2101
|
+
}
|
|
2102
|
+
return value;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Recursively revive a (possibly nested) initialValue tree against its
|
|
2106
|
+
* declared type. For `FixedArray<FixedArray<bigint, 2>, 2>` this walks
|
|
2107
|
+
* 2 levels deep and reviveJsonValues each leaf as `bigint`.
|
|
2108
|
+
*/
|
|
2109
|
+
function reviveNestedValue(value, type) {
|
|
2110
|
+
if (!type.startsWith('FixedArray<')) {
|
|
2111
|
+
return reviveJsonValue(value, type);
|
|
2112
|
+
}
|
|
2113
|
+
// Peel one FixedArray<inner, N> layer.
|
|
2114
|
+
const inner = type.slice('FixedArray<'.length, -1);
|
|
2115
|
+
let depth = 0;
|
|
2116
|
+
let splitAt = -1;
|
|
2117
|
+
for (let i = inner.length - 1; i >= 0; i--) {
|
|
2118
|
+
const ch = inner[i];
|
|
2119
|
+
if (ch === '>')
|
|
2120
|
+
depth++;
|
|
2121
|
+
else if (ch === '<')
|
|
2122
|
+
depth--;
|
|
2123
|
+
else if (ch === ',' && depth === 0) {
|
|
2124
|
+
splitAt = i;
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (splitAt < 0)
|
|
2129
|
+
return value;
|
|
2130
|
+
const elemType = inner.slice(0, splitAt).trim();
|
|
2131
|
+
if (!Array.isArray(value))
|
|
2132
|
+
return value;
|
|
2133
|
+
return value.map(v => reviveNestedValue(v, elemType));
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Parse a nested `FixedArray<...>` type string into its outer dimensions,
|
|
2137
|
+
* returning `[outerLen, innerLen, ...]`. For example:
|
|
2138
|
+
* "FixedArray<bigint, 9>" -> [9]
|
|
2139
|
+
* "FixedArray<FixedArray<bigint, 2>, 2>" -> [2, 2]
|
|
2140
|
+
* "FixedArray<FixedArray<FixedArray<bigint,2>,3>,4>" -> [4, 3, 2]
|
|
2141
|
+
* A non-FixedArray type returns `[]`.
|
|
2142
|
+
*/
|
|
2143
|
+
function parseFixedArrayDims(type) {
|
|
2144
|
+
const dims = [];
|
|
2145
|
+
let current = type.trim();
|
|
2146
|
+
while (current.startsWith('FixedArray<')) {
|
|
2147
|
+
const inner = current.slice('FixedArray<'.length, -1);
|
|
2148
|
+
// Find the matching comma that separates the element type from
|
|
2149
|
+
// the length — this is the last top-level comma, since the length
|
|
2150
|
+
// is always a bare integer and the element type may contain its
|
|
2151
|
+
// own `FixedArray<T, N>` commas.
|
|
2152
|
+
let depth = 0;
|
|
2153
|
+
let splitAt = -1;
|
|
2154
|
+
for (let i = inner.length - 1; i >= 0; i--) {
|
|
2155
|
+
const ch = inner[i];
|
|
2156
|
+
if (ch === '>')
|
|
2157
|
+
depth++;
|
|
2158
|
+
else if (ch === '<')
|
|
2159
|
+
depth--;
|
|
2160
|
+
else if (ch === ',' && depth === 0) {
|
|
2161
|
+
splitAt = i;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (splitAt < 0)
|
|
2166
|
+
return dims; // malformed
|
|
2167
|
+
const elemType = inner.slice(0, splitAt).trim();
|
|
2168
|
+
const lenStr = inner.slice(splitAt + 1).trim();
|
|
2169
|
+
const len = Number.parseInt(lenStr, 10);
|
|
2170
|
+
if (!Number.isFinite(len) || len <= 0)
|
|
2171
|
+
return dims;
|
|
2172
|
+
dims.push(len);
|
|
2173
|
+
current = elemType;
|
|
2174
|
+
}
|
|
2175
|
+
return dims;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Recursively flatten a nested JS array of depth `dims.length` into a
|
|
2179
|
+
* flat list of leaf values in declaration order.
|
|
2180
|
+
*/
|
|
2181
|
+
function flattenNested(value, dims) {
|
|
2182
|
+
if (dims.length === 0)
|
|
2183
|
+
return [value];
|
|
2184
|
+
const out = [];
|
|
2185
|
+
if (!Array.isArray(value)) {
|
|
2186
|
+
// Missing or wrong shape — emit `dims.reduce(*,1)` undefineds so
|
|
2187
|
+
// the caller can still reach the leaves.
|
|
2188
|
+
const total = dims.reduce((a, b) => a * b, 1);
|
|
2189
|
+
for (let i = 0; i < total; i++)
|
|
2190
|
+
out.push(undefined);
|
|
2191
|
+
return out;
|
|
2192
|
+
}
|
|
2193
|
+
const [, ...rest] = dims;
|
|
2194
|
+
for (const v of value) {
|
|
2195
|
+
out.push(...flattenNested(v, rest));
|
|
2196
|
+
}
|
|
2197
|
+
return out;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Recursively rebuild a nested JS array of depth `dims.length` from a
|
|
2201
|
+
* flat list of leaf values in declaration order.
|
|
2202
|
+
*/
|
|
2203
|
+
function regroupNested(flat, dims, offset = 0) {
|
|
2204
|
+
const [outerLen, ...rest] = dims;
|
|
2205
|
+
if (outerLen === undefined) {
|
|
2206
|
+
return { value: [], consumed: 0 };
|
|
2207
|
+
}
|
|
2208
|
+
const value = new Array(outerLen);
|
|
2209
|
+
let consumed = 0;
|
|
2210
|
+
if (rest.length === 0) {
|
|
2211
|
+
for (let i = 0; i < outerLen; i++) {
|
|
2212
|
+
value[i] = flat[offset + i];
|
|
2213
|
+
}
|
|
2214
|
+
consumed = outerLen;
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
for (let i = 0; i < outerLen; i++) {
|
|
2218
|
+
const sub = regroupNested(flat, rest, offset + consumed);
|
|
2219
|
+
value[i] = sub.value;
|
|
2220
|
+
consumed += sub.consumed;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return { value, consumed };
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Flatten a state record whose grouped FixedArray entries (`Board`) hold
|
|
2227
|
+
* a (possibly nested) JS array of length N into a new record where each
|
|
2228
|
+
* leaf element is keyed by its underlying synthetic scalar name
|
|
2229
|
+
* (`Board__0`..`Board__8`, `Grid__0__0`..`Grid__1__1`, etc.). The
|
|
2230
|
+
* grouped entries are also preserved for callers that read them later.
|
|
2231
|
+
*
|
|
2232
|
+
* Used at the ANF-interpreter boundary, which knows only the expanded
|
|
2233
|
+
* scalar property names.
|
|
2234
|
+
*/
|
|
2235
|
+
function flattenFixedArrayState(state, stateFields) {
|
|
2236
|
+
const out = { ...state };
|
|
2237
|
+
if (!stateFields)
|
|
2238
|
+
return out;
|
|
2239
|
+
for (const field of stateFields) {
|
|
2240
|
+
if (!field.fixedArray)
|
|
2241
|
+
continue;
|
|
2242
|
+
const value = state[field.name];
|
|
2243
|
+
if (!Array.isArray(value))
|
|
2244
|
+
continue;
|
|
2245
|
+
const dims = parseFixedArrayDims(field.type);
|
|
2246
|
+
const flat = flattenNested(value, dims);
|
|
2247
|
+
const syntheticNames = field.fixedArray.syntheticNames;
|
|
2248
|
+
for (let i = 0; i < syntheticNames.length; i++) {
|
|
2249
|
+
const synth = syntheticNames[i];
|
|
2250
|
+
// Do not overwrite an explicit scalar in the original state.
|
|
2251
|
+
if (!(synth in out))
|
|
2252
|
+
out[synth] = flat[i];
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return out;
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Re-group a state record's synthetic scalar entries back into arrays
|
|
2259
|
+
* (possibly nested) under their grouped names. Non-synthetic scalars
|
|
2260
|
+
* are passed through.
|
|
2261
|
+
*/
|
|
2262
|
+
function regroupFixedArrayState(state, stateFields) {
|
|
2263
|
+
const out = { ...state };
|
|
2264
|
+
if (!stateFields)
|
|
2265
|
+
return out;
|
|
2266
|
+
for (const field of stateFields) {
|
|
2267
|
+
if (!field.fixedArray)
|
|
2268
|
+
continue;
|
|
2269
|
+
const syntheticNames = field.fixedArray.syntheticNames;
|
|
2270
|
+
const flat = new Array(syntheticNames.length);
|
|
2271
|
+
let sawAny = false;
|
|
2272
|
+
for (let i = 0; i < syntheticNames.length; i++) {
|
|
2273
|
+
const synth = syntheticNames[i];
|
|
2274
|
+
if (synth in out) {
|
|
2275
|
+
flat[i] = out[synth];
|
|
2276
|
+
sawAny = true;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (!sawAny)
|
|
2280
|
+
continue;
|
|
2281
|
+
// Fall back to the prior grouped value for still-missing leaves
|
|
2282
|
+
// by re-flattening it alongside the scalar updates.
|
|
2283
|
+
const prior = state[field.name];
|
|
2284
|
+
const dims = parseFixedArrayDims(field.type);
|
|
2285
|
+
if (Array.isArray(prior)) {
|
|
2286
|
+
const priorFlat = flattenNested(prior, dims);
|
|
2287
|
+
for (let i = 0; i < flat.length; i++) {
|
|
2288
|
+
if (flat[i] === undefined)
|
|
2289
|
+
flat[i] = priorFlat[i];
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const rebuilt = regroupNested(flat, dims);
|
|
2293
|
+
out[field.name] = rebuilt.value;
|
|
2294
|
+
}
|
|
2295
|
+
return out;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* If a constructor arg list uses the grouped FixedArray form
|
|
2299
|
+
* (`[someArray, ...]`), expand each (possibly nested) array-valued arg
|
|
2300
|
+
* into consecutive positional slots so the ANF interpreter's index-based
|
|
2301
|
+
* lookup works.
|
|
2302
|
+
*/
|
|
2303
|
+
function flattenFixedArrayArgs(args, abiParams) {
|
|
2304
|
+
const out = [];
|
|
2305
|
+
for (let i = 0; i < args.length; i++) {
|
|
2306
|
+
const param = abiParams[i];
|
|
2307
|
+
const value = args[i];
|
|
2308
|
+
if (param?.fixedArray && Array.isArray(value)) {
|
|
2309
|
+
const dims = parseFixedArrayDims(param.type);
|
|
2310
|
+
if (dims.length > 0) {
|
|
2311
|
+
out.push(...flattenNested(value, dims));
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
for (const v of value)
|
|
2315
|
+
out.push(v);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
out.push(value);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return out;
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Recover the positional constructor argument list from a deployed locking
|
|
2326
|
+
* script, so a restored contract (`fromUtxo` / `fromTxId`) operates on the real
|
|
2327
|
+
* baked-in values rather than `0n` placeholders.
|
|
2328
|
+
*
|
|
2329
|
+
* `extractConstructorArgs` returns a name→value map keyed by ABI param name;
|
|
2330
|
+
* `abi.constructor.params` is already ordered by paramIndex, so mapping over it
|
|
2331
|
+
* yields the positional `unknown[]` the `RunarContract` constructor expects.
|
|
2332
|
+
*
|
|
2333
|
+
* Params that carry no constructor slot (mutable state fields — their value
|
|
2334
|
+
* lives in the OP_RETURN state section, restored separately) are absent from
|
|
2335
|
+
* the extracted map; they fall back to `0n`, matching the prior placeholder
|
|
2336
|
+
* behaviour, and are immediately overwritten by `extractStateFromScript`.
|
|
2337
|
+
*
|
|
2338
|
+
* FixedArray constructor params are rejected by the compiler ("Constructor
|
|
2339
|
+
* parameter cannot be a FixedArray"), so no FixedArray grouping arises here.
|
|
2340
|
+
*/
|
|
2341
|
+
function restoreConstructorArgs(artifact, scriptHex) {
|
|
2342
|
+
const params = artifact.abi.constructor.params;
|
|
2343
|
+
if (params.length === 0)
|
|
2344
|
+
return [];
|
|
2345
|
+
const extracted = extractConstructorArgs(artifact, scriptHex);
|
|
2346
|
+
return params.map((p) => (Object.prototype.hasOwnProperty.call(extracted, p.name) ? extracted[p.name] : 0n));
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Build a named argument map from positional args and user-visible params.
|
|
2350
|
+
* Used to feed the ANF interpreter for auto-state computation.
|
|
2351
|
+
*/
|
|
2352
|
+
function buildNamedArgs(userParams, resolvedArgs) {
|
|
2353
|
+
const named = {};
|
|
2354
|
+
for (let i = 0; i < userParams.length; i++) {
|
|
2355
|
+
named[userParams[i].name] = resolvedArgs[i];
|
|
2356
|
+
}
|
|
2357
|
+
return named;
|
|
2358
|
+
}
|
|
2359
|
+
// ---------------------------------------------------------------------------
|
|
2360
|
+
// Encoding helpers
|
|
2361
|
+
// ---------------------------------------------------------------------------
|
|
2362
|
+
/**
|
|
2363
|
+
* Compute the BIP-143 sighash digest — `hash256(preimage)` i.e.
|
|
2364
|
+
* `sha256(sha256(preimage))` — that is ACTUALLY ECDSA-signed by
|
|
2365
|
+
* `OP_CHECKSIG` on-chain.
|
|
2366
|
+
*
|
|
2367
|
+
* Deep-review finding C19: `PreparedCall.sighash` previously stored only
|
|
2368
|
+
* `sha256(preimage)` (a SINGLE hash). That happened to work for the default
|
|
2369
|
+
* `call()` path only because `LocalSigner.sign()` hands its OWN independently
|
|
2370
|
+
* computed `sha256(preimage)` to `PrivateKey.sign()`, which re-hashes its
|
|
2371
|
+
* input once more internally — so the total ends up correct by accident of
|
|
2372
|
+
* that one call chain. An external wallet / hardware signer wired to the
|
|
2373
|
+
* multi-signer `prepareCall()` / `finalizeCall()` API is handed
|
|
2374
|
+
* `PreparedCall.sighash` and is expected to ECDSA-sign it DIRECTLY (no
|
|
2375
|
+
* further hashing) — e.g. a BRC-100 `signHash`-style API. Handed the
|
|
2376
|
+
* single-hashed value, such a signer signs the wrong digest and the node's
|
|
2377
|
+
* real `OP_CHECKSIG` rejects the spend.
|
|
2378
|
+
*
|
|
2379
|
+
* This is the WIRE-LEVEL digest external signers must sign — the same value
|
|
2380
|
+
* the Java SDK already stores in its `PreparedCall`. Port this exact
|
|
2381
|
+
* `hash256(preimage)` computation (not `sha256(preimage)`) to every other
|
|
2382
|
+
* tier's `PreparedCall.sighash` equivalent (Go/Rust/Python).
|
|
2383
|
+
*/
|
|
2384
|
+
function computeBip143Sighash(preimageHex) {
|
|
2385
|
+
const preimageBytes = Utils.toArray(preimageHex, 'hex');
|
|
2386
|
+
return Utils.toHex(Hash.hash256(preimageBytes));
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Encode an argument value as a Bitcoin Script push data element.
|
|
2390
|
+
*
|
|
2391
|
+
* Exported as part of the low-level assembly surface: downstream tooling
|
|
2392
|
+
* (runar-testing's mock-preimage builders) and multi-contract tx assembly (and
|
|
2393
|
+
* any external unlocking-script construction) must encode arguments
|
|
2394
|
+
* byte-identically to `buildUnlockingScript` — share this, don't copy it.
|
|
2395
|
+
*/
|
|
2396
|
+
export function encodeArg(value) {
|
|
2397
|
+
if (isEmptySig(value)) {
|
|
2398
|
+
return '00'; // OP_0 — empty signature push for the failing OR-CHECKSIG branch (issue #106)
|
|
2399
|
+
}
|
|
2400
|
+
if (typeof value === 'bigint') {
|
|
2401
|
+
return encodeScriptNumber(value);
|
|
2402
|
+
}
|
|
2403
|
+
if (typeof value === 'number') {
|
|
2404
|
+
return encodeScriptNumber(BigInt(value));
|
|
2405
|
+
}
|
|
2406
|
+
if (typeof value === 'boolean') {
|
|
2407
|
+
return value ? '51' : '00';
|
|
2408
|
+
}
|
|
2409
|
+
if (typeof value === 'string') {
|
|
2410
|
+
// Assume hex-encoded data
|
|
2411
|
+
return encodePushData(value);
|
|
2412
|
+
}
|
|
2413
|
+
// Fallback: convert to string
|
|
2414
|
+
return encodePushData(String(value));
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Encode a bigint as a minimally-encoded Bitcoin Script number push
|
|
2418
|
+
* (OP_0 / OP_1..OP_16 / OP_1NEGATE / sign-magnitude LE push data).
|
|
2419
|
+
* Exported as part of the low-level assembly surface.
|
|
2420
|
+
*/
|
|
2421
|
+
export function encodeScriptNumber(n) {
|
|
2422
|
+
if (n === 0n) {
|
|
2423
|
+
return '00'; // OP_0
|
|
2424
|
+
}
|
|
2425
|
+
if (n >= 1n && n <= 16n) {
|
|
2426
|
+
// OP_1 through OP_16
|
|
2427
|
+
return (0x50 + Number(n)).toString(16);
|
|
2428
|
+
}
|
|
2429
|
+
if (n === -1n) {
|
|
2430
|
+
return '4f'; // OP_1NEGATE
|
|
2431
|
+
}
|
|
2432
|
+
const negative = n < 0n;
|
|
2433
|
+
let absVal = negative ? -n : n;
|
|
2434
|
+
const bytes = [];
|
|
2435
|
+
while (absVal > 0n) {
|
|
2436
|
+
bytes.push(Number(absVal & 0xffn));
|
|
2437
|
+
absVal >>= 8n;
|
|
2438
|
+
}
|
|
2439
|
+
if ((bytes[bytes.length - 1] & 0x80) !== 0) {
|
|
2440
|
+
bytes.push(negative ? 0x80 : 0x00);
|
|
2441
|
+
}
|
|
2442
|
+
else if (negative) {
|
|
2443
|
+
bytes[bytes.length - 1] |= 0x80;
|
|
2444
|
+
}
|
|
2445
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
2446
|
+
return encodePushData(hex);
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Normalize a witness-value input (hex string or Uint8Array) into a
|
|
2450
|
+
* lowercase hex string suitable for `encodePushData`. Hex inputs may
|
|
2451
|
+
* optionally carry a `0x` prefix and any casing.
|
|
2452
|
+
*/
|
|
2453
|
+
function normalizeWitnessBytes(value) {
|
|
2454
|
+
if (typeof value === 'string') {
|
|
2455
|
+
const h = value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value;
|
|
2456
|
+
if (h.length % 2 !== 0) {
|
|
2457
|
+
throw new Error(`witness value: hex string must have even length (got ${h.length})`);
|
|
2458
|
+
}
|
|
2459
|
+
if (!/^[0-9a-fA-F]*$/.test(h)) {
|
|
2460
|
+
throw new Error('witness value: invalid hex characters');
|
|
2461
|
+
}
|
|
2462
|
+
return h.toLowerCase();
|
|
2463
|
+
}
|
|
2464
|
+
// Uint8Array
|
|
2465
|
+
let out = '';
|
|
2466
|
+
for (let i = 0; i < value.length; i++) {
|
|
2467
|
+
out += value[i].toString(16).padStart(2, '0');
|
|
2468
|
+
}
|
|
2469
|
+
return out;
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Encode raw hex bytes as a Bitcoin Script push (direct push / PUSHDATA1/2/4).
|
|
2473
|
+
* Exported as part of the low-level assembly surface.
|
|
2474
|
+
*/
|
|
2475
|
+
export function encodePushData(dataHex) {
|
|
2476
|
+
if (dataHex.length === 0)
|
|
2477
|
+
return '00'; // OP_0
|
|
2478
|
+
const len = dataHex.length / 2;
|
|
2479
|
+
// MINIMALDATA: single-byte payloads in the OP_N range must use the
|
|
2480
|
+
// corresponding minimal opcode (`OP_1..OP_16` / `OP_1NEGATE`) rather than
|
|
2481
|
+
// the direct push `01 NN`, which is relay-rejected as "Data push larger
|
|
2482
|
+
// than necessary". `encodeScriptNumber` already short-circuits Int args to
|
|
2483
|
+
// OP_N; this brings the ByteString push path to the same standard. Kept
|
|
2484
|
+
// byte-identical with `encodePushDataState` in state.ts and the shared
|
|
2485
|
+
// `encode_push_data` in the other six SDKs.
|
|
2486
|
+
//
|
|
2487
|
+
// NOTE: 0x00 is deliberately NOT in that set. `OP_0` pushes the EMPTY byte
|
|
2488
|
+
// array, not a 1-byte `0x00` — so the minimal encoding of a 1-byte `0x00`
|
|
2489
|
+
// payload is the direct push `01 00` (matching the compiler's
|
|
2490
|
+
// `encodePushBytesHex` in push-encoding.ts), not `OP_0` (S1).
|
|
2491
|
+
if (len === 1) {
|
|
2492
|
+
const byte = parseInt(dataHex, 16);
|
|
2493
|
+
if (byte >= 0x01 && byte <= 0x10)
|
|
2494
|
+
return (0x50 + byte).toString(16).padStart(2, '0'); // OP_1..OP_16
|
|
2495
|
+
if (byte === 0x81)
|
|
2496
|
+
return '4f'; // OP_1NEGATE
|
|
2497
|
+
}
|
|
2498
|
+
if (len <= 75) {
|
|
2499
|
+
return len.toString(16).padStart(2, '0') + dataHex;
|
|
2500
|
+
}
|
|
2501
|
+
else if (len <= 0xff) {
|
|
2502
|
+
return '4c' + len.toString(16).padStart(2, '0') + dataHex;
|
|
2503
|
+
}
|
|
2504
|
+
else if (len <= 0xffff) {
|
|
2505
|
+
const lo = (len & 0xff).toString(16).padStart(2, '0');
|
|
2506
|
+
const hi = ((len >> 8) & 0xff).toString(16).padStart(2, '0');
|
|
2507
|
+
return '4d' + lo + hi + dataHex;
|
|
2508
|
+
}
|
|
2509
|
+
else {
|
|
2510
|
+
const b0 = (len & 0xff).toString(16).padStart(2, '0');
|
|
2511
|
+
const b1 = ((len >> 8) & 0xff).toString(16).padStart(2, '0');
|
|
2512
|
+
const b2 = ((len >> 16) & 0xff).toString(16).padStart(2, '0');
|
|
2513
|
+
const b3 = ((len >> 24) & 0xff).toString(16).padStart(2, '0');
|
|
2514
|
+
return '4e' + b0 + b1 + b2 + b3 + dataHex;
|
|
2515
|
+
}
|
|
2516
|
+
}
|