@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.
- package/README.md +17 -0
- package/dist/approvals.d.ts +7 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +287 -0
- package/dist/approvals.js.map +1 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +24 -0
- package/dist/constants.js.map +1 -0
- package/dist/controller.d.ts +96 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +1082 -0
- package/dist/controller.js.map +1 -0
- package/dist/crypto.d.ts +25 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +199 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
|
@@ -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
|