@ic-reactor/core 3.0.7-beta.1 → 3.0.7-beta.2
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/README.md +5 -3
- package/package.json +2 -1
- package/src/client.ts +570 -0
- package/src/display/helper.ts +92 -0
- package/src/display/index.ts +3 -0
- package/src/display/types.ts +91 -0
- package/src/display/visitor.ts +410 -0
- package/src/display-reactor.ts +409 -0
- package/src/errors/index.ts +246 -0
- package/src/index.ts +8 -0
- package/src/reactor.ts +461 -0
- package/src/types/client.ts +110 -0
- package/src/types/display-reactor.ts +73 -0
- package/src/types/index.ts +6 -0
- package/src/types/reactor.ts +184 -0
- package/src/types/result.ts +50 -0
- package/src/types/transform.ts +23 -0
- package/src/types/variant.ts +39 -0
- package/src/utils/agent.ts +201 -0
- package/src/utils/candid.ts +112 -0
- package/src/utils/constants.ts +12 -0
- package/src/utils/helper.ts +136 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/polling.ts +330 -0
- package/src/version.ts +4 -0
package/README.md
CHANGED
|
@@ -92,7 +92,8 @@ import { idlFactory, type _SERVICE } from "./declarations/my_canister"
|
|
|
92
92
|
const backend = new Reactor<_SERVICE>({
|
|
93
93
|
clientManager,
|
|
94
94
|
idlFactory,
|
|
95
|
-
|
|
95
|
+
name: "backend", // Required: explicit name
|
|
96
|
+
// canisterId: "...", // Optional: omitted if using environment variables
|
|
96
97
|
})
|
|
97
98
|
```
|
|
98
99
|
|
|
@@ -131,6 +132,7 @@ interface ClientManagerParameters {
|
|
|
131
132
|
port?: number // Local replica port (default: 4943)
|
|
132
133
|
withLocalEnv?: boolean // Force local network
|
|
133
134
|
withProcessEnv?: boolean // Read DFX_NETWORK from env
|
|
135
|
+
withCanisterEnv?: boolean // Read canister IDs from environment
|
|
134
136
|
agentOptions?: HttpAgentOptions // Custom agent options
|
|
135
137
|
authClient?: AuthClient // Pre-configured auth client
|
|
136
138
|
}
|
|
@@ -199,8 +201,8 @@ clientManager.isLocal // boolean
|
|
|
199
201
|
interface ReactorParameters<A> {
|
|
200
202
|
clientManager: ClientManager
|
|
201
203
|
idlFactory: IDL.InterfaceFactory
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
name: string // Required display name
|
|
205
|
+
canisterId?: string | Principal // Optional if using env vars
|
|
204
206
|
pollingOptions?: PollingOptions // Custom polling for update calls
|
|
205
207
|
}
|
|
206
208
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/core",
|
|
3
|
-
"version": "3.0.7-beta.
|
|
3
|
+
"version": "3.0.7-beta.2",
|
|
4
4
|
"description": "IC Reactor Core Library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"src",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
|
21
22
|
"repository": {
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import type { Identity } from "@icp-sdk/core/agent"
|
|
2
|
+
import type { AuthClient, AuthClientLoginOptions } from "@icp-sdk/auth/client"
|
|
3
|
+
import type {
|
|
4
|
+
ClientManagerParameters,
|
|
5
|
+
AgentState,
|
|
6
|
+
AuthState,
|
|
7
|
+
} from "./types/client"
|
|
8
|
+
import type { Principal } from "@icp-sdk/core/principal"
|
|
9
|
+
import type { QueryClient } from "@tanstack/react-query"
|
|
10
|
+
|
|
11
|
+
import { HttpAgent } from "@icp-sdk/core/agent"
|
|
12
|
+
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
|
|
13
|
+
import {
|
|
14
|
+
IC_HOST_NETWORK_URI,
|
|
15
|
+
IC_INTERNET_IDENTITY_PROVIDER,
|
|
16
|
+
LOCAL_INTERNET_IDENTITY_PROVIDER,
|
|
17
|
+
} from "./utils/constants"
|
|
18
|
+
import { getNetworkByHostname, getProcessEnvNetwork } from "./utils/helper"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ClientManager is a central class for managing the Internet Computer (IC) agent and authentication state.
|
|
22
|
+
*
|
|
23
|
+
* It initializes the agent (connecting to local or mainnet), handles authentication via AuthClient,
|
|
24
|
+
* and integrates with TanStack Query's QueryClient for state management.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { ClientManager } from "@ic-reactor/core";
|
|
29
|
+
* import { QueryClient } from "@tanstack/react-query";
|
|
30
|
+
*
|
|
31
|
+
* const queryClient = new QueryClient();
|
|
32
|
+
* const clientManager = new ClientManager({
|
|
33
|
+
* queryClient,
|
|
34
|
+
* withLocalEnv: true, // Use local replica
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* await clientManager.initialize();
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class ClientManager {
|
|
41
|
+
#agent: HttpAgent
|
|
42
|
+
#authClient?: AuthClient
|
|
43
|
+
#identitySubscribers: Array<(identity: Identity) => void> = []
|
|
44
|
+
#agentStateSubscribers: Array<(state: AgentState) => void> = []
|
|
45
|
+
#authStateSubscribers: Array<(state: AuthState) => void> = []
|
|
46
|
+
#targetCanisterIds: Set<string> = new Set()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The TanStack QueryClient used for managing cached canister data and invalidating queries on identity changes.
|
|
50
|
+
*/
|
|
51
|
+
public queryClient: QueryClient
|
|
52
|
+
/**
|
|
53
|
+
* Current state of the HttpAgent, including initialization status, network, and error information.
|
|
54
|
+
*/
|
|
55
|
+
public agentState: AgentState
|
|
56
|
+
/**
|
|
57
|
+
* Current authentication state, including the active identity, authentication progress, and errors.
|
|
58
|
+
*/
|
|
59
|
+
public authState: AuthState
|
|
60
|
+
|
|
61
|
+
private initPromise?: Promise<void>
|
|
62
|
+
private authPromise?: Promise<Identity | undefined>
|
|
63
|
+
private port: number
|
|
64
|
+
private internetIdentityId?: string
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new instance of ClientManager.
|
|
68
|
+
*
|
|
69
|
+
* @param parameters - Configuration options for the agent and network environment.
|
|
70
|
+
*/
|
|
71
|
+
constructor({
|
|
72
|
+
port = 4943,
|
|
73
|
+
withLocalEnv,
|
|
74
|
+
withProcessEnv,
|
|
75
|
+
withCanisterEnv,
|
|
76
|
+
agentOptions = {},
|
|
77
|
+
queryClient,
|
|
78
|
+
authClient,
|
|
79
|
+
}: ClientManagerParameters) {
|
|
80
|
+
this.queryClient = queryClient
|
|
81
|
+
|
|
82
|
+
this.agentState = {
|
|
83
|
+
isInitialized: false,
|
|
84
|
+
isInitializing: false,
|
|
85
|
+
error: undefined,
|
|
86
|
+
network: undefined,
|
|
87
|
+
isLocalhost: false,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.authState = {
|
|
91
|
+
identity: null,
|
|
92
|
+
isAuthenticating: false,
|
|
93
|
+
isAuthenticated: false,
|
|
94
|
+
error: undefined,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.port = port
|
|
98
|
+
|
|
99
|
+
// EXPERIMENTAL: Use canister environment from ic_env cookie when enabled
|
|
100
|
+
// ⚠️ This may cause issues with update calls on localhost development
|
|
101
|
+
if (withCanisterEnv) {
|
|
102
|
+
const canisterEnv =
|
|
103
|
+
typeof window !== "undefined" ? safeGetCanisterEnv() : undefined
|
|
104
|
+
|
|
105
|
+
if (canisterEnv) {
|
|
106
|
+
this.internetIdentityId =
|
|
107
|
+
canisterEnv["internet_identity"] ||
|
|
108
|
+
canisterEnv["PUBLIC_CANISTER_ID:internet_identity"] ||
|
|
109
|
+
canisterEnv["CANISTER_ID_INTERNET_IDENTITY"]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isDev =
|
|
113
|
+
typeof import.meta !== "undefined" && (import.meta as any).env?.DEV
|
|
114
|
+
|
|
115
|
+
if (isDev && typeof window !== "undefined") {
|
|
116
|
+
agentOptions.host = agentOptions.host ?? window.location.origin
|
|
117
|
+
agentOptions.verifyQuerySignatures =
|
|
118
|
+
agentOptions.verifyQuerySignatures ?? false
|
|
119
|
+
} else {
|
|
120
|
+
agentOptions.verifyQuerySignatures =
|
|
121
|
+
agentOptions.verifyQuerySignatures ?? true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (canisterEnv?.IC_ROOT_KEY) {
|
|
125
|
+
agentOptions.rootKey = agentOptions.rootKey ?? canisterEnv.IC_ROOT_KEY
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (withProcessEnv) {
|
|
130
|
+
const processNetwork = getProcessEnvNetwork()
|
|
131
|
+
if (processNetwork === "ic") {
|
|
132
|
+
agentOptions.host = IC_HOST_NETWORK_URI
|
|
133
|
+
} else if (processNetwork === "local") {
|
|
134
|
+
agentOptions.host =
|
|
135
|
+
typeof process !== "undefined" && process.env.IC_HOST
|
|
136
|
+
? process.env.IC_HOST
|
|
137
|
+
: `http://127.0.0.1:${port}`
|
|
138
|
+
}
|
|
139
|
+
} else if (withLocalEnv) {
|
|
140
|
+
agentOptions.host = `http://127.0.0.1:${port}`
|
|
141
|
+
} else if (!withCanisterEnv) {
|
|
142
|
+
// Only set default host if withCanisterEnv hasn't already configured it
|
|
143
|
+
agentOptions.host = agentOptions.host ?? IC_HOST_NETWORK_URI
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.#agent = HttpAgent.createSync(agentOptions)
|
|
147
|
+
this.updateAgentState({
|
|
148
|
+
isLocalhost: this.isLocal,
|
|
149
|
+
network: this.network,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (authClient) {
|
|
153
|
+
this.#authClient = authClient
|
|
154
|
+
const identity = this.#authClient.getIdentity()
|
|
155
|
+
this.updateAgent(identity)
|
|
156
|
+
this.authState = {
|
|
157
|
+
identity,
|
|
158
|
+
isAuthenticated: !identity.getPrincipal().isAnonymous(),
|
|
159
|
+
isAuthenticating: false,
|
|
160
|
+
error: undefined,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Orchestrates the complete initialization of the ClientManager.
|
|
167
|
+
* This method awaits the agent's core initialization (e.g., fetching root keys)
|
|
168
|
+
* and triggers the authentication (session restoration) in the background.
|
|
169
|
+
*
|
|
170
|
+
* @returns A promise that resolves to the ClientManager instance when core initialization is complete.
|
|
171
|
+
*/
|
|
172
|
+
public async initialize() {
|
|
173
|
+
await this.initializeAgent()
|
|
174
|
+
this.authenticate()
|
|
175
|
+
return this
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Specifically initializes the HttpAgent.
|
|
180
|
+
* On local networks, this includes fetching the root key for certificate verification.
|
|
181
|
+
*
|
|
182
|
+
* @returns A promise that resolves when the agent is fully initialized.
|
|
183
|
+
*/
|
|
184
|
+
public async initializeAgent() {
|
|
185
|
+
if (this.agentState.isInitialized) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (this.agentState.isInitializing) {
|
|
189
|
+
return this.initPromise
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.initPromise = (async () => {
|
|
193
|
+
this.updateAgentState({ isInitializing: true })
|
|
194
|
+
if (typeof window !== "undefined") {
|
|
195
|
+
console.info(
|
|
196
|
+
`%cic-reactor:%c Initializing agent for ${this.network} network`,
|
|
197
|
+
"color: #3b82f6; font-weight: bold",
|
|
198
|
+
"color: inherit",
|
|
199
|
+
{
|
|
200
|
+
host: this.agentHost?.toString(),
|
|
201
|
+
isLocal: this.isLocal,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
if (this.isLocal) {
|
|
207
|
+
await this.#agent.fetchRootKey()
|
|
208
|
+
}
|
|
209
|
+
this.updateAgentState({ isInitialized: true, isInitializing: false })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.updateAgentState({
|
|
212
|
+
error: error as Error,
|
|
213
|
+
isInitializing: false,
|
|
214
|
+
})
|
|
215
|
+
this.initPromise = undefined
|
|
216
|
+
throw error
|
|
217
|
+
}
|
|
218
|
+
})()
|
|
219
|
+
|
|
220
|
+
return this.initPromise
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private authModuleMissing = false
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Attempts to initialize the authentication client and restore a previous session.
|
|
227
|
+
*
|
|
228
|
+
* If an `AuthClient` is already initialized (passed in constructor or previously created),
|
|
229
|
+
* it uses that instance. Otherwise, it dynamically imports the `@icp-sdk/auth` module
|
|
230
|
+
* and creates a new AuthClient.
|
|
231
|
+
*
|
|
232
|
+
* If the module is missing and no client is provided, it fails gracefully by marking authentication as unavailable.
|
|
233
|
+
*
|
|
234
|
+
* @returns A promise that resolves to the restored Identity, or undefined if auth fails or is unavailable.
|
|
235
|
+
*/
|
|
236
|
+
public authenticate = async (): Promise<Identity | undefined> => {
|
|
237
|
+
if (this.authState.isAuthenticated) {
|
|
238
|
+
return this.authState.identity || undefined
|
|
239
|
+
}
|
|
240
|
+
if (this.authPromise) {
|
|
241
|
+
return this.authPromise
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.authModuleMissing) {
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.authPromise = (async () => {
|
|
249
|
+
if (typeof window !== "undefined") {
|
|
250
|
+
console.info(
|
|
251
|
+
`%cic-reactor:%c Authenticating...`,
|
|
252
|
+
"color: #3b82f6; font-weight: bold",
|
|
253
|
+
"color: inherit",
|
|
254
|
+
{
|
|
255
|
+
network: this.network,
|
|
256
|
+
authClient: this.#authClient ? "Shared Instance" : "Dynamic Import",
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
this.updateAuthState({ isAuthenticating: true })
|
|
261
|
+
try {
|
|
262
|
+
if (!this.#authClient) {
|
|
263
|
+
const authModule = await import("@icp-sdk/auth/client").catch(() => {
|
|
264
|
+
this.authModuleMissing = true
|
|
265
|
+
return null
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (!authModule) {
|
|
269
|
+
this.authModuleMissing = true
|
|
270
|
+
this.updateAuthState({ isAuthenticating: false })
|
|
271
|
+
return undefined
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { AuthClient } = authModule
|
|
275
|
+
this.#authClient = await AuthClient.create()
|
|
276
|
+
}
|
|
277
|
+
const identity = this.#authClient.getIdentity()
|
|
278
|
+
this.updateAgent(identity)
|
|
279
|
+
this.updateAuthState({
|
|
280
|
+
identity,
|
|
281
|
+
isAuthenticated: !identity.getPrincipal().isAnonymous(),
|
|
282
|
+
isAuthenticating: false,
|
|
283
|
+
})
|
|
284
|
+
return identity
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.updateAuthState({ error: error as Error, isAuthenticating: false })
|
|
287
|
+
console.error("Authentication failed:", error)
|
|
288
|
+
throw error
|
|
289
|
+
} finally {
|
|
290
|
+
this.authPromise = undefined
|
|
291
|
+
}
|
|
292
|
+
})()
|
|
293
|
+
|
|
294
|
+
return this.authPromise
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Triggers the login flow using the Internet Identity provider.
|
|
299
|
+
*
|
|
300
|
+
* @param loginOptions - Options for the login flow, including identity provider and callbacks.
|
|
301
|
+
* @throws An error if the authentication module is not installed.
|
|
302
|
+
*/
|
|
303
|
+
public login = async (loginOptions?: AuthClientLoginOptions) => {
|
|
304
|
+
try {
|
|
305
|
+
// Ensure agent is initialized before login
|
|
306
|
+
if (!this.agentState.isInitialized) {
|
|
307
|
+
await this.initializeAgent()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!this.#authClient) {
|
|
311
|
+
await this.authenticate()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!this.#authClient) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"Authentication module is missing or failed to initialize. To use login, please install the auth package: npm install @icp-sdk/auth"
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.updateAuthState({ isAuthenticating: true, error: undefined })
|
|
321
|
+
|
|
322
|
+
// Auto-detect identity provider based on network if not provided
|
|
323
|
+
const identityProvider =
|
|
324
|
+
loginOptions?.identityProvider || this.getDefaultIdentityProvider()
|
|
325
|
+
|
|
326
|
+
await this.#authClient.login({
|
|
327
|
+
...loginOptions,
|
|
328
|
+
identityProvider,
|
|
329
|
+
onSuccess: () => {
|
|
330
|
+
const identity = this.#authClient!.getIdentity()
|
|
331
|
+
if (identity) {
|
|
332
|
+
this.updateAgent(identity)
|
|
333
|
+
this.updateAuthState({
|
|
334
|
+
identity,
|
|
335
|
+
isAuthenticated: true,
|
|
336
|
+
isAuthenticating: false,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
;(loginOptions?.onSuccess as any)?.()
|
|
340
|
+
},
|
|
341
|
+
onError: (error) => {
|
|
342
|
+
this.updateAuthState({
|
|
343
|
+
error: new Error(error),
|
|
344
|
+
isAuthenticating: false,
|
|
345
|
+
})
|
|
346
|
+
loginOptions?.onError?.(error)
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.updateAuthState({
|
|
351
|
+
error: error as Error,
|
|
352
|
+
isAuthenticating: false,
|
|
353
|
+
})
|
|
354
|
+
throw error
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Logs out the user and reverts the agent to an anonymous identity.
|
|
360
|
+
*
|
|
361
|
+
* @throws An error if the authentication module is not installed.
|
|
362
|
+
*/
|
|
363
|
+
public logout = async () => {
|
|
364
|
+
if (!this.#authClient) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
"Authentication module is missing or failed to initialize. To use logout, please install the auth package: npm install @icp-sdk/auth"
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
this.updateAuthState({ isAuthenticating: true, error: undefined })
|
|
370
|
+
await this.#authClient.logout()
|
|
371
|
+
const identity = this.#authClient.getIdentity()
|
|
372
|
+
if (identity) {
|
|
373
|
+
this.updateAgent(identity)
|
|
374
|
+
this.updateAuthState({
|
|
375
|
+
identity,
|
|
376
|
+
isAuthenticated: false,
|
|
377
|
+
isAuthenticating: false,
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The underlying HttpAgent managed by this class.
|
|
384
|
+
*/
|
|
385
|
+
get agent() {
|
|
386
|
+
return this.#agent
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The host URL of the current IC agent.
|
|
391
|
+
*/
|
|
392
|
+
get agentHost(): URL | undefined {
|
|
393
|
+
return this.#agent.host
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The hostname of the current IC agent.
|
|
398
|
+
*/
|
|
399
|
+
get agentHostName() {
|
|
400
|
+
return this.agentHost?.hostname || ""
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Returns true if the agent is connecting to a local environment.
|
|
405
|
+
*/
|
|
406
|
+
get isLocal() {
|
|
407
|
+
return this.network !== "ic"
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Returns the current network type ('ic' or 'local').
|
|
412
|
+
*/
|
|
413
|
+
get network() {
|
|
414
|
+
const hostname = this.agentHostName
|
|
415
|
+
return getNetworkByHostname(hostname)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Returns the current user's Principal identity.
|
|
420
|
+
*/
|
|
421
|
+
public getUserPrincipal() {
|
|
422
|
+
return this.#agent.getPrincipal()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Registers a canister ID that this agent will interact with.
|
|
427
|
+
* This is used for informational purposes and network detection.
|
|
428
|
+
*/
|
|
429
|
+
public registerCanisterId(canisterId: string, name?: string): void {
|
|
430
|
+
if (typeof window !== "undefined") {
|
|
431
|
+
const actorName = name || canisterId
|
|
432
|
+
console.info(
|
|
433
|
+
`%cic-reactor:%c Adding actor ${actorName}`,
|
|
434
|
+
"color: #3b82f6; font-weight: bold",
|
|
435
|
+
"color: inherit",
|
|
436
|
+
{
|
|
437
|
+
network: this.network,
|
|
438
|
+
canisterId,
|
|
439
|
+
...(name && { name }),
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
this.#targetCanisterIds.add(canisterId)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Returns a list of all canister IDs registered with this agent.
|
|
448
|
+
*/
|
|
449
|
+
public connectedCanisterIds(): string[] {
|
|
450
|
+
return Array.from(this.#targetCanisterIds)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get the subnet ID for a canister.
|
|
455
|
+
*/
|
|
456
|
+
public getSubnetIdFromCanister(canisterId: string) {
|
|
457
|
+
return this.#agent.getSubnetIdFromCanister(canisterId)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Sync time with a specific subnet.
|
|
462
|
+
*/
|
|
463
|
+
public syncTimeWithSubnet(subnetId: Principal) {
|
|
464
|
+
return this.#agent.syncTimeWithSubnet(subnetId)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private getDefaultIdentityProvider(): string {
|
|
468
|
+
if (this.isLocal) {
|
|
469
|
+
if (this.internetIdentityId) {
|
|
470
|
+
return `http://${this.internetIdentityId}.localhost:${this.port}`
|
|
471
|
+
}
|
|
472
|
+
return LOCAL_INTERNET_IDENTITY_PROVIDER
|
|
473
|
+
} else {
|
|
474
|
+
return IC_INTERNET_IDENTITY_PROVIDER
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Subscribes to identity changes (e.g., after login/logout).
|
|
480
|
+
* @param callback - Function called with the new identity.
|
|
481
|
+
* @returns An unsubscribe function.
|
|
482
|
+
*/
|
|
483
|
+
public subscribe(callback: (identity: Identity) => void) {
|
|
484
|
+
this.#identitySubscribers.push(callback)
|
|
485
|
+
return () => {
|
|
486
|
+
this.#identitySubscribers = this.#identitySubscribers.filter(
|
|
487
|
+
(sub) => sub !== callback
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Subscribes to changes in the agent's initialization state.
|
|
494
|
+
* @param callback - Function called with the updated agent state.
|
|
495
|
+
* @returns An unsubscribe function.
|
|
496
|
+
*/
|
|
497
|
+
public subscribeAgentState(callback: (state: AgentState) => void) {
|
|
498
|
+
this.#agentStateSubscribers.push(callback)
|
|
499
|
+
return () => {
|
|
500
|
+
this.#agentStateSubscribers = this.#agentStateSubscribers.filter(
|
|
501
|
+
(sub) => sub !== callback
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Subscribes to changes in the authentication state.
|
|
508
|
+
* @param callback - Function called with the updated authentication state.
|
|
509
|
+
* @returns An unsubscribe function.
|
|
510
|
+
*/
|
|
511
|
+
public subscribeAuthState(callback: (state: AuthState) => void) {
|
|
512
|
+
this.#authStateSubscribers.push(callback)
|
|
513
|
+
return () => {
|
|
514
|
+
this.#authStateSubscribers = this.#authStateSubscribers.filter(
|
|
515
|
+
(sub) => sub !== callback
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Replaces the current agent's identity and invalidates TanStack queries.
|
|
522
|
+
* @param identity - The new identity to use.
|
|
523
|
+
*/
|
|
524
|
+
public updateAgent(identity: Identity) {
|
|
525
|
+
if (typeof window !== "undefined") {
|
|
526
|
+
console.info(
|
|
527
|
+
`%cic-reactor:%c Updating agent identity`,
|
|
528
|
+
"color: #3b82f6; font-weight: bold",
|
|
529
|
+
"color: inherit",
|
|
530
|
+
{
|
|
531
|
+
principal: identity.getPrincipal().toText(),
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
// Cancel all queries for connected canisters to prevent race conditions
|
|
536
|
+
// with the old identity
|
|
537
|
+
this.connectedCanisterIds().forEach((canisterId) => {
|
|
538
|
+
this.queryClient.cancelQueries({
|
|
539
|
+
queryKey: [canisterId],
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
this.#agent.replaceIdentity(identity)
|
|
544
|
+
this.notifySubscribers(identity)
|
|
545
|
+
this.queryClient.invalidateQueries()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private notifySubscribers(identity: Identity) {
|
|
549
|
+
this.#identitySubscribers.forEach((sub) => sub(identity))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private notifyAgentStateSubscribers(state: AgentState) {
|
|
553
|
+
this.#agentStateSubscribers.forEach((sub) => sub(state))
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private notifyAuthStateSubscribers(state: AuthState) {
|
|
557
|
+
this.#authStateSubscribers.forEach((sub) => sub(state))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private updateAgentState(newState: Partial<AgentState>) {
|
|
561
|
+
this.agentState = { ...this.agentState, ...newState }
|
|
562
|
+
this.notifyAgentStateSubscribers(this.agentState)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private updateAuthState(newState: Partial<AuthState>) {
|
|
566
|
+
console.debug("Updating Auth State:", newState)
|
|
567
|
+
this.authState = { ...this.authState, ...newState }
|
|
568
|
+
this.notifyAuthStateSubscribers(this.authState)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
2
|
+
import { ActorDisplayCodec, DisplayOf, DisplayCodec } from "./types"
|
|
3
|
+
import { DisplayCodecVisitor } from "./visitor"
|
|
4
|
+
|
|
5
|
+
export function didToDisplayCodec<
|
|
6
|
+
TCandid = unknown,
|
|
7
|
+
TDisplay = DisplayOf<TCandid>,
|
|
8
|
+
>(didType: IDL.Type): ActorDisplayCodec<TCandid, TDisplay> {
|
|
9
|
+
const visitor = new DisplayCodecVisitor()
|
|
10
|
+
const codec = visitor.visitType(didType, null) as DisplayCodec<
|
|
11
|
+
TCandid,
|
|
12
|
+
TDisplay
|
|
13
|
+
>
|
|
14
|
+
// Return a unified interface with both native methods and convenience aliases
|
|
15
|
+
return {
|
|
16
|
+
codec,
|
|
17
|
+
asDisplay: (val: TCandid) => codec.decode(val) as TDisplay,
|
|
18
|
+
asCandid: (val: TDisplay) => codec.encode(val) as TCandid,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function didToDisplayCodecs<TTypes extends Record<string, IDL.Type>>(
|
|
23
|
+
didTypes: TTypes
|
|
24
|
+
): {
|
|
25
|
+
[K in keyof TTypes]: TTypes[K] extends IDL.Type<infer TCandid>
|
|
26
|
+
? ActorDisplayCodec<TCandid>
|
|
27
|
+
: never
|
|
28
|
+
} {
|
|
29
|
+
const result = {} as any
|
|
30
|
+
|
|
31
|
+
for (const [name, idlType] of Object.entries(didTypes)) {
|
|
32
|
+
result[name] = didToDisplayCodec(idlType)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function transformArgsWithCodec<T extends unknown[]>(
|
|
39
|
+
argsCodec: ActorDisplayCodec,
|
|
40
|
+
args: unknown[] | undefined
|
|
41
|
+
): T {
|
|
42
|
+
// Empty args - no transformation needed
|
|
43
|
+
// This handles methods with 0 arguments where codec is IDL.Null
|
|
44
|
+
if (!args || args.length === 0) {
|
|
45
|
+
return (args || []) as T
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Single argument - unwrap from array and transform
|
|
49
|
+
if (args.length === 1) {
|
|
50
|
+
try {
|
|
51
|
+
return [argsCodec.asCandid(args[0])] as T
|
|
52
|
+
} catch {
|
|
53
|
+
// Fallback: return as-is if transformation fails
|
|
54
|
+
return args as T
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Multiple arguments - transform as tuple
|
|
59
|
+
try {
|
|
60
|
+
return argsCodec.asCandid(args) as T
|
|
61
|
+
} catch {
|
|
62
|
+
// Fallback: return as-is if transformation fails
|
|
63
|
+
return args as T
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function transformResultWithCodec<T>(
|
|
68
|
+
resultCodec: ActorDisplayCodec,
|
|
69
|
+
result: unknown
|
|
70
|
+
): T {
|
|
71
|
+
// Null/undefined results - no transformation needed
|
|
72
|
+
if (result === null || result === undefined) {
|
|
73
|
+
return result as T
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return resultCodec.asDisplay(result) as T
|
|
78
|
+
} catch {
|
|
79
|
+
// Fallback: return as-is if transformation fails
|
|
80
|
+
return result as T
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function didTypeFromArray(types: IDL.Type[]): IDL.Type {
|
|
85
|
+
if (types.length === 0) {
|
|
86
|
+
return IDL.Null
|
|
87
|
+
}
|
|
88
|
+
if (types.length === 1) {
|
|
89
|
+
return types[0]
|
|
90
|
+
}
|
|
91
|
+
return IDL.Tuple(...types)
|
|
92
|
+
}
|