@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,312 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { ContractDefinition } from "@qubic.ts/contracts";
3
+ import type { QueryRawResult, SendTransactionResult } from "@qubic.ts/sdk";
4
+ import { createSdk } from "@qubic.ts/sdk";
5
+ import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
6
+ import { QueryClient } from "@tanstack/react-query";
7
+ import React from "react";
8
+ import TestRenderer, { act } from "react-test-renderer";
9
+ import { QubicQueryProvider } from "../providers/query-provider.js";
10
+ import { SdkProvider } from "../providers/sdk-provider.js";
11
+ import { useContractMutation } from "./use-contract-mutation.js";
12
+ import { useContractQuery } from "./use-contract-query.js";
13
+
14
+ const demoContractsRegistry = [
15
+ {
16
+ name: "DEMO",
17
+ contractIndex: 7,
18
+ address: "A".repeat(60),
19
+ entries: [
20
+ {
21
+ kind: "function",
22
+ name: "Read",
23
+ inputType: 1,
24
+ inputTypeName: "uint64",
25
+ outputTypeName: "uint64",
26
+ outputSize: 8,
27
+ },
28
+ {
29
+ kind: "procedure",
30
+ name: "Write",
31
+ inputType: 2,
32
+ inputTypeName: "uint64",
33
+ },
34
+ ],
35
+ },
36
+ ] as const satisfies readonly ContractDefinition[];
37
+
38
+ describe("contract hooks", () => {
39
+ it("useContractQuery caches results by key and avoids duplicate calls", async () => {
40
+ const queryClient = createTestQueryClient();
41
+ let queryCalls = 0;
42
+ let latestInput:
43
+ | Readonly<{
44
+ contractIndex: number | bigint;
45
+ inputType: number | bigint;
46
+ inputBytes?: Uint8Array;
47
+ expectedOutputSize?: number;
48
+ }>
49
+ | undefined;
50
+
51
+ const sdk = createMockSdk({
52
+ queryRaw: async (input) => {
53
+ queryCalls += 1;
54
+ latestInput = input;
55
+ return {
56
+ responseBytes: new Uint8Array([1, 2, 3, 4]),
57
+ responseBase64: "AQIDBA==",
58
+ attempts: 1,
59
+ };
60
+ },
61
+ });
62
+
63
+ let hookResult: UseQueryResult<QueryRawResult, Error> | undefined;
64
+ let renderer: TestRenderer.ReactTestRenderer | undefined;
65
+
66
+ function Consumer() {
67
+ hookResult = useContractQuery(
68
+ "DEMO",
69
+ "Read",
70
+ {
71
+ inputBytes: new Uint8Array([9]),
72
+ decodeOutput: false,
73
+ },
74
+ { staleTime: Number.POSITIVE_INFINITY },
75
+ );
76
+ return null;
77
+ }
78
+
79
+ await act(async () => {
80
+ renderer = TestRenderer.create(
81
+ <TestProviders sdk={sdk} queryClient={queryClient}>
82
+ <Consumer />
83
+ </TestProviders>,
84
+ );
85
+ });
86
+
87
+ await waitFor(() => hookResult?.isSuccess === true);
88
+ expect(queryCalls).toBe(1);
89
+ expect(latestInput).toEqual({
90
+ contractIndex: 7,
91
+ inputType: 1,
92
+ inputBytes: new Uint8Array([9]),
93
+ expectedOutputSize: 8,
94
+ });
95
+
96
+ await act(async () => {
97
+ renderer?.unmount();
98
+ });
99
+
100
+ hookResult = undefined;
101
+
102
+ await act(async () => {
103
+ renderer = TestRenderer.create(
104
+ <TestProviders sdk={sdk} queryClient={queryClient}>
105
+ <Consumer />
106
+ </TestProviders>,
107
+ );
108
+ });
109
+
110
+ await waitFor(() => hookResult?.isSuccess === true);
111
+ expect(queryCalls).toBe(1);
112
+ });
113
+
114
+ it("useContractMutation exposes success and error states", async () => {
115
+ const queryClient = createTestQueryClient();
116
+ let sendCalls = 0;
117
+ let latestInput:
118
+ | Readonly<{
119
+ fromSeed?: string;
120
+ toIdentity: string;
121
+ amount: bigint;
122
+ targetTick?: bigint | number;
123
+ inputType: number;
124
+ inputBytes: Uint8Array;
125
+ }>
126
+ | undefined;
127
+
128
+ const successSdk = createMockSdk({
129
+ send: async (input) => {
130
+ sendCalls += 1;
131
+ latestInput = input;
132
+ return {
133
+ txBytes: new Uint8Array([7, 8, 9]),
134
+ txId: "tx-success",
135
+ networkTxId: "network-success",
136
+ targetTick: 12n,
137
+ broadcast: {
138
+ peersBroadcasted: 1,
139
+ encodedTransaction: "BwgJ",
140
+ transactionId: "network-success",
141
+ },
142
+ };
143
+ },
144
+ });
145
+
146
+ let successResult: UseMutationResult<any, Error, any> | undefined;
147
+
148
+ function SuccessConsumer() {
149
+ successResult = useContractMutation("DEMO");
150
+ return null;
151
+ }
152
+
153
+ await act(async () => {
154
+ TestRenderer.create(
155
+ <TestProviders sdk={successSdk} queryClient={queryClient}>
156
+ <SuccessConsumer />
157
+ </TestProviders>,
158
+ );
159
+ });
160
+
161
+ await act(async () => {
162
+ await successResult?.mutateAsync({
163
+ name: "Write",
164
+ fromSeed: "a".repeat(55),
165
+ toIdentity: "B".repeat(60),
166
+ inputBytes: new Uint8Array([5]),
167
+ amount: 2n,
168
+ targetTick: 11n,
169
+ });
170
+ });
171
+
172
+ await waitFor(() => successResult?.isSuccess === true);
173
+ expect(sendCalls).toBe(1);
174
+ expect(latestInput).toEqual({
175
+ fromSeed: "a".repeat(55),
176
+ toIdentity: "B".repeat(60),
177
+ amount: 2n,
178
+ targetTick: 11n,
179
+ inputType: 2,
180
+ inputBytes: new Uint8Array([5]),
181
+ });
182
+ expect(successResult?.data?.txId).toBe("tx-success");
183
+
184
+ const errorClient = createTestQueryClient();
185
+ const errorSdk = createMockSdk({
186
+ send: async () => {
187
+ throw new Error("send failed");
188
+ },
189
+ });
190
+
191
+ let errorResult: UseMutationResult<any, Error, any> | undefined;
192
+
193
+ function ErrorConsumer() {
194
+ errorResult = useContractMutation("DEMO");
195
+ return null;
196
+ }
197
+
198
+ await act(async () => {
199
+ TestRenderer.create(
200
+ <TestProviders sdk={errorSdk} queryClient={errorClient}>
201
+ <ErrorConsumer />
202
+ </TestProviders>,
203
+ );
204
+ });
205
+
206
+ await act(async () => {
207
+ try {
208
+ await errorResult?.mutateAsync({
209
+ name: "Write",
210
+ fromSeed: "a".repeat(55),
211
+ toIdentity: "B".repeat(60),
212
+ inputBytes: new Uint8Array([1]),
213
+ });
214
+ } catch {
215
+ // Assertion is state-based below.
216
+ }
217
+ });
218
+
219
+ await waitFor(() => errorResult?.isError === true);
220
+ expect(errorResult?.error?.message).toBe("send failed");
221
+ });
222
+ });
223
+
224
+ function createTestQueryClient(): QueryClient {
225
+ return new QueryClient({
226
+ defaultOptions: {
227
+ queries: {
228
+ retry: false,
229
+ },
230
+ mutations: {
231
+ retry: false,
232
+ },
233
+ },
234
+ });
235
+ }
236
+
237
+ function TestProviders(props: {
238
+ sdk: ReturnType<typeof createSdk>;
239
+ queryClient: QueryClient;
240
+ children: React.ReactNode;
241
+ }) {
242
+ return (
243
+ <QubicQueryProvider client={props.queryClient}>
244
+ <SdkProvider sdk={props.sdk} contractsRegistry={demoContractsRegistry}>
245
+ {props.children}
246
+ </SdkProvider>
247
+ </QubicQueryProvider>
248
+ );
249
+ }
250
+
251
+ function createMockSdk(overrides: Readonly<{
252
+ queryRaw?: (input: {
253
+ contractIndex: number | bigint;
254
+ inputType: number | bigint;
255
+ inputBytes?: Uint8Array;
256
+ expectedOutputSize?: number;
257
+ retries?: number;
258
+ retryDelayMs?: number;
259
+ signal?: AbortSignal;
260
+ }) => Promise<QueryRawResult>;
261
+ send?: (input: {
262
+ fromSeed?: string;
263
+ fromVault?: string;
264
+ toIdentity: string;
265
+ amount: bigint;
266
+ targetTick?: bigint | number;
267
+ inputType: number;
268
+ inputBytes: Uint8Array;
269
+ }) => Promise<SendTransactionResult>;
270
+ }>): ReturnType<typeof createSdk> {
271
+ return {
272
+ contracts: {
273
+ queryRaw:
274
+ overrides.queryRaw ??
275
+ (async () => ({
276
+ responseBytes: new Uint8Array(),
277
+ responseBase64: "",
278
+ attempts: 1,
279
+ })),
280
+ },
281
+ transactions: {
282
+ send:
283
+ overrides.send ??
284
+ (async () => ({
285
+ txBytes: new Uint8Array(),
286
+ txId: "tx-default",
287
+ networkTxId: "network-default",
288
+ targetTick: 0n,
289
+ broadcast: {
290
+ peersBroadcasted: 1,
291
+ encodedTransaction: "",
292
+ transactionId: "network-default",
293
+ },
294
+ })),
295
+ },
296
+ } as unknown as ReturnType<typeof createSdk>;
297
+ }
298
+
299
+ async function waitFor(predicate: () => boolean, timeoutMs = 3000): Promise<void> {
300
+ const startedAt = Date.now();
301
+ while (Date.now() - startedAt < timeoutMs) {
302
+ if (predicate()) return;
303
+ await act(async () => {
304
+ await sleep(10);
305
+ });
306
+ }
307
+ throw new Error("Timed out waiting for condition");
308
+ }
309
+
310
+ function sleep(ms: number): Promise<void> {
311
+ return new Promise((resolve) => setTimeout(resolve, ms));
312
+ }
@@ -0,0 +1,339 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type {
3
+ LastProcessedTick,
4
+ LiveBalance,
5
+ QueryTransaction,
6
+ TickInfo,
7
+ TransactionsForIdentityRequest,
8
+ TransactionsForIdentityResponse,
9
+ } from "@qubic.ts/sdk";
10
+ import { createSdk } from "@qubic.ts/sdk";
11
+ import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from "@tanstack/react-query";
12
+ import { QueryClient } from "@tanstack/react-query";
13
+ import React from "react";
14
+ import TestRenderer, { act } from "react-test-renderer";
15
+ import { QubicQueryProvider } from "../providers/query-provider.js";
16
+ import { SdkProvider } from "../providers/sdk-provider.js";
17
+ import { queryKeys } from "../query/keys.js";
18
+ import { useBalance } from "./use-balance.js";
19
+ import { useLastProcessedTick } from "./use-last-processed-tick.js";
20
+ import { useTickInfo } from "./use-tick-info.js";
21
+ import { useTransactions } from "./use-transactions.js";
22
+
23
+ describe("read hooks", () => {
24
+ it("useBalance caches by query key and reuses cached data on remount", async () => {
25
+ const queryClient = createTestQueryClient();
26
+ let balanceCalls = 0;
27
+ const sdk = createMockSdk({
28
+ balance: async (identity) => {
29
+ balanceCalls += 1;
30
+ return createBalance(identity, 100n);
31
+ },
32
+ });
33
+
34
+ let firstBalance: LiveBalance | undefined;
35
+ let secondBalance: LiveBalance | undefined;
36
+ let renderer: TestRenderer.ReactTestRenderer | undefined;
37
+
38
+ function Consumer() {
39
+ firstBalance = useBalance("ID1", { staleTime: Number.POSITIVE_INFINITY }).data;
40
+ secondBalance = useBalance("ID1", { staleTime: Number.POSITIVE_INFINITY }).data;
41
+ return null;
42
+ }
43
+
44
+ await act(async () => {
45
+ renderer = TestRenderer.create(
46
+ <TestProviders sdk={sdk} queryClient={queryClient}>
47
+ <Consumer />
48
+ </TestProviders>,
49
+ );
50
+ });
51
+
52
+ await waitFor(() => firstBalance !== undefined && secondBalance !== undefined);
53
+ expect(balanceCalls).toBe(1);
54
+
55
+ await act(async () => {
56
+ renderer?.unmount();
57
+ });
58
+
59
+ firstBalance = undefined;
60
+ secondBalance = undefined;
61
+
62
+ await act(async () => {
63
+ renderer = TestRenderer.create(
64
+ <TestProviders sdk={sdk} queryClient={queryClient}>
65
+ <Consumer />
66
+ </TestProviders>,
67
+ );
68
+ });
69
+
70
+ await waitFor(() => firstBalance !== undefined && secondBalance !== undefined);
71
+ expect(balanceCalls).toBe(1);
72
+ });
73
+
74
+ it("useTickInfo refetches on demand", async () => {
75
+ const queryClient = createTestQueryClient();
76
+ let tickCalls = 0;
77
+ const sdk = createMockSdk({
78
+ tickInfo: async () => {
79
+ tickCalls += 1;
80
+ return createTickInfo(BigInt(tickCalls));
81
+ },
82
+ });
83
+
84
+ let hookResult: UseQueryResult<TickInfo, Error> | undefined;
85
+
86
+ function Consumer() {
87
+ hookResult = useTickInfo({ staleTime: Number.POSITIVE_INFINITY });
88
+ return null;
89
+ }
90
+
91
+ await act(async () => {
92
+ TestRenderer.create(
93
+ <TestProviders sdk={sdk} queryClient={queryClient}>
94
+ <Consumer />
95
+ </TestProviders>,
96
+ );
97
+ });
98
+
99
+ await waitFor(() => hookResult?.isSuccess === true);
100
+ expect(hookResult?.data?.tick).toBe(1n);
101
+ expect(tickCalls).toBe(1);
102
+
103
+ await act(async () => {
104
+ await hookResult?.refetch();
105
+ });
106
+
107
+ await waitFor(() => hookResult?.data?.tick === 2n);
108
+ expect(tickCalls).toBe(2);
109
+ expect(hookResult?.data?.tick).toBe(2n);
110
+ });
111
+
112
+ it("useLastProcessedTick refetches when invalidated", async () => {
113
+ const queryClient = createTestQueryClient();
114
+ let processedTickCalls = 0;
115
+ const sdk = createMockSdk({
116
+ lastProcessedTick: async () => {
117
+ processedTickCalls += 1;
118
+ return createLastProcessedTick(BigInt(processedTickCalls));
119
+ },
120
+ });
121
+
122
+ let hookResult: UseQueryResult<LastProcessedTick, Error> | undefined;
123
+
124
+ function Consumer() {
125
+ hookResult = useLastProcessedTick({ staleTime: Number.POSITIVE_INFINITY });
126
+ return null;
127
+ }
128
+
129
+ await act(async () => {
130
+ TestRenderer.create(
131
+ <TestProviders sdk={sdk} queryClient={queryClient}>
132
+ <Consumer />
133
+ </TestProviders>,
134
+ );
135
+ });
136
+
137
+ await waitFor(() => hookResult?.isSuccess === true);
138
+ expect(hookResult?.data?.tickNumber).toBe(1n);
139
+ expect(processedTickCalls).toBe(1);
140
+
141
+ await act(async () => {
142
+ await queryClient.invalidateQueries({ queryKey: queryKeys.lastProcessedTick() });
143
+ });
144
+
145
+ await waitFor(() => hookResult?.data?.tickNumber === 2n);
146
+ expect(processedTickCalls).toBe(2);
147
+ expect(hookResult?.data?.tickNumber).toBe(2n);
148
+ });
149
+
150
+ it("useTransactions paginates and reuses cached pages", async () => {
151
+ const queryClient = createTestQueryClient();
152
+ const transactions = [createTransaction(1), createTransaction(2), createTransaction(3)];
153
+ let transactionCalls = 0;
154
+ const sdk = createMockSdk({
155
+ transactionsForIdentity: async (input) => {
156
+ transactionCalls += 1;
157
+ const offset = Number(input.pagination?.offset ?? 0n);
158
+ const size = Number(input.pagination?.size ?? 100n);
159
+ return createTransactionsResponse(transactions, offset, size);
160
+ },
161
+ });
162
+
163
+ let hookResult: UseInfiniteQueryResult<InfiniteData<TransactionsForIdentityResponse>, Error> | undefined;
164
+ let renderer: TestRenderer.ReactTestRenderer | undefined;
165
+
166
+ function Consumer() {
167
+ hookResult = useTransactions(
168
+ {
169
+ identity: "ID1",
170
+ pageSize: 2,
171
+ limit: 3,
172
+ },
173
+ { staleTime: Number.POSITIVE_INFINITY },
174
+ );
175
+ return null;
176
+ }
177
+
178
+ await act(async () => {
179
+ renderer = TestRenderer.create(
180
+ <TestProviders sdk={sdk} queryClient={queryClient}>
181
+ <Consumer />
182
+ </TestProviders>,
183
+ );
184
+ });
185
+
186
+ await waitFor(() => hookResult?.isSuccess === true);
187
+ expect(hookResult?.data?.pages[0]?.transactions.length).toBe(2);
188
+ expect(transactionCalls).toBe(1);
189
+
190
+ await act(async () => {
191
+ await hookResult?.fetchNextPage();
192
+ });
193
+
194
+ await waitFor(() => hookResult?.data?.pages.length === 2);
195
+ expect(hookResult?.data?.pages[1]?.transactions.length).toBe(1);
196
+ expect(transactionCalls).toBe(2);
197
+
198
+ await act(async () => {
199
+ renderer?.unmount();
200
+ });
201
+
202
+ hookResult = undefined;
203
+
204
+ await act(async () => {
205
+ renderer = TestRenderer.create(
206
+ <TestProviders sdk={sdk} queryClient={queryClient}>
207
+ <Consumer />
208
+ </TestProviders>,
209
+ );
210
+ });
211
+
212
+ await waitFor(() => hookResult?.isSuccess === true);
213
+ expect(transactionCalls).toBe(2);
214
+ });
215
+ });
216
+
217
+ function createTestQueryClient(): QueryClient {
218
+ return new QueryClient({
219
+ defaultOptions: {
220
+ queries: {
221
+ retry: false,
222
+ },
223
+ },
224
+ });
225
+ }
226
+
227
+ function TestProviders(props: {
228
+ sdk: ReturnType<typeof createSdk>;
229
+ queryClient: QueryClient;
230
+ children: React.ReactNode;
231
+ }) {
232
+ return (
233
+ <QubicQueryProvider client={props.queryClient}>
234
+ <SdkProvider sdk={props.sdk}>{props.children}</SdkProvider>
235
+ </QubicQueryProvider>
236
+ );
237
+ }
238
+
239
+ function createMockSdk(overrides: Readonly<{
240
+ balance?: (identity: string) => Promise<LiveBalance>;
241
+ tickInfo?: () => Promise<TickInfo>;
242
+ lastProcessedTick?: () => Promise<LastProcessedTick>;
243
+ transactionsForIdentity?: (
244
+ input: TransactionsForIdentityRequest,
245
+ ) => Promise<TransactionsForIdentityResponse>;
246
+ }>): ReturnType<typeof createSdk> {
247
+ return {
248
+ rpc: {
249
+ live: {
250
+ balance: overrides.balance ?? (async (identity) => createBalance(identity, 0n)),
251
+ tickInfo: overrides.tickInfo ?? (async () => createTickInfo(0n)),
252
+ },
253
+ query: {
254
+ getLastProcessedTick:
255
+ overrides.lastProcessedTick ?? (async () => createLastProcessedTick(0n)),
256
+ getTransactionsForIdentity:
257
+ overrides.transactionsForIdentity ??
258
+ (async () => createTransactionsResponse([], 0, 100)),
259
+ },
260
+ },
261
+ } as unknown as ReturnType<typeof createSdk>;
262
+ }
263
+
264
+ function createTickInfo(tick: bigint): TickInfo {
265
+ return {
266
+ tick,
267
+ duration: 5n,
268
+ epoch: 1n,
269
+ initialTick: 1n,
270
+ };
271
+ }
272
+
273
+ function createBalance(identity: string, balance: bigint): LiveBalance {
274
+ return {
275
+ id: identity,
276
+ balance,
277
+ validForTick: 1n,
278
+ latestIncomingTransferTick: 0n,
279
+ latestOutgoingTransferTick: 0n,
280
+ incomingAmount: 0n,
281
+ outgoingAmount: 0n,
282
+ numberOfIncomingTransfers: 0n,
283
+ numberOfOutgoingTransfers: 0n,
284
+ };
285
+ }
286
+
287
+ function createLastProcessedTick(tickNumber: bigint): LastProcessedTick {
288
+ return {
289
+ tickNumber,
290
+ epoch: 1n,
291
+ intervalInitialTick: 1n,
292
+ };
293
+ }
294
+
295
+ function createTransaction(index: number): QueryTransaction {
296
+ return {
297
+ hash: `hash-${index}`,
298
+ amount: BigInt(index),
299
+ source: `source-${index}`,
300
+ destination: `destination-${index}`,
301
+ tickNumber: BigInt(index),
302
+ timestamp: BigInt(index),
303
+ inputType: 0n,
304
+ inputSize: 0n,
305
+ inputData: "",
306
+ signature: `sig-${index}`,
307
+ };
308
+ }
309
+
310
+ function createTransactionsResponse(
311
+ allTransactions: readonly QueryTransaction[],
312
+ offset: number,
313
+ size: number,
314
+ ): TransactionsForIdentityResponse {
315
+ return {
316
+ validForTick: 1n,
317
+ hits: {
318
+ total: BigInt(allTransactions.length),
319
+ from: BigInt(offset),
320
+ size: BigInt(size),
321
+ },
322
+ transactions: allTransactions.slice(offset, offset + size),
323
+ };
324
+ }
325
+
326
+ async function waitFor(predicate: () => boolean, timeoutMs = 3000): Promise<void> {
327
+ const startedAt = Date.now();
328
+ while (Date.now() - startedAt < timeoutMs) {
329
+ if (predicate()) return;
330
+ await act(async () => {
331
+ await sleep(10);
332
+ });
333
+ }
334
+ throw new Error("Timed out waiting for condition");
335
+ }
336
+
337
+ function sleep(ms: number): Promise<void> {
338
+ return new Promise((resolve) => setTimeout(resolve, ms));
339
+ }