@xian-tech/wallet-core 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.
@@ -0,0 +1,1082 @@
1
+ import { Ed25519Signer, XianClient } from "@xian-tech/client";
2
+ import { ProviderChainMismatchError, ProviderUnauthorizedError, ProviderUnsupportedMethodError } from "@xian-tech/provider";
3
+ import { approvalKindFromMethod, buildApprovalView } from "./approvals";
4
+ import { DEFAULT_NETWORK_PRESETS, DEFAULT_DASHBOARD_URL, DEFAULT_RPC_URL, LOCAL_NETWORK_PRESET_NAME, DEFAULT_WALLET_CAPABILITIES, LOCAL_NETWORK_PRESET_ID } from "./constants";
5
+ import { createWalletSecret, decryptMnemonic, decryptPrivateKey, encryptMnemonic, encryptPrivateKey, isUnsafeMessageToSign } from "./crypto";
6
+ function firstParamObject(params) {
7
+ if (Array.isArray(params)) {
8
+ return (params[0] ?? {});
9
+ }
10
+ return (params ?? {});
11
+ }
12
+ function parseIntentNumber(value, fieldName) {
13
+ if (value == null) {
14
+ return undefined;
15
+ }
16
+ if (typeof value === "bigint") {
17
+ if (value < 0n) {
18
+ throw new TypeError(`${fieldName} must be a non-negative integer`);
19
+ }
20
+ return value;
21
+ }
22
+ if (typeof value === "number") {
23
+ if (!Number.isInteger(value) || value < 0) {
24
+ throw new TypeError(`${fieldName} must be a non-negative integer`);
25
+ }
26
+ return value;
27
+ }
28
+ if (typeof value === "string" && /^\d+$/.test(value)) {
29
+ const parsed = BigInt(value);
30
+ return parsed <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : parsed;
31
+ }
32
+ throw new TypeError(`${fieldName} must be a non-negative integer`);
33
+ }
34
+ function trimOptionalString(value) {
35
+ const trimmed = value?.trim();
36
+ return trimmed ? trimmed : undefined;
37
+ }
38
+ function createLocalNetworkPreset() {
39
+ const preset = DEFAULT_NETWORK_PRESETS[0];
40
+ if (preset) {
41
+ return {
42
+ ...preset
43
+ };
44
+ }
45
+ return {
46
+ id: LOCAL_NETWORK_PRESET_ID,
47
+ name: LOCAL_NETWORK_PRESET_NAME,
48
+ rpcUrl: DEFAULT_RPC_URL,
49
+ dashboardUrl: DEFAULT_DASHBOARD_URL,
50
+ builtin: true
51
+ };
52
+ }
53
+ function normalizePresetInputValue(preset, fallback) {
54
+ return {
55
+ id: trimOptionalString(preset.id) ?? fallback.id,
56
+ name: trimOptionalString(preset.name) ?? fallback.name,
57
+ chainId: trimOptionalString(preset.chainId),
58
+ rpcUrl: trimOptionalString(preset.rpcUrl) ?? fallback.rpcUrl,
59
+ dashboardUrl: trimOptionalString(preset.dashboardUrl) ??
60
+ trimOptionalString(fallback.dashboardUrl),
61
+ builtin: preset.builtin ?? fallback.builtin
62
+ };
63
+ }
64
+ function normalizeStoredWalletNetworks(state) {
65
+ const localPreset = createLocalNetworkPreset();
66
+ const rawPresets = Array.isArray(state.networkPresets) ? state.networkPresets : [];
67
+ if (rawPresets.length === 0) {
68
+ const rpcUrl = trimOptionalString(state.rpcUrl) ?? DEFAULT_RPC_URL;
69
+ const dashboardUrl = trimOptionalString(state.dashboardUrl) ?? DEFAULT_DASHBOARD_URL;
70
+ const isLocalDefault = rpcUrl === localPreset.rpcUrl &&
71
+ (dashboardUrl ?? "") === (localPreset.dashboardUrl ?? "");
72
+ if (isLocalDefault) {
73
+ return {
74
+ ...state,
75
+ rpcUrl: localPreset.rpcUrl,
76
+ dashboardUrl: localPreset.dashboardUrl,
77
+ activeNetworkId: localPreset.id,
78
+ networkPresets: [localPreset]
79
+ };
80
+ }
81
+ const customPreset = normalizePresetInputValue({
82
+ id: "custom-network",
83
+ name: "Custom network",
84
+ rpcUrl,
85
+ dashboardUrl
86
+ }, {
87
+ id: "custom-network",
88
+ name: "Custom network",
89
+ rpcUrl,
90
+ dashboardUrl
91
+ });
92
+ return {
93
+ ...state,
94
+ rpcUrl: customPreset.rpcUrl,
95
+ dashboardUrl: customPreset.dashboardUrl,
96
+ activeNetworkId: customPreset.id,
97
+ networkPresets: [localPreset, customPreset]
98
+ };
99
+ }
100
+ const presets = new Map();
101
+ for (const rawPreset of rawPresets) {
102
+ const preset = normalizePresetInputValue(rawPreset, {
103
+ id: trimOptionalString(rawPreset.id) ?? "network",
104
+ name: trimOptionalString(rawPreset.name) ?? "Network",
105
+ rpcUrl: trimOptionalString(rawPreset.rpcUrl) ?? DEFAULT_RPC_URL,
106
+ dashboardUrl: trimOptionalString(rawPreset.dashboardUrl),
107
+ builtin: rawPreset.builtin
108
+ });
109
+ presets.set(preset.id, preset);
110
+ }
111
+ if (!presets.has(LOCAL_NETWORK_PRESET_ID)) {
112
+ presets.set(LOCAL_NETWORK_PRESET_ID, localPreset);
113
+ }
114
+ const activeNetworkId = trimOptionalString(state.activeNetworkId) &&
115
+ presets.has(trimOptionalString(state.activeNetworkId))
116
+ ? trimOptionalString(state.activeNetworkId)
117
+ : presets.values().next().value.id;
118
+ const activePreset = presets.get(activeNetworkId) ?? localPreset;
119
+ return {
120
+ ...state,
121
+ rpcUrl: activePreset.rpcUrl,
122
+ dashboardUrl: activePreset.dashboardUrl,
123
+ activeNetworkId,
124
+ networkPresets: [...presets.values()]
125
+ };
126
+ }
127
+ function hydrateError(error) {
128
+ const hydrated = new Error(error.message);
129
+ hydrated.name = error.name ?? "Error";
130
+ hydrated.code = error.code;
131
+ hydrated.data = error.data;
132
+ return hydrated;
133
+ }
134
+ export class WalletController {
135
+ options;
136
+ requestWaiters = new Map();
137
+ unlockedPrivateKey = null;
138
+ unlockedSigner = null;
139
+ constructor(options) {
140
+ this.options = options;
141
+ }
142
+ get store() {
143
+ return this.options.store;
144
+ }
145
+ providerCapabilities() {
146
+ return { ...DEFAULT_WALLET_CAPABILITIES };
147
+ }
148
+ createId() {
149
+ return this.options.createId?.() ?? globalThis.crypto.randomUUID();
150
+ }
151
+ now() {
152
+ return this.options.now?.() ?? Date.now();
153
+ }
154
+ serializeError(error) {
155
+ if (typeof error === "object" && error != null) {
156
+ const candidate = error;
157
+ return {
158
+ name: typeof candidate.name === "string" ? candidate.name : "Error",
159
+ message: typeof candidate.message === "string"
160
+ ? candidate.message
161
+ : String(error),
162
+ code: typeof candidate.code === "number" ? candidate.code : undefined,
163
+ data: candidate.data
164
+ };
165
+ }
166
+ return {
167
+ name: "Error",
168
+ message: String(error)
169
+ };
170
+ }
171
+ getUnlockedSigner() {
172
+ if (!this.unlockedPrivateKey) {
173
+ throw new ProviderUnauthorizedError("wallet is locked");
174
+ }
175
+ if (!this.unlockedSigner) {
176
+ this.unlockedSigner = new Ed25519Signer(this.unlockedPrivateKey);
177
+ }
178
+ return this.unlockedSigner;
179
+ }
180
+ currentClient(state) {
181
+ if (this.options.createClient) {
182
+ return this.options.createClient(state);
183
+ }
184
+ return new XianClient({
185
+ rpcUrl: state.rpcUrl,
186
+ dashboardUrl: state.dashboardUrl
187
+ });
188
+ }
189
+ requireStoredWallet(state) {
190
+ if (!state) {
191
+ throw new ProviderUnauthorizedError("wallet is not configured");
192
+ }
193
+ return normalizeStoredWalletNetworks(state);
194
+ }
195
+ activeNetworkPreset(state) {
196
+ const normalized = normalizeStoredWalletNetworks(state);
197
+ return (normalized.networkPresets.find((preset) => preset.id === normalized.activeNetworkId) ??
198
+ normalized.networkPresets[0] ??
199
+ createLocalNetworkPreset());
200
+ }
201
+ async loadWalletState() {
202
+ const state = await this.store.loadState();
203
+ if (!state) {
204
+ return null;
205
+ }
206
+ const normalized = normalizeStoredWalletNetworks(state);
207
+ if (JSON.stringify(normalized) !== JSON.stringify(state)) {
208
+ await this.store.saveState(normalized);
209
+ }
210
+ return normalized;
211
+ }
212
+ displayChainId(preset, resolvedChainId) {
213
+ return resolvedChainId ?? preset.chainId;
214
+ }
215
+ networkStatus(preset, resolvedChainId) {
216
+ if (!resolvedChainId) {
217
+ return "unreachable";
218
+ }
219
+ if (preset.chainId && preset.chainId !== resolvedChainId) {
220
+ return "mismatch";
221
+ }
222
+ return "ready";
223
+ }
224
+ async emitChainChangedForConnectedOrigins(state, previousChainId) {
225
+ if (state.connectedOrigins.length === 0) {
226
+ return;
227
+ }
228
+ const preset = this.activeNetworkPreset(state);
229
+ const nextChainId = this.displayChainId(preset, await this.safeGetChainId(state));
230
+ if (!nextChainId || nextChainId === previousChainId) {
231
+ return;
232
+ }
233
+ await Promise.all(state.connectedOrigins.map((origin) => this.broadcastProviderEvent("chainChanged", [nextChainId], origin)));
234
+ }
235
+ applyActivePreset(state, presetId) {
236
+ const normalized = normalizeStoredWalletNetworks(state);
237
+ const preset = normalized.networkPresets.find((entry) => entry.id === presetId);
238
+ if (!preset) {
239
+ throw new Error("network preset not found");
240
+ }
241
+ return {
242
+ ...normalized,
243
+ activeNetworkId: preset.id,
244
+ rpcUrl: preset.rpcUrl,
245
+ dashboardUrl: preset.dashboardUrl
246
+ };
247
+ }
248
+ requireConnectedOrigin(state, origin) {
249
+ if (!state.connectedOrigins.includes(origin)) {
250
+ throw new ProviderUnauthorizedError("site is not connected to this wallet");
251
+ }
252
+ }
253
+ async safeGetChainId(state) {
254
+ if (!state) {
255
+ return undefined;
256
+ }
257
+ try {
258
+ return await this.currentClient(state).getChainId();
259
+ }
260
+ catch {
261
+ return undefined;
262
+ }
263
+ }
264
+ async buildWalletInfo(state, origin) {
265
+ if (!state) {
266
+ return {
267
+ accounts: [],
268
+ connected: false,
269
+ locked: true,
270
+ capabilities: this.providerCapabilities(),
271
+ wallet: this.options.wallet
272
+ };
273
+ }
274
+ const connected = state.connectedOrigins.includes(origin);
275
+ const unlocked = this.unlockedPrivateKey != null;
276
+ const preset = this.activeNetworkPreset(state);
277
+ const resolvedChainId = await this.safeGetChainId(state);
278
+ return {
279
+ accounts: connected && unlocked ? [state.publicKey] : [],
280
+ selectedAccount: connected && unlocked ? state.publicKey : undefined,
281
+ chainId: this.displayChainId(preset, resolvedChainId),
282
+ connected,
283
+ locked: !unlocked,
284
+ capabilities: this.providerCapabilities(),
285
+ wallet: this.options.wallet
286
+ };
287
+ }
288
+ async persistWalletState(state) {
289
+ await this.store.saveState(normalizeStoredWalletNetworks(state));
290
+ return this.getPopupState();
291
+ }
292
+ async updateConnectedOrigin(origin, connected) {
293
+ const state = this.requireStoredWallet(await this.loadWalletState());
294
+ const nextOrigins = new Set(state.connectedOrigins);
295
+ if (connected) {
296
+ nextOrigins.add(origin);
297
+ }
298
+ else {
299
+ nextOrigins.delete(origin);
300
+ }
301
+ const nextState = {
302
+ ...state,
303
+ connectedOrigins: [...nextOrigins]
304
+ };
305
+ await this.store.saveState(nextState);
306
+ return nextState;
307
+ }
308
+ async updateWatchedAssets(updater) {
309
+ const state = this.requireStoredWallet(await this.loadWalletState());
310
+ await this.store.saveState({
311
+ ...state,
312
+ watchedAssets: updater(state.watchedAssets)
313
+ });
314
+ }
315
+ sanitizeNetworkPresetInput(input) {
316
+ const name = input.name.trim();
317
+ const rpcUrl = input.rpcUrl.trim();
318
+ if (!name) {
319
+ throw new TypeError("network preset name is required");
320
+ }
321
+ if (!rpcUrl) {
322
+ throw new TypeError("network preset rpcUrl is required");
323
+ }
324
+ return {
325
+ ...input,
326
+ id: trimOptionalString(input.id),
327
+ name,
328
+ chainId: trimOptionalString(input.chainId),
329
+ rpcUrl,
330
+ dashboardUrl: trimOptionalString(input.dashboardUrl),
331
+ makeActive: input.makeActive ?? false
332
+ };
333
+ }
334
+ upsertNetworkPresetInState(state, input) {
335
+ const normalized = normalizeStoredWalletNetworks(state);
336
+ const sanitized = this.sanitizeNetworkPresetInput(input);
337
+ const presetId = sanitized.id ?? this.createId();
338
+ const existingPreset = normalized.networkPresets.find((preset) => preset.id === presetId);
339
+ if (existingPreset?.builtin) {
340
+ throw new Error("built-in network presets cannot be edited");
341
+ }
342
+ const nextPreset = normalizePresetInputValue({
343
+ id: presetId,
344
+ name: sanitized.name,
345
+ chainId: sanitized.chainId,
346
+ rpcUrl: sanitized.rpcUrl,
347
+ dashboardUrl: sanitized.dashboardUrl,
348
+ builtin: false
349
+ }, {
350
+ id: presetId,
351
+ name: sanitized.name,
352
+ rpcUrl: sanitized.rpcUrl,
353
+ dashboardUrl: sanitized.dashboardUrl,
354
+ builtin: false
355
+ });
356
+ const nextPresets = normalized.networkPresets.filter((preset) => preset.id !== presetId);
357
+ nextPresets.push(nextPreset);
358
+ const nextActiveNetworkId = sanitized.makeActive || normalized.activeNetworkId === presetId
359
+ ? presetId
360
+ : normalized.activeNetworkId;
361
+ return this.applyActivePreset({
362
+ ...normalized,
363
+ networkPresets: nextPresets
364
+ }, nextActiveNetworkId);
365
+ }
366
+ async broadcastProviderEvent(event, args, targetOrigin) {
367
+ await this.options.onProviderEvent?.(event, args, targetOrigin);
368
+ }
369
+ async emitConnectionLifecycle(origin, chainId, publicKey) {
370
+ await this.broadcastProviderEvent("connect", [{ chainId }], origin);
371
+ await this.broadcastProviderEvent("accountsChanged", [[publicKey]], origin);
372
+ await this.broadcastProviderEvent("chainChanged", [chainId], origin);
373
+ }
374
+ async emitDisconnectLifecycle(origin) {
375
+ await this.broadcastProviderEvent("accountsChanged", [[]], origin);
376
+ await this.broadcastProviderEvent("disconnect", [{ code: 4100, message: "wallet disconnected" }], origin);
377
+ }
378
+ async prepareTransaction(state, intent) {
379
+ const signer = this.getUnlockedSigner();
380
+ const client = this.currentClient(state);
381
+ const activeChainId = await client.getChainId();
382
+ if (intent.chainId && intent.chainId !== activeChainId) {
383
+ throw new ProviderChainMismatchError("wallet is connected to a different chain");
384
+ }
385
+ return client.buildTx({
386
+ sender: signer.address,
387
+ contract: intent.contract,
388
+ function: intent.function,
389
+ kwargs: intent.kwargs,
390
+ chainId: activeChainId,
391
+ stamps: parseIntentNumber(intent.stamps, "stamps"),
392
+ stampsSupplied: parseIntentNumber(intent.stampsSupplied, "stampsSupplied")
393
+ });
394
+ }
395
+ async signPreparedTransaction(state, tx) {
396
+ const signer = this.getUnlockedSigner();
397
+ const activeChainId = await this.currentClient(state).getChainId();
398
+ if (tx.payload.sender !== signer.address) {
399
+ throw new ProviderUnauthorizedError("transaction sender does not match the active wallet");
400
+ }
401
+ if (tx.payload.chain_id !== activeChainId) {
402
+ throw new ProviderChainMismatchError("transaction chain does not match the active wallet chain");
403
+ }
404
+ return this.currentClient(state).signTx(tx, signer);
405
+ }
406
+ async sendPreparedTransaction(state, tx, options) {
407
+ const signedTx = await this.signPreparedTransaction(state, tx);
408
+ return this.currentClient(state).broadcastTx(signedTx, options);
409
+ }
410
+ async executeApprovedRequest(origin, request) {
411
+ const state = this.requireStoredWallet(await this.loadWalletState());
412
+ switch (request.method) {
413
+ case "xian_requestAccounts": {
414
+ this.getUnlockedSigner();
415
+ const chainId = this.displayChainId(this.activeNetworkPreset(state), await this.safeGetChainId(state));
416
+ const nextState = await this.updateConnectedOrigin(origin, true);
417
+ await this.emitConnectionLifecycle(origin, chainId ?? "unknown", nextState.publicKey);
418
+ return [nextState.publicKey];
419
+ }
420
+ case "xian_watchAsset": {
421
+ this.requireConnectedOrigin(state, origin);
422
+ this.getUnlockedSigner();
423
+ const assetRequest = firstParamObject(request.params);
424
+ const asset = assetRequest.options;
425
+ await this.updateWatchedAssets((assets) => {
426
+ const next = assets.filter((entry) => entry.contract !== asset.contract);
427
+ next.push(asset);
428
+ return next;
429
+ });
430
+ return true;
431
+ }
432
+ case "xian_signMessage": {
433
+ this.requireConnectedOrigin(state, origin);
434
+ const signer = this.getUnlockedSigner();
435
+ const { message } = firstParamObject(request.params);
436
+ if (typeof message !== "string") {
437
+ throw new TypeError("xian_signMessage requires a message string");
438
+ }
439
+ if (isUnsafeMessageToSign(message)) {
440
+ throw new Error("refusing to sign a transaction-like payload as a plain message");
441
+ }
442
+ return signer.signMessage(message);
443
+ }
444
+ case "xian_signTransaction": {
445
+ this.requireConnectedOrigin(state, origin);
446
+ this.getUnlockedSigner();
447
+ const { tx } = firstParamObject(request.params);
448
+ return this.signPreparedTransaction(state, tx);
449
+ }
450
+ case "xian_sendTransaction": {
451
+ this.requireConnectedOrigin(state, origin);
452
+ this.getUnlockedSigner();
453
+ const { tx, mode, waitForTx, timeoutMs, pollIntervalMs } = firstParamObject(request.params);
454
+ return this.sendPreparedTransaction(state, tx, {
455
+ mode: mode,
456
+ waitForTx: waitForTx,
457
+ timeoutMs: timeoutMs,
458
+ pollIntervalMs: pollIntervalMs
459
+ });
460
+ }
461
+ case "xian_sendCall": {
462
+ this.requireConnectedOrigin(state, origin);
463
+ this.getUnlockedSigner();
464
+ const { intent, mode, waitForTx, timeoutMs, pollIntervalMs } = firstParamObject(request.params);
465
+ const tx = await this.prepareTransaction(state, intent);
466
+ return this.sendPreparedTransaction(state, tx, {
467
+ mode: mode,
468
+ waitForTx: waitForTx,
469
+ timeoutMs: timeoutMs,
470
+ pollIntervalMs: pollIntervalMs
471
+ });
472
+ }
473
+ default:
474
+ throw new ProviderUnsupportedMethodError(request.method);
475
+ }
476
+ }
477
+ async fulfillRequest(requestState, result) {
478
+ const nextState = {
479
+ ...requestState,
480
+ updatedAt: this.now(),
481
+ status: "fulfilled",
482
+ result,
483
+ error: undefined
484
+ };
485
+ await this.store.saveRequestState(nextState);
486
+ const waiter = this.requestWaiters.get(requestState.requestId);
487
+ if (waiter) {
488
+ this.requestWaiters.delete(requestState.requestId);
489
+ waiter.resolve(result);
490
+ }
491
+ return {
492
+ status: "fulfilled",
493
+ result
494
+ };
495
+ }
496
+ async rejectRequest(requestState, error) {
497
+ const serialized = this.serializeError(error);
498
+ const nextState = {
499
+ ...requestState,
500
+ updatedAt: this.now(),
501
+ status: "rejected",
502
+ result: undefined,
503
+ error: serialized
504
+ };
505
+ await this.store.saveRequestState(nextState);
506
+ const waiter = this.requestWaiters.get(requestState.requestId);
507
+ if (waiter) {
508
+ this.requestWaiters.delete(requestState.requestId);
509
+ waiter.reject(hydrateError(serialized));
510
+ }
511
+ return {
512
+ status: "rejected",
513
+ error: serialized
514
+ };
515
+ }
516
+ async createApprovalRequest(requestState, account, chainId) {
517
+ const record = {
518
+ id: this.createId(),
519
+ origin: requestState.origin,
520
+ kind: approvalKindFromMethod(requestState.request.method),
521
+ request: requestState.request,
522
+ createdAt: this.now()
523
+ };
524
+ const view = buildApprovalView(record, { account, chainId });
525
+ const approval = {
526
+ id: record.id,
527
+ requestId: requestState.requestId,
528
+ record,
529
+ view
530
+ };
531
+ await this.store.saveApprovalState(approval);
532
+ await this.store.saveRequestState({
533
+ ...requestState,
534
+ updatedAt: this.now(),
535
+ status: "pending",
536
+ approvalId: record.id
537
+ });
538
+ try {
539
+ await this.options.onApprovalRequested?.(record.id, view);
540
+ return {
541
+ status: "pending",
542
+ approvalId: record.id
543
+ };
544
+ }
545
+ catch (error) {
546
+ await this.store.deleteApprovalState(record.id);
547
+ const rejected = await this.rejectRequest({
548
+ ...requestState,
549
+ approvalId: record.id
550
+ }, error);
551
+ if (rejected.status !== "rejected") {
552
+ throw new Error("approval request rejection did not settle correctly");
553
+ }
554
+ return rejected;
555
+ }
556
+ }
557
+ async executeImmediateRequest(state, origin, request) {
558
+ switch (request.method) {
559
+ case "xian_getWalletInfo":
560
+ return {
561
+ kind: "result",
562
+ value: await this.buildWalletInfo(state, origin)
563
+ };
564
+ case "xian_requestAccounts": {
565
+ const walletState = this.requireStoredWallet(state);
566
+ this.getUnlockedSigner();
567
+ const approvalChainId = this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState));
568
+ if (walletState.connectedOrigins.includes(origin)) {
569
+ return {
570
+ kind: "result",
571
+ value: [walletState.publicKey]
572
+ };
573
+ }
574
+ return {
575
+ kind: "approval",
576
+ account: walletState.publicKey,
577
+ chainId: approvalChainId
578
+ };
579
+ }
580
+ case "xian_disconnect": {
581
+ if (!state) {
582
+ return {
583
+ kind: "result",
584
+ value: null
585
+ };
586
+ }
587
+ await this.updateConnectedOrigin(origin, false);
588
+ await this.emitDisconnectLifecycle(origin);
589
+ return {
590
+ kind: "result",
591
+ value: null
592
+ };
593
+ }
594
+ case "xian_accounts":
595
+ if (!state ||
596
+ this.unlockedPrivateKey == null ||
597
+ !state.connectedOrigins.includes(origin)) {
598
+ return {
599
+ kind: "result",
600
+ value: []
601
+ };
602
+ }
603
+ return {
604
+ kind: "result",
605
+ value: [state.publicKey]
606
+ };
607
+ case "xian_chainId":
608
+ {
609
+ const walletState = this.requireStoredWallet(state);
610
+ return {
611
+ kind: "result",
612
+ value: this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState)) ?? null
613
+ };
614
+ }
615
+ case "xian_switchChain": {
616
+ const walletState = this.requireStoredWallet(state);
617
+ const { chainId } = firstParamObject(request.params);
618
+ if (typeof chainId !== "string" || chainId.length === 0) {
619
+ throw new TypeError("xian_switchChain requires a chainId string");
620
+ }
621
+ const previousChainId = this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState));
622
+ if (previousChainId === chainId) {
623
+ return {
624
+ kind: "result",
625
+ value: null
626
+ };
627
+ }
628
+ const targetPreset = walletState.networkPresets.find((preset) => preset.chainId === chainId);
629
+ if (!targetPreset) {
630
+ throw new ProviderChainMismatchError("wallet has no configured network preset for the requested chain");
631
+ }
632
+ const nextState = this.applyActivePreset(walletState, targetPreset.id);
633
+ await this.store.saveState(nextState);
634
+ await this.emitChainChangedForConnectedOrigins(nextState, previousChainId);
635
+ return {
636
+ kind: "result",
637
+ value: null
638
+ };
639
+ }
640
+ case "xian_watchAsset": {
641
+ const walletState = this.requireStoredWallet(state);
642
+ this.requireConnectedOrigin(walletState, origin);
643
+ this.getUnlockedSigner();
644
+ return {
645
+ kind: "approval",
646
+ account: walletState.publicKey,
647
+ chainId: this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState))
648
+ };
649
+ }
650
+ case "xian_signMessage": {
651
+ const walletState = this.requireStoredWallet(state);
652
+ this.requireConnectedOrigin(walletState, origin);
653
+ this.getUnlockedSigner();
654
+ return {
655
+ kind: "approval",
656
+ account: walletState.publicKey,
657
+ chainId: this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState))
658
+ };
659
+ }
660
+ case "xian_prepareTransaction": {
661
+ const walletState = this.requireStoredWallet(state);
662
+ this.requireConnectedOrigin(walletState, origin);
663
+ this.getUnlockedSigner();
664
+ const { intent } = firstParamObject(request.params);
665
+ return {
666
+ kind: "result",
667
+ value: await this.prepareTransaction(walletState, intent)
668
+ };
669
+ }
670
+ case "xian_signTransaction":
671
+ case "xian_sendTransaction":
672
+ case "xian_sendCall": {
673
+ const walletState = this.requireStoredWallet(state);
674
+ this.requireConnectedOrigin(walletState, origin);
675
+ this.getUnlockedSigner();
676
+ return {
677
+ kind: "approval",
678
+ account: walletState.publicKey,
679
+ chainId: this.displayChainId(this.activeNetworkPreset(walletState), await this.safeGetChainId(walletState))
680
+ };
681
+ }
682
+ default:
683
+ throw new ProviderUnsupportedMethodError(request.method);
684
+ }
685
+ }
686
+ async getPopupState() {
687
+ const state = await this.loadWalletState();
688
+ const approvals = await this.store.listApprovalStates();
689
+ const pendingApprovals = approvals
690
+ .map((approval) => approval.view)
691
+ .sort((left, right) => right.createdAt - left.createdAt);
692
+ const activePreset = state ? this.activeNetworkPreset(state) : undefined;
693
+ const resolvedChainId = await this.safeGetChainId(state);
694
+ return {
695
+ hasWallet: state != null,
696
+ unlocked: this.unlockedPrivateKey != null,
697
+ publicKey: state?.publicKey,
698
+ rpcUrl: state?.rpcUrl ?? DEFAULT_RPC_URL,
699
+ dashboardUrl: state?.dashboardUrl ?? DEFAULT_DASHBOARD_URL,
700
+ chainId: activePreset
701
+ ? this.displayChainId(activePreset, resolvedChainId)
702
+ : undefined,
703
+ resolvedChainId,
704
+ configuredChainId: activePreset?.chainId,
705
+ networkStatus: activePreset
706
+ ? this.networkStatus(activePreset, resolvedChainId)
707
+ : "unreachable",
708
+ activeNetworkId: activePreset?.id,
709
+ activeNetworkName: activePreset?.name,
710
+ networkPresets: state?.networkPresets ?? DEFAULT_NETWORK_PRESETS,
711
+ watchedAssets: state?.watchedAssets ?? [],
712
+ connectedOrigins: state?.connectedOrigins ?? [],
713
+ pendingApprovalCount: pendingApprovals.length,
714
+ pendingApprovals,
715
+ hasRecoveryPhrase: Boolean(state?.encryptedMnemonic),
716
+ seedSource: state?.seedSource,
717
+ mnemonicWordCount: state?.mnemonicWordCount,
718
+ version: this.options.version
719
+ };
720
+ }
721
+ async createOrImportWallet(input) {
722
+ const secret = await createWalletSecret({
723
+ privateKey: input.privateKey,
724
+ mnemonic: input.mnemonic,
725
+ createWithMnemonic: input.createWithMnemonic
726
+ });
727
+ const signer = new Ed25519Signer(secret.privateKey);
728
+ const encryptedPrivateKey = await encryptPrivateKey(secret.privateKey, input.password);
729
+ const encryptedMnemonic = secret.mnemonic
730
+ ? await encryptMnemonic(secret.mnemonic, input.password)
731
+ : undefined;
732
+ this.unlockedPrivateKey = secret.privateKey;
733
+ this.unlockedSigner = signer;
734
+ const waiters = [...this.requestWaiters.values()];
735
+ this.requestWaiters.clear();
736
+ for (const waiter of waiters) {
737
+ waiter.reject(new ProviderUnauthorizedError("wallet was replaced"));
738
+ }
739
+ for (const requestState of await this.store.listRequestStates()) {
740
+ await this.store.deleteRequestState(requestState.requestId);
741
+ }
742
+ for (const approval of await this.store.listApprovalStates()) {
743
+ await this.store.deleteApprovalState(approval.id);
744
+ }
745
+ const setupRpcUrl = trimOptionalString(input.rpcUrl) ?? DEFAULT_RPC_URL;
746
+ const setupDashboardUrl = trimOptionalString(input.dashboardUrl) ?? DEFAULT_DASHBOARD_URL;
747
+ const localPreset = createLocalNetworkPreset();
748
+ const useLocalPreset = setupRpcUrl === localPreset.rpcUrl &&
749
+ (setupDashboardUrl ?? "") === (localPreset.dashboardUrl ?? "");
750
+ const customPresetId = useLocalPreset ? undefined : this.createId();
751
+ const activePreset = useLocalPreset
752
+ ? localPreset
753
+ : normalizePresetInputValue({
754
+ id: customPresetId,
755
+ name: trimOptionalString(input.networkName) ?? "Custom network",
756
+ chainId: trimOptionalString(input.expectedChainId),
757
+ rpcUrl: setupRpcUrl,
758
+ dashboardUrl: setupDashboardUrl,
759
+ builtin: false
760
+ }, {
761
+ id: customPresetId ?? "custom-network",
762
+ name: trimOptionalString(input.networkName) ?? "Custom network",
763
+ rpcUrl: setupRpcUrl,
764
+ dashboardUrl: setupDashboardUrl,
765
+ builtin: false
766
+ });
767
+ const networkPresets = useLocalPreset
768
+ ? [localPreset]
769
+ : [localPreset, activePreset];
770
+ const popupState = await this.persistWalletState({
771
+ publicKey: signer.address,
772
+ encryptedPrivateKey,
773
+ encryptedMnemonic,
774
+ seedSource: secret.seedSource,
775
+ mnemonicWordCount: secret.mnemonicWordCount,
776
+ rpcUrl: activePreset.rpcUrl,
777
+ dashboardUrl: activePreset.dashboardUrl,
778
+ activeNetworkId: activePreset.id,
779
+ networkPresets,
780
+ watchedAssets: [
781
+ {
782
+ contract: "currency",
783
+ name: "Xian",
784
+ symbol: "XIAN"
785
+ }
786
+ ],
787
+ connectedOrigins: [],
788
+ createdAt: new Date().toISOString()
789
+ });
790
+ return {
791
+ popupState,
792
+ generatedMnemonic: secret.generatedMnemonic,
793
+ importedSeedSource: secret.seedSource
794
+ };
795
+ }
796
+ async unlockWallet(password) {
797
+ const state = this.requireStoredWallet(await this.loadWalletState());
798
+ const privateKey = await decryptPrivateKey(state.encryptedPrivateKey, password);
799
+ const signer = new Ed25519Signer(privateKey);
800
+ if (signer.address !== state.publicKey) {
801
+ throw new Error("decrypted private key does not match stored wallet");
802
+ }
803
+ this.unlockedPrivateKey = privateKey;
804
+ this.unlockedSigner = signer;
805
+ const chainId = this.displayChainId(this.activeNetworkPreset(state), await this.safeGetChainId(state));
806
+ await Promise.all(state.connectedOrigins.map((origin) => this.emitConnectionLifecycle(origin, chainId ?? "unknown", state.publicKey)));
807
+ return this.getPopupState();
808
+ }
809
+ async revealMnemonic(password) {
810
+ const state = this.requireStoredWallet(await this.loadWalletState());
811
+ if (!state.encryptedMnemonic) {
812
+ throw new Error("wallet does not have a recovery phrase");
813
+ }
814
+ return decryptMnemonic(state.encryptedMnemonic, password);
815
+ }
816
+ async lockWallet() {
817
+ const state = await this.loadWalletState();
818
+ this.unlockedPrivateKey = null;
819
+ this.unlockedSigner = null;
820
+ if (state) {
821
+ await Promise.all(state.connectedOrigins.map((origin) => this.emitDisconnectLifecycle(origin)));
822
+ }
823
+ return this.getPopupState();
824
+ }
825
+ async updateSettings(input) {
826
+ const state = this.requireStoredWallet(await this.loadWalletState());
827
+ const activePreset = this.activeNetworkPreset(state);
828
+ const previousChainId = this.displayChainId(activePreset, await this.safeGetChainId(state));
829
+ const nextState = this.upsertNetworkPresetInState(state, {
830
+ id: activePreset.builtin ? undefined : activePreset.id,
831
+ name: trimOptionalString(input.networkName) ??
832
+ (activePreset.builtin ? "Custom network" : activePreset.name),
833
+ chainId: trimOptionalString(input.expectedChainId) ?? activePreset.chainId,
834
+ rpcUrl: input.rpcUrl.trim() || DEFAULT_RPC_URL,
835
+ dashboardUrl: input.dashboardUrl?.trim() || DEFAULT_DASHBOARD_URL,
836
+ makeActive: true
837
+ });
838
+ await this.store.saveState(nextState);
839
+ await this.emitChainChangedForConnectedOrigins(nextState, previousChainId);
840
+ return this.getPopupState();
841
+ }
842
+ async disconnectOrigin(origin) {
843
+ const state = await this.loadWalletState();
844
+ if (!state || !state.connectedOrigins.includes(origin)) {
845
+ return this.getPopupState();
846
+ }
847
+ await this.updateConnectedOrigin(origin, false);
848
+ await this.emitDisconnectLifecycle(origin);
849
+ return this.getPopupState();
850
+ }
851
+ async disconnectAllOrigins() {
852
+ const state = await this.loadWalletState();
853
+ if (!state || state.connectedOrigins.length === 0) {
854
+ return this.getPopupState();
855
+ }
856
+ const nextState = {
857
+ ...state,
858
+ connectedOrigins: []
859
+ };
860
+ await this.store.saveState(nextState);
861
+ await Promise.all(state.connectedOrigins.map((origin) => this.emitDisconnectLifecycle(origin)));
862
+ return this.getPopupState();
863
+ }
864
+ async removeWatchedAsset(contract) {
865
+ const trimmed = contract.trim();
866
+ if (trimmed.length === 0) {
867
+ throw new TypeError("asset contract is required");
868
+ }
869
+ const state = this.requireStoredWallet(await this.loadWalletState());
870
+ if (!state.watchedAssets.some((asset) => asset.contract === trimmed)) {
871
+ return this.getPopupState();
872
+ }
873
+ if (trimmed === "currency") {
874
+ throw new Error("the native XIAN asset is pinned in the wallet");
875
+ }
876
+ return this.persistWalletState({
877
+ ...state,
878
+ watchedAssets: state.watchedAssets.filter((asset) => asset.contract !== trimmed)
879
+ });
880
+ }
881
+ async saveNetworkPreset(input) {
882
+ const state = this.requireStoredWallet(await this.loadWalletState());
883
+ const previousChainId = this.displayChainId(this.activeNetworkPreset(state), await this.safeGetChainId(state));
884
+ const nextState = this.upsertNetworkPresetInState(state, input);
885
+ await this.store.saveState(nextState);
886
+ await this.emitChainChangedForConnectedOrigins(nextState, previousChainId);
887
+ return this.getPopupState();
888
+ }
889
+ async switchNetwork(presetId) {
890
+ const state = this.requireStoredWallet(await this.loadWalletState());
891
+ const normalizedPresetId = presetId.trim();
892
+ if (!normalizedPresetId) {
893
+ throw new TypeError("network preset id is required");
894
+ }
895
+ const previousChainId = this.displayChainId(this.activeNetworkPreset(state), await this.safeGetChainId(state));
896
+ const nextState = this.applyActivePreset(state, normalizedPresetId);
897
+ await this.store.saveState(nextState);
898
+ await this.emitChainChangedForConnectedOrigins(nextState, previousChainId);
899
+ return this.getPopupState();
900
+ }
901
+ async removeNetworkPreset(presetId) {
902
+ const state = this.requireStoredWallet(await this.loadWalletState());
903
+ const normalizedPresetId = presetId.trim();
904
+ if (!normalizedPresetId) {
905
+ throw new TypeError("network preset id is required");
906
+ }
907
+ const preset = state.networkPresets.find((entry) => entry.id === normalizedPresetId);
908
+ if (!preset) {
909
+ return this.getPopupState();
910
+ }
911
+ if (preset.builtin) {
912
+ throw new Error("built-in network presets cannot be deleted");
913
+ }
914
+ const previousChainId = this.displayChainId(this.activeNetworkPreset(state), await this.safeGetChainId(state));
915
+ const nextPresets = state.networkPresets.filter((entry) => entry.id !== normalizedPresetId);
916
+ const nextActiveNetworkId = state.activeNetworkId === normalizedPresetId
917
+ ? createLocalNetworkPreset().id
918
+ : state.activeNetworkId;
919
+ const nextState = this.applyActivePreset({
920
+ ...state,
921
+ networkPresets: nextPresets
922
+ }, nextActiveNetworkId);
923
+ await this.store.saveState(nextState);
924
+ await this.emitChainChangedForConnectedOrigins(nextState, previousChainId);
925
+ return this.getPopupState();
926
+ }
927
+ async startProviderRequest(requestId, origin, request) {
928
+ const existing = await this.store.loadRequestState(requestId);
929
+ if (existing) {
930
+ if (existing.status === "pending") {
931
+ return {
932
+ status: "pending",
933
+ approvalId: existing.approvalId ?? ""
934
+ };
935
+ }
936
+ if (existing.status === "fulfilled") {
937
+ return {
938
+ status: "fulfilled",
939
+ result: existing.result
940
+ };
941
+ }
942
+ return {
943
+ status: "rejected",
944
+ error: existing.error ?? {
945
+ name: "Error",
946
+ message: "request failed"
947
+ }
948
+ };
949
+ }
950
+ const requestState = {
951
+ requestId,
952
+ origin,
953
+ request,
954
+ createdAt: this.now(),
955
+ updatedAt: this.now(),
956
+ status: "pending"
957
+ };
958
+ await this.store.saveRequestState(requestState);
959
+ try {
960
+ const immediate = await this.executeImmediateRequest(await this.loadWalletState(), origin, request);
961
+ if (immediate.kind === "result") {
962
+ const fulfilled = await this.fulfillRequest(requestState, immediate.value);
963
+ if (fulfilled.status !== "fulfilled") {
964
+ throw new Error("immediate request did not settle correctly");
965
+ }
966
+ return fulfilled;
967
+ }
968
+ return this.createApprovalRequest(requestState, immediate.account, immediate.chainId);
969
+ }
970
+ catch (error) {
971
+ const rejected = await this.rejectRequest(requestState, error);
972
+ if (rejected.status !== "rejected") {
973
+ throw new Error("request rejection did not settle correctly");
974
+ }
975
+ return rejected;
976
+ }
977
+ }
978
+ async getProviderRequestStatus(requestId, options) {
979
+ const state = await this.store.loadRequestState(requestId);
980
+ if (!state) {
981
+ return {
982
+ status: "not_found"
983
+ };
984
+ }
985
+ if (state.status === "pending") {
986
+ return {
987
+ status: "pending",
988
+ approvalId: state.approvalId
989
+ };
990
+ }
991
+ if (options?.consume) {
992
+ await this.store.deleteRequestState(requestId);
993
+ }
994
+ if (state.status === "fulfilled") {
995
+ return {
996
+ status: "fulfilled",
997
+ result: state.result
998
+ };
999
+ }
1000
+ return {
1001
+ status: "rejected",
1002
+ error: state.error ?? {
1003
+ name: "Error",
1004
+ message: "request failed"
1005
+ }
1006
+ };
1007
+ }
1008
+ async getApprovalView(approvalId) {
1009
+ const approval = await this.store.loadApprovalState(approvalId);
1010
+ if (!approval) {
1011
+ throw new Error("approval request not found");
1012
+ }
1013
+ return approval.view;
1014
+ }
1015
+ async listApprovalStates() {
1016
+ return this.store.listApprovalStates();
1017
+ }
1018
+ async attachApprovalWindow(approvalId, windowId) {
1019
+ const approval = await this.store.loadApprovalState(approvalId);
1020
+ if (!approval) {
1021
+ return;
1022
+ }
1023
+ await this.store.saveApprovalState({
1024
+ ...approval,
1025
+ windowId
1026
+ });
1027
+ }
1028
+ async resolveApproval(approvalId, approved) {
1029
+ const approval = await this.store.loadApprovalState(approvalId);
1030
+ if (!approval) {
1031
+ throw new Error("approval request not found");
1032
+ }
1033
+ const requestState = await this.store.loadRequestState(approval.requestId);
1034
+ if (!requestState) {
1035
+ await this.store.deleteApprovalState(approval.id);
1036
+ throw new Error("approval request is no longer active");
1037
+ }
1038
+ await this.store.deleteApprovalState(approvalId);
1039
+ if (!approved) {
1040
+ await this.rejectRequest(requestState, new ProviderUnauthorizedError("user rejected the request"));
1041
+ return null;
1042
+ }
1043
+ try {
1044
+ const result = await this.executeApprovedRequest(approval.record.origin, approval.record.request);
1045
+ await this.fulfillRequest(requestState, result);
1046
+ return null;
1047
+ }
1048
+ catch (error) {
1049
+ await this.rejectRequest(requestState, error);
1050
+ return null;
1051
+ }
1052
+ }
1053
+ async dismissApproval(approvalId, reason = new ProviderUnauthorizedError("approval dismissed")) {
1054
+ const approval = await this.store.loadApprovalState(approvalId);
1055
+ if (!approval) {
1056
+ return false;
1057
+ }
1058
+ const requestState = await this.store.loadRequestState(approval.requestId);
1059
+ await this.store.deleteApprovalState(approvalId);
1060
+ if (requestState) {
1061
+ await this.rejectRequest(requestState, reason);
1062
+ }
1063
+ return true;
1064
+ }
1065
+ async handleProviderRequest(origin, request) {
1066
+ const requestId = this.createId();
1067
+ const start = await this.startProviderRequest(requestId, origin, request);
1068
+ if (start.status === "fulfilled") {
1069
+ return start.result;
1070
+ }
1071
+ if (start.status === "rejected") {
1072
+ throw hydrateError(start.error);
1073
+ }
1074
+ return new Promise((resolve, reject) => {
1075
+ this.requestWaiters.set(requestId, { resolve, reject });
1076
+ });
1077
+ }
1078
+ }
1079
+ export function errorFromSerializedWalletError(error) {
1080
+ return hydrateError(error);
1081
+ }
1082
+ //# sourceMappingURL=controller.js.map