@qubic.ts/react 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 (44) hide show
  1. package/fixtures/next-boundary/client-node-leak.tsx +5 -0
  2. package/fixtures/next-boundary/client-ok.tsx +8 -0
  3. package/fixtures/next-boundary/server-ok.ts +5 -0
  4. package/package.json +47 -0
  5. package/scripts/verify-next-boundary.mjs +63 -0
  6. package/src/browser.ts +27 -0
  7. package/src/contract-types.ts +309 -0
  8. package/src/contracts-types.typecheck.ts +85 -0
  9. package/src/hooks/contract-hooks.test.tsx +312 -0
  10. package/src/hooks/read-hooks.test.tsx +339 -0
  11. package/src/hooks/send-hooks.test.tsx +247 -0
  12. package/src/hooks/use-balance.ts +27 -0
  13. package/src/hooks/use-contract-mutation.ts +50 -0
  14. package/src/hooks/use-contract-query.ts +71 -0
  15. package/src/hooks/use-contract.ts +231 -0
  16. package/src/hooks/use-last-processed-tick.ts +21 -0
  17. package/src/hooks/use-send-and-confirm.ts +19 -0
  18. package/src/hooks/use-send.ts +21 -0
  19. package/src/hooks/use-tick-info.ts +16 -0
  20. package/src/hooks/use-transactions.ts +97 -0
  21. package/src/index.ts +67 -0
  22. package/src/node.ts +21 -0
  23. package/src/providers/query-provider.test.tsx +25 -0
  24. package/src/providers/query-provider.tsx +13 -0
  25. package/src/providers/sdk-provider.test.tsx +54 -0
  26. package/src/providers/sdk-provider.tsx +83 -0
  27. package/src/providers/wallet-provider.test.tsx +82 -0
  28. package/src/providers/wallet-provider.tsx +209 -0
  29. package/src/query/keys.ts +9 -0
  30. package/src/typecheck-stubs/contracts.d.ts +77 -0
  31. package/src/typecheck-stubs/sdk.d.ts +254 -0
  32. package/src/vault/browser.ts +52 -0
  33. package/src/vault/node.ts +39 -0
  34. package/src/vault/runtime-boundary.test.ts +22 -0
  35. package/src/wallet/metamask-snap.test.ts +73 -0
  36. package/src/wallet/metamask-snap.ts +121 -0
  37. package/src/wallet/types.ts +55 -0
  38. package/src/wallet/utils.ts +14 -0
  39. package/src/wallet/vault.test.ts +98 -0
  40. package/src/wallet/vault.ts +70 -0
  41. package/src/wallet/walletconnect.test.ts +141 -0
  42. package/src/wallet/walletconnect.ts +218 -0
  43. package/tsconfig.json +14 -0
  44. package/tsconfig.typecheck.json +12 -0
