@cashscript/utils 0.13.0-next.7 → 0.13.0-next.9
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/dist/artifact.d.ts +2 -0
- package/dist/artifact.js +54 -26
- package/dist/bitauth-script.js +53 -12
- package/dist/fingerprint.d.ts +5 -0
- package/dist/fingerprint.js +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/script.js +2 -0
- package/dist/types.d.ts +3 -1
- package/dist/types.js +3 -2
- package/package.json +2 -2
package/dist/artifact.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface CompilerOptions {
|
|
2
2
|
enforceFunctionParameterTypes?: boolean;
|
|
3
|
+
enforceLocktimeGuard?: boolean;
|
|
3
4
|
}
|
|
4
5
|
export interface AbiInput {
|
|
5
6
|
name: string;
|
|
@@ -46,5 +47,6 @@ export interface Artifact {
|
|
|
46
47
|
options?: CompilerOptions;
|
|
47
48
|
};
|
|
48
49
|
updatedAt: string;
|
|
50
|
+
fingerprint?: string;
|
|
49
51
|
}
|
|
50
52
|
export declare function formatArtifact(artifact: Artifact, format: 'json' | 'ts'): string;
|
package/dist/artifact.js
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
export function formatArtifact(artifact, format) {
|
|
2
2
|
if (format === 'ts') {
|
|
3
|
-
// We remove any undefined values to make the artifact serializable
|
|
3
|
+
// We remove any undefined values to make the artifact serializable
|
|
4
4
|
const normalisedArtifact = JSON.parse(JSON.stringify(artifact));
|
|
5
|
-
return `export default ${
|
|
5
|
+
return `export default ${stringify(normalisedArtifact, 'ts')} as const;\n`;
|
|
6
6
|
}
|
|
7
|
-
return
|
|
7
|
+
return stringify(artifact, 'json');
|
|
8
8
|
}
|
|
9
9
|
const indent = (level) => ' '.repeat(level);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Objects with this many or more properties are always expanded onto multiple lines, to keep
|
|
11
|
+
// generated `.artifact.ts` files compliant with the `object-curly-newline` lint rule (minProperties: 4)
|
|
12
|
+
const MAX_INLINE_OBJECT_PROPERTIES = 3;
|
|
13
|
+
// Recursively serialises an artifact to JSON or TS. Small records that are elements of an array
|
|
14
|
+
// (e.g. `requires`, `constructorInputs`, `abi` inputs) are kept on a single line so list-like data
|
|
15
|
+
// stays compact, while standalone objects assigned to a property (e.g. `compiler.options`) keep
|
|
16
|
+
// their values expanded one per line.
|
|
17
|
+
function stringify(obj, format, indentationLevel = 1, isArrayElement = false) {
|
|
18
|
+
if (typeof obj === 'string')
|
|
19
|
+
return formatString(obj, format);
|
|
20
|
+
// Numbers, booleans and null are represented identically in JSON and TS
|
|
21
|
+
if (obj === null || typeof obj === 'number' || typeof obj === 'boolean') {
|
|
18
22
|
return JSON.stringify(obj);
|
|
19
23
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const formattedItems = obj.map((item) => `${indent(indentationLevel)}${stringifyAsTs(item, indentationLevel + 1)}`);
|
|
25
|
-
return `[\n${formattedItems.join(',\n')},\n${indent(indentationLevel - 1)}]`;
|
|
26
|
-
}
|
|
27
|
-
// Objects are recursively formatted with indentation
|
|
28
|
-
if (typeof obj === 'object') {
|
|
29
|
-
const entries = Object.entries(obj);
|
|
30
|
-
if (entries.length === 0)
|
|
31
|
-
return '{}';
|
|
32
|
-
const formattedEntries = entries.map(([key, value]) => (`${indent(indentationLevel)}${key}: ${stringifyAsTs(value, indentationLevel + 1)}`));
|
|
33
|
-
return `{\n${formattedEntries.join(',\n')},\n${indent(indentationLevel - 1)}}`;
|
|
34
|
-
}
|
|
24
|
+
if (Array.isArray(obj))
|
|
25
|
+
return formatArray(obj, format, indentationLevel, isArrayElement);
|
|
26
|
+
if (typeof obj === 'object')
|
|
27
|
+
return formatObject(obj, format, indentationLevel, isArrayElement);
|
|
35
28
|
throw new Error(`Unsupported type: ${typeof obj}`);
|
|
36
29
|
}
|
|
30
|
+
function formatString(value, format) {
|
|
31
|
+
// JSON strings use standard double-quoted escaping
|
|
32
|
+
if (format === 'json')
|
|
33
|
+
return JSON.stringify(value);
|
|
34
|
+
// TS strings use single quotes instead of double quotes around string values, to match regular TS style
|
|
35
|
+
return `'${JSON.stringify(value).replace(/'/g, "\\'").replace(/\\"/g, '"').slice(1, -1)}'`;
|
|
36
|
+
}
|
|
37
|
+
function formatArray(array, format, indentationLevel, isArrayElement) {
|
|
38
|
+
if (array.length === 0)
|
|
39
|
+
return '[]';
|
|
40
|
+
const items = array.map((item) => stringify(item, format, indentationLevel + 1, true));
|
|
41
|
+
if (isArrayElement && canInline(array))
|
|
42
|
+
return `[${items.join(', ')}]`;
|
|
43
|
+
const lines = items.map((item) => `${indent(indentationLevel)}${item}`).join(',\n');
|
|
44
|
+
const trailingComma = format === 'ts' ? ',' : '';
|
|
45
|
+
return `[\n${lines}${trailingComma}\n${indent(indentationLevel - 1)}]`;
|
|
46
|
+
}
|
|
47
|
+
function formatObject(obj, format, indentationLevel, isArrayElement) {
|
|
48
|
+
const entries = Object.entries(obj).filter(([, value]) => value !== undefined);
|
|
49
|
+
if (entries.length === 0)
|
|
50
|
+
return '{}';
|
|
51
|
+
const formatKey = (key) => (format === 'json' ? JSON.stringify(key) : key);
|
|
52
|
+
const formatted = entries.map(([key, value]) => `${formatKey(key)}: ${stringify(value, format, indentationLevel + 1)}`);
|
|
53
|
+
if (isArrayElement && entries.length <= MAX_INLINE_OBJECT_PROPERTIES && canInline(obj)) {
|
|
54
|
+
return `{ ${formatted.join(', ')} }`;
|
|
55
|
+
}
|
|
56
|
+
const lines = formatted.map((entry) => `${indent(indentationLevel)}${entry}`).join(',\n');
|
|
57
|
+
const trailingComma = format === 'ts' ? ',' : '';
|
|
58
|
+
return `{\n${lines}${trailingComma}\n${indent(indentationLevel - 1)}}`;
|
|
59
|
+
}
|
|
60
|
+
// A container can be inlined when none of its values are themselves a non-empty object or array
|
|
61
|
+
function canInline(container) {
|
|
62
|
+
const values = Array.isArray(container) ? container : Object.values(container);
|
|
63
|
+
return values.every((value) => (value === undefined || value === null || typeof value !== 'object' || Object.keys(value).length === 0));
|
|
64
|
+
}
|
|
37
65
|
//# sourceMappingURL=artifact.js.map
|
package/dist/bitauth-script.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { range } from './data.js';
|
|
2
2
|
import { scriptToBitAuthAsm } from './script.js';
|
|
3
3
|
import { parseSourceTags, sourceMapToLocationData } from './source-map.js';
|
|
4
|
-
import { PositionHint } from './types.js';
|
|
4
|
+
import { PositionHint, SourceTagKind } from './types.js';
|
|
5
5
|
export function buildLineToOpcodesMap(bytecode, sourceMapOrLocationData) {
|
|
6
6
|
const locationData = typeof sourceMapOrLocationData === 'string' ? sourceMapToLocationData(sourceMapOrLocationData) : sourceMapOrLocationData;
|
|
7
7
|
return locationData.reduce((lineToOpcodeMap, singleLocation, index) => {
|
|
@@ -43,14 +43,38 @@ function getDisplayLine(singleLocation) {
|
|
|
43
43
|
function escapeCommentChars(text) {
|
|
44
44
|
return text.replaceAll('/*', '\\/*').replaceAll('*/', '*\\/');
|
|
45
45
|
}
|
|
46
|
+
// Compiler-injected prologue checks: emitted before the function body but executing at its top.
|
|
47
|
+
const PROLOGUE_KINDS = [
|
|
48
|
+
SourceTagKind.LOCKTIME_GUARD,
|
|
49
|
+
SourceTagKind.PARAMETER_VALIDATION,
|
|
50
|
+
];
|
|
46
51
|
function buildInsertions(locationData, sourceLines, sourceTags) {
|
|
47
52
|
const tags = (sourceTags ? parseSourceTags(sourceTags) : []);
|
|
48
53
|
return tags.map((tag) => {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
54
|
+
const { insertAfterLine, indent } = deriveAnchor(tag, tags, locationData, sourceLines);
|
|
55
|
+
const annotation = `${indent}${tagDescription(tag, locationData, sourceLines)}`;
|
|
51
56
|
return { insertAfterLine, annotation, startIndex: tag.startIndex, endIndex: tag.endIndex };
|
|
52
57
|
});
|
|
53
58
|
}
|
|
59
|
+
function deriveAnchor(tag, tags, locationData, sourceLines) {
|
|
60
|
+
if (PROLOGUE_KINDS.includes(tag.kind)) {
|
|
61
|
+
const prologueTags = tags.filter((t) => PROLOGUE_KINDS.includes(t.kind));
|
|
62
|
+
// The prologue is one contiguous opcode block at the function's start
|
|
63
|
+
const lastPrologueOpcode = Math.max(...prologueTags.map((t) => t.endIndex));
|
|
64
|
+
const firstBodyOpcode = lastPrologueOpcode + 1;
|
|
65
|
+
const firstBodyLine = getDisplayLine(locationData[firstBodyOpcode]);
|
|
66
|
+
return {
|
|
67
|
+
// `insertAfterLine` splices *after* a line, so `firstBodyLine - 1` lands all the prologue
|
|
68
|
+
// annotations directly above the first body statement, at its indentation.
|
|
69
|
+
insertAfterLine: firstBodyLine - 1,
|
|
70
|
+
indent: lineIndent(sourceLines, firstBodyLine),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
insertAfterLine: getDisplayLine(locationData[Math.max(tag.startIndex - 1, 0)]),
|
|
75
|
+
indent: deriveIndent(getDisplayLine(locationData[tag.startIndex]), sourceLines),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
54
78
|
function spliceSyntheticSourceLines(sourceLines, insertions) {
|
|
55
79
|
return insertions.reduceRight((lines, ins) => [...lines.slice(0, ins.insertAfterLine), ins.annotation, ...lines.slice(ins.insertAfterLine)], sourceLines);
|
|
56
80
|
}
|
|
@@ -80,19 +104,36 @@ const getUpdatedLineNumber = (currentLineNumber, insertion, opcodeIndex) => {
|
|
|
80
104
|
return currentLineNumber + 1;
|
|
81
105
|
return currentLineNumber;
|
|
82
106
|
};
|
|
83
|
-
|
|
84
|
-
function
|
|
107
|
+
// e.g. ">>> for-loop update (i = i + 1)"
|
|
108
|
+
function tagDescription(tag, locationData, sourceLines) {
|
|
109
|
+
switch (tag.kind) {
|
|
110
|
+
case SourceTagKind.LOCKTIME_GUARD:
|
|
111
|
+
return '>>> tx.locktime guard (auto-injected)';
|
|
112
|
+
case SourceTagKind.PARAMETER_VALIDATION: {
|
|
113
|
+
const parameter = deriveSourceText(tag, locationData, sourceLines);
|
|
114
|
+
return `>>> parameter type check${parameter ? ` (${parameter})` : ''}`;
|
|
115
|
+
}
|
|
116
|
+
case SourceTagKind.FOR_UPDATE:
|
|
117
|
+
default:
|
|
118
|
+
return `>>> for-loop update (${deriveSourceText(tag, locationData, sourceLines)})`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function deriveIndent(headerLine, sourceLines) {
|
|
122
|
+
const bodyLine = sourceLines.slice(headerLine).find((l) => l.trim().length > 0);
|
|
123
|
+
return bodyLine?.match(/^(\s*)/)?.[1] ?? '';
|
|
124
|
+
}
|
|
125
|
+
function lineIndent(sourceLines, line) {
|
|
126
|
+
return sourceLines[line - 1]?.match(/^(\s*)/)?.[1] ?? '';
|
|
127
|
+
}
|
|
128
|
+
function deriveSourceText(tag, locationData, sourceLines) {
|
|
85
129
|
const headerLine = getDisplayLine(locationData[tag.startIndex]);
|
|
86
|
-
// Find the column span of the update expression using single-line locations in the tag range
|
|
87
130
|
const singleLineLocations = range(tag.startIndex, tag.endIndex)
|
|
88
131
|
.map((idx) => locationData[idx].location)
|
|
89
|
-
.filter((loc) => loc.start.line === loc.end.line);
|
|
132
|
+
.filter((loc) => loc.start.line === loc.end.line && loc.start.column !== loc.end.column);
|
|
133
|
+
if (singleLineLocations.length === 0)
|
|
134
|
+
return '';
|
|
90
135
|
const startCol = Math.min(...singleLineLocations.map((loc) => loc.start.column));
|
|
91
136
|
const endCol = Math.max(...singleLineLocations.map((loc) => loc.end.column));
|
|
92
|
-
|
|
93
|
-
// Indentation: use first non-empty line after header
|
|
94
|
-
const bodyLine = sourceLines.slice(headerLine).find((l) => l.trim().length > 0);
|
|
95
|
-
const indent = bodyLine?.match(/^(\s*)/)?.[1] ?? '';
|
|
96
|
-
return `${indent}>>> for-loop update (${expression})`;
|
|
137
|
+
return sourceLines[headerLine - 1].substring(startCol, endCol).trim();
|
|
97
138
|
}
|
|
98
139
|
//# sourceMappingURL=bitauth-script.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Op, Script } from './script.js';
|
|
2
|
+
export declare function computeBytecodePattern(bytecode: Uint8Array): Uint8Array;
|
|
3
|
+
export declare function computeBytecodeFingerprint(bytecode: Uint8Array): string;
|
|
4
|
+
export declare function computeBytecodeFingerprintWithConstructorArgs(bytecodeScript: Script, constructorArgsCount: number): string;
|
|
5
|
+
export declare function isPushOpcode(opcode: Op): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { binToHex, decodeAuthenticationInstructions, encodeDataPush, flattenBinArray, isPushOperation, } from '@bitauth/libauth';
|
|
2
|
+
import { encodeInt } from './data.js';
|
|
3
|
+
import { sha256 } from './hash.js';
|
|
4
|
+
import { generateContractBytecodeScript, Op, scriptToBytecode } from './script.js';
|
|
5
|
+
// Transform locking bytecode into the normalized "pattern" form: every run of
|
|
6
|
+
// consecutive push instructions is replaced by a single push of the run length.
|
|
7
|
+
// See: https://gitlab.com/0353F40E/smart-contract-fingerprinting/
|
|
8
|
+
export function computeBytecodePattern(bytecode) {
|
|
9
|
+
const opcodes = decodeAuthenticationInstructions(bytecode).map((instruction) => instruction.opcode);
|
|
10
|
+
const chunks = groupByPushRun(opcodes).map((run) => (isPushOpcode(run[0]) ? encodePushCount(run.length) : Uint8Array.from(run)));
|
|
11
|
+
return flattenBinArray(chunks);
|
|
12
|
+
}
|
|
13
|
+
// Split opcodes into consecutive runs that are either all pushes or all non-pushes.
|
|
14
|
+
function groupByPushRun(opcodes) {
|
|
15
|
+
return opcodes.reduce((opcodeRuns, opcode) => {
|
|
16
|
+
const lastOpcodeRun = opcodeRuns.at(-1);
|
|
17
|
+
// If there are no runs yet, start a new run with the current opcode
|
|
18
|
+
if (!lastOpcodeRun)
|
|
19
|
+
return [[opcode]];
|
|
20
|
+
// If the last run is the same type of opcode as the current opcode, add the current opcode to the last run
|
|
21
|
+
// Else, start a new run with the current opcode
|
|
22
|
+
if (isPushOpcode(lastOpcodeRun[0]) === isPushOpcode(opcode)) {
|
|
23
|
+
lastOpcodeRun.push(opcode);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
opcodeRuns.push([opcode]);
|
|
27
|
+
}
|
|
28
|
+
return opcodeRuns;
|
|
29
|
+
}, []);
|
|
30
|
+
}
|
|
31
|
+
// Compute the SHA256 fingerprint of the normalized bytecode pattern, returned as hex.
|
|
32
|
+
export function computeBytecodeFingerprint(bytecode) {
|
|
33
|
+
return binToHex(sha256(computeBytecodePattern(bytecode)));
|
|
34
|
+
}
|
|
35
|
+
// Add placeholder constructor arguments to the bytecode script and compute the fingerprint.
|
|
36
|
+
export function computeBytecodeFingerprintWithConstructorArgs(bytecodeScript, constructorArgsCount) {
|
|
37
|
+
const placeholderConstructorArgs = new Array(constructorArgsCount).fill(0);
|
|
38
|
+
const bytecodeWithConstructorArgs = generateContractBytecodeScript(bytecodeScript, placeholderConstructorArgs);
|
|
39
|
+
return computeBytecodeFingerprint(scriptToBytecode(bytecodeWithConstructorArgs));
|
|
40
|
+
}
|
|
41
|
+
// Note: libauth's isPushOperation also matches OP_RESERVED (0x50) — the spec excludes it.
|
|
42
|
+
export function isPushOpcode(opcode) {
|
|
43
|
+
return isPushOperation(opcode) && opcode !== Op.OP_RESERVED;
|
|
44
|
+
}
|
|
45
|
+
// Encode a push count using the smallest possible push encoding.
|
|
46
|
+
// libauth's encodeDataPush minimally encodes VM Numbers in the -1..16 range as OP_N.
|
|
47
|
+
function encodePushCount(count) {
|
|
48
|
+
return encodeDataPush(encodeInt(BigInt(count)));
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=fingerprint.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/script.js
CHANGED
|
@@ -37,6 +37,8 @@ export function bytecodeToScript(bytecode) {
|
|
|
37
37
|
export function asmToBytecode(asm) {
|
|
38
38
|
// Remove any duplicate whitespace
|
|
39
39
|
asm = asm.replace(/\s+/g, ' ').trim();
|
|
40
|
+
if (asm === '')
|
|
41
|
+
return new Uint8Array();
|
|
40
42
|
// Convert the ASM tokens to AuthenticationInstructions
|
|
41
43
|
const instructions = asm.split(' ').map((token) => {
|
|
42
44
|
// Even though the OpcodesBch type allows for { [key: number]: string }, we know that the keys are always the opcodes
|
package/dist/types.d.ts
CHANGED
package/dist/types.js
CHANGED
|
@@ -191,10 +191,11 @@ export var PositionHint;
|
|
|
191
191
|
PositionHint[PositionHint["START"] = 0] = "START";
|
|
192
192
|
PositionHint[PositionHint["END"] = 1] = "END";
|
|
193
193
|
})(PositionHint || (PositionHint = {}));
|
|
194
|
-
// Semantic tags for opcodes that
|
|
195
|
-
// Currently used for loop constructs where opcode order diverges from source line order.
|
|
194
|
+
// Semantic tags for compiler-injected opcodes that have no (or misleading) user source
|
|
196
195
|
export var SourceTagKind;
|
|
197
196
|
(function (SourceTagKind) {
|
|
198
197
|
SourceTagKind["FOR_UPDATE"] = "fu";
|
|
198
|
+
SourceTagKind["LOCKTIME_GUARD"] = "lg";
|
|
199
|
+
SourceTagKind["PARAMETER_VALIDATION"] = "pv";
|
|
199
200
|
})(SourceTagKind || (SourceTagKind = {}));
|
|
200
201
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cashscript/utils",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.9",
|
|
4
4
|
"description": "CashScript utilities and types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin cash",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"typescript": "^5.9.2",
|
|
49
49
|
"vitest": "^4.0.15"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "1bbda54a000f6744bdb67a6379c7bfb0b784dceb"
|
|
52
52
|
}
|