@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.
- package/fixtures/next-boundary/client-node-leak.tsx +5 -0
- package/fixtures/next-boundary/client-ok.tsx +8 -0
- package/fixtures/next-boundary/server-ok.ts +5 -0
- package/package.json +47 -0
- package/scripts/verify-next-boundary.mjs +63 -0
- package/src/browser.ts +27 -0
- package/src/contract-types.ts +309 -0
- package/src/contracts-types.typecheck.ts +85 -0
- package/src/hooks/contract-hooks.test.tsx +312 -0
- package/src/hooks/read-hooks.test.tsx +339 -0
- package/src/hooks/send-hooks.test.tsx +247 -0
- package/src/hooks/use-balance.ts +27 -0
- package/src/hooks/use-contract-mutation.ts +50 -0
- package/src/hooks/use-contract-query.ts +71 -0
- package/src/hooks/use-contract.ts +231 -0
- package/src/hooks/use-last-processed-tick.ts +21 -0
- package/src/hooks/use-send-and-confirm.ts +19 -0
- package/src/hooks/use-send.ts +21 -0
- package/src/hooks/use-tick-info.ts +16 -0
- package/src/hooks/use-transactions.ts +97 -0
- package/src/index.ts +67 -0
- package/src/node.ts +21 -0
- package/src/providers/query-provider.test.tsx +25 -0
- package/src/providers/query-provider.tsx +13 -0
- package/src/providers/sdk-provider.test.tsx +54 -0
- package/src/providers/sdk-provider.tsx +83 -0
- package/src/providers/wallet-provider.test.tsx +82 -0
- package/src/providers/wallet-provider.tsx +209 -0
- package/src/query/keys.ts +9 -0
- package/src/typecheck-stubs/contracts.d.ts +77 -0
- package/src/typecheck-stubs/sdk.d.ts +254 -0
- package/src/vault/browser.ts +52 -0
- package/src/vault/node.ts +39 -0
- package/src/vault/runtime-boundary.test.ts +22 -0
- package/src/wallet/metamask-snap.test.ts +73 -0
- package/src/wallet/metamask-snap.ts +121 -0
- package/src/wallet/types.ts +55 -0
- package/src/wallet/utils.ts +14 -0
- package/src/wallet/vault.test.ts +98 -0
- package/src/wallet/vault.ts +70 -0
- package/src/wallet/walletconnect.test.ts +141 -0
- package/src/wallet/walletconnect.ts +218 -0
- package/tsconfig.json +14 -0
- package/tsconfig.typecheck.json +12 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TickInfo } from "@qubic.ts/sdk";
|
|
2
|
+
import type { UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useSdk } from "../providers/sdk-provider.js";
|
|
5
|
+
import { queryKeys } from "../query/keys.js";
|
|
6
|
+
|
|
7
|
+
export type UseTickInfoOptions = Omit<UseQueryOptions<TickInfo, Error>, "queryKey" | "queryFn">;
|
|
8
|
+
|
|
9
|
+
export function useTickInfo(options: UseTickInfoOptions = {}): UseQueryResult<TickInfo, Error> {
|
|
10
|
+
const sdk = useSdk();
|
|
11
|
+
return useQuery({
|
|
12
|
+
...options,
|
|
13
|
+
queryKey: queryKeys.tickInfo(),
|
|
14
|
+
queryFn: () => sdk.rpc.live.tickInfo(),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TransactionsForIdentityRequest,
|
|
3
|
+
TransactionsForIdentityResponse,
|
|
4
|
+
} from "@qubic.ts/sdk";
|
|
5
|
+
import type { InfiniteData, UseInfiniteQueryOptions, UseInfiniteQueryResult } from "@tanstack/react-query";
|
|
6
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useSdk } from "../providers/sdk-provider.js";
|
|
8
|
+
import { queryKeys } from "../query/keys.js";
|
|
9
|
+
|
|
10
|
+
export type UseTransactionsInput = Readonly<{
|
|
11
|
+
identity: string | undefined;
|
|
12
|
+
filters?: TransactionsForIdentityRequest["filters"];
|
|
13
|
+
ranges?: TransactionsForIdentityRequest["ranges"];
|
|
14
|
+
pageSize?: bigint | number;
|
|
15
|
+
limit?: bigint | number;
|
|
16
|
+
offset?: bigint | number;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type UseTransactionsOptions = Omit<
|
|
20
|
+
UseInfiniteQueryOptions<
|
|
21
|
+
TransactionsForIdentityResponse,
|
|
22
|
+
Error,
|
|
23
|
+
InfiniteData<TransactionsForIdentityResponse>,
|
|
24
|
+
ReturnType<typeof queryKeys.transactions>,
|
|
25
|
+
bigint
|
|
26
|
+
>,
|
|
27
|
+
"queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export function useTransactions(
|
|
31
|
+
input: UseTransactionsInput,
|
|
32
|
+
options: UseTransactionsOptions = {},
|
|
33
|
+
): UseInfiniteQueryResult<InfiniteData<TransactionsForIdentityResponse>, Error> {
|
|
34
|
+
const sdk = useSdk();
|
|
35
|
+
const identity = input.identity;
|
|
36
|
+
const enabled = Boolean(identity) && (options.enabled ?? true);
|
|
37
|
+
const startOffset = toBigint(input.offset ?? 0);
|
|
38
|
+
const pageSize = toBigint(input.pageSize ?? 100);
|
|
39
|
+
const limit = input.limit !== undefined ? toBigint(input.limit) : undefined;
|
|
40
|
+
const paramsKey = serializeParams(input);
|
|
41
|
+
|
|
42
|
+
return useInfiniteQuery({
|
|
43
|
+
...options,
|
|
44
|
+
queryKey: queryKeys.transactions(identity ?? "", paramsKey),
|
|
45
|
+
enabled,
|
|
46
|
+
initialPageParam: startOffset,
|
|
47
|
+
queryFn: async ({ pageParam }) => {
|
|
48
|
+
if (!identity) {
|
|
49
|
+
throw new Error("useTransactions requires identity");
|
|
50
|
+
}
|
|
51
|
+
const consumed = pageParam - startOffset;
|
|
52
|
+
const remaining = limit !== undefined ? limit - consumed : undefined;
|
|
53
|
+
const size = remaining !== undefined ? minBigint(pageSize, remaining) : pageSize;
|
|
54
|
+
return sdk.rpc.query.getTransactionsForIdentity({
|
|
55
|
+
identity,
|
|
56
|
+
...(input.filters === undefined ? {} : { filters: input.filters }),
|
|
57
|
+
...(input.ranges === undefined ? {} : { ranges: input.ranges }),
|
|
58
|
+
pagination: { offset: pageParam, size },
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
getNextPageParam: (lastPage, pages) => {
|
|
62
|
+
if (lastPage.transactions.length === 0) return undefined;
|
|
63
|
+
const totalFetched = pages.reduce((acc, page) => acc + BigInt(page.transactions.length), 0n);
|
|
64
|
+
if (limit !== undefined && totalFetched >= limit) return undefined;
|
|
65
|
+
const nextOffset = startOffset + totalFetched;
|
|
66
|
+
if (nextOffset >= lastPage.hits.total) return undefined;
|
|
67
|
+
return nextOffset;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function serializeParams(input: UseTransactionsInput): string {
|
|
73
|
+
const payload = {
|
|
74
|
+
identity: input.identity ?? "",
|
|
75
|
+
filters: input.filters ?? null,
|
|
76
|
+
ranges: input.ranges ?? null,
|
|
77
|
+
pageSize: input.pageSize ?? null,
|
|
78
|
+
limit: input.limit ?? null,
|
|
79
|
+
offset: input.offset ?? null,
|
|
80
|
+
};
|
|
81
|
+
return JSON.stringify(payload, (_key, value) => {
|
|
82
|
+
if (typeof value === "bigint") return value.toString();
|
|
83
|
+
return value;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toBigint(value: bigint | number): bigint {
|
|
88
|
+
if (typeof value === "bigint") return value;
|
|
89
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
90
|
+
throw new TypeError("Expected an integer");
|
|
91
|
+
}
|
|
92
|
+
return BigInt(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function minBigint(a: bigint, b: bigint): bigint {
|
|
96
|
+
return a < b ? a : b;
|
|
97
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ContractCodecSchema,
|
|
3
|
+
ContractFunctionInput,
|
|
4
|
+
ContractFunctionName,
|
|
5
|
+
ContractFunctionOutput,
|
|
6
|
+
ContractHandleLike,
|
|
7
|
+
ContractMutationResult,
|
|
8
|
+
ContractProcedureInput,
|
|
9
|
+
ContractProcedureName,
|
|
10
|
+
ContractProcedureOutput,
|
|
11
|
+
ContractProcedureTxInput,
|
|
12
|
+
ContractQueryInput,
|
|
13
|
+
ContractQueryResult,
|
|
14
|
+
ContractRegistryInput,
|
|
15
|
+
ContractRegistrySchema,
|
|
16
|
+
ContractRegistryToSchema,
|
|
17
|
+
} from "./contract-types.js";
|
|
18
|
+
export type { UseContractMutationOptions } from "./hooks/use-contract-mutation.js";
|
|
19
|
+
export { useContractMutation } from "./hooks/use-contract-mutation.js";
|
|
20
|
+
export type { UseContractQueryOptions } from "./hooks/use-contract-query.js";
|
|
21
|
+
export { useContractQuery } from "./hooks/use-contract-query.js";
|
|
22
|
+
export { useContract } from "./hooks/use-contract.js";
|
|
23
|
+
export type { UseBalanceOptions } from "./hooks/use-balance.js";
|
|
24
|
+
export { useBalance } from "./hooks/use-balance.js";
|
|
25
|
+
export type { UseLastProcessedTickOptions } from "./hooks/use-last-processed-tick.js";
|
|
26
|
+
export { useLastProcessedTick } from "./hooks/use-last-processed-tick.js";
|
|
27
|
+
export type { UseTickInfoOptions } from "./hooks/use-tick-info.js";
|
|
28
|
+
export { useTickInfo } from "./hooks/use-tick-info.js";
|
|
29
|
+
export type { UseTransactionsInput, UseTransactionsOptions } from "./hooks/use-transactions.js";
|
|
30
|
+
export { useTransactions } from "./hooks/use-transactions.js";
|
|
31
|
+
export type { UseSendAndConfirmOptions } from "./hooks/use-send-and-confirm.js";
|
|
32
|
+
export { useSendAndConfirm } from "./hooks/use-send-and-confirm.js";
|
|
33
|
+
export type { UseSendOptions } from "./hooks/use-send.js";
|
|
34
|
+
export { useSend } from "./hooks/use-send.js";
|
|
35
|
+
export type { QubicQueryProviderProps } from "./providers/query-provider.js";
|
|
36
|
+
export { QubicQueryProvider } from "./providers/query-provider.js";
|
|
37
|
+
export type { SdkProviderProps } from "./providers/sdk-provider.js";
|
|
38
|
+
export {
|
|
39
|
+
createSdkProvider,
|
|
40
|
+
SdkProvider,
|
|
41
|
+
useContractsRegistry,
|
|
42
|
+
useSdk,
|
|
43
|
+
} from "./providers/sdk-provider.js";
|
|
44
|
+
export type {
|
|
45
|
+
WalletContextValue,
|
|
46
|
+
WalletProviderProps,
|
|
47
|
+
WalletState,
|
|
48
|
+
} from "./providers/wallet-provider.js";
|
|
49
|
+
export { useWallet, WalletProvider } from "./providers/wallet-provider.js";
|
|
50
|
+
export { queryKeys } from "./query/keys.js";
|
|
51
|
+
export type { MetaMaskSnapConfig } from "./wallet/metamask-snap.js";
|
|
52
|
+
export { MetaMaskSnapConnector } from "./wallet/metamask-snap.js";
|
|
53
|
+
export type {
|
|
54
|
+
WalletAccount,
|
|
55
|
+
WalletConnector,
|
|
56
|
+
WalletConnectorType,
|
|
57
|
+
WalletConnectResult,
|
|
58
|
+
WalletSession,
|
|
59
|
+
WalletSignTransactionRequest,
|
|
60
|
+
WalletSignTransactionResult,
|
|
61
|
+
WalletStatus,
|
|
62
|
+
} from "./wallet/types.js";
|
|
63
|
+
export type { VaultConnectorConfig } from "./wallet/vault.js";
|
|
64
|
+
export { VaultConnector } from "./wallet/vault.js";
|
|
65
|
+
export type { WalletConnectConfig } from "./wallet/walletconnect.js";
|
|
66
|
+
export { WalletConnectConnector } from "./wallet/walletconnect.js";
|
|
67
|
+
export const reactPackageVersion = "0.0.0-development";
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from "./index.js";
|
|
2
|
+
export type {
|
|
3
|
+
OpenSeedVaultInput,
|
|
4
|
+
SeedVault,
|
|
5
|
+
VaultEntry,
|
|
6
|
+
VaultEntryEncrypted,
|
|
7
|
+
VaultExport,
|
|
8
|
+
VaultHeader,
|
|
9
|
+
VaultKdfParams,
|
|
10
|
+
VaultStore,
|
|
11
|
+
VaultSummary,
|
|
12
|
+
} from "@qubic.ts/sdk/node";
|
|
13
|
+
export {
|
|
14
|
+
VaultEntryExistsError,
|
|
15
|
+
VaultEntryNotFoundError,
|
|
16
|
+
VaultError,
|
|
17
|
+
VaultInvalidPassphraseError,
|
|
18
|
+
VaultNotFoundError,
|
|
19
|
+
} from "@qubic.ts/sdk/node";
|
|
20
|
+
export type { CreateVaultInput } from "./vault/node.js";
|
|
21
|
+
export { createNodeVault, importNodeVault, openNodeVault, vaultExists } from "./vault/node.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { QueryClient, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import TestRenderer from "react-test-renderer";
|
|
5
|
+
import { QubicQueryProvider } from "./query-provider.js";
|
|
6
|
+
|
|
7
|
+
describe("QubicQueryProvider", () => {
|
|
8
|
+
it("uses provided query client", () => {
|
|
9
|
+
const client = new QueryClient();
|
|
10
|
+
let seen: QueryClient | undefined;
|
|
11
|
+
|
|
12
|
+
function Consumer() {
|
|
13
|
+
seen = useQueryClient();
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
TestRenderer.create(
|
|
18
|
+
<QubicQueryProvider client={client}>
|
|
19
|
+
<Consumer />
|
|
20
|
+
</QubicQueryProvider>,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(seen).toBe(client);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
export type QubicQueryProviderProps = Readonly<{
|
|
6
|
+
client?: QueryClient;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export function QubicQueryProvider({ client, children }: QubicQueryProviderProps) {
|
|
11
|
+
const value = useMemo(() => client ?? new QueryClient(), [client]);
|
|
12
|
+
return <QueryClientProvider client={value}>{children}</QueryClientProvider>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { coreContractsRegistry } from "@qubic.ts/contracts";
|
|
3
|
+
import { createSdk } from "@qubic.ts/sdk";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import TestRenderer from "react-test-renderer";
|
|
6
|
+
import {
|
|
7
|
+
createSdkProvider,
|
|
8
|
+
SdkProvider,
|
|
9
|
+
useContractsRegistry,
|
|
10
|
+
useSdk,
|
|
11
|
+
} from "./sdk-provider.js";
|
|
12
|
+
|
|
13
|
+
describe("SdkProvider", () => {
|
|
14
|
+
it("provides sdk and contracts registry contexts", () => {
|
|
15
|
+
const sdk = createSdk({ txQueue: { enabled: false } });
|
|
16
|
+
let seenSdk: ReturnType<typeof createSdk> | undefined;
|
|
17
|
+
let seenRegistry: typeof coreContractsRegistry | undefined;
|
|
18
|
+
|
|
19
|
+
function Consumer() {
|
|
20
|
+
seenSdk = useSdk();
|
|
21
|
+
seenRegistry = useContractsRegistry() as typeof coreContractsRegistry;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TestRenderer.create(
|
|
26
|
+
<SdkProvider sdk={sdk} contractsRegistry={coreContractsRegistry}>
|
|
27
|
+
<Consumer />
|
|
28
|
+
</SdkProvider>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(seenSdk).toBe(sdk);
|
|
32
|
+
expect(seenRegistry).toBe(coreContractsRegistry);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("createSdkProvider exposes typed hook helpers", () => {
|
|
36
|
+
const sdk = createSdk({ txQueue: { enabled: false } });
|
|
37
|
+
const api = createSdkProvider();
|
|
38
|
+
let contractApi: ReturnType<typeof api.useContract> | undefined;
|
|
39
|
+
|
|
40
|
+
function Consumer() {
|
|
41
|
+
contractApi = api.useContract("QUTIL");
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
TestRenderer.create(
|
|
46
|
+
<api.SdkProvider sdk={sdk} contractsRegistry={coreContractsRegistry}>
|
|
47
|
+
<Consumer />
|
|
48
|
+
</api.SdkProvider>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(typeof contractApi?.query).toBe("function");
|
|
52
|
+
expect(typeof contractApi?.sendProcedure).toBe("function");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { coreContractsRegistry } from "@qubic.ts/contracts";
|
|
2
|
+
import type { SdkConfig } from "@qubic.ts/sdk/browser";
|
|
3
|
+
import { createSdk } from "@qubic.ts/sdk/browser";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { createContext, useContext, useMemo } from "react";
|
|
6
|
+
import type {
|
|
7
|
+
ContractFunctionInput,
|
|
8
|
+
ContractFunctionName,
|
|
9
|
+
ContractFunctionOutput,
|
|
10
|
+
ContractProcedureInput,
|
|
11
|
+
ContractProcedureName,
|
|
12
|
+
ContractQueryInput,
|
|
13
|
+
ContractRegistryInput,
|
|
14
|
+
ContractRegistrySchema,
|
|
15
|
+
ContractRegistryToSchema,
|
|
16
|
+
} from "../contract-types.js";
|
|
17
|
+
import { useContract } from "../hooks/use-contract.js";
|
|
18
|
+
import type { UseContractMutationOptions } from "../hooks/use-contract-mutation.js";
|
|
19
|
+
import { useContractMutation } from "../hooks/use-contract-mutation.js";
|
|
20
|
+
import type { UseContractQueryOptions } from "../hooks/use-contract-query.js";
|
|
21
|
+
import { useContractQuery } from "../hooks/use-contract-query.js";
|
|
22
|
+
|
|
23
|
+
type SdkInstance = ReturnType<typeof createSdk>;
|
|
24
|
+
|
|
25
|
+
const SdkContext = createContext<SdkInstance | null>(null);
|
|
26
|
+
const ContractsRegistryContext = createContext<ContractRegistryInput>(coreContractsRegistry);
|
|
27
|
+
|
|
28
|
+
export type SdkProviderProps = Readonly<{
|
|
29
|
+
sdk?: ReturnType<typeof createSdk>;
|
|
30
|
+
config?: SdkConfig;
|
|
31
|
+
contractsRegistry?: ContractRegistryInput;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export function SdkProvider({ sdk, config, contractsRegistry, children }: SdkProviderProps) {
|
|
36
|
+
const value = useMemo(() => sdk ?? createSdk(config), [sdk, config]);
|
|
37
|
+
const registry = useMemo(() => contractsRegistry ?? coreContractsRegistry, [contractsRegistry]);
|
|
38
|
+
return (
|
|
39
|
+
<SdkContext.Provider value={value}>
|
|
40
|
+
<ContractsRegistryContext.Provider value={registry}>
|
|
41
|
+
{children}
|
|
42
|
+
</ContractsRegistryContext.Provider>
|
|
43
|
+
</SdkContext.Provider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useSdk(): SdkInstance {
|
|
48
|
+
const value = useContext(SdkContext);
|
|
49
|
+
if (!value) throw new Error("useSdk must be used within SdkProvider");
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useContractsRegistry(): ContractRegistryInput {
|
|
54
|
+
return useContext(ContractsRegistryContext);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createSdkProvider<
|
|
58
|
+
Registry extends ContractRegistrySchema = ContractRegistryToSchema<typeof coreContractsRegistry>,
|
|
59
|
+
>() {
|
|
60
|
+
return {
|
|
61
|
+
SdkProvider,
|
|
62
|
+
useSdk,
|
|
63
|
+
useContract: (name: keyof Registry & string) => useContract(name),
|
|
64
|
+
useContractQuery: <
|
|
65
|
+
ContractName extends keyof Registry & string,
|
|
66
|
+
EntryName extends ContractFunctionName<Registry[ContractName]>,
|
|
67
|
+
>(
|
|
68
|
+
name: ContractName,
|
|
69
|
+
entry: EntryName,
|
|
70
|
+
input: ContractQueryInput<ContractFunctionInput<Registry[ContractName], EntryName>>,
|
|
71
|
+
options?: UseContractQueryOptions<ContractFunctionOutput<Registry[ContractName], EntryName>>,
|
|
72
|
+
) => useContractQuery<Registry[ContractName], EntryName>(name, entry, input, options),
|
|
73
|
+
useContractMutation: <
|
|
74
|
+
ContractName extends keyof Registry & string,
|
|
75
|
+
EntryName extends ContractProcedureName<Registry[ContractName]>,
|
|
76
|
+
>(
|
|
77
|
+
name: ContractName,
|
|
78
|
+
options?: UseContractMutationOptions<
|
|
79
|
+
ContractProcedureInput<Registry[ContractName], EntryName>
|
|
80
|
+
>,
|
|
81
|
+
) => useContractMutation<Registry[ContractName], EntryName>(name, options),
|
|
82
|
+
} as const;
|
|
83
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import TestRenderer, { act } from "react-test-renderer";
|
|
3
|
+
import type { WalletConnector } from "../wallet/types.js";
|
|
4
|
+
import { useWallet, WalletProvider } from "./wallet-provider.js";
|
|
5
|
+
|
|
6
|
+
describe("WalletProvider", () => {
|
|
7
|
+
it("connects with an immediate connector and updates state", async () => {
|
|
8
|
+
const connector: WalletConnector = {
|
|
9
|
+
type: "walletconnect",
|
|
10
|
+
isAvailable: () => true,
|
|
11
|
+
connect: async () => ({
|
|
12
|
+
status: "connected",
|
|
13
|
+
accounts: [{ address: "QUBICADDRESS1" }],
|
|
14
|
+
}),
|
|
15
|
+
disconnect: async () => {},
|
|
16
|
+
requestAccounts: async () => [{ address: "QUBICADDRESS1" }],
|
|
17
|
+
signTransaction: async () => ({ signedTxBase64: "" }),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let api: ReturnType<typeof useWallet> | undefined;
|
|
21
|
+
|
|
22
|
+
function Consumer() {
|
|
23
|
+
api = useWallet();
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TestRenderer.create(
|
|
28
|
+
<WalletProvider connectors={[connector]}>
|
|
29
|
+
<Consumer />
|
|
30
|
+
</WalletProvider>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await act(async () => {
|
|
34
|
+
await api?.connect("walletconnect");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(api?.state.status).toBe("connected");
|
|
38
|
+
expect(api?.state.accounts[0]?.address).toBe("QUBICADDRESS1");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles pending walletconnect sessions via approveWalletConnect", async () => {
|
|
42
|
+
const connector: WalletConnector = {
|
|
43
|
+
type: "walletconnect",
|
|
44
|
+
isAvailable: () => true,
|
|
45
|
+
connect: async () => ({
|
|
46
|
+
status: "pending",
|
|
47
|
+
uri: "wc://example",
|
|
48
|
+
approve: async () => [{ address: "QUBICADDRESS2" }],
|
|
49
|
+
}),
|
|
50
|
+
disconnect: async () => {},
|
|
51
|
+
requestAccounts: async () => [{ address: "QUBICADDRESS2" }],
|
|
52
|
+
signTransaction: async () => ({ signedTxBase64: "" }),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let api: ReturnType<typeof useWallet> | undefined;
|
|
56
|
+
|
|
57
|
+
function Consumer() {
|
|
58
|
+
api = useWallet();
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TestRenderer.create(
|
|
63
|
+
<WalletProvider connectors={[connector]}>
|
|
64
|
+
<Consumer />
|
|
65
|
+
</WalletProvider>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await act(async () => {
|
|
69
|
+
await api?.connect("walletconnect");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(api?.state.status).toBe("connecting");
|
|
73
|
+
expect(api?.state.pendingUri).toBe("wc://example");
|
|
74
|
+
|
|
75
|
+
await act(async () => {
|
|
76
|
+
await api?.approveWalletConnect();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(api?.state.status).toBe("connected");
|
|
80
|
+
expect(api?.state.accounts[0]?.address).toBe("QUBICADDRESS2");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
WalletAccount,
|
|
5
|
+
WalletConnector,
|
|
6
|
+
WalletConnectorType,
|
|
7
|
+
WalletConnectResult,
|
|
8
|
+
WalletSession,
|
|
9
|
+
WalletSignTransactionRequest,
|
|
10
|
+
WalletSignTransactionResult,
|
|
11
|
+
WalletStatus,
|
|
12
|
+
} from "../wallet/types.js";
|
|
13
|
+
|
|
14
|
+
export type WalletState = Readonly<{
|
|
15
|
+
status: WalletStatus;
|
|
16
|
+
connectorType?: WalletConnectorType;
|
|
17
|
+
accounts: readonly WalletAccount[];
|
|
18
|
+
pendingUri?: string;
|
|
19
|
+
error?: Error;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
export type WalletProviderProps = Readonly<{
|
|
23
|
+
connectors: readonly WalletConnector[];
|
|
24
|
+
autoConnect?: boolean;
|
|
25
|
+
storageKey?: string;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type WalletContextValue = Readonly<{
|
|
30
|
+
state: WalletState;
|
|
31
|
+
connect(type: WalletConnectorType): Promise<WalletConnectResult | undefined>;
|
|
32
|
+
approveWalletConnect(): Promise<void>;
|
|
33
|
+
disconnect(): Promise<void>;
|
|
34
|
+
requestAccounts(): Promise<readonly WalletAccount[]>;
|
|
35
|
+
signTransaction(input: WalletSignTransactionRequest): Promise<WalletSignTransactionResult>;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
const WalletContext = createContext<WalletContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
export function WalletProvider({
|
|
41
|
+
connectors,
|
|
42
|
+
autoConnect = true,
|
|
43
|
+
storageKey = "qubic.wallet.connector",
|
|
44
|
+
children,
|
|
45
|
+
}: WalletProviderProps) {
|
|
46
|
+
const [state, setState] = useState<WalletState>({
|
|
47
|
+
status: "idle",
|
|
48
|
+
accounts: [],
|
|
49
|
+
});
|
|
50
|
+
const pendingApproval = useRef<(() => Promise<readonly WalletAccount[]>) | null>(null);
|
|
51
|
+
|
|
52
|
+
const connectorMap = useMemo(() => {
|
|
53
|
+
const map = new Map<WalletConnectorType, WalletConnector>();
|
|
54
|
+
for (const connector of connectors) map.set(connector.type, connector);
|
|
55
|
+
return map;
|
|
56
|
+
}, [connectors]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!autoConnect) return;
|
|
60
|
+
const stored = getStorage()?.getItem(storageKey);
|
|
61
|
+
if (!stored) return;
|
|
62
|
+
const connector = connectorMap.get(stored as WalletConnectorType);
|
|
63
|
+
if (!connector || !connector.restore) return;
|
|
64
|
+
connector
|
|
65
|
+
.restore()
|
|
66
|
+
.then((accounts) => {
|
|
67
|
+
if (!accounts) return;
|
|
68
|
+
setState({
|
|
69
|
+
status: "connected",
|
|
70
|
+
connectorType: connector.type,
|
|
71
|
+
accounts,
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.catch((error) => {
|
|
75
|
+
setState({
|
|
76
|
+
status: "error",
|
|
77
|
+
connectorType: connector.type,
|
|
78
|
+
accounts: [],
|
|
79
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}, [autoConnect, connectorMap, storageKey]);
|
|
83
|
+
|
|
84
|
+
const connect = async (type: WalletConnectorType) => {
|
|
85
|
+
const connector = connectorMap.get(type);
|
|
86
|
+
if (!connector) {
|
|
87
|
+
throw new Error(`Unknown wallet connector: ${type}`);
|
|
88
|
+
}
|
|
89
|
+
const available = await connector.isAvailable();
|
|
90
|
+
if (!available) throw new Error(`${type} connector is not available`);
|
|
91
|
+
setState((current) => ({
|
|
92
|
+
status: "connecting",
|
|
93
|
+
connectorType: type,
|
|
94
|
+
accounts: current.accounts,
|
|
95
|
+
}));
|
|
96
|
+
try {
|
|
97
|
+
const result = await connector.connect();
|
|
98
|
+
if (result.status === "pending") {
|
|
99
|
+
pendingApproval.current = result.approve;
|
|
100
|
+
setState((current) => ({
|
|
101
|
+
...current,
|
|
102
|
+
status: "connecting",
|
|
103
|
+
pendingUri: result.uri,
|
|
104
|
+
}));
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
const session: WalletSession = { type, accounts: result.accounts };
|
|
108
|
+
persistConnector(session.type, storageKey);
|
|
109
|
+
setState({
|
|
110
|
+
status: "connected",
|
|
111
|
+
connectorType: session.type,
|
|
112
|
+
accounts: session.accounts,
|
|
113
|
+
});
|
|
114
|
+
return result;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
setState({
|
|
117
|
+
status: "error",
|
|
118
|
+
connectorType: type,
|
|
119
|
+
accounts: [],
|
|
120
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const approveWalletConnect = async () => {
|
|
127
|
+
if (!pendingApproval.current) return;
|
|
128
|
+
try {
|
|
129
|
+
const accounts = await pendingApproval.current();
|
|
130
|
+
const type = state.connectorType ?? "walletconnect";
|
|
131
|
+
persistConnector(type, storageKey);
|
|
132
|
+
setState({
|
|
133
|
+
status: "connected",
|
|
134
|
+
connectorType: type,
|
|
135
|
+
accounts,
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
setState((current) => ({
|
|
139
|
+
...current,
|
|
140
|
+
status: "error",
|
|
141
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
142
|
+
}));
|
|
143
|
+
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
pendingApproval.current = null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const disconnect = async () => {
|
|
150
|
+
if (!state.connectorType) return;
|
|
151
|
+
const connector = connectorMap.get(state.connectorType);
|
|
152
|
+
if (connector) {
|
|
153
|
+
await connector.disconnect();
|
|
154
|
+
}
|
|
155
|
+
pendingApproval.current = null;
|
|
156
|
+
setState({ status: "idle", accounts: [] });
|
|
157
|
+
clearConnector(storageKey);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const requestAccounts = async () => {
|
|
161
|
+
if (!state.connectorType) throw new Error("No connector selected");
|
|
162
|
+
const connector = connectorMap.get(state.connectorType);
|
|
163
|
+
if (!connector) throw new Error("Connector not available");
|
|
164
|
+
const accounts = await connector.requestAccounts();
|
|
165
|
+
setState((current) => ({ ...current, accounts }));
|
|
166
|
+
return accounts;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const signTransaction = async (input: WalletSignTransactionRequest) => {
|
|
170
|
+
if (!state.connectorType) throw new Error("No connector selected");
|
|
171
|
+
const connector = connectorMap.get(state.connectorType);
|
|
172
|
+
if (!connector) throw new Error("Connector not available");
|
|
173
|
+
return connector.signTransaction(input);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const value: WalletContextValue = {
|
|
177
|
+
state,
|
|
178
|
+
connect,
|
|
179
|
+
approveWalletConnect,
|
|
180
|
+
disconnect,
|
|
181
|
+
requestAccounts,
|
|
182
|
+
signTransaction,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function useWallet() {
|
|
189
|
+
const value = useContext(WalletContext);
|
|
190
|
+
if (!value) throw new Error("useWallet must be used within WalletProvider");
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getStorage(): Storage | undefined {
|
|
195
|
+
const anyGlobal = globalThis as typeof globalThis & { localStorage?: Storage };
|
|
196
|
+
return anyGlobal.localStorage;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function persistConnector(type: WalletConnectorType, storageKey: string) {
|
|
200
|
+
const storage = getStorage();
|
|
201
|
+
if (!storage) return;
|
|
202
|
+
storage.setItem(storageKey, type);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function clearConnector(storageKey: string) {
|
|
206
|
+
const storage = getStorage();
|
|
207
|
+
if (!storage) return;
|
|
208
|
+
storage.removeItem(storageKey);
|
|
209
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const queryKeys = {
|
|
2
|
+
tickInfo: () => ["qubic", "tickInfo"] as const,
|
|
3
|
+
lastProcessedTick: () => ["qubic", "lastProcessedTick"] as const,
|
|
4
|
+
balance: (identity: string) => ["qubic", "balance", identity] as const,
|
|
5
|
+
transactions: (identity: string, paramsKey = "default") =>
|
|
6
|
+
["qubic", "transactions", identity, paramsKey] as const,
|
|
7
|
+
contractQuery: (contract: string, entry: string, argsKey: string) =>
|
|
8
|
+
["qubic", "contracts", contract, entry, argsKey] as const,
|
|
9
|
+
};
|