epistery 1.0.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/Architecture.md +84 -0
- package/CLI.md +291 -0
- package/LICENSE +21 -0
- package/MONGODB_GOTCHA.md +69 -0
- package/README.md +50 -0
- package/SESSION.md +98 -0
- package/cli/epistery.mjs +576 -0
- package/client/client.js +18 -0
- package/client/ethers.js +24441 -0
- package/client/ethers.min.js +1 -0
- package/client/export.js +67 -0
- package/client/status.html +707 -0
- package/client/wallet.js +213 -0
- package/client/witness.js +663 -0
- package/contracts/agent.sol +108 -0
- package/default.ini +14 -0
- package/docs/EpisteryModuleConfig.md +317 -0
- package/docs/blog-unified-config.md +125 -0
- package/hardhat.config.js +33 -0
- package/index.mjs +385 -0
- package/package.json +46 -0
- package/scripts/deploy-agent.js +33 -0
- package/scripts/verify-agent.js +39 -0
- package/src/epistery.ts +275 -0
- package/src/utils/Aqua.ts +194 -0
- package/src/utils/CliWallet.ts +334 -0
- package/src/utils/Config.ts +196 -0
- package/src/utils/Utils.ts +571 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/types.ts +114 -0
- package/test/README.md +50 -0
- package/test/index.html +13 -0
- package/test/package.json +15 -0
- package/test/server.mjs +87 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Witness - Browser client for Epistery
|
|
3
|
+
*
|
|
4
|
+
* This is the browser-side client that connects to the Epistery server
|
|
5
|
+
* and provides local wallet functionality for signing data
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Wallet, Web3Wallet, BrowserWallet } from './wallet.js';
|
|
9
|
+
|
|
10
|
+
// Global ethers variable - will be loaded dynamically if needed
|
|
11
|
+
let ethers;
|
|
12
|
+
|
|
13
|
+
// Function to ensure ethers is loaded
|
|
14
|
+
async function ensureEthers() {
|
|
15
|
+
if (ethers) return ethers;
|
|
16
|
+
|
|
17
|
+
if (typeof window !== 'undefined' && window.ethers) {
|
|
18
|
+
ethers = window.ethers;
|
|
19
|
+
return ethers;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Dynamically import ethers from the epistery lib endpoint
|
|
23
|
+
try {
|
|
24
|
+
const ethersModule = await import('/.well-known/epistery/lib/ethers.js');
|
|
25
|
+
ethers = ethersModule.ethers || ethersModule.default || ethersModule;
|
|
26
|
+
// Make it available globally for future use
|
|
27
|
+
if (typeof window !== 'undefined') {
|
|
28
|
+
window.ethers = ethers;
|
|
29
|
+
}
|
|
30
|
+
return ethers;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to load ethers.js:', error);
|
|
33
|
+
throw new Error('ethers.js is required but not available');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default class Witness {
|
|
38
|
+
constructor() {
|
|
39
|
+
if (Witness.instance) return Witness.instance;
|
|
40
|
+
Witness.instance = this;
|
|
41
|
+
this.wallet = null;
|
|
42
|
+
this.server = null;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
save() {
|
|
47
|
+
const storageData = this.loadStorageData();
|
|
48
|
+
|
|
49
|
+
// If current wallet exists, update or add it to the wallets array
|
|
50
|
+
if (this.wallet) {
|
|
51
|
+
const walletData = {
|
|
52
|
+
id: this.wallet.id || this.generateWalletId(this.wallet.source),
|
|
53
|
+
wallet: this.wallet.toJSON(),
|
|
54
|
+
label: this.wallet.label || (this.wallet.source === 'web3' ? 'Web3 Wallet' : 'Browser Wallet'),
|
|
55
|
+
createdAt: this.wallet.createdAt || Date.now(),
|
|
56
|
+
lastUsed: Date.now()
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Store the ID back on the wallet object
|
|
60
|
+
this.wallet.id = walletData.id;
|
|
61
|
+
this.wallet.label = walletData.label;
|
|
62
|
+
this.wallet.createdAt = walletData.createdAt;
|
|
63
|
+
|
|
64
|
+
// Update or add wallet in the array
|
|
65
|
+
const existingIndex = storageData.wallets.findIndex(w => w.id === walletData.id);
|
|
66
|
+
if (existingIndex >= 0) {
|
|
67
|
+
storageData.wallets[existingIndex] = walletData;
|
|
68
|
+
} else {
|
|
69
|
+
storageData.wallets.push(walletData);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Set as default if no default exists
|
|
73
|
+
if (!storageData.defaultWalletId) {
|
|
74
|
+
storageData.defaultWalletId = walletData.id;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
storageData.server = this.server;
|
|
79
|
+
|
|
80
|
+
localStorage.setItem('epistery', JSON.stringify(storageData));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
loadStorageData() {
|
|
84
|
+
const data = localStorage.getItem('epistery');
|
|
85
|
+
if (!data) {
|
|
86
|
+
return { wallets: [], defaultWalletId: null, server: null };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(data);
|
|
91
|
+
|
|
92
|
+
// Migrate old single-wallet format to new multi-wallet format
|
|
93
|
+
if (parsed.wallet && !parsed.wallets) {
|
|
94
|
+
const migratedWalletId = this.generateWalletId(parsed.wallet.source);
|
|
95
|
+
return {
|
|
96
|
+
wallets: [{
|
|
97
|
+
id: migratedWalletId,
|
|
98
|
+
wallet: parsed.wallet,
|
|
99
|
+
label: parsed.wallet.source === 'web3' ? 'Web3 Wallet' : 'Browser Wallet',
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
lastUsed: Date.now()
|
|
102
|
+
}],
|
|
103
|
+
defaultWalletId: migratedWalletId,
|
|
104
|
+
server: parsed.server
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
wallets: parsed.wallets || [],
|
|
110
|
+
defaultWalletId: parsed.defaultWalletId || null,
|
|
111
|
+
server: parsed.server || null
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Failed to parse epistery data:', error);
|
|
115
|
+
return { wallets: [], defaultWalletId: null, server: null };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async load() {
|
|
120
|
+
const storageData = this.loadStorageData();
|
|
121
|
+
|
|
122
|
+
this.server = storageData.server;
|
|
123
|
+
|
|
124
|
+
// Check if migration happened and persist it immediately to avoid data loss
|
|
125
|
+
const currentData = localStorage.getItem('epistery');
|
|
126
|
+
if (currentData) {
|
|
127
|
+
const parsed = JSON.parse(currentData);
|
|
128
|
+
// If we migrated from old format (had wallet but no wallets), save the migration
|
|
129
|
+
if (parsed.wallet && !parsed.wallets && storageData.wallets.length > 0) {
|
|
130
|
+
console.log('[epistery] Migrating old wallet format to multi-wallet format');
|
|
131
|
+
localStorage.setItem('epistery', JSON.stringify(storageData));
|
|
132
|
+
console.log('[epistery] Migration complete - wallet preserved');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Load the default wallet if it exists (maintains backward compatibility)
|
|
137
|
+
if (storageData.defaultWalletId && ethers) {
|
|
138
|
+
const walletData = storageData.wallets.find(w => w.id === storageData.defaultWalletId);
|
|
139
|
+
if (walletData) {
|
|
140
|
+
this.wallet = await Wallet.fromJSON(walletData.wallet, ethers);
|
|
141
|
+
this.wallet.id = walletData.id;
|
|
142
|
+
this.wallet.label = walletData.label;
|
|
143
|
+
this.wallet.createdAt = walletData.createdAt;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
generateWalletId(source) {
|
|
149
|
+
const prefix = source === 'web3' ? 'web3-wallet' : 'browser-wallet';
|
|
150
|
+
return prefix + '-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static async connect(options = {}) {
|
|
154
|
+
let witness = new Witness();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Ensure ethers is loaded first
|
|
158
|
+
ethers = await ensureEthers();
|
|
159
|
+
|
|
160
|
+
// Load existing data (now that ethers is available)
|
|
161
|
+
await witness.load();
|
|
162
|
+
|
|
163
|
+
// Get server info to check chain compatibility
|
|
164
|
+
await witness.fetchServerInfo();
|
|
165
|
+
|
|
166
|
+
// Initialize wallet if needed
|
|
167
|
+
if (!witness.wallet) {
|
|
168
|
+
await witness.initialize();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Verify chain compatibility and switch if needed
|
|
172
|
+
await witness.ensureChainCompatibility();
|
|
173
|
+
|
|
174
|
+
// Perform key exchange (skip if skipKeyExchange option is true)
|
|
175
|
+
if (!options.skipKeyExchange) {
|
|
176
|
+
await witness.performKeyExchange();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error('Failed to connect to Epistery server:', e);
|
|
181
|
+
// For unclaimed domains, wallet discovery might succeed even if key exchange fails
|
|
182
|
+
if (!options.skipKeyExchange) {
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return witness;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async initialize() {
|
|
191
|
+
try {
|
|
192
|
+
// Try Web3 first, then fall back to browser wallet
|
|
193
|
+
this.wallet = await Web3Wallet.create(ethers);
|
|
194
|
+
|
|
195
|
+
if (!this.wallet) {
|
|
196
|
+
this.wallet = await BrowserWallet.create(ethers);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.wallet) {
|
|
200
|
+
console.log(`Wallet initialized: ${this.wallet.source} (${this.wallet.address})`);
|
|
201
|
+
this.save();
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error('Failed to create any wallet type');
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('Failed to initialize wallet:', e);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async fetchServerInfo() {
|
|
211
|
+
try {
|
|
212
|
+
const response = await fetch('/.well-known/epistery');
|
|
213
|
+
if (response.ok) {
|
|
214
|
+
const serverInfo = await response.json();
|
|
215
|
+
this.serverInfo = serverInfo.server;
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error('Failed to fetch server info');
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error('Failed to fetch server info:', e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async ensureChainCompatibility() {
|
|
225
|
+
if (!this.wallet || this.wallet.source !== 'web3' || !this.serverInfo) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const targetChainId = this.serverInfo.chainId;
|
|
231
|
+
const targetRpc = this.serverInfo.rpc;
|
|
232
|
+
|
|
233
|
+
// Get current chain ID
|
|
234
|
+
const currentNetwork = await this.wallet.provider.getNetwork();
|
|
235
|
+
|
|
236
|
+
// Parse server chain ID (remove comma if present)
|
|
237
|
+
const expectedChainId = parseInt(targetChainId.toString().replace(',', ''));
|
|
238
|
+
|
|
239
|
+
if (currentNetwork.chainId !== expectedChainId) {
|
|
240
|
+
await this.requestChainSwitch(expectedChainId, targetRpc, this.serverInfo.provider);
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.warn('Chain compatibility check failed:', e);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async requestChainSwitch(chainId, rpcUrl, networkName) {
|
|
248
|
+
if (!window.ethereum) {
|
|
249
|
+
throw new Error('No Web3 provider available for chain switching');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// First, try to switch to the chain
|
|
254
|
+
await window.ethereum.request({
|
|
255
|
+
method: 'wallet_switchEthereumChain',
|
|
256
|
+
params: [{ chainId: `0x${chainId.toString(16)}` }],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
} catch (switchError) {
|
|
260
|
+
// If the chain hasn't been added to MetaMask, add it
|
|
261
|
+
if (switchError.code === 4902) {
|
|
262
|
+
try {
|
|
263
|
+
// Build nativeCurrency from serverInfo with sensible defaults
|
|
264
|
+
const nativeCurrency = this.serverInfo?.nativeCurrency || {
|
|
265
|
+
name: 'ETH',
|
|
266
|
+
symbol: 'ETH',
|
|
267
|
+
decimals: 18
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
await window.ethereum.request({
|
|
271
|
+
method: 'wallet_addEthereumChain',
|
|
272
|
+
params: [{
|
|
273
|
+
chainId: `0x${chainId.toString(16)}`,
|
|
274
|
+
chainName: networkName || `Chain ${chainId}`,
|
|
275
|
+
rpcUrls: [rpcUrl],
|
|
276
|
+
nativeCurrency: nativeCurrency,
|
|
277
|
+
}],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
} catch (addError) {
|
|
281
|
+
console.error('Failed to add chain:', addError);
|
|
282
|
+
throw addError;
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
console.error('Failed to switch chain:', switchError);
|
|
286
|
+
throw switchError;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Recreate the provider connection after chain switch
|
|
291
|
+
if (this.wallet && this.wallet.source === 'web3') {
|
|
292
|
+
this.wallet.provider = new ethers.providers.Web3Provider(window.ethereum);
|
|
293
|
+
this.wallet.signer = this.wallet.provider.getSigner();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
generateChallenge() {
|
|
298
|
+
// Generate a random 32-byte challenge for key exchange
|
|
299
|
+
return ethers.utils.hexlify(ethers.utils.randomBytes(32));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async performKeyExchange() {
|
|
303
|
+
try {
|
|
304
|
+
if (!this.wallet) {
|
|
305
|
+
throw new Error('No wallet available for key exchange');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Create a message to sign for identity proof
|
|
309
|
+
const challenge = this.generateChallenge();
|
|
310
|
+
const message = `Epistery Key Exchange - ${this.wallet.address} - ${challenge}`;
|
|
311
|
+
|
|
312
|
+
// Sign the message using the wallet
|
|
313
|
+
const signature = await this.wallet.sign(message, ethers);
|
|
314
|
+
|
|
315
|
+
// Get the updated public key (especially important for Web3 wallets)
|
|
316
|
+
const publicKey = this.wallet.publicKey;
|
|
317
|
+
|
|
318
|
+
// Send key exchange request to server
|
|
319
|
+
const keyExchangeData = {
|
|
320
|
+
clientAddress: this.wallet.address,
|
|
321
|
+
clientPublicKey: publicKey,
|
|
322
|
+
challenge: challenge,
|
|
323
|
+
message: message,
|
|
324
|
+
signature: signature,
|
|
325
|
+
walletSource: this.wallet.source
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const response = await fetch('/.well-known/epistery/connect', {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
credentials: 'include',
|
|
331
|
+
headers: {'Content-Type': 'application/json'},
|
|
332
|
+
body: JSON.stringify(keyExchangeData)
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (response.ok) {
|
|
336
|
+
const serverResponse = await response.json();
|
|
337
|
+
|
|
338
|
+
// Verify server's identity by checking signature
|
|
339
|
+
if (this.verifyServerIdentity(serverResponse)) {
|
|
340
|
+
this.server = {
|
|
341
|
+
address: serverResponse.serverAddress,
|
|
342
|
+
publicKey: serverResponse.serverPublicKey,
|
|
343
|
+
services: serverResponse.services,
|
|
344
|
+
challenge: serverResponse.challenge,
|
|
345
|
+
signature: serverResponse.signature,
|
|
346
|
+
identified: true,
|
|
347
|
+
provider: this.serverInfo?.provider,
|
|
348
|
+
chainId: this.serverInfo?.chainId,
|
|
349
|
+
rpc: this.serverInfo?.rpc,
|
|
350
|
+
nativeCurrency: this.serverInfo?.nativeCurrency
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
this.save();
|
|
354
|
+
console.log('Key exchange completed successfully');
|
|
355
|
+
console.log('Server address:', this.server.address);
|
|
356
|
+
console.log('Available services:', this.server.services);
|
|
357
|
+
} else {
|
|
358
|
+
throw new Error('Server identity verification failed');
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const errorResponse = await response.json();
|
|
362
|
+
throw new Error(`Key exchange failed with status: ${response.status} - ${errorResponse.error || 'Unknown error'}`);
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error('Key exchange failed:', e);
|
|
366
|
+
throw e;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
verifyServerIdentity(serverResponse) {
|
|
371
|
+
try {
|
|
372
|
+
// Reconstruct the message the server should have signed
|
|
373
|
+
const expectedMessage = `Epistery Server Response - ${serverResponse.serverAddress} - ${serverResponse.challenge}`;
|
|
374
|
+
|
|
375
|
+
// Verify the signature matches the server's public key
|
|
376
|
+
const recoveredAddress = ethers.utils.verifyMessage(expectedMessage, serverResponse.signature);
|
|
377
|
+
return recoveredAddress === serverResponse.serverAddress;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
console.error('Server identity verification error:', e);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async transferOwnershipEvent(futureOwnerWalletAddress) {
|
|
385
|
+
if (!this.wallet) {
|
|
386
|
+
throw new Error('Wallet not initialized');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const clientWalletInfo = {
|
|
391
|
+
address: this.wallet.address,
|
|
392
|
+
publicKey: this.wallet.publicKey,
|
|
393
|
+
mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
|
|
394
|
+
privateKey: this.wallet.privateKey || '', // Only available for browser wallets
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
let options = {
|
|
398
|
+
method: 'PUT',
|
|
399
|
+
credentials: 'include',
|
|
400
|
+
headers: {'Content-Type': 'application/json'}
|
|
401
|
+
};
|
|
402
|
+
options.body = JSON.stringify({
|
|
403
|
+
clientWalletInfo: clientWalletInfo,
|
|
404
|
+
futureOwnerWalletAddress: futureOwnerWalletAddress
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
let result = await fetch('/.well-known/epistery/data/ownership', options);
|
|
408
|
+
|
|
409
|
+
if (result.ok) {
|
|
410
|
+
return await result.json();
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
throw new Error(`Transfer ownership failed with status: ${result.status}`);
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
console.error('Failed to execute transfer ownership event:', e);
|
|
417
|
+
throw e;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async readEvent() {
|
|
422
|
+
if (!this.wallet) {
|
|
423
|
+
throw new Error('Wallet not initialized');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const clientWalletInfo = {
|
|
428
|
+
address: this.wallet.address,
|
|
429
|
+
publicKey: this.wallet.publicKey,
|
|
430
|
+
mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
|
|
431
|
+
privateKey: this.wallet.privateKey || '', // Only available for browser wallets
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
let options = {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
credentials: 'include',
|
|
437
|
+
headers: {'Content-Type': 'application/json'}
|
|
438
|
+
};
|
|
439
|
+
options.body = JSON.stringify({
|
|
440
|
+
clientWalletInfo: clientWalletInfo,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
let result = await fetch('/.well-known/epistery/data/read', options);
|
|
444
|
+
if (result.status === 204) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (result.ok) {
|
|
449
|
+
return await result.json();
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
throw new Error(`Read failed with status: ${result.status}`);
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {
|
|
455
|
+
console.error('Failed to execute read event:', e);
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async writeEvent(data) {
|
|
461
|
+
if (!this.wallet) {
|
|
462
|
+
throw new Error('Wallet not initialized');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
// Convert wallet to the format expected by the server
|
|
467
|
+
const clientWalletInfo = {
|
|
468
|
+
address: this.wallet.address,
|
|
469
|
+
publicKey: this.wallet.publicKey,
|
|
470
|
+
mnemonic: this.wallet.mnemonic || '', // Only available for browser wallets
|
|
471
|
+
privateKey: this.wallet.privateKey || '', // Only available for browser wallets
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
let options = {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
credentials: 'include',
|
|
477
|
+
headers: {'Content-Type': 'application/json'}
|
|
478
|
+
};
|
|
479
|
+
options.body = JSON.stringify({
|
|
480
|
+
clientWalletInfo: clientWalletInfo,
|
|
481
|
+
data: data
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
let result = await fetch('/.well-known/epistery/data/write', options);
|
|
485
|
+
|
|
486
|
+
if (result.ok) {
|
|
487
|
+
return await result.json();
|
|
488
|
+
} else {
|
|
489
|
+
throw new Error(`Write failed with status: ${result.status}`);
|
|
490
|
+
}
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.error('Failed to write event:', e);
|
|
493
|
+
throw e;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Wallet management methods for multi-wallet support
|
|
498
|
+
|
|
499
|
+
getWallets() {
|
|
500
|
+
const storageData = this.loadStorageData();
|
|
501
|
+
return {
|
|
502
|
+
wallets: storageData.wallets.map(w => ({
|
|
503
|
+
id: w.id,
|
|
504
|
+
address: w.wallet.address,
|
|
505
|
+
source: w.wallet.source,
|
|
506
|
+
label: w.label,
|
|
507
|
+
createdAt: w.createdAt,
|
|
508
|
+
lastUsed: w.lastUsed,
|
|
509
|
+
isDefault: w.id === storageData.defaultWalletId
|
|
510
|
+
})),
|
|
511
|
+
defaultWalletId: storageData.defaultWalletId
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async addWeb3Wallet(label = null) {
|
|
516
|
+
await ensureEthers();
|
|
517
|
+
const newWallet = await Web3Wallet.create(ethers);
|
|
518
|
+
|
|
519
|
+
if (!newWallet) {
|
|
520
|
+
throw new Error('Failed to connect Web3 wallet. User may have cancelled or no Web3 provider available.');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
newWallet.label = label || 'Web3 Wallet';
|
|
524
|
+
|
|
525
|
+
// Temporarily set as active wallet to save it
|
|
526
|
+
const previousWallet = this.wallet;
|
|
527
|
+
this.wallet = newWallet;
|
|
528
|
+
this.save();
|
|
529
|
+
|
|
530
|
+
// Restore previous wallet if there was one
|
|
531
|
+
if (previousWallet) {
|
|
532
|
+
this.wallet = previousWallet;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
id: newWallet.id,
|
|
537
|
+
address: newWallet.address,
|
|
538
|
+
source: newWallet.source,
|
|
539
|
+
label: newWallet.label
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async addBrowserWallet(label = null) {
|
|
544
|
+
await ensureEthers();
|
|
545
|
+
const newWallet = await BrowserWallet.create(ethers);
|
|
546
|
+
|
|
547
|
+
if (!newWallet) {
|
|
548
|
+
throw new Error('Failed to create browser wallet');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
newWallet.label = label || 'Browser Wallet';
|
|
552
|
+
|
|
553
|
+
// Temporarily set as active wallet to save it
|
|
554
|
+
const previousWallet = this.wallet;
|
|
555
|
+
this.wallet = newWallet;
|
|
556
|
+
this.save();
|
|
557
|
+
|
|
558
|
+
// Restore previous wallet if there was one
|
|
559
|
+
if (previousWallet) {
|
|
560
|
+
this.wallet = previousWallet;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
id: newWallet.id,
|
|
565
|
+
address: newWallet.address,
|
|
566
|
+
source: newWallet.source,
|
|
567
|
+
label: newWallet.label
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async setDefaultWallet(walletId) {
|
|
572
|
+
const storageData = this.loadStorageData();
|
|
573
|
+
const walletData = storageData.wallets.find(w => w.id === walletId);
|
|
574
|
+
|
|
575
|
+
if (!walletData) {
|
|
576
|
+
throw new Error(`Wallet with ID ${walletId} not found`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await ensureEthers();
|
|
580
|
+
|
|
581
|
+
// Load the wallet
|
|
582
|
+
this.wallet = await Wallet.fromJSON(walletData.wallet, ethers);
|
|
583
|
+
this.wallet.id = walletData.id;
|
|
584
|
+
this.wallet.label = walletData.label;
|
|
585
|
+
this.wallet.createdAt = walletData.createdAt;
|
|
586
|
+
|
|
587
|
+
// Update default in storage
|
|
588
|
+
storageData.defaultWalletId = walletId;
|
|
589
|
+
storageData.wallets = storageData.wallets.map(w => {
|
|
590
|
+
if (w.id === walletId) {
|
|
591
|
+
w.lastUsed = Date.now();
|
|
592
|
+
}
|
|
593
|
+
return w;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
localStorage.setItem('epistery', JSON.stringify(storageData));
|
|
597
|
+
|
|
598
|
+
console.log(`Switched to wallet: ${this.wallet.source} (${this.wallet.address})`);
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
id: this.wallet.id,
|
|
602
|
+
address: this.wallet.address,
|
|
603
|
+
source: this.wallet.source,
|
|
604
|
+
label: this.wallet.label
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
removeWallet(walletId) {
|
|
609
|
+
const storageData = this.loadStorageData();
|
|
610
|
+
|
|
611
|
+
// Don't allow removing the default wallet if it's the only one
|
|
612
|
+
if (storageData.wallets.length === 1) {
|
|
613
|
+
throw new Error('Cannot remove the only wallet');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Don't allow removing the default wallet without switching first
|
|
617
|
+
if (storageData.defaultWalletId === walletId) {
|
|
618
|
+
throw new Error('Cannot remove default wallet. Switch to another wallet first.');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const walletIndex = storageData.wallets.findIndex(w => w.id === walletId);
|
|
622
|
+
if (walletIndex === -1) {
|
|
623
|
+
throw new Error(`Wallet with ID ${walletId} not found`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
storageData.wallets.splice(walletIndex, 1);
|
|
627
|
+
localStorage.setItem('epistery', JSON.stringify(storageData));
|
|
628
|
+
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
updateWalletLabel(walletId, newLabel) {
|
|
633
|
+
const storageData = this.loadStorageData();
|
|
634
|
+
const walletData = storageData.wallets.find(w => w.id === walletId);
|
|
635
|
+
|
|
636
|
+
if (!walletData) {
|
|
637
|
+
throw new Error(`Wallet with ID ${walletId} not found`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
walletData.label = newLabel;
|
|
641
|
+
|
|
642
|
+
// If this is the current wallet, update it too
|
|
643
|
+
if (this.wallet && this.wallet.id === walletId) {
|
|
644
|
+
this.wallet.label = newLabel;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
localStorage.setItem('epistery', JSON.stringify(storageData));
|
|
648
|
+
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
getStatus() {
|
|
653
|
+
return {
|
|
654
|
+
client: this.wallet ? {
|
|
655
|
+
address: this.wallet.address,
|
|
656
|
+
publicKey: this.wallet.publicKey,
|
|
657
|
+
source: this.wallet.source
|
|
658
|
+
} : null,
|
|
659
|
+
server: this.server,
|
|
660
|
+
connected: !!(this.wallet && this.server)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|