@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
@@ -0,0 +1,178 @@
1
+ import { AsyncQueue } from "./async-queue.js";
2
+ import type { Transport } from "./transport.js";
3
+
4
+ export type BridgeWebSocketLike = Readonly<{
5
+ readonly readyState: number;
6
+ readonly CONNECTING: number;
7
+ readonly OPEN: number;
8
+ readonly CLOSING: number;
9
+ readonly CLOSED: number;
10
+ binaryType?: string;
11
+
12
+ send(data: Uint8Array): void;
13
+ close(code?: number, reason?: string): void;
14
+ addEventListener(
15
+ type: "open" | "message" | "error" | "close",
16
+ listener: (event: unknown) => void,
17
+ options?: Readonly<{ once?: boolean }>,
18
+ ): void;
19
+ removeEventListener(
20
+ type: "open" | "message" | "error" | "close",
21
+ listener: (event: unknown) => void,
22
+ ): void;
23
+ }>;
24
+
25
+ export type BridgeWebSocketConstructor = new (url: string) => BridgeWebSocketLike;
26
+
27
+ export type CreateBridgeTransportOptions = Readonly<{
28
+ url: string;
29
+ signal?: AbortSignal;
30
+ WebSocketImpl?: BridgeWebSocketConstructor;
31
+ }>;
32
+
33
+ function toAbortError(reason: unknown): Error {
34
+ if (reason instanceof Error) return reason;
35
+ const error = new Error("Aborted");
36
+ error.name = "AbortError";
37
+ return error;
38
+ }
39
+
40
+ function toUint8ArrayMessage(data: unknown): Uint8Array {
41
+ if (data instanceof Uint8Array) return data;
42
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
43
+ if (ArrayBuffer.isView(data)) {
44
+ const view = data as ArrayBufferView;
45
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
46
+ }
47
+ throw new TypeError("WebSocket message must be binary (Uint8Array/ArrayBuffer)");
48
+ }
49
+
50
+ export async function createBridgeTransport(
51
+ options: CreateBridgeTransportOptions,
52
+ ): Promise<Transport> {
53
+ const WebSocketImpl =
54
+ options.WebSocketImpl ??
55
+ (globalThis as unknown as Readonly<{ WebSocket?: BridgeWebSocketConstructor }>).WebSocket;
56
+ if (!WebSocketImpl) {
57
+ throw new Error(
58
+ "WebSocket implementation not found; pass WebSocketImpl or run in a WebSocket-capable runtime",
59
+ );
60
+ }
61
+
62
+ const ws = new WebSocketImpl(options.url);
63
+ if ("binaryType" in ws) {
64
+ try {
65
+ (ws as unknown as { binaryType?: string }).binaryType = "arraybuffer";
66
+ } catch {
67
+ // ignore
68
+ }
69
+ }
70
+
71
+ const queue = new AsyncQueue<Uint8Array>();
72
+
73
+ const onMessage = (event: unknown) => {
74
+ try {
75
+ const data = (event as { data?: unknown } | null | undefined)?.data;
76
+ queue.push(toUint8ArrayMessage(data));
77
+ } catch (err) {
78
+ queue.error(err);
79
+ try {
80
+ ws.close();
81
+ } catch {
82
+ // ignore
83
+ }
84
+ }
85
+ };
86
+
87
+ const onError = () => {
88
+ queue.error(new Error("WebSocket error"));
89
+ };
90
+
91
+ const onClose = () => {
92
+ queue.close();
93
+ };
94
+
95
+ ws.addEventListener("message", onMessage);
96
+ ws.addEventListener("error", onError);
97
+ ws.addEventListener("close", onClose);
98
+
99
+ const abortListener = () => {
100
+ const err = toAbortError(options.signal?.reason);
101
+ queue.error(err);
102
+ try {
103
+ ws.close();
104
+ } catch {
105
+ // ignore
106
+ }
107
+ };
108
+ if (options.signal) {
109
+ if (options.signal.aborted) abortListener();
110
+ else options.signal.addEventListener("abort", abortListener, { once: true });
111
+ }
112
+
113
+ await new Promise<void>((resolve, reject) => {
114
+ const onOpen = () => {
115
+ cleanup();
116
+ resolve();
117
+ };
118
+ const onOpenError = () => {
119
+ cleanup();
120
+ reject(new Error("WebSocket connection failed"));
121
+ };
122
+ const cleanup = () => {
123
+ ws.removeEventListener("open", onOpen);
124
+ ws.removeEventListener("error", onOpenError);
125
+ };
126
+
127
+ if (ws.readyState === ws.OPEN) {
128
+ cleanup();
129
+ resolve();
130
+ return;
131
+ }
132
+ if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
133
+ cleanup();
134
+ reject(new Error("WebSocket is closed"));
135
+ return;
136
+ }
137
+
138
+ ws.addEventListener("open", onOpen, { once: true });
139
+ ws.addEventListener("error", onOpenError, { once: true });
140
+ });
141
+
142
+ const write = async (bytes: Uint8Array): Promise<void> => {
143
+ if (!(bytes instanceof Uint8Array)) {
144
+ throw new TypeError("bytes must be a Uint8Array");
145
+ }
146
+ if (ws.readyState !== ws.OPEN) {
147
+ throw new Error("WebSocket is not open");
148
+ }
149
+ ws.send(bytes);
150
+ };
151
+
152
+ const close = async (): Promise<void> => {
153
+ if (options.signal) options.signal.removeEventListener("abort", abortListener);
154
+ if (ws.readyState === ws.CLOSED) {
155
+ queue.close();
156
+ return;
157
+ }
158
+
159
+ await new Promise<void>((resolve) => {
160
+ const onClosed = () => {
161
+ ws.removeEventListener("close", onClosed);
162
+ resolve();
163
+ };
164
+ ws.addEventListener("close", onClosed);
165
+ try {
166
+ ws.close();
167
+ } catch {
168
+ resolve();
169
+ }
170
+ });
171
+ };
172
+
173
+ return {
174
+ read: () => queue,
175
+ write,
176
+ close,
177
+ };
178
+ }
@@ -0,0 +1,99 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import net from "node:net";
3
+ import { createTcpTransport } from "./tcp.js";
4
+
5
+ type ServerHandle = {
6
+ server: net.Server;
7
+ port: number;
8
+ };
9
+
10
+ async function startServer(
11
+ handler: (socket: net.Socket) => void | Promise<void>,
12
+ ): Promise<ServerHandle> {
13
+ const server = net.createServer((socket) => {
14
+ void handler(socket);
15
+ });
16
+
17
+ await new Promise<void>((resolve, reject) => {
18
+ server.listen(0, "127.0.0.1", () => resolve());
19
+ server.once("error", reject);
20
+ });
21
+
22
+ const address = server.address();
23
+ if (!address || typeof address === "string") {
24
+ throw new Error("Failed to bind test server");
25
+ }
26
+
27
+ return { server, port: Number(address.port) };
28
+ }
29
+
30
+ async function stopServer(handle: ServerHandle) {
31
+ await new Promise<void>((resolve, reject) => {
32
+ handle.server.close((err) => (err ? reject(err) : resolve()));
33
+ });
34
+ }
35
+
36
+ describe("createTcpTransport", () => {
37
+ let handle: ServerHandle | undefined;
38
+
39
+ afterEach(async () => {
40
+ if (handle) {
41
+ await stopServer(handle);
42
+ handle = undefined;
43
+ }
44
+ });
45
+
46
+ it("reads chunks from the socket", async () => {
47
+ handle = await startServer(async (socket) => {
48
+ socket.write(Uint8Array.from([1, 2, 3]));
49
+ await new Promise((r) => setTimeout(r, 5));
50
+ socket.write(Uint8Array.from([4, 5]));
51
+ socket.end();
52
+ });
53
+
54
+ const transport = await createTcpTransport({ host: "127.0.0.1", port: handle.port });
55
+
56
+ const received: number[] = [];
57
+ for await (const chunk of transport.read()) {
58
+ received.push(...chunk);
59
+ }
60
+
61
+ expect(received).toEqual([1, 2, 3, 4, 5]);
62
+ });
63
+
64
+ it("writes to the socket", async () => {
65
+ const received: number[] = [];
66
+ let receivedResolve: (() => void) | undefined;
67
+ const receivedDone = new Promise<void>((resolve) => {
68
+ receivedResolve = resolve;
69
+ });
70
+
71
+ handle = await startServer((socket) => {
72
+ socket.on("data", (d) => {
73
+ const bytes =
74
+ typeof d === "string"
75
+ ? new TextEncoder().encode(d)
76
+ : new Uint8Array(d.buffer, d.byteOffset, d.byteLength);
77
+ received.push(...bytes);
78
+ receivedResolve?.();
79
+ socket.end();
80
+ });
81
+ });
82
+
83
+ const transport = await createTcpTransport({ host: "127.0.0.1", port: handle.port });
84
+ await transport.write(Uint8Array.from([9, 8, 7]));
85
+ await transport.close();
86
+
87
+ await receivedDone;
88
+ expect(received).toEqual([9, 8, 7]);
89
+ });
90
+
91
+ it("aborts connect with AbortSignal", async () => {
92
+ const controller = new AbortController();
93
+ controller.abort();
94
+
95
+ await expect(
96
+ createTcpTransport({ host: "127.0.0.1", port: 1, signal: controller.signal }),
97
+ ).rejects.toBeTruthy();
98
+ });
99
+ });
@@ -0,0 +1,134 @@
1
+ import net from "node:net";
2
+ import { AsyncQueue } from "./async-queue.js";
3
+ import type { Transport } from "./transport.js";
4
+
5
+ export type CreateTcpTransportOptions = Readonly<{
6
+ host: string;
7
+ port: number;
8
+ signal?: AbortSignal;
9
+ }>;
10
+
11
+ function toAbortError(reason: unknown): Error {
12
+ if (reason instanceof Error) return reason;
13
+ const error = new Error("Aborted");
14
+ error.name = "AbortError";
15
+ return error;
16
+ }
17
+
18
+ export async function createTcpTransport(options: CreateTcpTransportOptions): Promise<Transport> {
19
+ const socket = new net.Socket();
20
+ socket.setNoDelay(true);
21
+
22
+ const queue = new AsyncQueue<Uint8Array>();
23
+
24
+ const onData = (chunk: Buffer) => {
25
+ queue.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
26
+ };
27
+ const onError = (err: unknown) => {
28
+ queue.error(err);
29
+ };
30
+ const onClose = () => {
31
+ queue.close();
32
+ };
33
+
34
+ socket.on("data", onData);
35
+ socket.on("error", onError);
36
+ socket.on("close", onClose);
37
+
38
+ const abortListener = () => {
39
+ const err = toAbortError(options.signal?.reason);
40
+ socket.destroy(err);
41
+ queue.error(err);
42
+ };
43
+ if (options.signal) {
44
+ if (options.signal.aborted) {
45
+ abortListener();
46
+ } else {
47
+ options.signal.addEventListener("abort", abortListener, { once: true });
48
+ }
49
+ }
50
+
51
+ await new Promise<void>((resolve, reject) => {
52
+ const onConnect = () => {
53
+ cleanup();
54
+ resolve();
55
+ };
56
+ const onConnectError = (err: unknown) => {
57
+ cleanup();
58
+ reject(err);
59
+ };
60
+ const cleanup = () => {
61
+ socket.off("connect", onConnect);
62
+ socket.off("error", onConnectError);
63
+ };
64
+ socket.once("connect", onConnect);
65
+ socket.once("error", onConnectError);
66
+ socket.connect(options.port, options.host);
67
+ });
68
+
69
+ const write = (bytes: Uint8Array): Promise<void> => {
70
+ if (socket.destroyed) {
71
+ return Promise.reject(new Error("Socket is closed"));
72
+ }
73
+
74
+ return new Promise<void>((resolve, reject) => {
75
+ let flushed = false;
76
+ let drained = false;
77
+
78
+ const finalizeIfDone = () => {
79
+ if (flushed && drained) {
80
+ cleanup();
81
+ resolve();
82
+ }
83
+ };
84
+
85
+ const onDrain = () => {
86
+ drained = true;
87
+ finalizeIfDone();
88
+ };
89
+
90
+ const onWrite = (err?: Error | null) => {
91
+ if (err) {
92
+ cleanup();
93
+ reject(err);
94
+ return;
95
+ }
96
+ flushed = true;
97
+ finalizeIfDone();
98
+ };
99
+
100
+ const cleanup = () => {
101
+ socket.off("drain", onDrain);
102
+ };
103
+
104
+ const ok = socket.write(bytes, onWrite);
105
+ if (ok) {
106
+ drained = true;
107
+ finalizeIfDone();
108
+ } else {
109
+ socket.on("drain", onDrain);
110
+ }
111
+ });
112
+ };
113
+
114
+ const close = async (): Promise<void> => {
115
+ if (options.signal) {
116
+ options.signal.removeEventListener("abort", abortListener);
117
+ }
118
+
119
+ if (socket.destroyed) {
120
+ queue.close();
121
+ return;
122
+ }
123
+
124
+ await new Promise<void>((resolve) => {
125
+ socket.end(() => resolve());
126
+ });
127
+ };
128
+
129
+ return {
130
+ read: () => queue,
131
+ write,
132
+ close,
133
+ };
134
+ }
@@ -0,0 +1,5 @@
1
+ export type Transport = Readonly<{
2
+ read(): AsyncIterable<Uint8Array>;
3
+ write(bytes: Uint8Array): Promise<void>;
4
+ close(): Promise<void>;
5
+ }>;