@typeberry/lib 0.5.4-e1cdb43 → 0.5.5-d447af6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/lib",
3
- "version": "0.5.4-e1cdb43",
3
+ "version": "0.5.5-d447af6",
4
4
  "description": "Typeberry Library",
5
5
  "main": "./bin/lib/index.js",
6
6
  "types": "./bin/lib/index.d.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"descriptors.d.ts","sourceRoot":"","sources":["../../../../../packages/core/json-parser/descriptors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEhF,yBAAiB,IAAI,CAAC;IACpB,kDAAkD;IAClD,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAElG;IAED,kDAAkD;IAClD,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAElG;IAED,4DAA4D;IAC5D,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAUnG;IAED,mDAAmD;IACnD,SAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,uCAE9C;IAED,+DAA+D;IAC/D,SAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAEjG;IAED,wFAAwF;IACxF,SAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC,CAEtE;IAED,sEAAsE;IACtE,SAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAEjE;IAED,6CAA6C;IAC7C,SAAgB,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,CAErE;IAED,kDAAkD;IAClD,SAAgB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAWpF;IAED,uCAAuC;IACvC,SAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAWrG;IAED,0FAA0F;IAC1F,SAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,EACzC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EACrB,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,GAC7B,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAKpC;CACF"}
1
+ {"version":3,"file":"descriptors.d.ts","sourceRoot":"","sources":["../../../../../packages/core/json-parser/descriptors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEhF,yBAAiB,IAAI,CAAC;IACpB,kDAAkD;IAClD,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAElG;IAED,kDAAkD;IAClD,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAElG;IAED,4DAA4D;IAC5D,SAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAUnG;IAED,mDAAmD;IACnD,SAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,uCAE9C;IAED,+DAA+D;IAC/D,SAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAEjG;IAED,wFAAwF;IACxF,SAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC,CAEtE;IAED,sEAAsE;IACtE,SAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAEjE;IAED,6CAA6C;IAC7C,SAAgB,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,CAErE;IAED,kDAAkD;IAClD,SAAgB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAWpF;IAED,uCAAuC;IACvC,SAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAcrG;IAED,0FAA0F;IAC1F,SAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,EACzC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EACrB,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,GAC7B,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAKpC;CACF"}
@@ -69,6 +69,9 @@ export var json;
69
69
  if (typeof inJson !== "object" || inJson === null) {
70
70
  throw new Error("Expected map for parsing");
71
71
  }
72
+ if (Array.isArray(inJson)) {
73
+ throw new Error("Expected map, got array");
74
+ }
72
75
  const result = new Map();
73
76
  for (const [key, value] of Object.entries(inJson)) {
74
77
  result.set(parseFromJson(key, K, `${context}.key`), parseFromJson(value, V, `${context}.value`));
@@ -135,4 +135,34 @@ test("JSON parser", async (t) => {
135
135
  assert.strictEqual(result.k, undefined);
136
136
  assert.strictEqual(result.v, true);
137
137
  });
138
+ await t.test("map", () => {
139
+ const j = `{"k": { "a": "b", "c": "d" } }`;
140
+ class TestClass {
141
+ static fromJson = {
142
+ k: json.map("string", "string"),
143
+ };
144
+ k = new Map();
145
+ }
146
+ const result = parseFromJson(JSON.parse(j), TestClass.fromJson);
147
+ assert.deepStrictEqual(result.k, new Map([
148
+ ["a", "b"],
149
+ ["c", "d"],
150
+ ]));
151
+ });
152
+ await t.test("map type mismatch", () => {
153
+ const j = `{"k": [["a", "b"], ["c", "d"]]}`;
154
+ class TestClass {
155
+ static fromJson = {
156
+ k: json.map("string", "string"),
157
+ };
158
+ k = new Map();
159
+ }
160
+ try {
161
+ parseFromJson(JSON.parse(j), TestClass.fromJson);
162
+ assert.fail("Expected error to be thrown");
163
+ }
164
+ catch (e) {
165
+ assert.strictEqual(`${e}`, "Error: [<root>.k] Error while parsing the value: Error: Expected map, got array");
166
+ }
167
+ });
138
168
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/networking",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "QUIC-based p2p networking.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "Fluffy Labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/telemetry",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "OpenTelemetry initialization utilities for Typeberry",
5
5
  "license": "MPL-2.0",
6
6
  "author": "Fluffy Labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/node",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "The main typeberry node.",
5
5
  "main": "index.ts",
6
6
  "dependencies": {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.test.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/generator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,200 @@
1
+ import { afterEach, beforeEach, describe, it, mock } from "node:test";
2
+ import { Block, DisputesExtrinsic, EpochMarker, Extrinsic, Header, tryAsTimeSlot, tryAsValidatorIndex, ValidatorKeys, } from "#@typeberry/block";
3
+ import { Bytes, BytesBlob } from "#@typeberry/bytes";
4
+ import { asKnownSize, FixedSizeArray } from "#@typeberry/collections";
5
+ import { tinyChainSpec } from "#@typeberry/config";
6
+ import { BANDERSNATCH_KEY_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, BLS_KEY_BYTES, ED25519_KEY_BYTES, initWasm, } from "#@typeberry/crypto";
7
+ import { BANDERSNATCH_RING_ROOT_BYTES } from "#@typeberry/crypto/bandersnatch.js";
8
+ import { Blake2b, HASH_SIZE, keccak } from "#@typeberry/hash";
9
+ import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
10
+ import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
11
+ import { JAM_FALLBACK_SEAL } from "#@typeberry/safrole/constants.js";
12
+ import { VALIDATOR_META_BYTES, ValidatorData } from "#@typeberry/state";
13
+ import { SafroleSealingKeysKind } from "#@typeberry/state/safrole-data.js";
14
+ import { asOpaqueType, deepEqual, Result } from "#@typeberry/utils";
15
+ import { Generator } from "./generator.js";
16
+ // Test validator data - need 6 validators to match tinyChainSpec.validatorsCount
17
+ const validatorDataArray = [
18
+ {
19
+ bandersnatch: "0xf16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d",
20
+ ed25519: "0x837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02",
21
+ bls: Bytes.zero(BLS_KEY_BYTES),
22
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
23
+ },
24
+ {
25
+ bandersnatch: "0x7f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33",
26
+ ed25519: "0xb3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e9",
27
+ bls: Bytes.zero(BLS_KEY_BYTES),
28
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
29
+ },
30
+ {
31
+ bandersnatch: "0x48e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e3",
32
+ ed25519: "0x5c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faff",
33
+ bls: Bytes.zero(BLS_KEY_BYTES),
34
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
35
+ },
36
+ {
37
+ bandersnatch: "0x5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d",
38
+ ed25519: "0x3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29",
39
+ bls: Bytes.zero(BLS_KEY_BYTES),
40
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
41
+ },
42
+ {
43
+ bandersnatch: "0x3d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d0",
44
+ ed25519: "0x22351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862",
45
+ bls: Bytes.zero(BLS_KEY_BYTES),
46
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
47
+ },
48
+ {
49
+ bandersnatch: "0xaa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bc",
50
+ ed25519: "0xe68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf00",
51
+ bls: Bytes.zero(BLS_KEY_BYTES),
52
+ metadata: Bytes.zero(VALIDATOR_META_BYTES),
53
+ },
54
+ ].map(({ bandersnatch, bls, ed25519, metadata }) => ValidatorData.create({
55
+ bandersnatch: Bytes.parseBytes(bandersnatch, BANDERSNATCH_KEY_BYTES).asOpaque(),
56
+ bls: bls.asOpaque(),
57
+ ed25519: Bytes.parseBytes(ed25519, ED25519_KEY_BYTES).asOpaque(),
58
+ metadata: metadata.asOpaque(),
59
+ }));
60
+ const validators = asKnownSize(validatorDataArray);
61
+ // Expected mock values - these are returned by mocked VRF functions
62
+ const MOCK_SEAL_SIGNATURE = Bytes.fill(BANDERSNATCH_VRF_SIGNATURE_BYTES, 2);
63
+ const MOCK_STATE_ROOT = Bytes.fill(HASH_SIZE, 3);
64
+ const MOCK_PARENT_HASH = Bytes.fill(HASH_SIZE, 0xab);
65
+ // Common test inputs
66
+ const MOCK_BANDERSNATCH_SECRET = Bytes.zero(BANDERSNATCH_KEY_BYTES).asOpaque();
67
+ const MOCK_SEAL_PAYLOAD = asOpaqueType(BytesBlob.blobFromParts(JAM_FALLBACK_SEAL, Bytes.zero(HASH_SIZE).raw));
68
+ // Mock state entropy values
69
+ const MOCK_ENTROPY_0 = Bytes.fill(HASH_SIZE, 10).asOpaque();
70
+ const MOCK_ENTROPY_1 = Bytes.fill(HASH_SIZE, 20).asOpaque();
71
+ const MOCK_ENTROPY_2 = Bytes.fill(HASH_SIZE, 30).asOpaque();
72
+ const MOCK_ENTROPY_3 = Bytes.fill(HASH_SIZE, 40).asOpaque();
73
+ // Mock BlocksDb
74
+ function createMockBlocksDb(headerHash) {
75
+ return {
76
+ getBestHeaderHash: () => headerHash.asOpaque(),
77
+ };
78
+ }
79
+ // Mock StatesDb
80
+ function createMockStatesDb(state) {
81
+ return {
82
+ getState: () => state,
83
+ getStateRoot: () => Promise.resolve(MOCK_STATE_ROOT.asOpaque()),
84
+ };
85
+ }
86
+ function createMockState(timeslot) {
87
+ const bandersnatchKeys = validatorDataArray.map((v) => v.bandersnatch);
88
+ return {
89
+ timeslot: tryAsTimeSlot(timeslot),
90
+ entropy: FixedSizeArray.new([MOCK_ENTROPY_0, MOCK_ENTROPY_1, MOCK_ENTROPY_2, MOCK_ENTROPY_3], 4),
91
+ previousValidatorData: validators,
92
+ currentValidatorData: validators,
93
+ designatedValidatorData: validators,
94
+ nextValidatorData: validators,
95
+ ticketsAccumulator: asKnownSize([]),
96
+ sealingKeySeries: {
97
+ kind: SafroleSealingKeysKind.Keys,
98
+ keys: asKnownSize(bandersnatchKeys),
99
+ },
100
+ epochRoot: Bytes.zero(BANDERSNATCH_RING_ROOT_BYTES).asOpaque(),
101
+ disputesRecords: {
102
+ punishSet: { size: 0, has: () => false },
103
+ },
104
+ };
105
+ }
106
+ /**
107
+ * Creates an expected block based on mock values and provided parameters.
108
+ * Used for asserting generated blocks match expected structure.
109
+ */
110
+ function createExpectedBlock(params) {
111
+ return Block.create({
112
+ header: Header.create({
113
+ parentHeaderHash: MOCK_PARENT_HASH.asOpaque(),
114
+ priorStateRoot: MOCK_STATE_ROOT.asOpaque(),
115
+ extrinsicHash: params.extrinsicHash.asOpaque(),
116
+ timeSlotIndex: params.timeSlot,
117
+ bandersnatchBlockAuthorIndex: params.validatorIndex,
118
+ entropySource: MOCK_SEAL_SIGNATURE.asOpaque(),
119
+ seal: MOCK_SEAL_SIGNATURE.asOpaque(),
120
+ epochMarker: params.epochMarker ?? null,
121
+ ticketsMarker: params.ticketsMarker ?? null,
122
+ offendersMarker: [],
123
+ }),
124
+ extrinsic: Extrinsic.create({
125
+ tickets: asOpaqueType([]),
126
+ preimages: [],
127
+ guarantees: asOpaqueType([]),
128
+ assurances: asOpaqueType([]),
129
+ disputes: DisputesExtrinsic.create({
130
+ verdicts: [],
131
+ culprits: [],
132
+ faults: [],
133
+ }),
134
+ }),
135
+ });
136
+ }
137
+ describe("Generator", () => {
138
+ let blake2b;
139
+ let keccakHasher;
140
+ let bandersnatch;
141
+ beforeEach(async () => {
142
+ await initWasm();
143
+ blake2b = await Blake2b.createHasher();
144
+ keccakHasher = await keccak.KeccakHasher.create();
145
+ bandersnatch = await BandernsatchWasm.new();
146
+ // Mock VRF functions to return predictable results
147
+ mock.method(bandersnatchVrf, "getVrfOutputHash", () => Promise.resolve(Result.ok(Bytes.zero(HASH_SIZE).asOpaque())));
148
+ mock.method(bandersnatchVrf, "generateSeal", () => Promise.resolve(Result.ok(MOCK_SEAL_SIGNATURE.asOpaque())));
149
+ mock.method(bandersnatchVrf, "getRingCommitment", () => Promise.resolve(Result.ok(Bytes.zero(BANDERSNATCH_RING_ROOT_BYTES).asOpaque())));
150
+ });
151
+ afterEach(() => {
152
+ mock.restoreAll();
153
+ });
154
+ describe("nextBlock - fallback mode", () => {
155
+ it("should create block for same-epoch slot", async () => {
156
+ const state = createMockState(0);
157
+ const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
158
+ const statesDb = createMockStatesDb(state);
159
+ const generator = new Generator(tinyChainSpec, bandersnatch, keccakHasher, blake2b, blocksDb, statesDb);
160
+ const validatorIndex = tryAsValidatorIndex(0);
161
+ const timeSlot = tryAsTimeSlot(1);
162
+ const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot);
163
+ const expectedBlock = createExpectedBlock({
164
+ timeSlot,
165
+ validatorIndex,
166
+ extrinsicHash: block.header.extrinsicHash,
167
+ });
168
+ deepEqual(block, expectedBlock);
169
+ });
170
+ it("should create block with epoch marker at epoch boundary", async () => {
171
+ // tinyChainSpec.epochLength = 12, so:
172
+ // - timeslot 11 is last slot of epoch 0
173
+ // - timeslot 12 is first slot of epoch 1
174
+ const lastSlotOfEpoch0 = tinyChainSpec.epochLength - 1;
175
+ const firstSlotOfEpoch1 = tinyChainSpec.epochLength;
176
+ const state = createMockState(lastSlotOfEpoch0);
177
+ const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
178
+ const statesDb = createMockStatesDb(state);
179
+ const generator = new Generator(tinyChainSpec, bandersnatch, keccakHasher, blake2b, blocksDb, statesDb);
180
+ const validatorIndex = tryAsValidatorIndex(0);
181
+ const timeSlot = tryAsTimeSlot(firstSlotOfEpoch1);
182
+ const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot);
183
+ const expectedEpochMarker = EpochMarker.create({
184
+ entropy: MOCK_ENTROPY_0,
185
+ ticketsEntropy: MOCK_ENTROPY_1,
186
+ validators: asKnownSize(validatorDataArray.map((v) => ValidatorKeys.create({
187
+ bandersnatch: v.bandersnatch,
188
+ ed25519: v.ed25519,
189
+ }))),
190
+ });
191
+ const expectedBlock = createExpectedBlock({
192
+ timeSlot,
193
+ validatorIndex,
194
+ extrinsicHash: block.header.extrinsicHash,
195
+ epochMarker: expectedEpochMarker,
196
+ });
197
+ deepEqual(block, expectedBlock);
198
+ });
199
+ });
200
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/block-authorship",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "A test block generator simulating blocks received over the network.",
5
5
  "main": "index.ts",
6
6
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/importer",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "A JAM block importer queue.",
5
5
  "main": "index.ts",
6
6
  "dependencies": {