@qubic.ts/contracts 0.1.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/package.json +17 -0
- package/scripts/generate-artifacts.ts +178 -0
- package/src/codec/entry-input.test.ts +198 -0
- package/src/codec/entry-input.ts +959 -0
- package/src/codec/index.ts +8 -0
- package/src/generated/contracts/CCF.registry.json +741 -0
- package/src/generated/contracts/CCF.types.ts +307 -0
- package/src/generated/contracts/GQMPROP.registry.json +518 -0
- package/src/generated/contracts/GQMPROP.types.ts +238 -0
- package/src/generated/contracts/MLM.registry.json +8 -0
- package/src/generated/contracts/MLM.types.ts +42 -0
- package/src/generated/contracts/MSVAULT.registry.json +1162 -0
- package/src/generated/contracts/MSVAULT.types.ts +598 -0
- package/src/generated/contracts/NOST.registry.json +1131 -0
- package/src/generated/contracts/NOST.types.ts +515 -0
- package/src/generated/contracts/QBAY.registry.json +1492 -0
- package/src/generated/contracts/QBAY.types.ts +681 -0
- package/src/generated/contracts/QBOND.registry.json +734 -0
- package/src/generated/contracts/QBOND.types.ts +397 -0
- package/src/generated/contracts/QDRAW.registry.json +112 -0
- package/src/generated/contracts/QDRAW.types.ts +110 -0
- package/src/generated/contracts/QDUEL.registry.json +466 -0
- package/src/generated/contracts/QDUEL.types.ts +265 -0
- package/src/generated/contracts/QEARN.registry.json +458 -0
- package/src/generated/contracts/QEARN.types.ts +265 -0
- package/src/generated/contracts/QIP.registry.json +483 -0
- package/src/generated/contracts/QIP.types.ts +194 -0
- package/src/generated/contracts/QRAFFLE.registry.json +916 -0
- package/src/generated/contracts/QRAFFLE.types.ts +446 -0
- package/src/generated/contracts/QRP.registry.json +139 -0
- package/src/generated/contracts/QRP.types.ts +144 -0
- package/src/generated/contracts/QRWA.registry.json +765 -0
- package/src/generated/contracts/QRWA.types.ts +402 -0
- package/src/generated/contracts/QSWAP.registry.json +941 -0
- package/src/generated/contracts/QSWAP.types.ts +479 -0
- package/src/generated/contracts/QTF.registry.json +480 -0
- package/src/generated/contracts/QTF.types.ts +346 -0
- package/src/generated/contracts/QUOTTERY.registry.json +530 -0
- package/src/generated/contracts/QUOTTERY.types.ts +262 -0
- package/src/generated/contracts/QUTIL.registry.json +1378 -0
- package/src/generated/contracts/QUTIL.types.ts +612 -0
- package/src/generated/contracts/QVAULT.registry.json +527 -0
- package/src/generated/contracts/QVAULT.types.ts +309 -0
- package/src/generated/contracts/QX.registry.json +610 -0
- package/src/generated/contracts/QX.types.ts +323 -0
- package/src/generated/contracts/RANDOM.registry.json +51 -0
- package/src/generated/contracts/RANDOM.types.ts +65 -0
- package/src/generated/contracts/RL.registry.json +490 -0
- package/src/generated/contracts/RL.types.ts +304 -0
- package/src/generated/contracts/SWATCH.registry.json +8 -0
- package/src/generated/contracts/SWATCH.types.ts +42 -0
- package/src/generated/core-registry.codecs.ts +6622 -0
- package/src/generated/core-registry.source.json +14342 -0
- package/src/generated/core-registry.ts +14349 -0
- package/src/generated/core-registry.types.ts +100 -0
- package/src/generator/contract-codecs.fixture.test.ts +17 -0
- package/src/generator/contract-codecs.test.ts +115 -0
- package/src/generator/contract-codecs.ts +416 -0
- package/src/generator/index.ts +14 -0
- package/src/generator/per-contract-files.test.ts +70 -0
- package/src/generator/per-contract-files.ts +122 -0
- package/src/generator/registry-runtime.fixture.test.ts +17 -0
- package/src/generator/registry-runtime.test.ts +55 -0
- package/src/generator/registry-runtime.ts +28 -0
- package/src/generator/registry-types.fixture.test.ts +17 -0
- package/src/generator/registry-types.test.ts +55 -0
- package/src/generator/registry-types.ts +75 -0
- package/src/index.test.ts +29 -0
- package/src/index.ts +49 -0
- package/src/registry/index.ts +17 -0
- package/src/registry/io-layout.fixture.test.ts +24 -0
- package/src/registry/io-layout.test.ts +93 -0
- package/src/registry/io-layout.ts +57 -0
- package/src/registry/normalize.ts +61 -0
- package/src/registry/schema.fixture.test.ts +21 -0
- package/src/registry/schema.ts +97 -0
- package/src/registry/types.ts +98 -0
- package/test/fixtures/io-layout.contracts.json +32 -0
- package/test/fixtures/io-layout.layouts.json +14 -0
- package/test/fixtures/registry.sample.codecs.ts +100 -0
- package/test/fixtures/registry.sample.json +27 -0
- package/test/fixtures/registry.sample.runtime.ts +54 -0
- package/test/fixtures/registry.sample.types.ts +16 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { ContractsRegistry } from "../registry/types.js";
|
|
3
|
+
import { generatePerContractFiles } from "./per-contract-files.js";
|
|
4
|
+
|
|
5
|
+
describe("generatePerContractFiles", () => {
|
|
6
|
+
it("generates one registry json and one typings file per contract", () => {
|
|
7
|
+
const output = generatePerContractFiles(makeFixture(), {
|
|
8
|
+
banner: "// generated",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(output).toHaveLength(2);
|
|
12
|
+
|
|
13
|
+
const qutil = output.find((item) => item.contractName === "QUTIL");
|
|
14
|
+
const qvault = output.find((item) => item.contractName === "QVAULT");
|
|
15
|
+
expect(qutil).toBeDefined();
|
|
16
|
+
expect(qvault).toBeDefined();
|
|
17
|
+
|
|
18
|
+
expect(qutil?.fileStem).toBe("QUTIL");
|
|
19
|
+
expect(qutil?.registryJson).toContain('"contractName": "QUTIL"');
|
|
20
|
+
expect(qutil?.registryJson).toContain('"contractIndex": 4');
|
|
21
|
+
expect(qutil?.typesTs).toContain("export interface ContractTypeMap {");
|
|
22
|
+
expect(qutil?.typesTs).toContain("QUTIL: {");
|
|
23
|
+
expect(qutil?.typesTs).toContain("// generated");
|
|
24
|
+
expect(qvault?.registryJson).toContain('"contractName": "QVAULT"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is deterministic for equivalent input", () => {
|
|
28
|
+
const first = generatePerContractFiles(makeFixture());
|
|
29
|
+
const second = generatePerContractFiles(makeFixture());
|
|
30
|
+
expect(first).toEqual(second);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws when contract address is not a valid identity", () => {
|
|
34
|
+
const fixture = makeFixture();
|
|
35
|
+
const broken: ContractsRegistry = {
|
|
36
|
+
...fixture,
|
|
37
|
+
contracts: fixture.contracts.map((contract) =>
|
|
38
|
+
contract.name === "QUTIL" ? { ...contract, address: "QUTIL" } : contract,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
expect(() => generatePerContractFiles(broken)).toThrow("invalid address");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function makeFixture(): ContractsRegistry {
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
metadata: {
|
|
49
|
+
generatedAt: "2026-02-18T00:00:00.000Z",
|
|
50
|
+
sources: [{ name: "qubic/core" }],
|
|
51
|
+
},
|
|
52
|
+
contracts: [
|
|
53
|
+
{
|
|
54
|
+
name: "QVAULT",
|
|
55
|
+
contractIndex: 10,
|
|
56
|
+
address: "KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFSW",
|
|
57
|
+
entries: [{ kind: "procedure", name: "SubmitAuthAddress", inputType: 7 }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "QUTIL",
|
|
61
|
+
contractIndex: 4,
|
|
62
|
+
address: "EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVWRF",
|
|
63
|
+
entries: [
|
|
64
|
+
{ kind: "procedure", name: "BurnQubic", inputType: 2 },
|
|
65
|
+
{ kind: "function", name: "GetFees", inputType: 1 },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { normalizeContractsRegistry } from "../registry/normalize.js";
|
|
2
|
+
import type {
|
|
3
|
+
ContractDefinition,
|
|
4
|
+
ContractEntry,
|
|
5
|
+
ContractIoTypeDefinition,
|
|
6
|
+
ContractsRegistry,
|
|
7
|
+
} from "../registry/types.js";
|
|
8
|
+
import { generateContractCodecs } from "./contract-codecs.js";
|
|
9
|
+
|
|
10
|
+
export type GeneratePerContractFilesOptions = Readonly<{
|
|
11
|
+
banner?: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type GeneratedPerContractFile = Readonly<{
|
|
15
|
+
contractName: string;
|
|
16
|
+
fileStem: string;
|
|
17
|
+
registryJson: string;
|
|
18
|
+
typesTs: string;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export type PerContractRegistryEntry = Readonly<{
|
|
22
|
+
kind: ContractEntry["kind"];
|
|
23
|
+
name: string;
|
|
24
|
+
inputType: number;
|
|
25
|
+
inputTypeName?: string;
|
|
26
|
+
outputTypeName?: string;
|
|
27
|
+
inputSize?: number;
|
|
28
|
+
outputSize?: number;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type PerContractRegistry = Readonly<{
|
|
32
|
+
contractName: string;
|
|
33
|
+
contractIndex: number;
|
|
34
|
+
filename?: string;
|
|
35
|
+
address: string;
|
|
36
|
+
entries: readonly PerContractRegistryEntry[];
|
|
37
|
+
ioTypes?: readonly ContractIoTypeDefinition[];
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
export function generatePerContractFiles(
|
|
41
|
+
registry: ContractsRegistry,
|
|
42
|
+
options: GeneratePerContractFilesOptions = {},
|
|
43
|
+
): readonly GeneratedPerContractFile[] {
|
|
44
|
+
const normalized = normalizeContractsRegistry(registry);
|
|
45
|
+
const usedStems = new Set<string>();
|
|
46
|
+
|
|
47
|
+
return normalized.contracts.map((contract) => {
|
|
48
|
+
const fileStem = uniqueStem(toFileStem(contract.name), usedStems);
|
|
49
|
+
const singleContractRegistry = makeSingleContractRegistry(normalized, contract);
|
|
50
|
+
const perContractRegistry = makePerContractRegistry(contract);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
contractName: contract.name,
|
|
54
|
+
fileStem,
|
|
55
|
+
registryJson: `${JSON.stringify(perContractRegistry, null, 2)}\n`,
|
|
56
|
+
typesTs: generateContractCodecs(singleContractRegistry, {
|
|
57
|
+
...(options.banner ? { banner: options.banner } : {}),
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeSingleContractRegistry(
|
|
64
|
+
registry: ContractsRegistry,
|
|
65
|
+
contract: ContractDefinition,
|
|
66
|
+
): ContractsRegistry {
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
metadata: registry.metadata,
|
|
70
|
+
contracts: [contract],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makePerContractRegistry(contract: ContractDefinition): PerContractRegistry {
|
|
75
|
+
if (!isLikelyIdentityAddress(contract.address)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Contract ${contract.name} has invalid address "${contract.address}". Use static-enriched contract data for per-contract export.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
contractName: contract.name,
|
|
83
|
+
contractIndex: contract.contractIndex,
|
|
84
|
+
filename: contract.filename,
|
|
85
|
+
address: contract.address,
|
|
86
|
+
entries: contract.entries.map((entry) => ({
|
|
87
|
+
kind: entry.kind,
|
|
88
|
+
name: entry.name,
|
|
89
|
+
inputType: entry.inputType,
|
|
90
|
+
inputTypeName: entry.inputTypeName,
|
|
91
|
+
outputTypeName: entry.outputTypeName,
|
|
92
|
+
inputSize: entry.inputSize,
|
|
93
|
+
outputSize: entry.outputSize,
|
|
94
|
+
})),
|
|
95
|
+
ioTypes: contract.ioTypes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isLikelyIdentityAddress(value: string): boolean {
|
|
100
|
+
return /^[A-Z]{60}$/.test(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toFileStem(value: string): string {
|
|
104
|
+
const cleaned = value
|
|
105
|
+
.trim()
|
|
106
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
107
|
+
.replace(/^-+|-+$/g, "");
|
|
108
|
+
return cleaned.length > 0 ? cleaned : "contract";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function uniqueStem(base: string, usedStems: Set<string>): string {
|
|
112
|
+
if (!usedStems.has(base)) {
|
|
113
|
+
usedStems.add(base);
|
|
114
|
+
return base;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let suffix = 2;
|
|
118
|
+
while (usedStems.has(`${base}-${suffix}`)) suffix += 1;
|
|
119
|
+
const resolved = `${base}-${suffix}`;
|
|
120
|
+
usedStems.add(resolved);
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { validateContractsRegistry } from "../registry/index.js";
|
|
3
|
+
import { generateRegistryRuntime } from "./registry-runtime.js";
|
|
4
|
+
|
|
5
|
+
describe("generateRegistryRuntime fixture", () => {
|
|
6
|
+
it("matches the expected generated file", async () => {
|
|
7
|
+
const fixturePath = new URL("../../test/fixtures/registry.sample.json", import.meta.url);
|
|
8
|
+
const expectedPath = new URL("../../test/fixtures/registry.sample.runtime.ts", import.meta.url);
|
|
9
|
+
|
|
10
|
+
const fixtureRaw = (await Bun.file(fixturePath).json()) as unknown;
|
|
11
|
+
const expected = await Bun.file(expectedPath).text();
|
|
12
|
+
const registry = validateContractsRegistry(fixtureRaw);
|
|
13
|
+
const generated = generateRegistryRuntime(registry);
|
|
14
|
+
|
|
15
|
+
expect(generated).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { ContractsRegistry } from "../registry/types.js";
|
|
3
|
+
import { generateRegistryRuntime } from "./registry-runtime.js";
|
|
4
|
+
|
|
5
|
+
describe("generateRegistryRuntime", () => {
|
|
6
|
+
it("renders a typed runtime registry export", () => {
|
|
7
|
+
const output = generateRegistryRuntime(makeFixture(), {
|
|
8
|
+
banner: "/* test */",
|
|
9
|
+
exportName: "testRegistry",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(output).toContain('import type { ContractsRegistry } from "../registry/types.js";');
|
|
13
|
+
expect(output).toContain("export const testRegistry = {");
|
|
14
|
+
expect(output).toContain("satisfies ContractsRegistry;");
|
|
15
|
+
expect(output).toContain("export const coreContractDefinitions = testRegistry.contracts;");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("is deterministic for equivalent input", () => {
|
|
19
|
+
const first = generateRegistryRuntime(makeFixture());
|
|
20
|
+
const second = generateRegistryRuntime(makeFixture());
|
|
21
|
+
expect(first).toBe(second);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function makeFixture(): ContractsRegistry {
|
|
26
|
+
return {
|
|
27
|
+
version: 1,
|
|
28
|
+
metadata: {
|
|
29
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
30
|
+
sources: [{ name: "core" }],
|
|
31
|
+
},
|
|
32
|
+
contracts: [
|
|
33
|
+
{
|
|
34
|
+
name: "QX",
|
|
35
|
+
contractIndex: 2,
|
|
36
|
+
address: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
37
|
+
entries: [
|
|
38
|
+
{
|
|
39
|
+
kind: "function",
|
|
40
|
+
name: "GetBalance",
|
|
41
|
+
inputType: 1,
|
|
42
|
+
inputTypeName: "GetBalance_input",
|
|
43
|
+
outputTypeName: "GetBalance_output",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "QUTIL",
|
|
49
|
+
contractIndex: 1,
|
|
50
|
+
address: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
51
|
+
entries: [],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { normalizeContractsRegistry } from "../registry/normalize.js";
|
|
2
|
+
import type { ContractsRegistry } from "../registry/types.js";
|
|
3
|
+
|
|
4
|
+
export type GenerateRegistryRuntimeOptions = Readonly<{
|
|
5
|
+
banner?: string;
|
|
6
|
+
exportName?: string;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export function generateRegistryRuntime(
|
|
10
|
+
registry: ContractsRegistry,
|
|
11
|
+
options: GenerateRegistryRuntimeOptions = {},
|
|
12
|
+
): string {
|
|
13
|
+
const normalized = normalizeContractsRegistry(registry);
|
|
14
|
+
const exportName = options.exportName ?? "coreContractsRegistry";
|
|
15
|
+
const banner =
|
|
16
|
+
options.banner ?? "/* eslint-disable */\n/* This file is generated. Do not edit manually. */";
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
banner,
|
|
20
|
+
"",
|
|
21
|
+
'import type { ContractsRegistry } from "../registry/types.js";',
|
|
22
|
+
"",
|
|
23
|
+
`export const ${exportName} = ${JSON.stringify(normalized, null, 2)} as const satisfies ContractsRegistry;`,
|
|
24
|
+
"",
|
|
25
|
+
`export const coreContractDefinitions = ${exportName}.contracts;`,
|
|
26
|
+
"",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { validateContractsRegistry } from "../registry/index.js";
|
|
3
|
+
import { generateRegistryTypes } from "./registry-types.js";
|
|
4
|
+
|
|
5
|
+
describe("generateRegistryTypes fixture", () => {
|
|
6
|
+
it("matches the expected generated file", async () => {
|
|
7
|
+
const fixturePath = new URL("../../test/fixtures/registry.sample.json", import.meta.url);
|
|
8
|
+
const expectedPath = new URL("../../test/fixtures/registry.sample.types.ts", import.meta.url);
|
|
9
|
+
|
|
10
|
+
const fixtureRaw = (await Bun.file(fixturePath).json()) as unknown;
|
|
11
|
+
const expected = await Bun.file(expectedPath).text();
|
|
12
|
+
const registry = validateContractsRegistry(fixtureRaw);
|
|
13
|
+
const generated = generateRegistryTypes(registry);
|
|
14
|
+
|
|
15
|
+
expect(generated).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { ContractsRegistry } from "../registry/types.js";
|
|
3
|
+
import { generateRegistryTypes } from "./registry-types.js";
|
|
4
|
+
|
|
5
|
+
describe("generateRegistryTypes", () => {
|
|
6
|
+
it("generates contract/function/procedure unions", () => {
|
|
7
|
+
const output = generateRegistryTypes(makeFixture(), {
|
|
8
|
+
banner: "// generated",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(output).toContain('export type ContractName = "QUTIL" | "QVAULT";');
|
|
12
|
+
expect(output).toContain(
|
|
13
|
+
'export type FunctionName<C extends ContractName> = C extends "QUTIL"',
|
|
14
|
+
);
|
|
15
|
+
expect(output).toContain('? "GetFees"');
|
|
16
|
+
expect(output).toContain(
|
|
17
|
+
'export type ProcedureName<C extends ContractName> = C extends "QUTIL"',
|
|
18
|
+
);
|
|
19
|
+
expect(output).toContain('C extends "QVAULT"');
|
|
20
|
+
expect(output).toContain("// generated");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is deterministic", () => {
|
|
24
|
+
const first = generateRegistryTypes(makeFixture());
|
|
25
|
+
const second = generateRegistryTypes(makeFixture());
|
|
26
|
+
expect(first).toBe(second);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function makeFixture(): ContractsRegistry {
|
|
31
|
+
return {
|
|
32
|
+
version: 1,
|
|
33
|
+
metadata: {
|
|
34
|
+
generatedAt: "2026-02-18T00:00:00.000Z",
|
|
35
|
+
sources: [{ name: "qubic/core" }],
|
|
36
|
+
},
|
|
37
|
+
contracts: [
|
|
38
|
+
{
|
|
39
|
+
name: "QVAULT",
|
|
40
|
+
contractIndex: 10,
|
|
41
|
+
address: "KAAAA",
|
|
42
|
+
entries: [{ kind: "procedure", name: "SubmitAuthAddress", inputType: 7 }],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "QUTIL",
|
|
46
|
+
contractIndex: 4,
|
|
47
|
+
address: "EAAAA",
|
|
48
|
+
entries: [
|
|
49
|
+
{ kind: "procedure", name: "BurnQubic", inputType: 2 },
|
|
50
|
+
{ kind: "function", name: "GetFees", inputType: 1 },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { normalizeContractsRegistry } from "../registry/normalize.js";
|
|
2
|
+
import type { ContractDefinition, ContractsRegistry } from "../registry/types.js";
|
|
3
|
+
|
|
4
|
+
export type GenerateRegistryTypesOptions = Readonly<{
|
|
5
|
+
banner?: string;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
export function generateRegistryTypes(
|
|
9
|
+
registry: ContractsRegistry,
|
|
10
|
+
options: GenerateRegistryTypesOptions = {},
|
|
11
|
+
): string {
|
|
12
|
+
const normalized = normalizeContractsRegistry(registry);
|
|
13
|
+
const contracts = normalized.contracts;
|
|
14
|
+
|
|
15
|
+
const contractNameUnion = makeUnion(contracts.map((c) => c.name));
|
|
16
|
+
const functionType = makeConditionalType(contracts, "function");
|
|
17
|
+
const procedureType = makeConditionalType(contracts, "procedure");
|
|
18
|
+
|
|
19
|
+
const banner =
|
|
20
|
+
options.banner ?? "/* eslint-disable */\n/* This file is generated. Do not edit manually. */";
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
banner,
|
|
24
|
+
"",
|
|
25
|
+
`export type ContractName = ${contractNameUnion};`,
|
|
26
|
+
"",
|
|
27
|
+
`export type FunctionName<C extends ContractName> = ${functionType};`,
|
|
28
|
+
"",
|
|
29
|
+
`export type ProcedureName<C extends ContractName> = ${procedureType};`,
|
|
30
|
+
"",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeConditionalType(
|
|
35
|
+
contracts: readonly ContractDefinition[],
|
|
36
|
+
kind: "function" | "procedure",
|
|
37
|
+
depth = 0,
|
|
38
|
+
): string {
|
|
39
|
+
if (contracts.length === 0) return "never";
|
|
40
|
+
const [head, ...tail] = contracts;
|
|
41
|
+
if (!head) return "never";
|
|
42
|
+
|
|
43
|
+
const indent = " ".repeat(depth);
|
|
44
|
+
const nestedIndent = " ".repeat(depth + 1);
|
|
45
|
+
const union = makeUnion(
|
|
46
|
+
head.entries
|
|
47
|
+
.filter((entry) => entry.kind === kind)
|
|
48
|
+
.map((entry) => entry.name)
|
|
49
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (tail.length === 0) {
|
|
53
|
+
return `${indent}C extends ${quote(head.name)}\n${nestedIndent}? ${union}\n${nestedIndent}: never`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const next = makeConditionalType(tail, kind, depth + 1);
|
|
57
|
+
const [first = "never", ...rest] = next.split("\n");
|
|
58
|
+
const firstWithoutIndent = first.slice((depth + 1) * 2);
|
|
59
|
+
return [
|
|
60
|
+
`${indent}C extends ${quote(head.name)}`,
|
|
61
|
+
`${nestedIndent}? ${union}`,
|
|
62
|
+
`${nestedIndent}: ${firstWithoutIndent}`,
|
|
63
|
+
...rest,
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeUnion(values: readonly string[]): string {
|
|
68
|
+
if (values.length === 0) return "never";
|
|
69
|
+
const unique = [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
70
|
+
return unique.map((item) => quote(item)).join(" | ");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function quote(value: string): string {
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
bytesFromBase64,
|
|
4
|
+
bytesToBase64,
|
|
5
|
+
coreContractsRegistry,
|
|
6
|
+
createContractsRegistryIndex,
|
|
7
|
+
validateContractsRegistry,
|
|
8
|
+
} from "./index.js";
|
|
9
|
+
|
|
10
|
+
describe("base64 helpers", () => {
|
|
11
|
+
it("round trips bytes", () => {
|
|
12
|
+
const input = new Uint8Array([1, 2, 3, 4, 5, 250]);
|
|
13
|
+
const encoded = bytesToBase64(input);
|
|
14
|
+
const decoded = bytesFromBase64(encoded);
|
|
15
|
+
expect(Array.from(decoded)).toEqual(Array.from(input));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("bundled core registry", () => {
|
|
20
|
+
it("exports and validates a non-empty contracts registry", () => {
|
|
21
|
+
const parsed = validateContractsRegistry(coreContractsRegistry);
|
|
22
|
+
expect(parsed.version).toBe(1);
|
|
23
|
+
expect(parsed.contracts.length).toBeGreaterThan(0);
|
|
24
|
+
|
|
25
|
+
const index = createContractsRegistryIndex(parsed);
|
|
26
|
+
expect(index.byName.size).toBeGreaterThan(0);
|
|
27
|
+
expect(index.byIndex.size).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type DecodeMode = "typed" | "named" | "raw";
|
|
2
|
+
|
|
3
|
+
export type DecodedInputData<T = unknown> =
|
|
4
|
+
| Readonly<{ mode: "typed"; value: T; rawBytes: Uint8Array }>
|
|
5
|
+
| Readonly<{ mode: "named"; name: string; rawBytes: Uint8Array }>
|
|
6
|
+
| Readonly<{ mode: "raw"; rawBytes: Uint8Array }>;
|
|
7
|
+
|
|
8
|
+
export function bytesFromBase64(value: string): Uint8Array {
|
|
9
|
+
if (typeof atob !== "function") {
|
|
10
|
+
throw new Error("Base64 decoding is not available in this runtime");
|
|
11
|
+
}
|
|
12
|
+
const bin = atob(value);
|
|
13
|
+
const out = new Uint8Array(bin.length);
|
|
14
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
19
|
+
if (typeof btoa !== "function") {
|
|
20
|
+
throw new Error("Base64 encoding is not available in this runtime");
|
|
21
|
+
}
|
|
22
|
+
let bin = "";
|
|
23
|
+
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
24
|
+
return btoa(bin);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export * from "./codec/index.js";
|
|
28
|
+
export * from "./generator/index.js";
|
|
29
|
+
export { coreContractDefinitions, coreContractsRegistry } from "./generated/core-registry.js";
|
|
30
|
+
export {
|
|
31
|
+
applyIoTypeLayouts,
|
|
32
|
+
createContractsRegistryIndex,
|
|
33
|
+
normalizeContractsRegistry,
|
|
34
|
+
validateContractsRegistry,
|
|
35
|
+
} from "./registry/index.js";
|
|
36
|
+
export type {
|
|
37
|
+
CompilerIoTypeLayout,
|
|
38
|
+
ContractDefinition,
|
|
39
|
+
ContractEntry,
|
|
40
|
+
ContractEntryKind,
|
|
41
|
+
ContractIoField,
|
|
42
|
+
ContractIoTypeDefinition,
|
|
43
|
+
ContractOverlay,
|
|
44
|
+
ContractsRegistry,
|
|
45
|
+
ContractsRegistryIndex,
|
|
46
|
+
RegistrySource,
|
|
47
|
+
StaticContractEnrichment,
|
|
48
|
+
StaticProcedureEnrichment,
|
|
49
|
+
} from "./registry/types.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { applyIoTypeLayouts } from "./io-layout.js";
|
|
2
|
+
export type { CompilerIoTypeLayout } from "./io-layout.js";
|
|
3
|
+
export { createContractsRegistryIndex, normalizeContractsRegistry } from "./normalize.js";
|
|
4
|
+
export { validateContractsRegistry } from "./schema.js";
|
|
5
|
+
export type {
|
|
6
|
+
ContractDefinition,
|
|
7
|
+
ContractEntry,
|
|
8
|
+
ContractEntryKind,
|
|
9
|
+
ContractIoField,
|
|
10
|
+
ContractIoTypeDefinition,
|
|
11
|
+
ContractOverlay,
|
|
12
|
+
ContractsRegistry,
|
|
13
|
+
ContractsRegistryIndex,
|
|
14
|
+
RegistrySource,
|
|
15
|
+
StaticContractEnrichment,
|
|
16
|
+
StaticProcedureEnrichment,
|
|
17
|
+
} from "./types.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { applyIoTypeLayouts } from "./io-layout.js";
|
|
3
|
+
import type { CompilerIoTypeLayout } from "./io-layout.js";
|
|
4
|
+
import type { ContractDefinition } from "./types.js";
|
|
5
|
+
|
|
6
|
+
describe("io layout fixture", () => {
|
|
7
|
+
it("applies byte size and alignment metadata to io types and entries", async () => {
|
|
8
|
+
const contractsPath = new URL("../../test/fixtures/io-layout.contracts.json", import.meta.url);
|
|
9
|
+
const layoutsPath = new URL("../../test/fixtures/io-layout.layouts.json", import.meta.url);
|
|
10
|
+
|
|
11
|
+
const contracts = (await Bun.file(contractsPath).json()) as readonly ContractDefinition[];
|
|
12
|
+
const layouts = (await Bun.file(layoutsPath).json()) as readonly CompilerIoTypeLayout[];
|
|
13
|
+
|
|
14
|
+
const enriched = applyIoTypeLayouts(contracts, layouts);
|
|
15
|
+
const qutil = enriched[0];
|
|
16
|
+
expect(qutil?.entries[0]?.inputSize).toBe(8);
|
|
17
|
+
expect(qutil?.entries[0]?.outputSize).toBe(16);
|
|
18
|
+
|
|
19
|
+
const inputType = qutil?.ioTypes?.find((value) => value.name === "GetFees_input");
|
|
20
|
+
const outputType = qutil?.ioTypes?.find((value) => value.name === "GetFees_output");
|
|
21
|
+
expect(inputType).toMatchObject({ byteSize: 8, byteAlign: 8 });
|
|
22
|
+
expect(outputType).toMatchObject({ byteSize: 16, byteAlign: 8 });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { applyIoTypeLayouts } from "./io-layout.js";
|
|
3
|
+
import type { ContractDefinition } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("applyIoTypeLayouts", () => {
|
|
6
|
+
it("applies compiler layout metadata to io types and entry input/output sizes", () => {
|
|
7
|
+
const contracts = makeContracts();
|
|
8
|
+
|
|
9
|
+
const enriched = applyIoTypeLayouts(contracts, [
|
|
10
|
+
{
|
|
11
|
+
contractName: "QUTIL",
|
|
12
|
+
typeName: "GetFees_input",
|
|
13
|
+
byteSize: 8,
|
|
14
|
+
byteAlign: 8,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
contractName: "QUTIL",
|
|
18
|
+
typeName: "GetFees_output",
|
|
19
|
+
byteSize: 16,
|
|
20
|
+
byteAlign: 8,
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const qutil = enriched[0];
|
|
25
|
+
expect(qutil).toBeDefined();
|
|
26
|
+
expect(qutil?.entries[0]?.inputSize).toBe(8);
|
|
27
|
+
expect(qutil?.entries[0]?.outputSize).toBe(16);
|
|
28
|
+
|
|
29
|
+
const inputType = qutil?.ioTypes?.find((typeDef) => typeDef.name === "GetFees_input");
|
|
30
|
+
expect(inputType).toMatchObject({ byteSize: 8, byteAlign: 8 });
|
|
31
|
+
|
|
32
|
+
const outputType = qutil?.ioTypes?.find((typeDef) => typeDef.name === "GetFees_output");
|
|
33
|
+
expect(outputType).toMatchObject({ byteSize: 16, byteAlign: 8 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps existing entry sizes when they are already defined", () => {
|
|
37
|
+
const contracts = makeContracts({ presetSizes: true });
|
|
38
|
+
|
|
39
|
+
const enriched = applyIoTypeLayouts(contracts, [
|
|
40
|
+
{
|
|
41
|
+
contractName: "QUTIL",
|
|
42
|
+
typeName: "GetFees_input",
|
|
43
|
+
byteSize: 8,
|
|
44
|
+
byteAlign: 8,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
contractName: "QUTIL",
|
|
48
|
+
typeName: "GetFees_output",
|
|
49
|
+
byteSize: 16,
|
|
50
|
+
byteAlign: 8,
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const entry = enriched[0]?.entries[0];
|
|
55
|
+
expect(entry?.inputSize).toBe(9);
|
|
56
|
+
expect(entry?.outputSize).toBe(17);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function makeContracts(
|
|
61
|
+
options?: Readonly<{ presetSizes?: boolean }>,
|
|
62
|
+
): readonly ContractDefinition[] {
|
|
63
|
+
const entry = {
|
|
64
|
+
kind: "function" as const,
|
|
65
|
+
name: "GetFees",
|
|
66
|
+
inputType: 7,
|
|
67
|
+
inputTypeName: "GetFees_input",
|
|
68
|
+
outputTypeName: "GetFees_output",
|
|
69
|
+
inputSize: options?.presetSizes ? 9 : undefined,
|
|
70
|
+
outputSize: options?.presetSizes ? 17 : undefined,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name: "QUTIL",
|
|
76
|
+
contractIndex: 4,
|
|
77
|
+
address: "QUTIL",
|
|
78
|
+
entries: [entry],
|
|
79
|
+
ioTypes: [
|
|
80
|
+
{
|
|
81
|
+
kind: "struct" as const,
|
|
82
|
+
name: "GetFees_input",
|
|
83
|
+
fields: [{ name: "contractIndex", type: "uint32" }],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: "struct" as const,
|
|
87
|
+
name: "GetFees_output",
|
|
88
|
+
fields: [{ name: "fee", type: "uint64" }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|