@qubic.ts/core 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.
Files changed (43) hide show
  1. package/package.json +37 -0
  2. package/scripts/verify-browser.mjs +15 -0
  3. package/scripts/verify-node.mjs +11 -0
  4. package/src/crypto/k12.ts +13 -0
  5. package/src/crypto/schnorrq.ts +3 -0
  6. package/src/crypto/seed.test.ts +11 -0
  7. package/src/crypto/seed.ts +63 -0
  8. package/src/index.browser.ts +85 -0
  9. package/src/index.node.ts +2 -0
  10. package/src/index.ts +85 -0
  11. package/src/primitives/identity.test.ts +41 -0
  12. package/src/primitives/identity.ts +108 -0
  13. package/src/primitives/number64.ts +58 -0
  14. package/src/protocol/assets.test.ts +86 -0
  15. package/src/protocol/assets.ts +276 -0
  16. package/src/protocol/broadcast-transaction.test.ts +34 -0
  17. package/src/protocol/broadcast-transaction.ts +17 -0
  18. package/src/protocol/contract-function.test.ts +37 -0
  19. package/src/protocol/contract-function.ts +53 -0
  20. package/src/protocol/entity.test.ts +58 -0
  21. package/src/protocol/entity.ts +82 -0
  22. package/src/protocol/message-types.ts +16 -0
  23. package/src/protocol/packet-framer.test.ts +101 -0
  24. package/src/protocol/packet-framer.ts +120 -0
  25. package/src/protocol/request-packet.ts +48 -0
  26. package/src/protocol/request-response-header.test.ts +69 -0
  27. package/src/protocol/request-response-header.ts +56 -0
  28. package/src/protocol/stream.test.ts +74 -0
  29. package/src/protocol/stream.ts +48 -0
  30. package/src/protocol/system-info.test.ts +76 -0
  31. package/src/protocol/system-info.ts +111 -0
  32. package/src/protocol/tick-data.test.ts +63 -0
  33. package/src/protocol/tick-data.ts +127 -0
  34. package/src/protocol/tick-info.test.ts +35 -0
  35. package/src/protocol/tick-info.ts +34 -0
  36. package/src/transactions/transaction.test.ts +80 -0
  37. package/src/transactions/transaction.ts +150 -0
  38. package/src/transport/async-queue.ts +60 -0
  39. package/src/transport/bridge.test.ts +114 -0
  40. package/src/transport/bridge.ts +178 -0
  41. package/src/transport/tcp.test.ts +99 -0
  42. package/src/transport/tcp.ts +134 -0
  43. package/src/transport/transport.ts +5 -0
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@qubic.ts/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "browser": "./src/index.browser.ts",
11
+ "import": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./browser": {
15
+ "types": "./src/index.browser.ts",
16
+ "import": "./src/index.browser.ts",
17
+ "default": "./src/index.browser.ts"
18
+ },
19
+ "./node": {
20
+ "types": "./src/index.node.ts",
21
+ "import": "./src/index.node.ts",
22
+ "default": "./src/index.node.ts"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "dependencies": {
27
+ "@noble/hashes": "^1.8.0",
28
+ "@qubic-labs/schnorrq": "^1.0.2"
29
+ },
30
+ "scripts": {
31
+ "build": "echo \"qubic-ts-core build pending\"",
32
+ "check": "bun run verify:browser && bun run verify:node",
33
+ "verify:browser": "bun --conditions=browser ./scripts/verify-browser.mjs",
34
+ "verify:node": "bun ./scripts/verify-node.mjs",
35
+ "test": "bun test ./src/**/*.test.ts"
36
+ }
37
+ }
@@ -0,0 +1,15 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ const browserModule = await import("@qubic.ts/core");
4
+
5
+ if ("createTcpTransport" in browserModule) {
6
+ throw new Error("Browser entry must not export createTcpTransport.");
7
+ }
8
+
9
+ const browserEntry = await readFile(new URL("../src/index.browser.ts", import.meta.url), "utf8");
10
+ if (browserEntry.includes("transport/tcp")) {
11
+ throw new Error("Browser entry must not reference transport/tcp.");
12
+ }
13
+
14
+ console.log("Browser boundary verification passed.");
15
+
@@ -0,0 +1,11 @@
1
+ const nodeModule = await import("@qubic.ts/core/node");
2
+
3
+ if (typeof nodeModule.createTcpTransport !== "function") {
4
+ throw new Error("Node entry must export createTcpTransport.");
5
+ }
6
+
7
+ if (typeof nodeModule.createBridgeTransport !== "function") {
8
+ throw new Error("Node entry must export createBridgeTransport.");
9
+ }
10
+
11
+ console.log("Node boundary verification passed.");
@@ -0,0 +1,13 @@
1
+ import { k12 as k12Hash } from "@qubic-labs/schnorrq";
2
+
3
+ export async function k12(input: Uint8Array, dkLen: number): Promise<Uint8Array> {
4
+ if (!(input instanceof Uint8Array)) {
5
+ throw new TypeError("input must be a Uint8Array");
6
+ }
7
+ if (!Number.isSafeInteger(dkLen) || dkLen < 0) {
8
+ throw new RangeError("dkLen must be a non-negative integer");
9
+ }
10
+
11
+ return k12Hash(input, dkLen);
12
+ }
13
+
@@ -0,0 +1,3 @@
1
+ export type { CreateSchnorrqParams, Schnorrq, SchnorrqSigner } from "@qubic-labs/schnorrq";
2
+ export { generatePublicKey, sign, verify } from "@qubic-labs/schnorrq";
3
+
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { identityFromSeed } from "./seed.js";
3
+
4
+ describe("seed", () => {
5
+ it("derives the expected identity from a known seed", async () => {
6
+ const seed = "jvhbyzjinlyutyuhsweuxiwootqoevjqwqmdhjeohrytxjxidpbcfyg";
7
+ const identity = await identityFromSeed(seed);
8
+ expect(identity).toBe("HZEBBDSKZRTAWGYMTTSDZQDXYWPBUKBEAIYZNFLVWARZJBEBIJRRFKUDVETA");
9
+ });
10
+ });
11
+
@@ -0,0 +1,63 @@
1
+ import { identityFromPublicKey } from "../primitives/identity.js";
2
+ import { k12 } from "./k12.js";
3
+ import { generatePublicKey } from "./schnorrq.js";
4
+
5
+ export const SEED_LENGTH = 55;
6
+
7
+ function assertSeed(seed: string) {
8
+ if (typeof seed !== "string") {
9
+ throw new TypeError("seed must be a string");
10
+ }
11
+ if (seed.length !== SEED_LENGTH) {
12
+ throw new RangeError(`seed must be ${SEED_LENGTH} characters`);
13
+ }
14
+ for (let i = 0; i < seed.length; i++) {
15
+ const c = seed.charCodeAt(i);
16
+ if (c < 97 || c > 122) {
17
+ throw new Error("seed must contain only lowercase letters a-z");
18
+ }
19
+ }
20
+ }
21
+
22
+ function seedToBytes(seed: string): Uint8Array {
23
+ const bytes = new Uint8Array(SEED_LENGTH);
24
+ for (let i = 0; i < SEED_LENGTH; i++) {
25
+ bytes[i] = seed.charCodeAt(i) - 97;
26
+ }
27
+ return bytes;
28
+ }
29
+
30
+ function applySeedIndex(preimage: Uint8Array, index: number) {
31
+ if (!Number.isSafeInteger(index) || index < 0) {
32
+ throw new RangeError("index must be a non-negative integer");
33
+ }
34
+ while (index-- > 0) {
35
+ for (let i = 0; i < preimage.length; i++) {
36
+ const next = (preimage[i] ?? 0) + 1;
37
+ if (next > 26) {
38
+ preimage[i] = 1;
39
+ } else {
40
+ preimage[i] = next;
41
+ break;
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function privateKeyFromSeed(seed: string, index = 0): Promise<Uint8Array> {
48
+ assertSeed(seed);
49
+ const preimage = seedToBytes(seed);
50
+ applySeedIndex(preimage, index);
51
+ return k12(preimage, 32);
52
+ }
53
+
54
+ export async function publicKeyFromSeed(seed: string, index = 0): Promise<Uint8Array> {
55
+ const privateKey = await privateKeyFromSeed(seed, index);
56
+ return generatePublicKey(privateKey);
57
+ }
58
+
59
+ export async function identityFromSeed(seed: string, index = 0): Promise<string> {
60
+ const publicKey = await publicKeyFromSeed(seed, index);
61
+ return identityFromPublicKey(publicKey);
62
+ }
63
+
@@ -0,0 +1,85 @@
1
+ export {
2
+ identityFromSeed,
3
+ privateKeyFromSeed,
4
+ publicKeyFromSeed,
5
+ SEED_LENGTH,
6
+ } from "./crypto/seed.js";
7
+ export {
8
+ identityFromPublicKey,
9
+ publicKeyFromIdentity,
10
+ verifyIdentity,
11
+ } from "./primitives/identity.js";
12
+ export {
13
+ MAX_I64,
14
+ MAX_U64,
15
+ MIN_I64,
16
+ readI64LE,
17
+ readU64LE,
18
+ writeI64LE,
19
+ writeU64LE,
20
+ } from "./primitives/number64.js";
21
+ export type {
22
+ AssetRecord,
23
+ RespondAssets,
24
+ RespondAssetsWithSiblings,
25
+ } from "./protocol/assets.js";
26
+ export {
27
+ AssetRecordType,
28
+ decodeAssetRecord,
29
+ decodeRespondAssets,
30
+ decodeRespondAssetsWithSiblings,
31
+ encodeRequestAssetsByFilter,
32
+ encodeRequestAssetsByUniverseIndex,
33
+ } from "./protocol/assets.js";
34
+ export { encodeBroadcastTransactionPacket } from "./protocol/broadcast-transaction.js";
35
+ export type { RequestContractFunctionParams } from "./protocol/contract-function.js";
36
+ export {
37
+ decodeRespondContractFunction,
38
+ encodeRequestContractFunction,
39
+ } from "./protocol/contract-function.js";
40
+ export type { RespondEntity } from "./protocol/entity.js";
41
+ export { decodeRespondEntity, encodeRequestEntity } from "./protocol/entity.js";
42
+ export { NetworkMessageType } from "./protocol/message-types.js";
43
+ export type { Packet, PacketFramer } from "./protocol/packet-framer.js";
44
+ export { createPacketFramer } from "./protocol/packet-framer.js";
45
+ export type { EncodeRequestPacketOptions } from "./protocol/request-packet.js";
46
+ export { encodeRequestPacket } from "./protocol/request-packet.js";
47
+ export type { RequestResponseHeaderFields } from "./protocol/request-response-header.js";
48
+ export {
49
+ decodeRequestResponseHeader,
50
+ encodeRequestResponseHeader,
51
+ MAX_PACKET_SIZE,
52
+ } from "./protocol/request-response-header.js";
53
+ export { END_RESPONSE_TYPE, readUntilEndResponse } from "./protocol/stream.js";
54
+ export type { SystemInfo } from "./protocol/system-info.js";
55
+ export { decodeRespondSystemInfo, encodeRequestSystemInfo } from "./protocol/system-info.js";
56
+ export type { TickDataView } from "./protocol/tick-data.js";
57
+ export {
58
+ countNonZeroTransactionDigests,
59
+ decodeBroadcastFutureTickData,
60
+ encodeRequestTickData,
61
+ MAX_NUMBER_OF_CONTRACTS,
62
+ NUMBER_OF_TRANSACTIONS_PER_TICK,
63
+ TICK_DATA_PAYLOAD_SIZE,
64
+ } from "./protocol/tick-data.js";
65
+ export type { CurrentTickInfo } from "./protocol/tick-info.js";
66
+ export {
67
+ decodeRespondCurrentTickInfo,
68
+ encodeRequestCurrentTickInfo,
69
+ } from "./protocol/tick-info.js";
70
+ export type { BuildUnsignedTransactionParams } from "./transactions/transaction.js";
71
+ export {
72
+ buildSignedTransaction,
73
+ buildUnsignedTransaction,
74
+ MAX_INPUT_SIZE,
75
+ MAX_TRANSACTION_SIZE,
76
+ SIGNATURE_LENGTH,
77
+ signTransaction,
78
+ TRANSACTION_HEADER_SIZE,
79
+ transactionDigest,
80
+ transactionId,
81
+ unsignedTransactionDigest,
82
+ } from "./transactions/transaction.js";
83
+ export { createBridgeTransport } from "./transport/bridge.js";
84
+ export type { Transport } from "./transport/transport.js";
85
+
@@ -0,0 +1,2 @@
1
+ export * from "./index.js";
2
+
package/src/index.ts ADDED
@@ -0,0 +1,85 @@
1
+ export {
2
+ identityFromPublicKey,
3
+ publicKeyFromIdentity,
4
+ verifyIdentity,
5
+ } from "./primitives/identity.js";
6
+ export {
7
+ MAX_I64,
8
+ MAX_U64,
9
+ MIN_I64,
10
+ readI64LE,
11
+ readU64LE,
12
+ writeI64LE,
13
+ writeU64LE,
14
+ } from "./primitives/number64.js";
15
+ export {
16
+ identityFromSeed,
17
+ privateKeyFromSeed,
18
+ publicKeyFromSeed,
19
+ SEED_LENGTH,
20
+ } from "./crypto/seed.js";
21
+ export type {
22
+ AssetRecord,
23
+ RespondAssets,
24
+ RespondAssetsWithSiblings,
25
+ } from "./protocol/assets.js";
26
+ export {
27
+ AssetRecordType,
28
+ decodeAssetRecord,
29
+ decodeRespondAssets,
30
+ decodeRespondAssetsWithSiblings,
31
+ encodeRequestAssetsByFilter,
32
+ encodeRequestAssetsByUniverseIndex,
33
+ } from "./protocol/assets.js";
34
+ export { encodeBroadcastTransactionPacket } from "./protocol/broadcast-transaction.js";
35
+ export type { RequestContractFunctionParams } from "./protocol/contract-function.js";
36
+ export {
37
+ decodeRespondContractFunction,
38
+ encodeRequestContractFunction,
39
+ } from "./protocol/contract-function.js";
40
+ export type { RespondEntity } from "./protocol/entity.js";
41
+ export { decodeRespondEntity, encodeRequestEntity } from "./protocol/entity.js";
42
+ export { NetworkMessageType } from "./protocol/message-types.js";
43
+ export type { Packet, PacketFramer } from "./protocol/packet-framer.js";
44
+ export { createPacketFramer } from "./protocol/packet-framer.js";
45
+ export type { EncodeRequestPacketOptions } from "./protocol/request-packet.js";
46
+ export { encodeRequestPacket } from "./protocol/request-packet.js";
47
+ export type { RequestResponseHeaderFields } from "./protocol/request-response-header.js";
48
+ export {
49
+ decodeRequestResponseHeader,
50
+ encodeRequestResponseHeader,
51
+ MAX_PACKET_SIZE,
52
+ } from "./protocol/request-response-header.js";
53
+ export { END_RESPONSE_TYPE, readUntilEndResponse } from "./protocol/stream.js";
54
+ export type { SystemInfo } from "./protocol/system-info.js";
55
+ export { decodeRespondSystemInfo, encodeRequestSystemInfo } from "./protocol/system-info.js";
56
+ export type { TickDataView } from "./protocol/tick-data.js";
57
+ export {
58
+ countNonZeroTransactionDigests,
59
+ decodeBroadcastFutureTickData,
60
+ encodeRequestTickData,
61
+ MAX_NUMBER_OF_CONTRACTS,
62
+ NUMBER_OF_TRANSACTIONS_PER_TICK,
63
+ TICK_DATA_PAYLOAD_SIZE,
64
+ } from "./protocol/tick-data.js";
65
+ export type { CurrentTickInfo } from "./protocol/tick-info.js";
66
+ export {
67
+ decodeRespondCurrentTickInfo,
68
+ encodeRequestCurrentTickInfo,
69
+ } from "./protocol/tick-info.js";
70
+ export type { BuildUnsignedTransactionParams } from "./transactions/transaction.js";
71
+ export {
72
+ buildSignedTransaction,
73
+ buildUnsignedTransaction,
74
+ MAX_INPUT_SIZE,
75
+ MAX_TRANSACTION_SIZE,
76
+ SIGNATURE_LENGTH,
77
+ signTransaction,
78
+ transactionDigest,
79
+ transactionId,
80
+ TRANSACTION_HEADER_SIZE,
81
+ unsignedTransactionDigest,
82
+ } from "./transactions/transaction.js";
83
+ export { createBridgeTransport } from "./transport/bridge.js";
84
+ export { createTcpTransport } from "./transport/tcp.js";
85
+ export type { Transport } from "./transport/transport.js";
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { identityFromPublicKey, publicKeyFromIdentity, verifyIdentity } from "./identity.js";
3
+
4
+ describe("identity", () => {
5
+ it("matches the known empty address for zero pubkey (uppercase)", () => {
6
+ const zero = new Uint8Array(32);
7
+ expect(identityFromPublicKey(zero)).toBe(
8
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB",
9
+ );
10
+ });
11
+
12
+ it("supports lowercase identities", () => {
13
+ const zero = new Uint8Array(32);
14
+ expect(identityFromPublicKey(zero, { lowerCase: true })).toBe(
15
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafxib",
16
+ );
17
+ });
18
+
19
+ it("roundtrips identity -> pubkey -> identity", () => {
20
+ const ids = [
21
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB",
22
+ "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ",
23
+ "XPXYKFLGSWRHRGAUKWFWVXCDVEYAPCPCNUTMUDWFGDYQCWZNJMWFZEEGCFFO",
24
+ ];
25
+
26
+ for (const id of ids) {
27
+ const pub = publicKeyFromIdentity(id);
28
+ const back = identityFromPublicKey(pub, { lowerCase: false });
29
+ expect(back).toBe(id);
30
+ expect(verifyIdentity(id)).toBe(true);
31
+ }
32
+ });
33
+
34
+ it("rejects checksum mismatch", () => {
35
+ const id = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB";
36
+ const bad = `${id.slice(0, 59)}A`;
37
+ expect(verifyIdentity(bad)).toBe(false);
38
+ expect(() => publicKeyFromIdentity(bad)).toThrow();
39
+ });
40
+ });
41
+
@@ -0,0 +1,108 @@
1
+ import { k12 } from "@noble/hashes/sha3-addons";
2
+ import { readU64LE, writeU64LE } from "./number64.js";
3
+
4
+ export type IdentityOptions = Readonly<{
5
+ lowerCase?: boolean;
6
+ }>;
7
+
8
+ function assertUint8ArrayLength(bytes: Uint8Array, length: number, name: string) {
9
+ if (!(bytes instanceof Uint8Array)) {
10
+ throw new TypeError(`${name} must be a Uint8Array`);
11
+ }
12
+ if (bytes.byteLength !== length) {
13
+ throw new RangeError(`${name} must be ${length} bytes`);
14
+ }
15
+ }
16
+
17
+ function isAllUppercaseLetters(s: string): boolean {
18
+ for (let i = 0; i < s.length; i++) {
19
+ const c = s.charCodeAt(i);
20
+ if (c < 65 || c > 90) return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ function isAllLowercaseLetters(s: string): boolean {
26
+ for (let i = 0; i < s.length; i++) {
27
+ const c = s.charCodeAt(i);
28
+ if (c < 97 || c > 122) return false;
29
+ }
30
+ return true;
31
+ }
32
+
33
+ export function identityFromPublicKey(
34
+ publicKey32: Uint8Array,
35
+ options: IdentityOptions = {},
36
+ ): string {
37
+ assertUint8ArrayLength(publicKey32, 32, "publicKey32");
38
+
39
+ const letter = options.lowerCase ? 97 : 65;
40
+ const identityChars = new Uint8Array(60);
41
+
42
+ for (let i = 0; i < 4; i++) {
43
+ let fragment = readU64LE(publicKey32, i * 8);
44
+ for (let j = 0; j < 14; j++) {
45
+ identityChars[i * 14 + j] = Number((fragment % 26n) + BigInt(letter));
46
+ fragment /= 26n;
47
+ }
48
+ }
49
+
50
+ const checksum = k12(publicKey32, { dkLen: 3 });
51
+ let checksumInt =
52
+ ((checksum[0] ?? 0) | ((checksum[1] ?? 0) << 8) | ((checksum[2] ?? 0) << 16)) & 0x3ffff;
53
+
54
+ for (let i = 0; i < 4; i++) {
55
+ identityChars[56 + i] = (checksumInt % 26) + letter;
56
+ checksumInt = Math.floor(checksumInt / 26);
57
+ }
58
+
59
+ return new TextDecoder().decode(identityChars);
60
+ }
61
+
62
+ export function publicKeyFromIdentity(identity60: string): Uint8Array {
63
+ if (typeof identity60 !== "string") {
64
+ throw new TypeError("identity60 must be a string");
65
+ }
66
+ if (identity60.length !== 60) {
67
+ throw new RangeError("identity60 must be 60 characters");
68
+ }
69
+
70
+ const isUpper = isAllUppercaseLetters(identity60);
71
+ const isLower = isAllLowercaseLetters(identity60);
72
+ if (!isUpper && !isLower) {
73
+ throw new Error("identity60 must contain only A-Z or only a-z letters");
74
+ }
75
+
76
+ const base = isLower ? 97 : 65;
77
+ const publicKey = new Uint8Array(32);
78
+
79
+ for (let i = 0; i < 4; i++) {
80
+ let acc = 0n;
81
+ for (let j = 13; j >= 0; j--) {
82
+ const c = identity60.charCodeAt(i * 14 + j);
83
+ const v = c - base;
84
+ if (v < 0 || v > 25) {
85
+ throw new Error("identity60 contains invalid characters");
86
+ }
87
+ acc = acc * 26n + BigInt(v);
88
+ }
89
+ writeU64LE(acc, publicKey, i * 8);
90
+ }
91
+
92
+ const expected = identityFromPublicKey(publicKey, { lowerCase: isLower });
93
+ if (expected !== identity60) {
94
+ throw new Error("identity60 checksum mismatch");
95
+ }
96
+
97
+ return publicKey;
98
+ }
99
+
100
+ export function verifyIdentity(identity60: string): boolean {
101
+ try {
102
+ publicKeyFromIdentity(identity60);
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
@@ -0,0 +1,58 @@
1
+ export const MIN_I64 = -(1n << 63n);
2
+ export const MAX_I64 = (1n << 63n) - 1n;
3
+ export const MAX_U64 = (1n << 64n) - 1n;
4
+
5
+ function assertIsUint8Array(value: unknown, name: string): asserts value is Uint8Array {
6
+ if (!(value instanceof Uint8Array)) {
7
+ throw new TypeError(`${name} must be a Uint8Array`);
8
+ }
9
+ }
10
+
11
+ function assertSafeOffset(bytes: Uint8Array, offset: number, length: number) {
12
+ if (!Number.isInteger(offset)) {
13
+ throw new TypeError("offset must be an integer");
14
+ }
15
+ if (offset < 0) {
16
+ throw new RangeError("offset must be >= 0");
17
+ }
18
+ if (offset + length > bytes.byteLength) {
19
+ throw new RangeError("Not enough bytes available at offset");
20
+ }
21
+ }
22
+
23
+ function assertBigIntInRange(value: bigint, min: bigint, max: bigint, name: string) {
24
+ if (value < min || value > max) {
25
+ throw new RangeError(`${name} must be between ${min} and ${max}`);
26
+ }
27
+ }
28
+
29
+ export function readI64LE(bytes: Uint8Array, offset: number): bigint {
30
+ assertIsUint8Array(bytes, "bytes");
31
+ assertSafeOffset(bytes, offset, 8);
32
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
33
+ return view.getBigInt64(offset, true);
34
+ }
35
+
36
+ export function readU64LE(bytes: Uint8Array, offset: number): bigint {
37
+ assertIsUint8Array(bytes, "bytes");
38
+ assertSafeOffset(bytes, offset, 8);
39
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
40
+ return view.getBigUint64(offset, true);
41
+ }
42
+
43
+ export function writeI64LE(value: bigint, bytes: Uint8Array, offset: number): void {
44
+ assertIsUint8Array(bytes, "bytes");
45
+ assertSafeOffset(bytes, offset, 8);
46
+ assertBigIntInRange(value, MIN_I64, MAX_I64, "value");
47
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
48
+ view.setBigInt64(offset, value, true);
49
+ }
50
+
51
+ export function writeU64LE(value: bigint, bytes: Uint8Array, offset: number): void {
52
+ assertIsUint8Array(bytes, "bytes");
53
+ assertSafeOffset(bytes, offset, 8);
54
+ assertBigIntInRange(value, 0n, MAX_U64, "value");
55
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
56
+ view.setBigUint64(offset, value, true);
57
+ }
58
+
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ ASSETS_DEPTH,
4
+ AssetRecordType,
5
+ decodeAssetRecord,
6
+ decodeRespondAssets,
7
+ decodeRespondAssetsWithSiblings,
8
+ encodeRequestAssetsByFilter,
9
+ encodeRequestAssetsByUniverseIndex,
10
+ RESPOND_ASSETS_PAYLOAD_SIZE,
11
+ RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE,
12
+ RequestAssetsFlag,
13
+ RequestAssetsType,
14
+ } from "./assets.js";
15
+ import { NetworkMessageType } from "./message-types.js";
16
+ import { decodeRequestResponseHeader } from "./request-response-header.js";
17
+
18
+ describe("assets", () => {
19
+ it("encodes RequestAssets by universe index", () => {
20
+ const packet = encodeRequestAssetsByUniverseIndex({ universeIndex: 123, getSiblings: true });
21
+ const header = decodeRequestResponseHeader(packet.subarray(0, 8));
22
+ expect(header.type).toBe(NetworkMessageType.REQUEST_ASSETS);
23
+
24
+ const payload = packet.subarray(8);
25
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
26
+ expect(view.getUint16(0, true)).toBe(RequestAssetsType.BY_UNIVERSE_INDEX);
27
+ expect(view.getUint16(2, true) & RequestAssetsFlag.GET_SIBLINGS).toBe(
28
+ RequestAssetsFlag.GET_SIBLINGS,
29
+ );
30
+ expect(view.getUint32(4, true)).toBe(123);
31
+ });
32
+
33
+ it("encodes RequestAssets issuance by filter with anyIssuer/anyAssetName flags", () => {
34
+ const packet = encodeRequestAssetsByFilter({
35
+ requestType: RequestAssetsType.ISSUANCE,
36
+ getSiblings: false,
37
+ });
38
+ const payload = packet.subarray(8);
39
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
40
+ expect(view.getUint16(0, true)).toBe(RequestAssetsType.ISSUANCE);
41
+ const flags = view.getUint16(2, true);
42
+ expect((flags & RequestAssetsFlag.ANY_ISSUER) !== 0).toBe(true);
43
+ expect((flags & RequestAssetsFlag.ANY_ASSET_NAME) !== 0).toBe(true);
44
+ });
45
+
46
+ it("decodes issuance AssetRecord", () => {
47
+ const record = new Uint8Array(48);
48
+ record.set(new Uint8Array(32).fill(1), 0);
49
+ record[32] = AssetRecordType.ISSUANCE;
50
+ record.set(Uint8Array.from([65, 66, 67, 0, 0, 0, 0]), 33); // "ABC"
51
+ record[40] = 8;
52
+ record.set(new Uint8Array(7).fill(2), 41);
53
+
54
+ const decoded = decodeAssetRecord(record);
55
+ expect(decoded.type).toBe(AssetRecordType.ISSUANCE);
56
+ if (decoded.type === AssetRecordType.ISSUANCE) {
57
+ expect(decoded.name7).toEqual(Uint8Array.from([65, 66, 67, 0, 0, 0, 0]));
58
+ expect(decoded.numberOfDecimalPlaces).toBe(8);
59
+ }
60
+ });
61
+
62
+ it("decodes RespondAssets and RespondAssetsWithSiblings", () => {
63
+ const payload = new Uint8Array(RESPOND_ASSETS_PAYLOAD_SIZE);
64
+ payload[32] = AssetRecordType.OWNERSHIP;
65
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
66
+ view.setUint16(34, 10, true);
67
+ view.setUint32(36, 20, true);
68
+ view.setBigInt64(40, 30n, true);
69
+ view.setUint32(48, 111, true);
70
+ view.setUint32(52, 222, true);
71
+
72
+ const decoded = decodeRespondAssets(payload);
73
+ expect(decoded.tick).toBe(111);
74
+ expect(decoded.universeIndex).toBe(222);
75
+ expect(decoded.asset.type).toBe(AssetRecordType.OWNERSHIP);
76
+
77
+ const payload2 = new Uint8Array(RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE);
78
+ payload2.set(payload, 0);
79
+ for (let i = 0; i < ASSETS_DEPTH; i++)
80
+ payload2.set(new Uint8Array(32).fill(i), RESPOND_ASSETS_PAYLOAD_SIZE + i * 32);
81
+ const decoded2 = decodeRespondAssetsWithSiblings(payload2);
82
+ expect(decoded2.siblings).toHaveLength(ASSETS_DEPTH);
83
+ expect(decoded2.siblings[0]).toEqual(new Uint8Array(32).fill(0));
84
+ expect(decoded2.siblings[23]).toEqual(new Uint8Array(32).fill(23));
85
+ });
86
+ });