@dcasegr/amm-client 0.0.0-stage → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +107 -2
  2. package/dist/client.d.ts +534 -0
  3. package/dist/client.js +1319 -0
  4. package/dist/errors.d.ts +17 -0
  5. package/dist/errors.js +75 -0
  6. package/dist/index.d.ts +51 -0
  7. package/dist/index.js +53 -0
  8. package/dist/lib/config.d.ts +42 -0
  9. package/dist/lib/config.js +64 -0
  10. package/dist/lib/keys.d.ts +42 -0
  11. package/dist/lib/keys.js +48 -0
  12. package/dist/lib/overlay.d.ts +190 -0
  13. package/dist/lib/overlay.js +302 -0
  14. package/dist/lib/skein.d.ts +82 -0
  15. package/dist/lib/skein.js +142 -0
  16. package/dist/lib/tokenId.d.ts +72 -0
  17. package/dist/lib/tokenId.js +121 -0
  18. package/dist/lp/amounts.d.ts +12 -0
  19. package/dist/lp/amounts.js +38 -0
  20. package/dist/lp/close.d.ts +90 -0
  21. package/dist/lp/close.js +140 -0
  22. package/dist/lp/images.d.ts +11 -0
  23. package/dist/lp/images.js +45 -0
  24. package/dist/lp/inventory.d.ts +68 -0
  25. package/dist/lp/inventory.js +84 -0
  26. package/dist/lp/poolDeploy.d.ts +220 -0
  27. package/dist/lp/poolDeploy.js +291 -0
  28. package/dist/lp/poolRows.d.ts +13 -0
  29. package/dist/lp/poolRows.js +19 -0
  30. package/dist/lp/positions.d.ts +81 -0
  31. package/dist/lp/positions.js +144 -0
  32. package/dist/lp/rebalance.d.ts +101 -0
  33. package/dist/lp/rebalance.js +168 -0
  34. package/dist/lp/wallet.d.ts +12 -0
  35. package/dist/lp/wallet.js +19 -0
  36. package/dist/market/activeSwap.d.ts +158 -0
  37. package/dist/market/activeSwap.js +320 -0
  38. package/dist/market/plan.d.ts +85 -0
  39. package/dist/market/plan.js +149 -0
  40. package/dist/market/prices.d.ts +13 -0
  41. package/dist/market/prices.js +14 -0
  42. package/dist/market/relay.d.ts +84 -0
  43. package/dist/market/relay.js +201 -0
  44. package/dist/market/swapAction.d.ts +306 -0
  45. package/dist/market/swapAction.js +508 -0
  46. package/dist/market/swapFlow.d.ts +58 -0
  47. package/dist/market/swapFlow.js +86 -0
  48. package/dist/market/view.d.ts +58 -0
  49. package/dist/market/view.js +63 -0
  50. package/dist/pool/artifact.d.ts +23 -0
  51. package/dist/pool/artifact.js +38 -0
  52. package/dist/pool/index.d.ts +8 -0
  53. package/dist/pool/index.js +8 -0
  54. package/dist/pool/oneColor.d.ts +19 -0
  55. package/dist/pool/oneColor.js +39 -0
  56. package/dist/pool/pool.artifact.json +5069 -0
  57. package/dist/pool/runar-ir-schema.d.ts +21 -0
  58. package/dist/pool/runar-ir-schema.js +18 -0
  59. package/dist/pool/template.d.ts +295 -0
  60. package/dist/pool/template.js +715 -0
  61. package/dist/vendor/runar/LICENSE +21 -0
  62. package/dist/vendor/runar/NOTICE +15 -0
  63. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
  64. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
  65. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
  66. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
  67. package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
  68. package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
  69. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
  70. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
  71. package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
  72. package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
  73. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
  74. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
  75. package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
  76. package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
  77. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
  78. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
  79. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
  80. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
  81. package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
  82. package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
  83. package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
  84. package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
  85. package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
  86. package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
  87. package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
  88. package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
  89. package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
  90. package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
  91. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
  92. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
  93. package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
  94. package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
  95. package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
  96. package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
  97. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
  98. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
  99. package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
  100. package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
  101. package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
  102. package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
  103. package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
  104. package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
  105. package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
  106. package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
  107. package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
  108. package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
  109. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
  110. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
  111. package/dist/vendor/runar/runar-sdk.d.ts +9 -0
  112. package/dist/vendor/runar/runar-sdk.js +8 -0
  113. package/dist/wallet/action.d.ts +28 -0
  114. package/dist/wallet/action.js +42 -0
  115. package/dist/wallet/brc29.d.ts +83 -0
  116. package/dist/wallet/brc29.js +80 -0
  117. package/dist/wallet/color.d.ts +110 -0
  118. package/dist/wallet/color.js +134 -0
  119. package/dist/wallet/swapPayouts.d.ts +35 -0
  120. package/dist/wallet/swapPayouts.js +146 -0
  121. package/package.json +39 -4
