@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,77 @@
|
|
|
1
|
+
export type ContractEntryKind = "function" | "procedure";
|
|
2
|
+
|
|
3
|
+
export type ContractEntry = Readonly<{
|
|
4
|
+
kind: ContractEntryKind;
|
|
5
|
+
name: string;
|
|
6
|
+
inputType: number;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
inputTypeName?: string;
|
|
9
|
+
outputTypeName?: string;
|
|
10
|
+
inputSize?: number;
|
|
11
|
+
outputSize?: number;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type ContractIoFieldDefinition = Readonly<{
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
byteOffset?: number;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export type ContractIoTypeDefinition =
|
|
21
|
+
| Readonly<{
|
|
22
|
+
kind: "struct";
|
|
23
|
+
name: string;
|
|
24
|
+
fields: readonly ContractIoFieldDefinition[];
|
|
25
|
+
byteSize?: number;
|
|
26
|
+
byteAlign?: number;
|
|
27
|
+
}>
|
|
28
|
+
| Readonly<{
|
|
29
|
+
kind: "alias";
|
|
30
|
+
name: string;
|
|
31
|
+
target: string;
|
|
32
|
+
byteSize?: number;
|
|
33
|
+
byteAlign?: number;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
export type ContractDefinition = Readonly<{
|
|
37
|
+
name: string;
|
|
38
|
+
contractIndex: number;
|
|
39
|
+
address: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
filename?: string;
|
|
42
|
+
githubUrl?: string;
|
|
43
|
+
website?: string;
|
|
44
|
+
proposalUrl?: string;
|
|
45
|
+
entries: readonly ContractEntry[];
|
|
46
|
+
ioTypes?: readonly ContractIoTypeDefinition[];
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export type ContractsRegistry = Readonly<{
|
|
50
|
+
contracts: readonly ContractDefinition[];
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
export declare const coreContractsRegistry: ContractsRegistry;
|
|
54
|
+
|
|
55
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
56
|
+
|
|
57
|
+
export declare function encodeContractEntryInputData(input: Readonly<{
|
|
58
|
+
registry: ContractsRegistry | readonly ContractDefinition[];
|
|
59
|
+
contractName: string;
|
|
60
|
+
entryName: string;
|
|
61
|
+
kind?: ContractEntryKind | undefined;
|
|
62
|
+
value: unknown;
|
|
63
|
+
allowSequentialLayout?: boolean | undefined;
|
|
64
|
+
}>): Readonly<{
|
|
65
|
+
bytes: Uint8Array;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
export declare function decodeContractEntryInputData<Value = unknown>(input: Readonly<{
|
|
69
|
+
registry: ContractsRegistry | readonly ContractDefinition[];
|
|
70
|
+
contractName: string;
|
|
71
|
+
entryName: string;
|
|
72
|
+
kind?: ContractEntryKind | undefined;
|
|
73
|
+
bytes: Uint8Array;
|
|
74
|
+
allowSequentialLayout?: boolean | undefined;
|
|
75
|
+
}>): Readonly<{
|
|
76
|
+
value: Value;
|
|
77
|
+
}>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
export type SdkConfig = Readonly<{
|
|
2
|
+
txQueue?: Readonly<{
|
|
3
|
+
enabled?: boolean | undefined;
|
|
4
|
+
}> | undefined;
|
|
5
|
+
}>;
|
|
6
|
+
|
|
7
|
+
export type SeedSourceInput =
|
|
8
|
+
| Readonly<{
|
|
9
|
+
fromSeed: string;
|
|
10
|
+
fromVault?: never;
|
|
11
|
+
}>
|
|
12
|
+
| Readonly<{
|
|
13
|
+
fromVault: string;
|
|
14
|
+
fromSeed?: never;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
export type QueryRawResult = Readonly<{
|
|
18
|
+
responseBytes: Uint8Array;
|
|
19
|
+
responseBase64: string;
|
|
20
|
+
attempts: number;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export type SendTransactionResult = Readonly<{
|
|
24
|
+
txBytes: Uint8Array;
|
|
25
|
+
txId: string;
|
|
26
|
+
networkTxId: string;
|
|
27
|
+
targetTick: bigint;
|
|
28
|
+
broadcast: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type QueryRawInput = Readonly<{
|
|
32
|
+
contractIndex: number | bigint;
|
|
33
|
+
inputType: number | bigint;
|
|
34
|
+
inputBytes?: Uint8Array | undefined;
|
|
35
|
+
inputBase64?: string | undefined;
|
|
36
|
+
expectedOutputSize?: number | undefined;
|
|
37
|
+
retries?: number | undefined;
|
|
38
|
+
retryDelayMs?: number | undefined;
|
|
39
|
+
signal?: AbortSignal | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type SendTransactionInput = SeedSourceInput &
|
|
43
|
+
Readonly<{
|
|
44
|
+
toIdentity: string;
|
|
45
|
+
amount: bigint;
|
|
46
|
+
targetTick?: number | bigint | undefined;
|
|
47
|
+
inputType?: number | undefined;
|
|
48
|
+
inputBytes?: Uint8Array | undefined;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export type BuildSignedTransferInput = SeedSourceInput &
|
|
52
|
+
Readonly<{
|
|
53
|
+
toIdentity: string;
|
|
54
|
+
amount: bigint;
|
|
55
|
+
targetTick?: bigint | number | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
export type SendAndConfirmInput = BuildSignedTransferInput &
|
|
59
|
+
Readonly<{
|
|
60
|
+
timeoutMs?: number | undefined;
|
|
61
|
+
pollIntervalMs?: number | undefined;
|
|
62
|
+
signal?: AbortSignal | undefined;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
export type SendTransferResult = Readonly<{
|
|
66
|
+
txBytes: Uint8Array;
|
|
67
|
+
txId: string;
|
|
68
|
+
networkTxId: string;
|
|
69
|
+
targetTick: bigint;
|
|
70
|
+
broadcast: unknown;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
export type TickInfo = Readonly<{
|
|
74
|
+
tick: bigint;
|
|
75
|
+
duration: bigint;
|
|
76
|
+
epoch: bigint;
|
|
77
|
+
initialTick: bigint;
|
|
78
|
+
}>;
|
|
79
|
+
|
|
80
|
+
export type LiveBalance = Readonly<{
|
|
81
|
+
id: string;
|
|
82
|
+
balance: bigint;
|
|
83
|
+
validForTick: bigint;
|
|
84
|
+
latestIncomingTransferTick: bigint;
|
|
85
|
+
latestOutgoingTransferTick: bigint;
|
|
86
|
+
incomingAmount: bigint;
|
|
87
|
+
outgoingAmount: bigint;
|
|
88
|
+
numberOfIncomingTransfers: bigint;
|
|
89
|
+
numberOfOutgoingTransfers: bigint;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
export type LastProcessedTick = Readonly<{
|
|
93
|
+
tickNumber: bigint;
|
|
94
|
+
epoch: bigint;
|
|
95
|
+
intervalInitialTick: bigint;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
export type QueryTransaction = Readonly<{
|
|
99
|
+
hash: string;
|
|
100
|
+
amount: bigint;
|
|
101
|
+
source: string;
|
|
102
|
+
destination: string;
|
|
103
|
+
tickNumber: bigint;
|
|
104
|
+
timestamp: bigint;
|
|
105
|
+
inputType: bigint;
|
|
106
|
+
inputSize: bigint;
|
|
107
|
+
inputData: string;
|
|
108
|
+
signature: string;
|
|
109
|
+
moneyFlew?: boolean;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
export type Hits = Readonly<{
|
|
113
|
+
total: bigint;
|
|
114
|
+
from: bigint;
|
|
115
|
+
size: bigint;
|
|
116
|
+
}>;
|
|
117
|
+
|
|
118
|
+
export type Range = Readonly<{
|
|
119
|
+
gt?: string | undefined;
|
|
120
|
+
gte?: string | undefined;
|
|
121
|
+
lt?: string | undefined;
|
|
122
|
+
lte?: string | undefined;
|
|
123
|
+
}>;
|
|
124
|
+
|
|
125
|
+
export type Pagination = Readonly<{
|
|
126
|
+
offset?: bigint | undefined;
|
|
127
|
+
size?: bigint | undefined;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
export type TransactionsForIdentityRequest = Readonly<{
|
|
131
|
+
identity: string;
|
|
132
|
+
filters?: Readonly<Record<string, string>> | undefined;
|
|
133
|
+
ranges?: Readonly<Record<string, Range>> | undefined;
|
|
134
|
+
pagination?: Pagination | undefined;
|
|
135
|
+
}>;
|
|
136
|
+
|
|
137
|
+
export type TransactionsForIdentityResponse = Readonly<{
|
|
138
|
+
validForTick: bigint;
|
|
139
|
+
hits: Hits;
|
|
140
|
+
transactions: readonly QueryTransaction[];
|
|
141
|
+
}>;
|
|
142
|
+
|
|
143
|
+
export type VaultSummary = Readonly<{
|
|
144
|
+
name: string;
|
|
145
|
+
identity: string;
|
|
146
|
+
seedIndex: number;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
updatedAt: string;
|
|
149
|
+
}>;
|
|
150
|
+
|
|
151
|
+
export type VaultEntryEncrypted = Readonly<{
|
|
152
|
+
nonceBase64: string;
|
|
153
|
+
ciphertextBase64: string;
|
|
154
|
+
tagBase64: string;
|
|
155
|
+
}>;
|
|
156
|
+
|
|
157
|
+
export type VaultEntry = Readonly<{
|
|
158
|
+
name: string;
|
|
159
|
+
identity: string;
|
|
160
|
+
seedIndex: number;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
updatedAt: string;
|
|
163
|
+
encrypted: VaultEntryEncrypted;
|
|
164
|
+
}>;
|
|
165
|
+
|
|
166
|
+
export type VaultKdfParams = Readonly<Record<string, unknown>>;
|
|
167
|
+
|
|
168
|
+
export type VaultHeader = Readonly<{
|
|
169
|
+
vaultVersion: number;
|
|
170
|
+
kdf: Readonly<{
|
|
171
|
+
name: string;
|
|
172
|
+
params: VaultKdfParams;
|
|
173
|
+
}>;
|
|
174
|
+
}>;
|
|
175
|
+
|
|
176
|
+
export type VaultExport = VaultHeader &
|
|
177
|
+
Readonly<{
|
|
178
|
+
entries: readonly VaultEntry[];
|
|
179
|
+
}>;
|
|
180
|
+
|
|
181
|
+
export type VaultStore = Readonly<{
|
|
182
|
+
read(): Promise<string | null>;
|
|
183
|
+
write(value: string): Promise<void>;
|
|
184
|
+
remove?(): Promise<void>;
|
|
185
|
+
label?: string;
|
|
186
|
+
}>;
|
|
187
|
+
|
|
188
|
+
export type OpenSeedVaultBrowserInput = Readonly<{
|
|
189
|
+
passphrase: string;
|
|
190
|
+
create?: boolean | undefined;
|
|
191
|
+
autoSave?: boolean | undefined;
|
|
192
|
+
kdfParams?: Readonly<Record<string, unknown>> | undefined;
|
|
193
|
+
store: VaultStore;
|
|
194
|
+
path?: string | undefined;
|
|
195
|
+
}>;
|
|
196
|
+
|
|
197
|
+
export type OpenSeedVaultInput = Readonly<{
|
|
198
|
+
path: string;
|
|
199
|
+
passphrase: string;
|
|
200
|
+
create?: boolean | undefined;
|
|
201
|
+
autoSave?: boolean | undefined;
|
|
202
|
+
lock?: boolean | undefined;
|
|
203
|
+
lockTimeoutMs?: number | undefined;
|
|
204
|
+
kdfParams?: Readonly<Record<string, unknown>> | undefined;
|
|
205
|
+
}>;
|
|
206
|
+
|
|
207
|
+
export class VaultError extends Error {}
|
|
208
|
+
export class VaultNotFoundError extends VaultError {}
|
|
209
|
+
export class VaultInvalidPassphraseError extends VaultError {}
|
|
210
|
+
export class VaultEntryNotFoundError extends VaultError {}
|
|
211
|
+
export class VaultEntryExistsError extends VaultError {}
|
|
212
|
+
|
|
213
|
+
export type SeedVault = Readonly<{
|
|
214
|
+
path: string;
|
|
215
|
+
list(): readonly VaultSummary[];
|
|
216
|
+
getSeed(ref: string): Promise<string>;
|
|
217
|
+
importEncrypted(
|
|
218
|
+
input: VaultExport | string,
|
|
219
|
+
options?: Readonly<{ mode?: "merge" | "replace"; sourcePassphrase?: string }>,
|
|
220
|
+
): Promise<void>;
|
|
221
|
+
}>;
|
|
222
|
+
|
|
223
|
+
export declare function openSeedVaultBrowser(input: OpenSeedVaultBrowserInput): Promise<SeedVault>;
|
|
224
|
+
export declare function createLocalStorageVaultStore(key: string, storage?: Storage): VaultStore;
|
|
225
|
+
export declare function createMemoryVaultStore(label?: string): VaultStore;
|
|
226
|
+
export declare function openSeedVault(input: OpenSeedVaultInput): Promise<SeedVault>;
|
|
227
|
+
export declare function vaultExists(path: string): Promise<boolean>;
|
|
228
|
+
|
|
229
|
+
export type SdkInstance = Readonly<{
|
|
230
|
+
rpc: Readonly<{
|
|
231
|
+
live: Readonly<{
|
|
232
|
+
tickInfo(): Promise<TickInfo>;
|
|
233
|
+
balance(identity: string): Promise<LiveBalance>;
|
|
234
|
+
}>;
|
|
235
|
+
query: Readonly<{
|
|
236
|
+
getLastProcessedTick(): Promise<LastProcessedTick>;
|
|
237
|
+
getTransactionsForIdentity(
|
|
238
|
+
input: TransactionsForIdentityRequest,
|
|
239
|
+
): Promise<TransactionsForIdentityResponse>;
|
|
240
|
+
}>;
|
|
241
|
+
}>;
|
|
242
|
+
contracts: Readonly<{
|
|
243
|
+
queryRaw(input: QueryRawInput): Promise<QueryRawResult>;
|
|
244
|
+
}>;
|
|
245
|
+
transactions: Readonly<{
|
|
246
|
+
send(input: SendTransactionInput): Promise<SendTransactionResult>;
|
|
247
|
+
}>;
|
|
248
|
+
transfers: Readonly<{
|
|
249
|
+
send(input: BuildSignedTransferInput): Promise<SendTransferResult>;
|
|
250
|
+
sendAndConfirm(input: SendAndConfirmInput): Promise<SendTransferResult>;
|
|
251
|
+
}>;
|
|
252
|
+
}>;
|
|
253
|
+
|
|
254
|
+
export declare function createSdk(config?: SdkConfig): SdkInstance;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLocalStorageVaultStore as createLocalStorageVaultStoreSdk,
|
|
3
|
+
createMemoryVaultStore as createMemoryVaultStoreSdk,
|
|
4
|
+
openSeedVaultBrowser,
|
|
5
|
+
} from "@qubic.ts/sdk/browser";
|
|
6
|
+
import type {
|
|
7
|
+
OpenSeedVaultBrowserInput,
|
|
8
|
+
SeedVault,
|
|
9
|
+
VaultExport,
|
|
10
|
+
VaultStore,
|
|
11
|
+
} from "@qubic.ts/sdk/browser";
|
|
12
|
+
|
|
13
|
+
export type CreateBrowserVaultInput = Omit<OpenSeedVaultBrowserInput, "create">;
|
|
14
|
+
|
|
15
|
+
export async function openBrowserVault(input: OpenSeedVaultBrowserInput): Promise<SeedVault> {
|
|
16
|
+
return openSeedVaultBrowser(input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function createBrowserVault(input: CreateBrowserVaultInput): Promise<SeedVault> {
|
|
20
|
+
return openBrowserVault({ ...input, create: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function importBrowserVault(
|
|
24
|
+
input: Readonly<{
|
|
25
|
+
store: VaultStore;
|
|
26
|
+
passphrase: string;
|
|
27
|
+
exportData: VaultExport | string;
|
|
28
|
+
mode?: "merge" | "replace";
|
|
29
|
+
sourcePassphrase?: string;
|
|
30
|
+
}>,
|
|
31
|
+
): Promise<SeedVault> {
|
|
32
|
+
const vault = await openBrowserVault({
|
|
33
|
+
store: input.store,
|
|
34
|
+
passphrase: input.passphrase,
|
|
35
|
+
create: true,
|
|
36
|
+
});
|
|
37
|
+
await vault.importEncrypted(input.exportData, {
|
|
38
|
+
...(input.mode === undefined ? {} : { mode: input.mode }),
|
|
39
|
+
...(input.sourcePassphrase === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { sourcePassphrase: input.sourcePassphrase }),
|
|
42
|
+
});
|
|
43
|
+
return vault;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createLocalStorageVaultStore(key: string): VaultStore {
|
|
47
|
+
return createLocalStorageVaultStoreSdk(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createMemoryVaultStore(label?: string): VaultStore {
|
|
51
|
+
return createMemoryVaultStoreSdk(label);
|
|
52
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { openSeedVault, vaultExists as vaultExistsSdk } from "@qubic.ts/sdk/node";
|
|
2
|
+
import type { OpenSeedVaultInput, SeedVault, VaultExport } from "@qubic.ts/sdk/node";
|
|
3
|
+
|
|
4
|
+
export type CreateVaultInput = Omit<OpenSeedVaultInput, "create">;
|
|
5
|
+
|
|
6
|
+
export async function openNodeVault(input: OpenSeedVaultInput): Promise<SeedVault> {
|
|
7
|
+
return openSeedVault(input);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function createNodeVault(input: CreateVaultInput): Promise<SeedVault> {
|
|
11
|
+
return openSeedVault({ ...input, create: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function importNodeVault(
|
|
15
|
+
input: Readonly<{
|
|
16
|
+
path: string;
|
|
17
|
+
passphrase: string;
|
|
18
|
+
exportData: VaultExport | string;
|
|
19
|
+
mode?: "merge" | "replace";
|
|
20
|
+
sourcePassphrase?: string;
|
|
21
|
+
}>,
|
|
22
|
+
): Promise<SeedVault> {
|
|
23
|
+
const vault = await openNodeVault({
|
|
24
|
+
path: input.path,
|
|
25
|
+
passphrase: input.passphrase,
|
|
26
|
+
create: true,
|
|
27
|
+
});
|
|
28
|
+
await vault.importEncrypted(input.exportData, {
|
|
29
|
+
...(input.mode === undefined ? {} : { mode: input.mode }),
|
|
30
|
+
...(input.sourcePassphrase === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { sourcePassphrase: input.sourcePassphrase }),
|
|
33
|
+
});
|
|
34
|
+
return vault;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function vaultExists(path: string): Promise<boolean> {
|
|
38
|
+
return vaultExistsSdk(path);
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as browserRuntime from "../browser.js";
|
|
3
|
+
import * as nodeRuntime from "../node.js";
|
|
4
|
+
|
|
5
|
+
describe("vault runtime boundaries", () => {
|
|
6
|
+
it("browser runtime exports browser vault helpers only", () => {
|
|
7
|
+
expect(typeof browserRuntime.openBrowserVault).toBe("function");
|
|
8
|
+
expect(typeof browserRuntime.createBrowserVault).toBe("function");
|
|
9
|
+
expect(typeof browserRuntime.importBrowserVault).toBe("function");
|
|
10
|
+
expect(typeof browserRuntime.createLocalStorageVaultStore).toBe("function");
|
|
11
|
+
expect(typeof browserRuntime.createMemoryVaultStore).toBe("function");
|
|
12
|
+
expect("openNodeVault" in browserRuntime).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("node runtime exports node vault helpers only", () => {
|
|
16
|
+
expect(typeof nodeRuntime.openNodeVault).toBe("function");
|
|
17
|
+
expect(typeof nodeRuntime.createNodeVault).toBe("function");
|
|
18
|
+
expect(typeof nodeRuntime.importNodeVault).toBe("function");
|
|
19
|
+
expect(typeof nodeRuntime.vaultExists).toBe("function");
|
|
20
|
+
expect("openBrowserVault" in nodeRuntime).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { MetaMaskSnapConnector } from "./metamask-snap.js";
|
|
3
|
+
|
|
4
|
+
type FakeProvider = {
|
|
5
|
+
request: (input: { method: string; params?: unknown }) => Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe("MetaMaskSnapConnector", () => {
|
|
9
|
+
const previous = (globalThis as typeof globalThis & { ethereum?: FakeProvider | undefined })
|
|
10
|
+
.ethereum;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
(globalThis as typeof globalThis & { ethereum?: FakeProvider }).ethereum = {
|
|
14
|
+
request: async ({ method }) => {
|
|
15
|
+
if (method === "wallet_requestSnaps") return {};
|
|
16
|
+
if (method === "wallet_invokeSnap") {
|
|
17
|
+
return [{ address: "QUBICADDRESS1", alias: "main" }, { address: "QUBICADDRESS2" }];
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
const anyGlobal = globalThis as typeof globalThis & { ethereum?: FakeProvider | undefined };
|
|
26
|
+
if (previous) {
|
|
27
|
+
anyGlobal.ethereum = previous;
|
|
28
|
+
} else {
|
|
29
|
+
delete anyGlobal.ethereum;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("detects availability", () => {
|
|
34
|
+
const connector = new MetaMaskSnapConnector();
|
|
35
|
+
expect(connector.isAvailable()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("connects and requests accounts", async () => {
|
|
39
|
+
const connector = new MetaMaskSnapConnector();
|
|
40
|
+
const result = await connector.connect();
|
|
41
|
+
expect(result.status).toBe("connected");
|
|
42
|
+
if (result.status !== "connected") {
|
|
43
|
+
throw new Error("Expected immediate connected result from snap connector");
|
|
44
|
+
}
|
|
45
|
+
expect(result.accounts.length).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("requests accounts", async () => {
|
|
49
|
+
const connector = new MetaMaskSnapConnector();
|
|
50
|
+
const accounts = await connector.requestAccounts();
|
|
51
|
+
expect(accounts.length).toBe(2);
|
|
52
|
+
expect(accounts[0]?.address).toBe("QUBICADDRESS1");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("signs transactions", async () => {
|
|
56
|
+
const signed = Buffer.from([1, 2, 3]).toString("base64");
|
|
57
|
+
(globalThis as typeof globalThis & { ethereum?: FakeProvider }).ethereum = {
|
|
58
|
+
request: async ({ method }) => {
|
|
59
|
+
if (method === "wallet_requestSnaps") return {};
|
|
60
|
+
if (method === "wallet_invokeSnap") return { signedTx: signed };
|
|
61
|
+
return {};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const connector = new MetaMaskSnapConnector();
|
|
65
|
+
const result = await connector.signTransaction({
|
|
66
|
+
kind: "snap",
|
|
67
|
+
txBytes: new Uint8Array([9, 9, 9]),
|
|
68
|
+
signatureOffset: 0,
|
|
69
|
+
});
|
|
70
|
+
expect(result.signedTxBase64).toBe(signed);
|
|
71
|
+
expect(result.signedTxBytes?.length).toBe(3);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { MetaMaskInpageProvider } from "@metamask/providers";
|
|
2
|
+
import type {
|
|
3
|
+
WalletAccount,
|
|
4
|
+
WalletConnector,
|
|
5
|
+
WalletConnectResult,
|
|
6
|
+
WalletSignTransactionRequest,
|
|
7
|
+
WalletSignTransactionResult,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { base64ToBytes, bytesToBase64 } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
export type MetaMaskSnapConfig = Readonly<{
|
|
12
|
+
snapId?: string;
|
|
13
|
+
requestAccountsMethod?: string;
|
|
14
|
+
signTransactionMethod?: string;
|
|
15
|
+
accountIndex?: number;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export class MetaMaskSnapConnector implements WalletConnector {
|
|
19
|
+
readonly type = "metamask-snap" as const;
|
|
20
|
+
private readonly snapId: string;
|
|
21
|
+
private readonly requestAccountsMethod: string;
|
|
22
|
+
private readonly signTransactionMethod: string;
|
|
23
|
+
private readonly accountIndex: number | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(config: MetaMaskSnapConfig = {}) {
|
|
26
|
+
this.snapId = config.snapId ?? "npm:@qubic-lib/qubic-mm-snap";
|
|
27
|
+
this.requestAccountsMethod = config.requestAccountsMethod ?? "getAccounts";
|
|
28
|
+
this.signTransactionMethod = config.signTransactionMethod ?? "signTransaction";
|
|
29
|
+
this.accountIndex = config.accountIndex;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isAvailable(): boolean {
|
|
33
|
+
return Boolean(this.getProvider());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async connect(): Promise<WalletConnectResult> {
|
|
37
|
+
const provider = this.requireProvider();
|
|
38
|
+
await provider.request({
|
|
39
|
+
method: "wallet_requestSnaps",
|
|
40
|
+
params: {
|
|
41
|
+
[this.snapId]: {},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const accounts = await this.requestAccounts();
|
|
45
|
+
return { status: "connected", accounts };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async disconnect(): Promise<void> {
|
|
49
|
+
// Snaps do not expose an explicit disconnect; clear local state in provider.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async requestAccounts(): Promise<readonly WalletAccount[]> {
|
|
53
|
+
const provider = this.requireProvider();
|
|
54
|
+
try {
|
|
55
|
+
const result = await provider.request({
|
|
56
|
+
method: "wallet_invokeSnap",
|
|
57
|
+
params: {
|
|
58
|
+
snapId: this.snapId,
|
|
59
|
+
request: {
|
|
60
|
+
method: this.requestAccountsMethod,
|
|
61
|
+
params: {
|
|
62
|
+
accountIdx: this.accountIndex ?? 0,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (Array.isArray(result)) {
|
|
68
|
+
return result
|
|
69
|
+
.filter((entry) => entry && typeof entry.address === "string")
|
|
70
|
+
.map((entry) =>
|
|
71
|
+
entry.alias === undefined
|
|
72
|
+
? { address: entry.address }
|
|
73
|
+
: { address: entry.address, alias: entry.alias },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async signTransaction(input: WalletSignTransactionRequest): Promise<WalletSignTransactionResult> {
|
|
83
|
+
if (input.kind !== "snap") {
|
|
84
|
+
throw new Error("MetaMask Snap expects kind: 'snap'");
|
|
85
|
+
}
|
|
86
|
+
const provider = this.requireProvider();
|
|
87
|
+
const signed = await provider.request({
|
|
88
|
+
method: "wallet_invokeSnap",
|
|
89
|
+
params: {
|
|
90
|
+
snapId: this.snapId,
|
|
91
|
+
request: {
|
|
92
|
+
method: this.signTransactionMethod,
|
|
93
|
+
params: {
|
|
94
|
+
base64Tx: bytesToBase64(input.txBytes),
|
|
95
|
+
offset: input.signatureOffset,
|
|
96
|
+
accountIdx: input.accountIndex ?? this.accountIndex ?? 0,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const result = signed as { signedTx?: string } | undefined;
|
|
102
|
+
const signedBase64 = result?.signedTx ?? String(signed);
|
|
103
|
+
return {
|
|
104
|
+
signedTxBase64: signedBase64,
|
|
105
|
+
signedTxBytes: base64ToBytes(signedBase64),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getProvider(): MetaMaskInpageProvider | undefined {
|
|
110
|
+
const anyGlobal = globalThis as typeof globalThis & { ethereum?: MetaMaskInpageProvider };
|
|
111
|
+
return anyGlobal.ethereum;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private requireProvider(): MetaMaskInpageProvider {
|
|
115
|
+
const provider = this.getProvider();
|
|
116
|
+
if (!provider) {
|
|
117
|
+
throw new Error("MetaMask provider not available");
|
|
118
|
+
}
|
|
119
|
+
return provider;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type WalletConnectorType = "metamask-snap" | "walletconnect" | "vault";
|
|
2
|
+
|
|
3
|
+
export type WalletStatus = "idle" | "connecting" | "connected" | "error";
|
|
4
|
+
|
|
5
|
+
export type WalletAccount = Readonly<{
|
|
6
|
+
address: string;
|
|
7
|
+
alias?: string;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type WalletSession = Readonly<{
|
|
11
|
+
type: WalletConnectorType;
|
|
12
|
+
accounts: readonly WalletAccount[];
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
export type WalletSignTransactionRequest =
|
|
16
|
+
| Readonly<{
|
|
17
|
+
kind: "snap";
|
|
18
|
+
txBytes: Uint8Array;
|
|
19
|
+
signatureOffset: number;
|
|
20
|
+
accountIndex?: number;
|
|
21
|
+
}>
|
|
22
|
+
| Readonly<{
|
|
23
|
+
kind: "walletconnect";
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
amount: number;
|
|
27
|
+
tick?: number;
|
|
28
|
+
inputType?: number;
|
|
29
|
+
payloadBase64?: string | null;
|
|
30
|
+
nonce?: string;
|
|
31
|
+
}>
|
|
32
|
+
| Readonly<{
|
|
33
|
+
kind: "vault";
|
|
34
|
+
unsignedTxBytes: Uint8Array;
|
|
35
|
+
vaultRef?: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export type WalletSignTransactionResult = Readonly<{
|
|
39
|
+
signedTxBase64: string;
|
|
40
|
+
signedTxBytes?: Uint8Array;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
export type WalletConnectResult =
|
|
44
|
+
| Readonly<{ status: "connected"; accounts: readonly WalletAccount[] }>
|
|
45
|
+
| Readonly<{ status: "pending"; uri: string; approve: () => Promise<readonly WalletAccount[]> }>;
|
|
46
|
+
|
|
47
|
+
export type WalletConnector = Readonly<{
|
|
48
|
+
type: WalletConnectorType;
|
|
49
|
+
isAvailable(): boolean | Promise<boolean>;
|
|
50
|
+
connect(): Promise<WalletConnectResult>;
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
requestAccounts(): Promise<readonly WalletAccount[]>;
|
|
53
|
+
signTransaction(input: WalletSignTransactionRequest): Promise<WalletSignTransactionResult>;
|
|
54
|
+
restore?(): Promise<readonly WalletAccount[] | null>;
|
|
55
|
+
}>;
|