ecash-lib 4.3.3 → 4.4.0

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/src/index.ts CHANGED
@@ -30,4 +30,5 @@ export * from './token/common.js';
30
30
  export * from './token/empp.js';
31
31
  export * from './token/slp.js';
32
32
  export * from './token/slp.parse.js';
33
+ export * from './parse/opreturn.js';
33
34
  export * as payment from './payment';
@@ -0,0 +1,256 @@
1
+ // Copyright (c) 2025 The Bitcoin developers
2
+ // Distributed under the MIT software license, see the accompanying
3
+ // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
+
5
+ import { OP_RETURN_MAX_BYTES } from '../consts.js';
6
+ import { toHex } from '../io/hex.js';
7
+ import {
8
+ OP_RETURN,
9
+ OP_0,
10
+ OP_1NEGATE,
11
+ OP_RESERVED,
12
+ OP_1,
13
+ OP_2,
14
+ OP_3,
15
+ OP_4,
16
+ OP_5,
17
+ OP_6,
18
+ OP_7,
19
+ OP_8,
20
+ OP_9,
21
+ OP_10,
22
+ OP_11,
23
+ OP_12,
24
+ OP_13,
25
+ OP_14,
26
+ OP_15,
27
+ OP_16,
28
+ OP_PUSHDATA1,
29
+ OP_PUSHDATA2,
30
+ OP_PUSHDATA4,
31
+ } from '../opcode.js';
32
+
33
+ /**
34
+ * Convert an OP_RETURN outputScript into an array of pushes
35
+ * @param outputScript - An OP_RETURN output script, e.g. 6a042e7865630003333333150076458db0ed96fe9863fc1ccec9fa2cfab884b0f6
36
+ * @returns An array of hex pushes, e.g. ['2e786563', '00', '333333', '0076458db0ed96fe9863fc1ccec9fa2cfab884b0f6']
37
+ * @throws Error if outputScript is not a valid OP_RETURN outputScript
38
+ */
39
+ export function getStackArray(outputScript: string): string[] {
40
+ const opReturnHex = toHex(new Uint8Array([OP_RETURN]));
41
+
42
+ // Validate for OP_RETURN outputScript
43
+ if (
44
+ typeof outputScript !== 'string' ||
45
+ !outputScript.startsWith(opReturnHex)
46
+ ) {
47
+ throw new Error(
48
+ `outputScript must be a string that starts with ${opReturnHex}`,
49
+ );
50
+ }
51
+ if (outputScript.length > 2 * OP_RETURN_MAX_BYTES) {
52
+ throw new Error(
53
+ `Invalid eCash OP_RETURN size: ${
54
+ outputScript.length / 2
55
+ } bytes. eCash OP_RETURN outputs cannot exceed ${OP_RETURN_MAX_BYTES} bytes.`,
56
+ );
57
+ }
58
+
59
+ // Create stack, the input object required by consumeNextPush
60
+ const stack = {
61
+ remainingHex: outputScript.slice(opReturnHex.length),
62
+ };
63
+
64
+ // Initialize stackArray
65
+ const stackArray: string[] = [];
66
+
67
+ while (stack.remainingHex.length > 0) {
68
+ stackArray.push(consumeNextPush(stack).data);
69
+ }
70
+
71
+ return stackArray;
72
+ }
73
+
74
+ /**
75
+ * One-byte stack additions that can be pushed to OP_RETURN in isolation
76
+ */
77
+ const ONE_BYTE_STACK_ADDS = [
78
+ OP_0,
79
+ OP_1NEGATE,
80
+ OP_RESERVED,
81
+ OP_1,
82
+ OP_2,
83
+ OP_3,
84
+ OP_4,
85
+ OP_5,
86
+ OP_6,
87
+ OP_7,
88
+ OP_8,
89
+ OP_9,
90
+ OP_10,
91
+ OP_11,
92
+ OP_12,
93
+ OP_13,
94
+ OP_14,
95
+ OP_15,
96
+ OP_16,
97
+ ];
98
+
99
+ /**
100
+ * One-byte pushdata opcodes (0x01-0x4b)
101
+ */
102
+ const ONE_BYTE_PUSHDATAS: number[] = [];
103
+ for (let i = 1; i <= 0x4b; i++) {
104
+ ONE_BYTE_PUSHDATAS.push(i);
105
+ }
106
+
107
+ /**
108
+ * Swap endianness of a hex string
109
+ * @param hexString a string of hex bytes, e.g. 04000000
110
+ * @returns a string of hex bytes with swapped endianness, e.g. for 04000000, returns 00000004
111
+ */
112
+ export function swapEndianness(hexString: string): string {
113
+ const byteLength = 2;
114
+
115
+ if (hexString.length % byteLength === 1) {
116
+ throw new Error(
117
+ `Invalid input length ${hexString.length}: hexString must be divisible by bytes, i.e. have an even length.`,
118
+ );
119
+ }
120
+
121
+ // Check if input contains only hex characters
122
+ if (!/^[\da-f]+$/i.test(hexString)) {
123
+ throw new Error(
124
+ `Invalid input. ${hexString} contains non-hexadecimal characters.`,
125
+ );
126
+ }
127
+
128
+ let swappedEndianHexString = '';
129
+ let remainingHex = hexString;
130
+ while (remainingHex.length > 0) {
131
+ // Get the last byte on the string
132
+ const thisByte = remainingHex.slice(-byteLength);
133
+ // Add thisByte to swappedEndianHexString in swapped-endian order
134
+ swappedEndianHexString += thisByte;
135
+
136
+ // Remove thisByte from remainingHex
137
+ remainingHex = remainingHex.slice(0, -byteLength);
138
+ }
139
+ return swappedEndianHexString;
140
+ }
141
+
142
+ /**
143
+ * Consume a specified number of bytes from a stack object
144
+ * @param stack an object containing a hex string outputScript of an eCash tx, e.g. {remainingHex: '6a...'}
145
+ * @param byteCount integer
146
+ * @returns consumed, a hex string of byteCount bytes
147
+ * The stack object is modified in place so that consumed bytes are removed
148
+ */
149
+ export function consume(
150
+ stack: { remainingHex: string },
151
+ byteCount: number,
152
+ ): string {
153
+ // Validation for stack
154
+ if (typeof stack !== 'object' || typeof stack.remainingHex !== 'string') {
155
+ throw new Error(
156
+ 'Invalid input. Stack must be an object with string stored at key remainingHex.',
157
+ );
158
+ }
159
+ if (stack.remainingHex.length % 2 === 1) {
160
+ throw new Error(
161
+ 'Invalid input: stack.remainingHex must be divisible by bytes, i.e. have an even length.',
162
+ );
163
+ }
164
+ // Throw an error if byteCount input is not an integer
165
+ if (!Number.isInteger(byteCount)) {
166
+ throw new Error(`byteCount must be an integer, received ${byteCount}`);
167
+ }
168
+ // One byte is 2 characters of a hex string
169
+ const byteLength = 2;
170
+
171
+ // Get byte slice size
172
+ const byteSliceSize = byteCount * byteLength;
173
+
174
+ // Throw an error if byteCount is greater than consumable hex bytes in outputScript
175
+ if (byteSliceSize > stack.remainingHex.length) {
176
+ throw new Error(
177
+ `consume called with byteCount (${byteCount}) greater than remaining bytes in outputScript (${
178
+ stack.remainingHex.length / byteLength
179
+ })`,
180
+ );
181
+ }
182
+ // Get consumed bytes
183
+ const consumed = stack.remainingHex.slice(0, byteSliceSize);
184
+ // Remove consumed from the stack
185
+ stack.remainingHex = stack.remainingHex.slice(byteSliceSize);
186
+ return consumed;
187
+ }
188
+
189
+ /**
190
+ * Parse, decode and consume the data push from the top of the stack.
191
+ * If the stack does not start with a valid push, it raises an error and the stack is left untouched.
192
+ * @param stack an object containing a hex string outputScript of an eCash tx, e.g. {remainingHex: '4d...'}
193
+ * @returns {data, pushedWith}
194
+ * stack is modified in place so that the push is removed
195
+ */
196
+ export function consumeNextPush(stack: { remainingHex: string }): {
197
+ data: string;
198
+ pushedWith: string;
199
+ } {
200
+ // Clone stack in case you have an error and wish to leave it unmodified
201
+ const clonedStack = structuredClone(stack);
202
+
203
+ // Get the first byte on the stack
204
+ const pushOpCodeHex = consume(clonedStack, 1);
205
+ const pushOpCode = parseInt(pushOpCodeHex, 16);
206
+
207
+ if (ONE_BYTE_STACK_ADDS.includes(pushOpCode)) {
208
+ // If this is a one-byte push, consume stack and return the byte
209
+ stack.remainingHex = clonedStack.remainingHex;
210
+ return { data: pushOpCodeHex, pushedWith: pushOpCodeHex };
211
+ }
212
+
213
+ // Initialize variables
214
+ let pushBytecountHex: string;
215
+
216
+ // Apply conditional checks to determine the size of this push
217
+ if (ONE_BYTE_PUSHDATAS.includes(pushOpCode)) {
218
+ // If the first byte on the stack is 0x01-0x4b, then this is pushedBytesHex
219
+ pushBytecountHex = pushOpCodeHex;
220
+ } else if (pushOpCode === OP_PUSHDATA1) {
221
+ // The next byte contains the number of bytes to be pushed onto the stack.
222
+ pushBytecountHex = consume(clonedStack, 1);
223
+ } else if (pushOpCode === OP_PUSHDATA2) {
224
+ // The next two bytes contain the number of bytes to be pushed onto the stack in little endian order.
225
+ pushBytecountHex = consume(clonedStack, 2);
226
+ } else if (pushOpCode === OP_PUSHDATA4) {
227
+ // The next four bytes contain the number of bytes to be pushed onto the stack in little endian order.
228
+ pushBytecountHex = consume(clonedStack, 4);
229
+ } else {
230
+ throw new Error(`${pushOpCodeHex} is invalid pushdata`);
231
+ }
232
+
233
+ // Now that you know how many bytes are in the push, get the pushed data
234
+ const data = consume(
235
+ clonedStack,
236
+ parseInt(swapEndianness(pushBytecountHex), 16),
237
+ );
238
+
239
+ // If no error, consume stack
240
+ stack.remainingHex = clonedStack.remainingHex;
241
+
242
+ /*
243
+ Return {data, pushedWith}
244
+ Note that if the first byte on the stack is 0x01-0x4b,
245
+ this is both pushOpCode and pushBytecountHex
246
+
247
+ You don't want to return '0404' for e.g. '042e786563'
248
+ Conditionally remove pushBytecountHex for this case
249
+ */
250
+ return {
251
+ data,
252
+ pushedWith: `${pushOpCodeHex}${
253
+ pushOpCodeHex !== pushBytecountHex ? pushBytecountHex : ''
254
+ }`,
255
+ };
256
+ }