@@ -0,0 +1,35 @@
1
+ /**
2
+ * NEW-005: @bsv/sdk's `Spend` destroys the scripts it is given.
3
+ *
4
+ * `Spend` pushes a script chunk's `data` array onto its stack BY REFERENCE, and
5
+ * at least one opcode mutates its operand in place — `OP_NUM2BIN` does
6
+ * `rawnum[rawnum.length - 1] &= 0x7f` (see `Spend.js`) to strip the sign bit
7
+ * before zero-padding. A stateful continuation serialises every mutable state
8
+ * field with OP_NUM2BIN, so evaluating a call that writes a NEGATIVE
9
+ * unlocking-script push into state permanently clears the sign bit of that push
10
+ * inside the caller's own `Script` object: a `-34` push becomes `+34`.
11
+ *
12
+ * The corruption is invisible to serialisation. `Script.fromBinary` seeds
13
+ * `rawBytesCache`/`hexCache` from the original bytes, and mutating
14
+ * `chunks[i].data` does not invalidate them, so `toHex()` keeps returning the
15
+ * true bytes and nothing wrong is ever broadcast. The damage is confined to the
16
+ * NEXT in-memory evaluation of the same object, which sees the wrong value and
17
+ * rejects a transaction the network accepts — a FALSE REJECTION.
18
+ *
19
+ * Every `new Spend(...)` must therefore be handed scripts it is free to
20
+ * destroy. `fromHex` re-parses into fresh chunks with fresh `data` arrays.
21
+ *
22
+ * Only the two scripts `Spend` EXECUTES need detaching. The `outputs` array is
23
+ * read solely by `TransactionSignature.formatBytes`, which serialises each
24
+ * output's locking script and never pushes it, so passing `tx.outputs` by
25
+ * reference is harmless (and cheaper than deep-copying every output script).
26
+ */
27
+ import { LockingScript, UnlockingScript } from '@bsv/sdk';
28
+ /** A private copy of `script` that `Spend` may corrupt without consequence. */
29
+ export function detachLockingScript(script) {
30
+ return LockingScript.fromHex(script.toHex());
31
+ }
32
+ /** A private copy of `script` that `Spend` may corrupt without consequence. */
33
+ export function detachUnlockingScript(script) {
34
+ return UnlockingScript.fromHex(script.toHex());
35
+ }
@@ -0,0 +1,54 @@
1
+ import type { StateField, RunarArtifact } from '../../../../pool/runar-ir-schema.js';
2
+ /**
3
+ * Serialize a set of state values into a hex-encoded Bitcoin Script data
4
+ * section (without the OP_RETURN prefix -- that is handled by the caller).
5
+ *
6
+ * Field order is determined by the `index` property of each StateField.
7
+ *
8
+ * Fields with a `fixedArray` annotation are expanded into N element writes
9
+ * in declaration order; callers may supply either a plain array on the
10
+ * grouped name (`values.Board = [...]`) or the underlying scalar fields
11
+ * (`values.Board__0 = ..., values.Board__1 = ..., ...`) — scalars win if
12
+ * both are present, for backward compatibility.
13
+ */
14
+ export declare function serializeState(fields: StateField[], values: Record<string, unknown>): string;
15
+ /**
16
+ * Deserialize state values from a hex-encoded Bitcoin Script data section.
17
+ *
18
+ * The caller must strip the code prefix and OP_RETURN byte before passing
19
+ * the data section.
20
+ *
21
+ * Fields with a `fixedArray` annotation are returned as a plain JS array on
22
+ * the grouped name, not as N individual scalar fields.
23
+ *
24
+ * FAILS CLOSED (C28). The blob is read back out of a locking script that any
25
+ * third party can construct, so it is untrusted input. A state section that
26
+ * does not describe EXACTLY the artifact's `stateFields` is rejected:
27
+ *
28
+ * - truncation — a field that runs past the end of the blob throws instead of
29
+ * yielding a short slice (which previously decoded as a plausible-but-wrong
30
+ * value: a missing `boolean` byte read as `true`, a clipped `bigint` as a
31
+ * different number, a clipped `PubKey` as a stub key);
32
+ * - overlong tails — bytes left over after the last declared field throw
33
+ * instead of being silently dropped.
34
+ *
35
+ * Restoring wrong-but-plausible state from a corrupted continuation is worse
36
+ * than not restoring it at all: the caller then signs a continuation output
37
+ * committing to that state.
38
+ */
39
+ export declare function deserializeState(fields: StateField[], scriptHex: string): Record<string, unknown>;
40
+ /**
41
+ * Extract state from a full locking script hex, given the artifact.
42
+ *
43
+ * Returns null if the artifact has no state fields or the script doesn't
44
+ * contain a recognisable state section.
45
+ */
46
+ export declare function extractStateFromScript(artifact: RunarArtifact, scriptHex: string): Record<string, unknown> | null;
47
+ /**
48
+ * Walk the script hex as Bitcoin Script opcodes to find the last OP_RETURN
49
+ * (0x6a) at a real opcode boundary. Unlike `lastIndexOf('6a')`, this
50
+ * properly skips push data so it won't match 0x6a bytes inside data payloads.
51
+ *
52
+ * Returns the hex-char offset of the last OP_RETURN, or -1 if not found.
53
+ */
54
+ export declare function findLastOpReturn(scriptHex: string): number;
@@ -0,0 +1,584 @@
1
+ // ---------------------------------------------------------------------------
2
+ // runar-sdk/state.ts -- State management for stateful contracts
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Stateful Runar contracts embed their state in the locking script as a
6
+ // suffix of OP_RETURN-delimited data pushes. The state section follows
7
+ // the contract's compiled code and is structured as:
8
+ //
9
+ // <code> OP_RETURN <field0> <field1> ... <fieldN>
10
+ //
11
+ // Each field is encoded as a Bitcoin Script push according to its type.
12
+ // ---------------------------------------------------------------------------
13
+ import { STATE_FIELD_WIDTHS } from '../../../../pool/runar-ir-schema.js';
14
+ /**
15
+ * Serialize a set of state values into a hex-encoded Bitcoin Script data
16
+ * section (without the OP_RETURN prefix -- that is handled by the caller).
17
+ *
18
+ * Field order is determined by the `index` property of each StateField.
19
+ *
20
+ * Fields with a `fixedArray` annotation are expanded into N element writes
21
+ * in declaration order; callers may supply either a plain array on the
22
+ * grouped name (`values.Board = [...]`) or the underlying scalar fields
23
+ * (`values.Board__0 = ..., values.Board__1 = ..., ...`) — scalars win if
24
+ * both are present, for backward compatibility.
25
+ */
26
+ export function serializeState(fields, values) {
27
+ const sorted = [...fields].sort((a, b) => a.index - b.index);
28
+ let hex = '';
29
+ for (const field of sorted) {
30
+ if (field.fixedArray) {
31
+ const arr = values[field.name];
32
+ const names = field.fixedArray.syntheticNames;
33
+ // Derive the leaf scalar type by peeling off every FixedArray
34
+ // layer from the declared `field.type`. The grouped entry's
35
+ // `elementType` is only the immediate child — for nested arrays
36
+ // it is itself another FixedArray<...>, which `encodeStateValue`
37
+ // does not know how to serialise.
38
+ const leafType = unwrapFixedArrayLeaf(field.type);
39
+ const dims = parseFixedArrayDims(field.type);
40
+ const flatFromArr = Array.isArray(arr) ? flattenNested(arr, dims) : null;
41
+ for (let i = 0; i < names.length; i++) {
42
+ let elem;
43
+ const synthName = names[i];
44
+ if (synthName in values) {
45
+ elem = values[synthName];
46
+ }
47
+ else if (flatFromArr) {
48
+ elem = flatFromArr[i];
49
+ }
50
+ else {
51
+ elem = undefined;
52
+ }
53
+ hex += encodeStateValue(elem, leafType, synthName);
54
+ }
55
+ }
56
+ else {
57
+ const value = values[field.name];
58
+ hex += encodeStateValue(value, field.type, field.name);
59
+ }
60
+ }
61
+ return hex;
62
+ }
63
+ /**
64
+ * Parse a nested `FixedArray<...>` type string into its outer
65
+ * dimensions: `"FixedArray<FixedArray<bigint, 2>, 3>"` → `[3, 2]`.
66
+ * Non-FixedArray types return `[]`.
67
+ */
68
+ function parseFixedArrayDims(type) {
69
+ const dims = [];
70
+ let current = type.trim();
71
+ while (current.startsWith('FixedArray<')) {
72
+ const inner = current.slice('FixedArray<'.length, -1);
73
+ let depth = 0;
74
+ let splitAt = -1;
75
+ for (let i = inner.length - 1; i >= 0; i--) {
76
+ const ch = inner[i];
77
+ if (ch === '>')
78
+ depth++;
79
+ else if (ch === '<')
80
+ depth--;
81
+ else if (ch === ',' && depth === 0) {
82
+ splitAt = i;
83
+ break;
84
+ }
85
+ }
86
+ if (splitAt < 0)
87
+ return dims;
88
+ const elemType = inner.slice(0, splitAt).trim();
89
+ const lenStr = inner.slice(splitAt + 1).trim();
90
+ const len = Number.parseInt(lenStr, 10);
91
+ if (!Number.isFinite(len) || len <= 0)
92
+ return dims;
93
+ dims.push(len);
94
+ current = elemType;
95
+ }
96
+ return dims;
97
+ }
98
+ /** Return the innermost scalar type of a (possibly nested) FixedArray string. */
99
+ function unwrapFixedArrayLeaf(type) {
100
+ let current = type.trim();
101
+ while (current.startsWith('FixedArray<')) {
102
+ const inner = current.slice('FixedArray<'.length, -1);
103
+ let depth = 0;
104
+ let splitAt = -1;
105
+ for (let i = inner.length - 1; i >= 0; i--) {
106
+ const ch = inner[i];
107
+ if (ch === '>')
108
+ depth++;
109
+ else if (ch === '<')
110
+ depth--;
111
+ else if (ch === ',' && depth === 0) {
112
+ splitAt = i;
113
+ break;
114
+ }
115
+ }
116
+ if (splitAt < 0)
117
+ return current;
118
+ current = inner.slice(0, splitAt).trim();
119
+ }
120
+ return current;
121
+ }
122
+ /** Flatten a nested JS array of depth `dims.length` to a flat leaf list. */
123
+ function flattenNested(value, dims) {
124
+ if (dims.length === 0)
125
+ return [value];
126
+ if (!Array.isArray(value)) {
127
+ const total = dims.reduce((a, b) => a * b, 1);
128
+ return new Array(total).fill(undefined);
129
+ }
130
+ const rest = dims.slice(1);
131
+ const out = [];
132
+ for (const v of value)
133
+ out.push(...flattenNested(v, rest));
134
+ return out;
135
+ }
136
+ /** Rebuild a nested JS array of depth `dims.length` from a flat leaf list. */
137
+ function regroupNested(flat, dims, offset = 0) {
138
+ const [outerLen, ...rest] = dims;
139
+ if (outerLen === undefined)
140
+ return { value: [], consumed: 0 };
141
+ const value = new Array(outerLen);
142
+ let consumed = 0;
143
+ if (rest.length === 0) {
144
+ for (let i = 0; i < outerLen; i++)
145
+ value[i] = flat[offset + i];
146
+ consumed = outerLen;
147
+ }
148
+ else {
149
+ for (let i = 0; i < outerLen; i++) {
150
+ const sub = regroupNested(flat, rest, offset + consumed);
151
+ value[i] = sub.value;
152
+ consumed += sub.consumed;
153
+ }
154
+ }
155
+ return { value, consumed };
156
+ }
157
+ /**
158
+ * Deserialize state values from a hex-encoded Bitcoin Script data section.
159
+ *
160
+ * The caller must strip the code prefix and OP_RETURN byte before passing
161
+ * the data section.
162
+ *
163
+ * Fields with a `fixedArray` annotation are returned as a plain JS array on
164
+ * the grouped name, not as N individual scalar fields.
165
+ *
166
+ * FAILS CLOSED (C28). The blob is read back out of a locking script that any
167
+ * third party can construct, so it is untrusted input. A state section that
168
+ * does not describe EXACTLY the artifact's `stateFields` is rejected:
169
+ *
170
+ * - truncation — a field that runs past the end of the blob throws instead of
171
+ * yielding a short slice (which previously decoded as a plausible-but-wrong
172
+ * value: a missing `boolean` byte read as `true`, a clipped `bigint` as a
173
+ * different number, a clipped `PubKey` as a stub key);
174
+ * - overlong tails — bytes left over after the last declared field throw
175
+ * instead of being silently dropped.
176
+ *
177
+ * Restoring wrong-but-plausible state from a corrupted continuation is worse
178
+ * than not restoring it at all: the caller then signs a continuation output
179
+ * committing to that state.
180
+ */
181
+ export function deserializeState(fields, scriptHex) {
182
+ if (scriptHex.length % 2 !== 0) {
183
+ throw new Error(`deserializeState: state blob is ${scriptHex.length} hex chars — not a whole number of bytes`);
184
+ }
185
+ const sorted = [...fields].sort((a, b) => a.index - b.index);
186
+ const result = {};
187
+ let offset = 0;
188
+ for (const field of sorted) {
189
+ if (field.fixedArray) {
190
+ const leafType = unwrapFixedArrayLeaf(field.type);
191
+ const dims = parseFixedArrayDims(field.type);
192
+ const total = field.fixedArray.syntheticNames.length;
193
+ const flat = new Array(total);
194
+ for (let i = 0; i < total; i++) {
195
+ const { value, bytesRead } = decodeStateValue(scriptHex, offset, leafType, `${field.name}[${i}]`);
196
+ flat[i] = value;
197
+ offset += bytesRead;
198
+ }
199
+ result[field.name] = regroupNested(flat, dims).value;
200
+ }
201
+ else {
202
+ const { value, bytesRead } = decodeStateValue(scriptHex, offset, field.type, field.name);
203
+ result[field.name] = value;
204
+ offset += bytesRead;
205
+ }
206
+ }
207
+ if (offset !== scriptHex.length) {
208
+ throw new Error(`deserializeState: ${(scriptHex.length - offset) / 2} unexpected trailing byte(s) after the ` +
209
+ `last state field (consumed ${offset / 2} of ${scriptHex.length / 2} bytes) — the state ` +
210
+ `section does not match the artifact's stateFields`);
211
+ }
212
+ return result;
213
+ }
214
+ /**
215
+ * Extract state from a full locking script hex, given the artifact.
216
+ *
217
+ * Returns null if the artifact has no state fields or the script doesn't
218
+ * contain a recognisable state section.
219
+ */
220
+ export function extractStateFromScript(artifact, scriptHex) {
221
+ if (!artifact.stateFields || artifact.stateFields.length === 0) {
222
+ return null;
223
+ }
224
+ const opReturnPos = findLastOpReturn(scriptHex);
225
+ if (opReturnPos === -1) {
226
+ return null;
227
+ }
228
+ // State data starts after the OP_RETURN byte (2 hex chars)
229
+ const stateHex = scriptHex.slice(opReturnPos + 2);
230
+ return deserializeState(artifact.stateFields, stateHex);
231
+ }
232
+ /**
233
+ * Walk the script hex as Bitcoin Script opcodes to find the last OP_RETURN
234
+ * (0x6a) at a real opcode boundary. Unlike `lastIndexOf('6a')`, this
235
+ * properly skips push data so it won't match 0x6a bytes inside data payloads.
236
+ *
237
+ * Returns the hex-char offset of the last OP_RETURN, or -1 if not found.
238
+ */
239
+ export function findLastOpReturn(scriptHex) {
240
+ let offset = 0;
241
+ const len = scriptHex.length;
242
+ while (offset + 2 <= len) {
243
+ const opcode = parseInt(scriptHex.slice(offset, offset + 2), 16);
244
+ if (opcode === 0x6a) {
245
+ // OP_RETURN at a real opcode boundary. Everything after OP_RETURN is
246
+ // raw state data (not opcodes), so stop walking immediately.
247
+ return offset;
248
+ }
249
+ else if (opcode >= 0x01 && opcode <= 0x4b) {
250
+ // Direct push: opcode is the number of bytes to push
251
+ offset += 2 + opcode * 2;
252
+ }
253
+ else if (opcode === 0x4c) {
254
+ // OP_PUSHDATA1: next 1 byte is the length
255
+ if (offset + 4 > len)
256
+ break;
257
+ const pushLen = parseInt(scriptHex.slice(offset + 2, offset + 4), 16);
258
+ offset += 4 + pushLen * 2;
259
+ }
260
+ else if (opcode === 0x4d) {
261
+ // OP_PUSHDATA2: next 2 bytes (LE) are the length
262
+ if (offset + 6 > len)
263
+ break;
264
+ const lo = parseInt(scriptHex.slice(offset + 2, offset + 4), 16);
265
+ const hi = parseInt(scriptHex.slice(offset + 4, offset + 6), 16);
266
+ const pushLen = lo | (hi << 8);
267
+ offset += 6 + pushLen * 2;
268
+ }
269
+ else if (opcode === 0x4e) {
270
+ // OP_PUSHDATA4: next 4 bytes (LE) are the length
271
+ if (offset + 10 > len)
272
+ break;
273
+ const b0 = parseInt(scriptHex.slice(offset + 2, offset + 4), 16);
274
+ const b1 = parseInt(scriptHex.slice(offset + 4, offset + 6), 16);
275
+ const b2 = parseInt(scriptHex.slice(offset + 6, offset + 8), 16);
276
+ const b3 = parseInt(scriptHex.slice(offset + 8, offset + 10), 16);
277
+ const pushLen = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
278
+ offset += 10 + pushLen * 2;
279
+ }
280
+ else {
281
+ // All other opcodes (OP_0, OP_1..OP_16, OP_IF, OP_ADD, etc.)
282
+ offset += 2;
283
+ }
284
+ }
285
+ return -1;
286
+ }
287
+ // ---------------------------------------------------------------------------
288
+ // Encoding helpers
289
+ // ---------------------------------------------------------------------------
290
+ /**
291
+ * Encode a state field as raw bytes (no push opcode wrapper) matching the
292
+ * compiler's OP_NUM2BIN-based fixed-width serialization.
293
+ * The result is raw hex bytes that are concatenated after OP_RETURN.
294
+ */
295
+ function encodeStateValue(value, type, label = '?') {
296
+ switch (type) {
297
+ case 'int':
298
+ case 'bigint': {
299
+ let n;
300
+ if (typeof value === 'bigint') {
301
+ n = value;
302
+ }
303
+ else if (typeof value === 'string' && value.endsWith('n')) {
304
+ // BigInt string from JSON without reviver (e.g. "0n", "1000n")
305
+ n = BigInt(value.slice(0, -1));
306
+ }
307
+ else {
308
+ n = BigInt(value);
309
+ }
310
+ return encodeNum2Bin(n, 8, label);
311
+ }
312
+ case 'bool':
313
+ case 'boolean': {
314
+ // 1 raw byte — matches on-chain serialization (05-stack-lower.ts
315
+ // `case 'boolean': propSizes.push(1)`), decodeStateValue, and the shared
316
+ // STATE_FIELD_WIDTHS table. The canonical Rúnar primitive name is
317
+ // `boolean`; `bool` is accepted as an alias. Without the `boolean` case
318
+ // a real boolean state field fell through to the push-data `default`,
319
+ // silently disagreeing with all three (the descriptor drift #115's
320
+ // review flagged).
321
+ return value ? '01' : '00';
322
+ }
323
+ default: {
324
+ // Fixed-size byte types (PubKey 33, Addr/Ripemd160 20, Sha256 32,
325
+ // Point 64, P256Point 64, P384Point 96): raw hex, no framing needed.
326
+ //
327
+ // Table-driven off the SAME runar-ir-schema table `decodeStateValue`
328
+ // reads, so this writer and that reader cannot drift. It used to be a
329
+ // hand-maintained `case` list, and every type the compiler emitted raw
330
+ // but the list omitted deployed a framed state section the on-chain
331
+ // reader could not parse — the #115 `boolean` drift above, and then
332
+ // P256Point / P384Point, which locked funds on the first spend.
333
+ const rawWidth = STATE_FIELD_WIDTHS[type];
334
+ if (rawWidth?.encoding === 'raw') {
335
+ // Refuse a missing value rather than stringifying it. `String(undefined)`
336
+ // writes the nine characters `undefined` where the artifact declares
337
+ // `rawWidth.size` bytes — not hex, wrong width, and silent. The six peer
338
+ // SDKs refuse this (C-2); TypeScript writing garbage would make the
339
+ // reference implementation the only tier that does.
340
+ //
341
+ // Deliberately NOT applied to the variable-length branch below: an empty
342
+ // ByteString is a legitimate state value that encodes as OP_0, whereas a
343
+ // missing PubKey is not a value at all.
344
+ if (value === undefined || value === null) {
345
+ throw new Error(`serializeState: state field "${label}" (${type}) has no value. ` +
346
+ `A raw fixed-width field must carry exactly ${rawWidth.size} bytes; ` +
347
+ `writing a placeholder here would put a state section on chain that ` +
348
+ `the contract's own on-chain reader cannot parse.`);
349
+ }
350
+ return String(value);
351
+ }
352
+ // Variable-length types (bytes, ByteString, etc.): use push-data
353
+ // encoding so the decoder can determine the length.
354
+ const hex = String(value);
355
+ if (hex.length === 0)
356
+ return '00'; // OP_0
357
+ return encodePushDataState(hex);
358
+ }
359
+ }
360
+ }
361
+ /**
362
+ * Encode an integer as a fixed-width LE sign-magnitude byte string,
363
+ * matching OP_NUM2BIN behaviour. The sign bit is in the MSB of the last byte.
364
+ *
365
+ * FAILS CLOSED on an out-of-range magnitude. `width` bytes of sign-magnitude
366
+ * hold `8*width - 1` magnitude bits — the top bit of the last byte is the
367
+ * sign. The loop below writes the low `width` bytes and drops everything
368
+ * above, then ORs the sign bit in on top of whatever landed there, so an
369
+ * oversized value used to serialise to a plausible but WRONG word:
370
+ *
371
+ * 2^63 -> 0000000000000080 reads back as 0 (negative zero)
372
+ * 2^63 + 5 -> 0500000000000080 reads back as -5 (sign flip)
373
+ * 2^64 -> 0000000000000000 reads back as 0
374
+ *
375
+ * The deploy then succeeded and the UTXO was unspendable: the covenant
376
+ * rebuilds the continuation with the compiler's own OP_NUM2BIN 8, which cannot
377
+ * produce those bytes from that number, so hash256(outputs) never matches.
378
+ * Throwing here is the only place a runtime-computed state value can be
379
+ * stopped — `±(2^63 - 1)` remains representable and is unaffected.
380
+ */
381
+ function encodeNum2Bin(n, width, label = '?') {
382
+ const limit = 1n << BigInt(8 * width - 1);
383
+ if (n >= limit || n <= -limit) {
384
+ throw new Error(`serializeState: bigint state field "${label}" = ${n} does not fit the ` +
385
+ `fixed ${width}-byte sign-magnitude state word (magnitude must be ` +
386
+ `< 2^${8 * width - 1}). Serializing it would write a different number ` +
387
+ `into the state section than the contract's on-chain OP_NUM2BIN ` +
388
+ `${width} rebuilds, leaving the output unspendable.`);
389
+ }
390
+ const bytes = new Uint8Array(width);
391
+ const negative = n < 0n;
392
+ let absVal = negative ? -n : n;
393
+ for (let i = 0; i < width && absVal > 0n; i++) {
394
+ bytes[i] = Number(absVal & 0xffn);
395
+ absVal >>= 8n;
396
+ }
397
+ if (negative) {
398
+ bytes[width - 1] |= 0x80;
399
+ }
400
+ return Array.from(bytes)
401
+ .map((b) => b.toString(16).padStart(2, '0'))
402
+ .join('');
403
+ }
404
+ /**
405
+ * Encode variable-length data as a length-prefixed state-section field.
406
+ *
407
+ * This is the `<len><data>` framing the COMPILER uses on-chain, and it is
408
+ * deliberately NOT the MINIMALDATA push encoding.
409
+ *
410
+ * The state section is raw data living after `OP_RETURN` in the locking
411
+ * script. The interpreter never executes it, so `SCRIPT_VERIFY_MINIMALDATA`
412
+ * — a rule applied to push opcodes as they are executed — does not reach it.
413
+ * What does read it is the compiler's own on-chain state codec:
414
+ * `emitPushDataEncode` in packages/runar-compiler/src/passes/05-stack-lower.ts
415
+ * writes `<len><data>` when rebuilding the continuation, and the on-chain
416
+ * reader parses `<len><data>`. Both sides must agree byte for byte or the
417
+ * `hash256(outputs) == extractOutputHash(preimage)` check fails and the
418
+ * contract is unspendable.
419
+ *
420
+ * #110 briefly applied the MINIMALDATA short-circuit here (1-byte payloads in
421
+ * `{0x01..=0x10, 0x81}` -> `OP_1..OP_16` / `OP_1NEGATE`), changing all seven
422
+ * SDKs and none of the seven compilers. That desynchronised the two sides for
423
+ * exactly those values: a 1-byte `0x05` state field serialised off-chain as
424
+ * `55` while the script rebuilt it as `0105`, so any contract carrying such a
425
+ * value became permanently unspendable — on the WRITE path (continuation hash
426
+ * mismatch) and on the READ path (the on-chain reader takes `0x55` as a
427
+ * length-85 push and dies in OP_SPLIT).
428
+ *
429
+ * MINIMALDATA still applies — and is still enforced — where the interpreter
430
+ * really does execute the push: `encodePushData`/`encodeArg` in contract.ts,
431
+ * which build UNLOCKING scripts. That is the path #110's evidence came from
432
+ * (ByteString call args), and it is unchanged.
433
+ */
434
+ function encodePushDataState(dataHex) {
435
+ const len = dataHex.length / 2;
436
+ if (len <= 75) {
437
+ return len.toString(16).padStart(2, '0') + dataHex;
438
+ }
439
+ else if (len <= 0xff) {
440
+ return '4c' + len.toString(16).padStart(2, '0') + dataHex;
441
+ }
442
+ else if (len <= 0xffff) {
443
+ const lo = (len & 0xff).toString(16).padStart(2, '0');
444
+ const hi = ((len >> 8) & 0xff).toString(16).padStart(2, '0');
445
+ return '4d' + lo + hi + dataHex;
446
+ }
447
+ const b0 = (len & 0xff).toString(16).padStart(2, '0');
448
+ const b1 = ((len >> 8) & 0xff).toString(16).padStart(2, '0');
449
+ const b2 = ((len >> 16) & 0xff).toString(16).padStart(2, '0');
450
+ const b3 = ((len >> 24) & 0xff).toString(16).padStart(2, '0');
451
+ return '4e' + b0 + b1 + b2 + b3 + dataHex;
452
+ }
453
+ // ---------------------------------------------------------------------------
454
+ // Decoding helpers
455
+ // ---------------------------------------------------------------------------
456
+ function decodeStateValue(hex, offset, type, label) {
457
+ // Fixed-width types read exactly `size` raw bytes; widths come from the
458
+ // shared runar-ir-schema table so the codec, the compiler's stateField
459
+ // layout annotations, and verifier tooling can never drift apart.
460
+ const width = STATE_FIELD_WIDTHS[type];
461
+ if (width) {
462
+ const hexWidth = width.size * 2;
463
+ if (offset + hexWidth > hex.length) {
464
+ throw new Error(`deserializeState: truncated state — field "${label}" (${type}) needs ${width.size} byte(s) ` +
465
+ `at offset ${offset / 2} but only ${(hex.length - offset) / 2} byte(s) remain`);
466
+ }
467
+ const data = hex.slice(offset, offset + hexWidth);
468
+ switch (width.encoding) {
469
+ case 'bool1':
470
+ // 1 raw byte: 0x00 = false, 0x01 = true
471
+ return { value: data !== '00', bytesRead: hexWidth };
472
+ case 'num2bin-le8':
473
+ // 8 raw bytes LE sign-magnitude (NUM2BIN 8)
474
+ return { value: decodeNum2Bin(data), bytesRead: hexWidth };
475
+ default:
476
+ // Raw fixed-size byte types (PubKey 33, Addr/Ripemd160 20, Sha256 32, Point 64)
477
+ return { value: data, bytesRead: hexWidth };
478
+ }
479
+ }
480
+ // For variable-length / unknown types, fall back to push-data decoding
481
+ const { data, bytesRead } = decodePushData(hex, offset, label);
482
+ return { value: data, bytesRead };
483
+ }
484
+ /**
485
+ * Decode a fixed-width LE sign-magnitude number.
486
+ */
487
+ function decodeNum2Bin(hex) {
488
+ if (hex.length === 0)
489
+ return 0n;
490
+ const bytes = [];
491
+ for (let i = 0; i < hex.length; i += 2) {
492
+ bytes.push(parseInt(hex.slice(i, i + 2), 16));
493
+ }
494
+ const negative = (bytes[bytes.length - 1] & 0x80) !== 0;
495
+ bytes[bytes.length - 1] &= 0x7f;
496
+ let result = 0n;
497
+ for (let i = bytes.length - 1; i >= 0; i--) {
498
+ result = (result << 8n) | BigInt(bytes[i]);
499
+ }
500
+ if (result === 0n)
501
+ return 0n;
502
+ return negative ? -result : result;
503
+ }
504
+ /**
505
+ * Decode a state-section field at the given hex offset.
506
+ * Returns the field data (hex) and the total number of hex chars consumed.
507
+ *
508
+ * Exact inverse of `encodePushDataState`, and deliberately as strict as the
509
+ * compiler's on-chain state reader: only `<len><data>` framing is understood.
510
+ * `OP_1..OP_16` (0x51..0x60) and `OP_1NEGATE` (0x4f) are NOT decoded as
511
+ * single-byte values — accepting them here would let the SDK read a state
512
+ * section that the contract's own script cannot parse (the on-chain reader
513
+ * takes 0x55 as a length-85 push), restoring exactly the wrong-but-plausible
514
+ * state C28 exists to prevent. `OP_0` (0x00) falls through to the
515
+ * `opcode <= 75` branch below and correctly decodes as the empty byte array.
516
+ */
517
+ function decodePushData(hex, offset, label) {
518
+ /** Assert `chars` hex chars are available from `offset`, else fail closed. */
519
+ const need = (chars, what) => {
520
+ if (offset + chars > hex.length) {
521
+ throw new Error(`deserializeState: truncated state — field "${label}" ${what} runs past the end of the ` +
522
+ `state section (needs ${chars / 2} byte(s) at offset ${offset / 2}, ` +
523
+ `only ${(hex.length - offset) / 2} remain)`);
524
+ }
525
+ };
526
+ need(2, 'push opcode');
527
+ const opcode = parseInt(hex.slice(offset, offset + 2), 16);
528
+ if (Number.isNaN(opcode)) {
529
+ throw new Error(`deserializeState: field "${label}" — non-hex byte at offset ${offset / 2} in the state section`);
530
+ }
531
+ if (opcode <= 75) {
532
+ // Direct push: opcode is the byte length
533
+ const dataLen = opcode * 2;
534
+ need(2 + dataLen, 'push payload');
535
+ return {
536
+ data: hex.slice(offset + 2, offset + 2 + dataLen),
537
+ bytesRead: 2 + dataLen,
538
+ };
539
+ }
540
+ else if (opcode === 0x4c) {
541
+ // OP_PUSHDATA1
542
+ need(4, 'OP_PUSHDATA1 length prefix');
543
+ const len = parseInt(hex.slice(offset + 2, offset + 4), 16);
544
+ const dataLen = len * 2;
545
+ need(4 + dataLen, 'OP_PUSHDATA1 payload');
546
+ return {
547
+ data: hex.slice(offset + 4, offset + 4 + dataLen),
548
+ bytesRead: 4 + dataLen,
549
+ };
550
+ }
551
+ else if (opcode === 0x4d) {
552
+ // OP_PUSHDATA2
553
+ need(6, 'OP_PUSHDATA2 length prefix');
554
+ const lo = parseInt(hex.slice(offset + 2, offset + 4), 16);
555
+ const hi = parseInt(hex.slice(offset + 4, offset + 6), 16);
556
+ const len = lo | (hi << 8);
557
+ const dataLen = len * 2;
558
+ need(6 + dataLen, 'OP_PUSHDATA2 payload');
559
+ return {
560
+ data: hex.slice(offset + 6, offset + 6 + dataLen),
561
+ bytesRead: 6 + dataLen,
562
+ };
563
+ }
564
+ else if (opcode === 0x4e) {
565
+ // OP_PUSHDATA4
566
+ need(10, 'OP_PUSHDATA4 length prefix');
567
+ const b0 = parseInt(hex.slice(offset + 2, offset + 4), 16);
568
+ const b1 = parseInt(hex.slice(offset + 4, offset + 6), 16);
569
+ const b2 = parseInt(hex.slice(offset + 6, offset + 8), 16);
570
+ const b3 = parseInt(hex.slice(offset + 8, offset + 10), 16);
571
+ const len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
572
+ const dataLen = len * 2;
573
+ need(10 + dataLen, 'OP_PUSHDATA4 payload');
574
+ return {
575
+ data: hex.slice(offset + 10, offset + 10 + dataLen),
576
+ bytesRead: 10 + dataLen,
577
+ };
578
+ }
579
+ // Not a push opcode at all — `encodePushDataState` can never emit one, so the
580
+ // state section is malformed. Previously this silently consumed one byte and
581
+ // returned an empty value, which desynchronised every subsequent field.
582
+ throw new Error(`deserializeState: field "${label}" — byte 0x${opcode.toString(16).padStart(2, '0')} at offset ` +
583
+ `${offset / 2} is not a push opcode; the state section is malformed`);
584
+ }