@@ -0,0 +1,14 @@
1
+ export function bytesToBase64(bytes: Uint8Array): string {
2
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
3
+ let binary = "";
4
+ for (const b of bytes) binary += String.fromCharCode(b);
5
+ return btoa(binary);
6
+ }
7
+
8
+ export function base64ToBytes(base64: string): Uint8Array {
9
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(base64, "base64"));
10
+ const binary = atob(base64);
11
+ const bytes = new Uint8Array(binary.length);
12
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
13
+ return bytes;
14
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { SeedVault } from "@qubic.ts/sdk";
3
+ import { VaultConnector } from "./vault.js";
4
+
5
+ describe("VaultConnector", () => {
6
+ it("connects and lists accounts from vault", async () => {
7
+ const connector = new VaultConnector({
8
+ vault: createFakeVault(),
9
+ defaultRef: "main",
10
+ });
11
+
12
+ const result = await connector.connect();
13
+ expect(result.status).toBe("connected");
14
+ if (result.status !== "connected") throw new Error("Expected immediate connected result");
15
+ expect(result.accounts[0]?.address.length).toBe(60);
16
+ });
17
+
18
+ it("signs unsigned transactions with a vault seed", async () => {
19
+ const connector = new VaultConnector({
20
+ vault: createFakeVault(),
21
+ defaultRef: "main",
22
+ });
23
+
24
+ const unsignedTxBytes = new Uint8Array(80);
25
+ unsignedTxBytes.fill(1, 0, 32);
26
+ unsignedTxBytes.fill(2, 32, 64);
27
+ const view = new DataView(unsignedTxBytes.buffer);
28
+ view.setBigInt64(64, 1n, true);
29
+ view.setUint32(72, 1, true);
30
+ view.setUint16(76, 0, true);
31
+ view.setUint16(78, 0, true);
32
+
33
+ const signed = await connector.signTransaction({
34
+ kind: "vault",
35
+ unsignedTxBytes,
36
+ });
37
+
38
+ expect(signed.signedTxBytes).toBeInstanceOf(Uint8Array);
39
+ expect(signed.signedTxBytes?.length).toBe(144);
40
+ expect(typeof signed.signedTxBase64).toBe("string");
41
+ expect(signed.signedTxBase64.length > 0).toBe(true);
42
+ });
43
+ });
44
+
45
+ function createFakeVault(): SeedVault {
46
+ const identity = "A".repeat(60);
47
+ const seed = "a".repeat(55);
48
+
49
+ return {
50
+ path: "/tmp/test-vault",
51
+ list() {
52
+ return [
53
+ {
54
+ name: "main",
55
+ identity,
56
+ seedIndex: 0,
57
+ createdAt: "2025-01-01T00:00:00.000Z",
58
+ updatedAt: "2025-01-01T00:00:00.000Z",
59
+ },
60
+ ];
61
+ },
62
+ getEntry() {
63
+ throw new Error("not implemented");
64
+ },
65
+ getIdentity() {
66
+ return identity;
67
+ },
68
+ signer() {
69
+ return { fromVault: "main" };
70
+ },
71
+ async getSeed() {
72
+ return seed;
73
+ },
74
+ async addSeed() {
75
+ throw new Error("not implemented");
76
+ },
77
+ async remove() {
78
+ throw new Error("not implemented");
79
+ },
80
+ async rotatePassphrase() {
81
+ throw new Error("not implemented");
82
+ },
83
+ exportEncrypted() {
84
+ throw new Error("not implemented");
85
+ },
86
+ exportJson() {
87
+ throw new Error("not implemented");
88
+ },
89
+ async importEncrypted() {
90
+ throw new Error("not implemented");
91
+ },
92
+ async getSeedSource() {
93
+ return { fromSeed: seed };
94
+ },
95
+ async save() {},
96
+ async close() {},
97
+ } as unknown as SeedVault;
98
+ }
@@ -0,0 +1,70 @@
1
+ import { privateKeyFromSeed, signTransaction } from "@qubic.ts/core";
2
+ import type { SeedVault } from "@qubic.ts/sdk";
3
+ import type {
4
+ WalletAccount,
5
+ WalletConnector,
6
+ WalletConnectResult,
7
+ WalletSignTransactionRequest,
8
+ WalletSignTransactionResult,
9
+ } from "./types.js";
10
+ import { bytesToBase64 } from "./utils.js";
11
+
12
+ export type VaultConnectorConfig = Readonly<{
13
+ vault: SeedVault;
14
+ defaultRef?: string;
15
+ }>;
16
+
17
+ export class VaultConnector implements WalletConnector {
18
+ readonly type = "vault" as const;
19
+ private readonly vault: SeedVault;
20
+ private readonly defaultRef: string | undefined;
21
+
22
+ constructor(config: VaultConnectorConfig) {
23
+ this.vault = config.vault;
24
+ this.defaultRef = config.defaultRef;
25
+ }
26
+
27
+ isAvailable(): boolean {
28
+ return true;
29
+ }
30
+
31
+ async connect(): Promise<WalletConnectResult> {
32
+ const accounts = this.listAccounts();
33
+ return { status: "connected", accounts };
34
+ }
35
+
36
+ async disconnect(): Promise<void> {
37
+ return;
38
+ }
39
+
40
+ async requestAccounts(): Promise<readonly WalletAccount[]> {
41
+ return this.listAccounts();
42
+ }
43
+
44
+ async signTransaction(input: WalletSignTransactionRequest): Promise<WalletSignTransactionResult> {
45
+ if (input.kind !== "vault") {
46
+ throw new Error("Vault connector expects kind: 'vault'");
47
+ }
48
+ const ref = input.vaultRef ?? this.defaultRef;
49
+ if (!ref) {
50
+ throw new Error("vaultRef is required for vault signing");
51
+ }
52
+ const seed = await this.vault.getSeed(ref);
53
+ const secretKey32 = await privateKeyFromSeed(seed);
54
+ const signature64 = await signTransaction(input.unsignedTxBytes, secretKey32);
55
+ const signed = new Uint8Array(input.unsignedTxBytes.length + signature64.length);
56
+ signed.set(input.unsignedTxBytes, 0);
57
+ signed.set(signature64, input.unsignedTxBytes.length);
58
+ return {
59
+ signedTxBase64: bytesToBase64(signed),
60
+ signedTxBytes: signed,
61
+ };
62
+ }
63
+
64
+ private listAccounts(): readonly WalletAccount[] {
65
+ return this.vault.list().map((entry: { identity: string; name: string }) => ({
66
+ address: entry.identity,
67
+ alias: entry.name,
68
+ }));
69
+ }
70
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { WalletConnectClient } from "./walletconnect.js";
3
+ import { WalletConnectConnector } from "./walletconnect.js";
4
+
5
+ function createMemoryStorage(): Storage {
6
+ const store = new Map<string, string>();
7
+ return {
8
+ getItem: (key) => store.get(key) ?? null,
9
+ setItem: (key, value) => {
10
+ store.set(key, value);
11
+ },
12
+ removeItem: (key) => {
13
+ store.delete(key);
14
+ },
15
+ clear: () => store.clear(),
16
+ key: (index) => Array.from(store.keys())[index] ?? null,
17
+ get length() {
18
+ return store.size;
19
+ },
20
+ };
21
+ }
22
+
23
+ function createFakeClient(): WalletConnectClient {
24
+ const sessions = new Map<string, { topic: string }>();
25
+ return {
26
+ async connect() {
27
+ return {
28
+ uri: "wc://example",
29
+ approval: async () => {
30
+ const session = { topic: "topic-1" };
31
+ sessions.set(session.topic, session);
32
+ return session as never;
33
+ },
34
+ };
35
+ },
36
+ async request({ request }: { request: { method: string } }) {
37
+ if (request.method === "qubic_requestAccounts") {
38
+ return [{ address: "QUBICADDRESS1", alias: "wallet" }];
39
+ }
40
+ if (request.method === "qubic_signTransaction") {
41
+ return { signedTransaction: Buffer.from([1, 2, 3]).toString("base64") };
42
+ }
43
+ return null;
44
+ },
45
+ async disconnect() {
46
+ sessions.clear();
47
+ },
48
+ on() {
49
+ return this;
50
+ },
51
+ session: {
52
+ get: (topic: string) => sessions.get(topic),
53
+ } as WalletConnectClient["session"],
54
+ } as unknown as WalletConnectClient;
55
+ }
56
+
57
+ describe("WalletConnectConnector", () => {
58
+ it("connects and approves pending session", async () => {
59
+ const storage = createMemoryStorage();
60
+ const connector = new WalletConnectConnector({
61
+ projectId: "project",
62
+ metadata: {
63
+ name: "Test",
64
+ description: "Test",
65
+ url: "https://example.com",
66
+ icons: ["https://example.com/icon.png"],
67
+ },
68
+ storage,
69
+ signClientFactory: async () => createFakeClient(),
70
+ });
71
+
72
+ const result = await connector.connect();
73
+ expect(result.status).toBe("pending");
74
+ if (result.status !== "pending") throw new Error("Expected pending WalletConnect session");
75
+ const accounts = await result.approve();
76
+ expect(accounts[0]?.address).toBe("QUBICADDRESS1");
77
+ expect(storage.getItem("wcSessionTopic")).toBe("topic-1");
78
+ });
79
+
80
+ it("restores a persisted session", async () => {
81
+ const storage = createMemoryStorage();
82
+ const client = createFakeClient();
83
+ const connector = new WalletConnectConnector({
84
+ projectId: "project",
85
+ metadata: {
86
+ name: "Test",
87
+ description: "Test",
88
+ url: "https://example.com",
89
+ icons: ["https://example.com/icon.png"],
90
+ },
91
+ storage,
92
+ signClientFactory: async () => client,
93
+ });
94
+ const pending = await connector.connect();
95
+ if (pending.status !== "pending") throw new Error("Expected pending WalletConnect session");
96
+ await pending.approve();
97
+
98
+ const restoredConnector = new WalletConnectConnector({
99
+ projectId: "project",
100
+ metadata: {
101
+ name: "Test",
102
+ description: "Test",
103
+ url: "https://example.com",
104
+ icons: ["https://example.com/icon.png"],
105
+ },
106
+ storage,
107
+ signClientFactory: async () => client,
108
+ });
109
+ const accounts = await restoredConnector.restore();
110
+ expect(accounts?.[0]?.address).toBe("QUBICADDRESS1");
111
+ });
112
+
113
+ it("signs transactions through an approved session", async () => {
114
+ const storage = createMemoryStorage();
115
+ const connector = new WalletConnectConnector({
116
+ projectId: "project",
117
+ metadata: {
118
+ name: "Test",
119
+ description: "Test",
120
+ url: "https://example.com",
121
+ icons: ["https://example.com/icon.png"],
122
+ },
123
+ storage,
124
+ signClientFactory: async () => createFakeClient(),
125
+ });
126
+
127
+ const pending = await connector.connect();
128
+ if (pending.status !== "pending") throw new Error("Expected pending WalletConnect session");
129
+ await pending.approve();
130
+
131
+ const result = await connector.signTransaction({
132
+ kind: "walletconnect",
133
+ from: "QUBICADDRESS1",
134
+ to: "QUBICADDRESS2",
135
+ amount: 1,
136
+ });
137
+
138
+ expect(result.signedTxBase64).toBe(Buffer.from([1, 2, 3]).toString("base64"));
139
+ expect(result.signedTxBytes?.length).toBe(3);
140
+ });
141
+ });
@@ -0,0 +1,218 @@
1
+ import type { SessionTypes } from "@walletconnect/types";
2
+ import type {
3
+ WalletAccount,
4
+ WalletConnector,
5
+ WalletConnectResult,
6
+ WalletSignTransactionRequest,
7
+ WalletSignTransactionResult,
8
+ } from "./types.js";
9
+ import { base64ToBytes } from "./utils.js";
10
+
11
+ export type WalletConnectClient = Readonly<{
12
+ connect(input: unknown): Promise<Readonly<{ uri?: string; approval: () => Promise<SessionTypes.Struct> }>>;
13
+ request(input: unknown): Promise<unknown>;
14
+ disconnect(input: unknown): Promise<void>;
15
+ on(event: string, listener: () => void): unknown;
16
+ session: Readonly<{
17
+ get(topic: string): SessionTypes.Struct | undefined;
18
+ }>;
19
+ }>;
20
+
21
+ export type WalletConnectConfig = Readonly<{
22
+ projectId: string;
23
+ metadata: Readonly<{
24
+ name: string;
25
+ description: string;
26
+ url: string;
27
+ icons: readonly string[];
28
+ }>;
29
+ chainId?: string;
30
+ methods?: readonly string[];
31
+ events?: readonly string[];
32
+ storageKey?: string;
33
+ storage?: Storage;
34
+ signClientFactory?: () => Promise<WalletConnectClient>;
35
+ }>;
36
+
37
+ export class WalletConnectConnector implements WalletConnector {
38
+ readonly type = "walletconnect" as const;
39
+ private readonly config: WalletConnectConfig;
40
+ private client: WalletConnectClient | null = null;
41
+ private sessionTopic = "";
42
+
43
+ constructor(config: WalletConnectConfig) {
44
+ this.config = config;
45
+ }
46
+
47
+ isAvailable(): boolean {
48
+ return true;
49
+ }
50
+
51
+ async connect(): Promise<WalletConnectResult> {
52
+ const client = await this.getClient();
53
+ const { uri, approval } = await client.connect({
54
+ requiredNamespaces: {
55
+ qubic: {
56
+ chains: [this.config.chainId ?? "qubic:mainnet"],
57
+ methods: [
58
+ ...(this.config.methods ?? [
59
+ "qubic_requestAccounts",
60
+ "qubic_sendQubic",
61
+ "qubic_signTransaction",
62
+ "qubic_sign",
63
+ ]),
64
+ ],
65
+ events: [...(this.config.events ?? ["accountsChanged", "amountChanged"])],
66
+ },
67
+ },
68
+ });
69
+
70
+ const approve = async () => {
71
+ const session = await approval();
72
+ await this.storeSession(session);
73
+ return this.requestAccounts();
74
+ };
75
+
76
+ return {
77
+ status: "pending",
78
+ uri: uri ?? "",
79
+ approve,
80
+ };
81
+ }
82
+
83
+ async disconnect(): Promise<void> {
84
+ if (!this.client || !this.sessionTopic) return;
85
+ await this.client.disconnect({
86
+ topic: this.sessionTopic,
87
+ reason: { code: 6000, message: "User disconnected" },
88
+ });
89
+ this.clearSession();
90
+ }
91
+
92
+ async requestAccounts(): Promise<readonly WalletAccount[]> {
93
+ const client = await this.getClient();
94
+ if (!this.sessionTopic) {
95
+ const restored = await this.restore();
96
+ if (!restored) throw new Error("WalletConnect session not connected");
97
+ }
98
+ const result = (await client.request({
99
+ topic: this.sessionTopic,
100
+ chainId: this.config.chainId ?? "qubic:mainnet",
101
+ request: {
102
+ method: "qubic_requestAccounts",
103
+ params: { nonce: Date.now().toString() },
104
+ },
105
+ })) as Array<{ address: string; alias?: string }>;
106
+ return result.map((entry) =>
107
+ entry.alias === undefined ? { address: entry.address } : { address: entry.address, alias: entry.alias },
108
+ );
109
+ }
110
+
111
+ async signTransaction(input: WalletSignTransactionRequest): Promise<WalletSignTransactionResult> {
112
+ if (input.kind !== "walletconnect") {
113
+ throw new Error("WalletConnect expects kind: 'walletconnect'");
114
+ }
115
+ const client = await this.getClient();
116
+ if (!this.sessionTopic) {
117
+ const restored = await this.restore();
118
+ if (!restored) throw new Error("WalletConnect session not connected");
119
+ }
120
+ const signed = (await client.request({
121
+ topic: this.sessionTopic,
122
+ chainId: this.config.chainId ?? "qubic:mainnet",
123
+ request: {
124
+ method: "qubic_signTransaction",
125
+ params: {
126
+ from: input.from,
127
+ to: input.to,
128
+ amount: input.amount,
129
+ tick: input.tick,
130
+ inputType: input.inputType,
131
+ payload: input.payloadBase64 ?? null,
132
+ nonce: input.nonce ?? Date.now().toString(),
133
+ },
134
+ },
135
+ })) as { signedTransaction: string };
136
+ return {
137
+ signedTxBase64: signed.signedTransaction,
138
+ signedTxBytes: base64ToBytes(signed.signedTransaction),
139
+ };
140
+ }
141
+
142
+ async restore(): Promise<readonly WalletAccount[] | null> {
143
+ const client = await this.getClient();
144
+ const storage = this.config.storage ?? getDefaultStorage();
145
+ if (!storage) return null;
146
+ const storedTopic = storage.getItem(this.config.storageKey ?? "wcSessionTopic");
147
+ if (!storedTopic) return null;
148
+ const session = client.session.get(storedTopic);
149
+ if (!session) {
150
+ storage.removeItem(this.config.storageKey ?? "wcSessionTopic");
151
+ return null;
152
+ }
153
+ this.sessionTopic = storedTopic;
154
+ return this.requestAccounts();
155
+ }
156
+
157
+ private async getClient(): Promise<WalletConnectClient> {
158
+ if (this.client) return this.client;
159
+ const client = this.config.signClientFactory
160
+ ? await this.config.signClientFactory()
161
+ : await createDefaultClient({
162
+ projectId: this.config.projectId,
163
+ metadata: {
164
+ ...this.config.metadata,
165
+ icons: [...this.config.metadata.icons],
166
+ },
167
+ });
168
+ client.on("session_delete", () => this.clearSession());
169
+ client.on("session_expire", () => this.clearSession());
170
+ this.client = client;
171
+ return client;
172
+ }
173
+
174
+ private async storeSession(session: SessionTypes.Struct): Promise<void> {
175
+ this.sessionTopic = session.topic;
176
+ const storage = this.config.storage ?? getDefaultStorage();
177
+ if (storage) {
178
+ storage.setItem(this.config.storageKey ?? "wcSessionTopic", session.topic);
179
+ }
180
+ }
181
+
182
+ private clearSession(): void {
183
+ this.sessionTopic = "";
184
+ const storage = this.config.storage ?? getDefaultStorage();
185
+ if (storage) {
186
+ storage.removeItem(this.config.storageKey ?? "wcSessionTopic");
187
+ }
188
+ }
189
+ }
190
+
191
+ function getDefaultStorage(): Storage | undefined {
192
+ const anyGlobal = globalThis as typeof globalThis & { localStorage?: Storage };
193
+ return anyGlobal.localStorage;
194
+ }
195
+
196
+ async function createDefaultClient(input: {
197
+ projectId: string;
198
+ metadata: {
199
+ name: string;
200
+ description: string;
201
+ url: string;
202
+ icons: readonly string[];
203
+ };
204
+ }): Promise<WalletConnectClient> {
205
+ const signClient = await import("@walletconnect/sign-client");
206
+ const init =
207
+ (signClient as { SignClient?: { init?: (value: unknown) => Promise<WalletConnectClient> } })
208
+ .SignClient?.init ??
209
+ (signClient as { default?: { init?: (value: unknown) => Promise<WalletConnectClient> } })
210
+ .default?.init ??
211
+ (signClient as { init?: (value: unknown) => Promise<WalletConnectClient> }).init;
212
+
213
+ if (!init) {
214
+ throw new Error("@walletconnect/sign-client does not expose an init function");
215
+ }
216
+
217
+ return init(input);
218
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["ES2022", "DOM"],
5
+ "module": "Preserve",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "allowImportingTsExtensions": true,
9
+ "noEmit": true,
10
+ "types": ["bun", "node", "react", "react-dom"]
11
+ },
12
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
13
+ "exclude": ["dist/**", "node_modules/**"]
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@qubic.ts/contracts": ["./src/typecheck-stubs/contracts.d.ts"],
7
+ "@qubic.ts/sdk": ["./src/typecheck-stubs/sdk.d.ts"],
8
+ "@qubic.ts/sdk/browser": ["./src/typecheck-stubs/sdk.d.ts"],
9
+ "@qubic.ts/sdk/node": ["./src/typecheck-stubs/sdk.d.ts"]
10
+ }
11
+ }
12
+ }