@qubic.ts/sdk 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.
@@ -0,0 +1,322 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { FetchLike } from "../http.js";
3
+ import { createRpcClient, RpcError } from "./client.js";
4
+
5
+ function createTestFetch(): FetchLike {
6
+ return async (input, init) => {
7
+ const url = new URL(getUrl(input));
8
+ const method = getMethod(input, init);
9
+
10
+ if (method === "GET" && url.pathname === "/live/v1/tick-info") {
11
+ return Response.json({
12
+ tickInfo: { tick: 123, duration: 1500, epoch: 12, initialTick: 100 },
13
+ });
14
+ }
15
+
16
+ if (method === "GET" && url.pathname.startsWith("/live/v1/balances/")) {
17
+ const id = decodeURIComponent(url.pathname.split("/").pop() ?? "");
18
+ return Response.json({
19
+ balance: {
20
+ id,
21
+ balance: "1000",
22
+ validForTick: 123,
23
+ latestIncomingTransferTick: 120,
24
+ latestOutgoingTransferTick: 121,
25
+ incomingAmount: "10",
26
+ outgoingAmount: "5",
27
+ numberOfIncomingTransfers: 2,
28
+ numberOfOutgoingTransfers: 1,
29
+ },
30
+ });
31
+ }
32
+
33
+ if (method === "POST" && url.pathname === "/live/v1/broadcast-transaction") {
34
+ const body = readJsonBody(input, init);
35
+ if (body.encodedTransaction !== "AQID") {
36
+ return new Response(JSON.stringify({ error: "bad tx base64" }), { status: 400 });
37
+ }
38
+ return Response.json({
39
+ peersBroadcasted: 3,
40
+ encodedTransaction: body.encodedTransaction,
41
+ transactionId: "tx_abc",
42
+ });
43
+ }
44
+
45
+ if (method === "POST" && url.pathname === "/live/v1/querySmartContract") {
46
+ const body = readJsonBody(input, init);
47
+ if (body.inputSize !== 2) {
48
+ return new Response(JSON.stringify({ error: "bad inputSize" }), { status: 400 });
49
+ }
50
+ if (body.requestData !== "CQk=") {
51
+ return new Response(JSON.stringify({ error: "bad requestData" }), { status: 400 });
52
+ }
53
+ return Response.json({ responseData: "AA==" });
54
+ }
55
+
56
+ if (method === "GET" && url.pathname === "/query/v1/getLastProcessedTick") {
57
+ return Response.json({ tickNumber: 10, epoch: 1, intervalInitialTick: 0 });
58
+ }
59
+
60
+ if (method === "GET" && url.pathname === "/query/v1/getProcessedTickIntervals") {
61
+ return Response.json({
62
+ processedTickIntervals: [
63
+ { epoch: 1, firstTick: 0, lastTick: 10 },
64
+ { epoch: 2, firstTick: 11, lastTick: 20 },
65
+ ],
66
+ });
67
+ }
68
+
69
+ if (method === "POST" && url.pathname === "/query/v1/getComputorListsForEpoch") {
70
+ const body = readJsonBody(input, init);
71
+ if (body.epoch !== 1) {
72
+ return new Response(JSON.stringify({ error: "bad epoch" }), { status: 400 });
73
+ }
74
+ return Response.json({
75
+ computorsLists: [
76
+ {
77
+ epoch: 1,
78
+ tickNumber: 10,
79
+ identities: ["A", "B"],
80
+ signature: "SIG",
81
+ },
82
+ ],
83
+ });
84
+ }
85
+
86
+ if (method === "POST" && url.pathname === "/query/v1/getTransactionByHash") {
87
+ const body = readJsonBody(input, init);
88
+ if (body.hash !== "deadbeef") {
89
+ return new Response(JSON.stringify({ error: "bad hash" }), { status: 400 });
90
+ }
91
+ return Response.json({
92
+ hash: "deadbeef",
93
+ amount: "42",
94
+ source: "SOURCE",
95
+ destination: "DEST",
96
+ tickNumber: 9,
97
+ timestamp: "0",
98
+ inputType: 0,
99
+ inputSize: 0,
100
+ inputData: "",
101
+ signature: "",
102
+ moneyFlew: true,
103
+ });
104
+ }
105
+
106
+ if (method === "POST" && url.pathname === "/query/v1/getTransactionsForIdentity") {
107
+ const body = readJsonBody(input, init);
108
+ const pagination =
109
+ body.pagination && typeof body.pagination === "object"
110
+ ? (body.pagination as Record<string, unknown>)
111
+ : {};
112
+ const offset = Number(pagination.offset ?? 0);
113
+ const size = Number(pagination.size ?? 100);
114
+ const transactions = baseTransactions().slice(offset, offset + size);
115
+ return Response.json({
116
+ validForTick: 10,
117
+ hits: { total: baseTransactions().length, from: offset, size },
118
+ transactions,
119
+ });
120
+ }
121
+
122
+ return new Response("not found", { status: 404 });
123
+ };
124
+ }
125
+
126
+ describe("rpc client", () => {
127
+ it("calls live tick-info and parses ints", async () => {
128
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
129
+ const tickInfo = await rpc.live.tickInfo();
130
+ expect(tickInfo.tick).toBe(123n);
131
+ expect(tickInfo.epoch).toBe(12n);
132
+ });
133
+
134
+ it("calls live balances and parses bigints", async () => {
135
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
136
+ const b = await rpc.live.balance("ID");
137
+ expect(b.id).toBe("ID");
138
+ expect(b.balance).toBe(1000n);
139
+ expect(b.incomingAmount).toBe(10n);
140
+ });
141
+
142
+ it("broadcasts a base64 transaction", async () => {
143
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
144
+ const res = await rpc.live.broadcastTransaction(new Uint8Array([1, 2, 3]));
145
+ expect(res.transactionId).toBe("tx_abc");
146
+ expect(res.peersBroadcasted).toBe(3);
147
+ });
148
+
149
+ it("queries a smart contract and decodes response bytes", async () => {
150
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
151
+ const res = await rpc.live.querySmartContract({
152
+ contractIndex: 0,
153
+ inputType: 0,
154
+ input: new Uint8Array([9, 9]),
155
+ });
156
+ expect(res.responseBase64).toBe("AA==");
157
+ expect(Array.from(res.responseBytes)).toEqual([0]);
158
+ });
159
+
160
+ it("calls query getTransactionByHash and parses fields", async () => {
161
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
162
+ const tx = await rpc.query.getTransactionByHash("deadbeef");
163
+ expect(tx.amount).toBe(42n);
164
+ expect(tx.moneyFlew).toBe(true);
165
+ });
166
+
167
+ it("calls query getProcessedTickIntervals", async () => {
168
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
169
+ const intervals = await rpc.query.getProcessedTickIntervals();
170
+ expect(intervals.length).toBe(2);
171
+ expect(mustGet(intervals, 0).epoch).toBe(1n);
172
+ expect(mustGet(intervals, 1).lastTick).toBe(20n);
173
+ });
174
+
175
+ it("calls query getComputorListsForEpoch", async () => {
176
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
177
+ const lists = await rpc.query.getComputorListsForEpoch(1);
178
+ expect(lists.length).toBe(1);
179
+ expect(mustGet(lists, 0).epoch).toBe(1n);
180
+ expect(mustGet(lists, 0).identities).toEqual(["A", "B"]);
181
+ });
182
+
183
+ it("throws RpcError on non-2xx responses", async () => {
184
+ const rpc = createRpcClient({ baseUrl: "https://example.test", fetch: createTestFetch() });
185
+ await expect(rpc.query.getTransactionsForTick(1)).rejects.toBeInstanceOf(RpcError);
186
+ });
187
+
188
+ it("invokes onRequest/onResponse/onError hooks", async () => {
189
+ const requests: string[] = [];
190
+ const responses: number[] = [];
191
+ const errors: RpcError[] = [];
192
+ const rpc = createRpcClient({
193
+ baseUrl: "https://example.test",
194
+ fetch: createTestFetch(),
195
+ onRequest: (info) => requests.push(`${info.method} ${info.url}`),
196
+ onResponse: (info) => responses.push(info.status),
197
+ onError: (err) => errors.push(err),
198
+ });
199
+
200
+ await rpc.live.tickInfo();
201
+ await expect(rpc.query.getTransactionsForTick(1)).rejects.toBeInstanceOf(RpcError);
202
+
203
+ expect(requests.length).toBeGreaterThan(0);
204
+ expect(responses.length).toBeGreaterThan(0);
205
+ expect(errors.length).toBe(1);
206
+ expect(errors[0]?.code).toBe("rpc_request_failed");
207
+ });
208
+
209
+ it("retries failed requests based on retry config", async () => {
210
+ let attempts = 0;
211
+ const fetch: FetchLike = async (input, init) => {
212
+ const url = new URL(getUrl(input));
213
+ const method = getMethod(input, init);
214
+ if (method === "GET" && url.pathname === "/live/v1/tick-info") {
215
+ attempts += 1;
216
+ if (attempts < 2) {
217
+ return new Response("temporarily unavailable", { status: 503 });
218
+ }
219
+ return Response.json({
220
+ tickInfo: { tick: 123, duration: 1500, epoch: 12, initialTick: 100 },
221
+ });
222
+ }
223
+ return new Response("not found", { status: 404 });
224
+ };
225
+ const rpc = createRpcClient({
226
+ baseUrl: "https://example.test",
227
+ fetch,
228
+ retry: { maxRetries: 2, baseDelayMs: 1, jitterMs: 0 },
229
+ });
230
+ const tickInfo = await rpc.live.tickInfo();
231
+ expect(attempts).toBe(2);
232
+ expect(tickInfo.tick).toBe(123n);
233
+ });
234
+
235
+ it("paginates transactions for identity", async () => {
236
+ const rpc = createRpcClient({
237
+ baseUrl: "https://example.test",
238
+ fetch: createTestFetch(),
239
+ });
240
+ const res = await rpc.query.getTransactionsForIdentityAll({
241
+ identity: "ID",
242
+ pageSize: 2,
243
+ });
244
+ expect(res.length).toBe(3);
245
+ expect(res[0]?.hash).toBe("tx0");
246
+ expect(res[2]?.hash).toBe("tx2");
247
+ });
248
+ });
249
+
250
+ function mustGet<T>(arr: readonly T[], index: number): T {
251
+ const value = arr[index];
252
+ if (value === undefined) throw new Error(`Missing index: ${index}`);
253
+ return value;
254
+ }
255
+
256
+ function getUrl(input: Parameters<FetchLike>[0]): string {
257
+ if (typeof input === "string") return input;
258
+ if (input instanceof URL) return input.toString();
259
+ return input.url;
260
+ }
261
+
262
+ function getMethod(input: Parameters<FetchLike>[0], init: Parameters<FetchLike>[1]): string {
263
+ if (init?.method) return init.method;
264
+ if (input instanceof Request) return input.method;
265
+ return "GET";
266
+ }
267
+
268
+ function readJsonBody(
269
+ input: Parameters<FetchLike>[0],
270
+ init: Parameters<FetchLike>[1],
271
+ ): Record<string, unknown> {
272
+ if (input instanceof Request) {
273
+ throw new Error("Unexpected Request body");
274
+ }
275
+ const body = init?.body;
276
+ if (typeof body === "string") {
277
+ return JSON.parse(body) as Record<string, unknown>;
278
+ }
279
+ if (!body) return {};
280
+ throw new Error("Unsupported body type");
281
+ }
282
+
283
+ function baseTransactions() {
284
+ return [
285
+ {
286
+ hash: "tx0",
287
+ amount: "1",
288
+ source: "A",
289
+ destination: "B",
290
+ tickNumber: 1,
291
+ timestamp: "0",
292
+ inputType: 0,
293
+ inputSize: 0,
294
+ inputData: "",
295
+ signature: "",
296
+ },
297
+ {
298
+ hash: "tx1",
299
+ amount: "2",
300
+ source: "A",
301
+ destination: "B",
302
+ tickNumber: 2,
303
+ timestamp: "0",
304
+ inputType: 0,
305
+ inputSize: 0,
306
+ inputData: "",
307
+ signature: "",
308
+ },
309
+ {
310
+ hash: "tx2",
311
+ amount: "3",
312
+ source: "A",
313
+ destination: "B",
314
+ tickNumber: 3,
315
+ timestamp: "0",
316
+ inputType: 0,
317
+ inputSize: 0,
318
+ inputData: "",
319
+ signature: "",
320
+ },
321
+ ];
322
+ }