@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.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@qubic.ts/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "import": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ },
11
+ "./browser": {
12
+ "types": "./src/browser.ts",
13
+ "import": "./src/browser.ts",
14
+ "default": "./src/browser.ts"
15
+ },
16
+ "./node": {
17
+ "types": "./src/node.ts",
18
+ "import": "./src/node.ts",
19
+ "default": "./src/node.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "main": "./src/index.ts",
24
+ "types": "./src/index.ts",
25
+ "bin": {
26
+ "qubic-ts-vault": "./src/vault-cli.ts"
27
+ },
28
+ "dependencies": {
29
+ "@qubic.ts/contracts": "^0.1.0",
30
+ "@qubic.ts/core": "^0.1.0"
31
+ },
32
+ "scripts": {
33
+ "build": "echo \"qubic-ts-sdk build pending\"",
34
+ "check": "bun test ./src/**/*.test.ts ./src/*.test.ts",
35
+ "test": "bun test ./src/**/*.test.ts ./src/*.test.ts",
36
+ "vault:cli": "bun ./src/vault-cli.ts"
37
+ }
38
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ AssetRecordType,
4
+ encodeRequestAssetsByFilter,
5
+ encodeRequestAssetsByUniverseIndex,
6
+ publicKeyFromIdentity,
7
+ writeI64LE,
8
+ } from "@qubic.ts/core";
9
+ import { createAssetsHelpers } from "./assets.js";
10
+
11
+ const zeroId = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFXIB";
12
+ const RESPOND_ASSETS_PAYLOAD_SIZE = 56;
13
+ const RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE = 824;
14
+ const RequestAssetsType = {
15
+ ISSUANCE: 0,
16
+ OWNERSHIP: 1,
17
+ POSSESSION: 2,
18
+ BY_UNIVERSE_INDEX: 3,
19
+ } as const;
20
+
21
+ describe("assets helpers", () => {
22
+ it("listIssued encodes filter and decodes response", async () => {
23
+ const payload = new Uint8Array(RESPOND_ASSETS_PAYLOAD_SIZE);
24
+ payload[32] = AssetRecordType.ISSUANCE;
25
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
26
+ view.setUint32(48, 7, true);
27
+ view.setUint32(52, 9, true);
28
+
29
+ const expectedRequest = encodeRequestAssetsByFilter({
30
+ requestType: RequestAssetsType.ISSUANCE,
31
+ issuerPublicKey32: publicKeyFromIdentity(zeroId),
32
+ assetNameU64LE: assetNameToU64LE("ASSET"),
33
+ });
34
+
35
+ const assets = createAssetsHelpers({
36
+ requestAssets: async (request) => {
37
+ expect(bytesEqual(request.slice(8), expectedRequest.slice(8))).toBe(true);
38
+ return [payload];
39
+ },
40
+ });
41
+
42
+ const res = await assets.listIssued({
43
+ issuerIdentity: zeroId,
44
+ assetName: "ASSET",
45
+ });
46
+
47
+ expect(res.length).toBe(1);
48
+ expect(res[0]?.tick).toBe(7);
49
+ expect(res[0]?.universeIndex).toBe(9);
50
+ expect(res[0]?.asset.type).toBe(AssetRecordType.ISSUANCE);
51
+ });
52
+
53
+ it("listOwned decodes siblings response", async () => {
54
+ const payload = new Uint8Array(RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE);
55
+ payload[32] = AssetRecordType.OWNERSHIP;
56
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
57
+ view.setUint16(34, 12, true);
58
+ view.setUint32(36, 99, true);
59
+ writeI64LE(123n, payload, 40);
60
+
61
+ const assets = createAssetsHelpers({
62
+ requestAssets: async () => [payload],
63
+ });
64
+
65
+ const res = await assets.listOwned({
66
+ ownerIdentity: zeroId,
67
+ getSiblings: true,
68
+ });
69
+
70
+ expect(res.length).toBe(1);
71
+ const first = res[0];
72
+ expect(first).toBeDefined();
73
+ if (!first) throw new Error("Missing assets response");
74
+ expect("siblings" in first).toBe(true);
75
+ const record = first.asset;
76
+ expect(record.type).toBe(AssetRecordType.OWNERSHIP);
77
+ });
78
+
79
+ it("listByUniverseIndex encodes request", async () => {
80
+ const expectedRequest = encodeRequestAssetsByUniverseIndex({
81
+ universeIndex: 123,
82
+ getSiblings: false,
83
+ });
84
+
85
+ const assets = createAssetsHelpers({
86
+ requestAssets: async (request) => {
87
+ expect(bytesEqual(request.slice(8), expectedRequest.slice(8))).toBe(true);
88
+ return [new Uint8Array(RESPOND_ASSETS_PAYLOAD_SIZE)];
89
+ },
90
+ });
91
+
92
+ await assets.listByUniverseIndex({ universeIndex: 123 });
93
+ });
94
+ });
95
+
96
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
97
+ if (a.byteLength !== b.byteLength) return false;
98
+ for (let i = 0; i < a.byteLength; i++) {
99
+ if (a[i] !== b[i]) return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ function assetNameToU64LE(name: string): bigint {
105
+ if (name.length > 8) throw new RangeError("assetName must be <= 8 characters");
106
+ const bytes = new Uint8Array(8);
107
+ for (let i = 0; i < name.length; i++) {
108
+ const c = name.charCodeAt(i);
109
+ if (c < 32 || c > 126) throw new Error("assetName must be ASCII");
110
+ bytes[i] = c;
111
+ }
112
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
113
+ return view.getBigUint64(0, true);
114
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,175 @@
1
+ import {
2
+ type AssetRecord,
3
+ AssetRecordType,
4
+ decodeRespondAssets,
5
+ decodeRespondAssetsWithSiblings,
6
+ encodeRequestAssetsByFilter,
7
+ encodeRequestAssetsByUniverseIndex,
8
+ publicKeyFromIdentity,
9
+ type RespondAssets,
10
+ type RespondAssetsWithSiblings,
11
+ } from "@qubic.ts/core";
12
+
13
+ const RESPOND_ASSETS_PAYLOAD_SIZE = 56;
14
+ const RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE = 824;
15
+ const RequestAssetsType = {
16
+ ISSUANCE: 0,
17
+ OWNERSHIP: 1,
18
+ POSSESSION: 2,
19
+ BY_UNIVERSE_INDEX: 3,
20
+ } as const;
21
+
22
+ export type AssetsRequestFn = (
23
+ request: Uint8Array,
24
+ signal?: AbortSignal,
25
+ ) => Promise<readonly Uint8Array[]>;
26
+
27
+ export type AssetsHelpersConfig = Readonly<{
28
+ requestAssets: AssetsRequestFn;
29
+ }>;
30
+
31
+ export type AssetsQueryInput = Readonly<{
32
+ getSiblings?: boolean;
33
+ signal?: AbortSignal;
34
+ }>;
35
+
36
+ export type ListIssuedInput = AssetsQueryInput &
37
+ Readonly<{
38
+ issuerIdentity?: string;
39
+ assetName?: string;
40
+ }>;
41
+
42
+ export type ListOwnedInput = AssetsQueryInput &
43
+ Readonly<{
44
+ ownerIdentity?: string;
45
+ issuerIdentity?: string;
46
+ assetName?: string;
47
+ managingContractIndex?: number;
48
+ }>;
49
+
50
+ export type ListPossessedInput = AssetsQueryInput &
51
+ Readonly<{
52
+ possessorIdentity?: string;
53
+ issuerIdentity?: string;
54
+ assetName?: string;
55
+ managingContractIndex?: number;
56
+ }>;
57
+
58
+ export type AssetsHelpers = Readonly<{
59
+ listIssued(
60
+ input: ListIssuedInput,
61
+ ): Promise<readonly (RespondAssets | RespondAssetsWithSiblings)[]>;
62
+ listOwned(input: ListOwnedInput): Promise<readonly (RespondAssets | RespondAssetsWithSiblings)[]>;
63
+ listPossessed(
64
+ input: ListPossessedInput,
65
+ ): Promise<readonly (RespondAssets | RespondAssetsWithSiblings)[]>;
66
+ listByUniverseIndex(input: {
67
+ universeIndex: number;
68
+ getSiblings?: boolean;
69
+ signal?: AbortSignal;
70
+ }): Promise<readonly (RespondAssets | RespondAssetsWithSiblings)[]>;
71
+ }>;
72
+
73
+ export function createAssetsHelpers(config: AssetsHelpersConfig): AssetsHelpers {
74
+ const requestAssets = config.requestAssets;
75
+
76
+ const decodeResponses = (payloads: readonly Uint8Array[]) =>
77
+ payloads.map((payload) => {
78
+ if (payload.byteLength === RESPOND_ASSETS_WITH_SIBLINGS_PAYLOAD_SIZE) {
79
+ return decodeRespondAssetsWithSiblings(payload);
80
+ }
81
+ if (payload.byteLength === RESPOND_ASSETS_PAYLOAD_SIZE) {
82
+ return decodeRespondAssets(payload);
83
+ }
84
+ throw new RangeError("Unexpected RespondAssets payload length");
85
+ });
86
+
87
+ return {
88
+ async listIssued(input: ListIssuedInput) {
89
+ const request = encodeRequestAssetsByFilter({
90
+ requestType: RequestAssetsType.ISSUANCE,
91
+ getSiblings: input.getSiblings,
92
+ issuerPublicKey32: input.issuerIdentity
93
+ ? publicKeyFromIdentity(input.issuerIdentity)
94
+ : undefined,
95
+ assetNameU64LE: input.assetName ? assetNameToU64LE(input.assetName) : undefined,
96
+ });
97
+ return decodeResponses(await requestAssets(request, input.signal));
98
+ },
99
+
100
+ async listOwned(input: ListOwnedInput) {
101
+ const request = encodeRequestAssetsByFilter({
102
+ requestType: RequestAssetsType.OWNERSHIP,
103
+ getSiblings: input.getSiblings,
104
+ issuerPublicKey32: input.issuerIdentity
105
+ ? publicKeyFromIdentity(input.issuerIdentity)
106
+ : undefined,
107
+ assetNameU64LE: input.assetName ? assetNameToU64LE(input.assetName) : undefined,
108
+ ownerPublicKey32: input.ownerIdentity
109
+ ? publicKeyFromIdentity(input.ownerIdentity)
110
+ : undefined,
111
+ ownershipManagingContractIndex: input.managingContractIndex,
112
+ });
113
+ return decodeResponses(await requestAssets(request, input.signal));
114
+ },
115
+
116
+ async listPossessed(input: ListPossessedInput) {
117
+ const request = encodeRequestAssetsByFilter({
118
+ requestType: RequestAssetsType.POSSESSION,
119
+ getSiblings: input.getSiblings,
120
+ issuerPublicKey32: input.issuerIdentity
121
+ ? publicKeyFromIdentity(input.issuerIdentity)
122
+ : undefined,
123
+ assetNameU64LE: input.assetName ? assetNameToU64LE(input.assetName) : undefined,
124
+ possessorPublicKey32: input.possessorIdentity
125
+ ? publicKeyFromIdentity(input.possessorIdentity)
126
+ : undefined,
127
+ possessionManagingContractIndex: input.managingContractIndex,
128
+ });
129
+ return decodeResponses(await requestAssets(request, input.signal));
130
+ },
131
+
132
+ async listByUniverseIndex(input) {
133
+ const request = encodeRequestAssetsByUniverseIndex({
134
+ universeIndex: input.universeIndex,
135
+ getSiblings: input.getSiblings,
136
+ });
137
+ return decodeResponses(await requestAssets(request, input.signal));
138
+ },
139
+ };
140
+ }
141
+
142
+ export function isIssuanceAsset(
143
+ record: AssetRecord,
144
+ ): record is AssetRecord & { type: typeof AssetRecordType.ISSUANCE } {
145
+ return record.type === AssetRecordType.ISSUANCE;
146
+ }
147
+
148
+ export function isOwnershipAsset(
149
+ record: AssetRecord,
150
+ ): record is AssetRecord & { type: typeof AssetRecordType.OWNERSHIP } {
151
+ return record.type === AssetRecordType.OWNERSHIP;
152
+ }
153
+
154
+ export function isPossessionAsset(
155
+ record: AssetRecord,
156
+ ): record is AssetRecord & { type: typeof AssetRecordType.POSSESSION } {
157
+ return record.type === AssetRecordType.POSSESSION;
158
+ }
159
+
160
+ function assetNameToU64LE(name: string): bigint {
161
+ if (typeof name !== "string") {
162
+ throw new TypeError("assetName must be a string");
163
+ }
164
+ if (name.length > 8) {
165
+ throw new RangeError("assetName must be <= 8 characters");
166
+ }
167
+ const bytes = new Uint8Array(8);
168
+ for (let i = 0; i < name.length; i++) {
169
+ const c = name.charCodeAt(i);
170
+ if (c < 32 || c > 126) throw new Error("assetName must be ASCII");
171
+ bytes[i] = c;
172
+ }
173
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
174
+ return view.getBigUint64(0, true);
175
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { FetchLike } from "../http.js";
3
+ import { createBobClient } from "./client.js";
4
+
5
+ describe("bob client", () => {
6
+ it("querySmartContract sends hex payload and parses response", async () => {
7
+ let body: Record<string, unknown> | undefined;
8
+ const requests: string[] = [];
9
+ const responses: number[] = [];
10
+ const fetch: FetchLike = async (...args) => {
11
+ const url = new URL(getUrl(args[0]));
12
+ const method = getMethod(args[0], args[1]);
13
+ if (method === "POST" && url.pathname === "/querySmartContract") {
14
+ body = readJsonBody(args[0], args[1]);
15
+ return Response.json({ nonce: body?.nonce, data: "abcd" });
16
+ }
17
+ return new Response("not found", { status: 404 });
18
+ };
19
+
20
+ const bob = createBobClient({
21
+ baseUrl: "http://example.test",
22
+ fetch,
23
+ onRequest: (info) => requests.push(info.url),
24
+ onResponse: (info) => responses.push(info.status),
25
+ });
26
+ const res = await bob.querySmartContract({
27
+ scIndex: 1,
28
+ funcNumber: 2,
29
+ dataBytes: new Uint8Array([0xab, 0xcd]),
30
+ });
31
+
32
+ expect(res.pending).toBe(false);
33
+ expect(res.dataHex).toBe("abcd");
34
+ expect(body?.data).toBe("abcd");
35
+ expect(requests.length).toBeGreaterThan(0);
36
+ expect(responses.length).toBeGreaterThan(0);
37
+ });
38
+
39
+ it("querySmartContract handles pending responses", async () => {
40
+ const fetch: FetchLike = async (...args) => {
41
+ const url = new URL(getUrl(args[0]));
42
+ const method = getMethod(args[0], args[1]);
43
+ if (method === "POST" && url.pathname === "/querySmartContract") {
44
+ return new Response(JSON.stringify({ error: "pending", message: "try later" }), {
45
+ status: 202,
46
+ headers: { "content-type": "application/json" },
47
+ });
48
+ }
49
+ return new Response("not found", { status: 404 });
50
+ };
51
+
52
+ const bob = createBobClient({ baseUrl: "http://example.test", fetch });
53
+ const res = await bob.querySmartContract({ scIndex: 1, funcNumber: 2, dataHex: "00" });
54
+ expect(res.pending).toBe(true);
55
+ expect(res.message).toBe("try later");
56
+ });
57
+
58
+ it("retries failed requests based on retry config", async () => {
59
+ let attempts = 0;
60
+ const fetch: FetchLike = async (...args) => {
61
+ const url = new URL(getUrl(args[0]));
62
+ const method = getMethod(args[0], args[1]);
63
+ if (method === "GET" && url.pathname === "/status") {
64
+ attempts += 1;
65
+ if (attempts < 2) {
66
+ return new Response("temporary", { status: 503 });
67
+ }
68
+ return Response.json({ ok: true });
69
+ }
70
+ return new Response("not found", { status: 404 });
71
+ };
72
+
73
+ const bob = createBobClient({
74
+ baseUrl: "http://example.test",
75
+ fetch,
76
+ retry: { maxRetries: 2, baseDelayMs: 1, jitterMs: 0 },
77
+ });
78
+ const res = await bob.status();
79
+ expect(attempts).toBe(2);
80
+ expect(res).toEqual({ ok: true });
81
+ });
82
+ });
83
+
84
+ function getUrl(input: Parameters<typeof fetch>[0]): string {
85
+ if (typeof input === "string") return input;
86
+ if (input instanceof URL) return input.toString();
87
+ return input.url;
88
+ }
89
+
90
+ function getMethod(input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]): string {
91
+ if (init?.method) return init.method;
92
+ if (input instanceof Request) return input.method;
93
+ return "GET";
94
+ }
95
+
96
+ function readJsonBody(
97
+ input: Parameters<typeof fetch>[0],
98
+ init: Parameters<typeof fetch>[1],
99
+ ): Record<string, unknown> {
100
+ if (input instanceof Request) {
101
+ throw new Error("Unexpected Request body");
102
+ }
103
+ const body = init?.body;
104
+ if (typeof body === "string") {
105
+ return JSON.parse(body) as Record<string, unknown>;
106
+ }
107
+ if (!body) return {};
108
+ throw new Error("Unsupported body type");
109
+ }