sphere-connect 1.0.1
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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/index.d.mts +553 -0
- package/dist/index.d.ts +553 -0
- package/dist/index.js +1407 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1364 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
import { Cedra, PrivateKey, PrivateKeyVariants, Ed25519PrivateKey, Account, CedraConfig, Network } from '@cedra-labs/ts-sdk';
|
|
2
|
+
export { Account, Cedra, CedraConfig, Network as CedraNetwork, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants } from '@cedra-labs/ts-sdk';
|
|
3
|
+
import { decodeJwt, jwtVerify } from 'jose';
|
|
4
|
+
import CryptoJS2 from 'crypto-js';
|
|
5
|
+
import { createContext, useState, useCallback, useEffect, useContext } from 'react';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
// src/core/EventEmitter.ts
|
|
9
|
+
var EventEmitter = class {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
12
|
+
}
|
|
13
|
+
on(event, listener) {
|
|
14
|
+
if (!this.listeners.has(event)) {
|
|
15
|
+
this.listeners.set(event, []);
|
|
16
|
+
}
|
|
17
|
+
this.listeners.get(event).push(listener);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
off(event, listener) {
|
|
21
|
+
const eventListeners = this.listeners.get(event);
|
|
22
|
+
if (eventListeners) {
|
|
23
|
+
const index = eventListeners.indexOf(listener);
|
|
24
|
+
if (index !== -1) {
|
|
25
|
+
eventListeners.splice(index, 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
emit(event, ...args) {
|
|
31
|
+
const eventListeners = this.listeners.get(event);
|
|
32
|
+
if (eventListeners && eventListeners.length > 0) {
|
|
33
|
+
eventListeners.forEach((listener) => listener(...args));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
removeAllListeners(event) {
|
|
39
|
+
if (event) {
|
|
40
|
+
this.listeners.delete(event);
|
|
41
|
+
} else {
|
|
42
|
+
this.listeners.clear();
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/types/index.ts
|
|
49
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
50
|
+
ErrorCode2["AUTHENTICATION_FAILED"] = "AUTHENTICATION_FAILED";
|
|
51
|
+
ErrorCode2["WALLET_CREATION_FAILED"] = "WALLET_CREATION_FAILED";
|
|
52
|
+
ErrorCode2["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
|
|
53
|
+
ErrorCode2["INSUFFICIENT_BALANCE"] = "INSUFFICIENT_BALANCE";
|
|
54
|
+
ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
55
|
+
ErrorCode2["INVALID_ADDRESS"] = "INVALID_ADDRESS";
|
|
56
|
+
ErrorCode2["SESSION_EXPIRED"] = "SESSION_EXPIRED";
|
|
57
|
+
ErrorCode2["STORAGE_ERROR"] = "STORAGE_ERROR";
|
|
58
|
+
ErrorCode2["KEY_DERIVATION_FAILED"] = "KEY_DERIVATION_FAILED";
|
|
59
|
+
ErrorCode2["TRANSACTION_ABORTED"] = "TRANSACTION_ABORTED";
|
|
60
|
+
ErrorCode2["DISCARDED_TRANSACTION"] = "DISCARDED_TRANSACTION";
|
|
61
|
+
ErrorCode2["INVALID_CONFIG"] = "INVALID_CONFIG";
|
|
62
|
+
return ErrorCode2;
|
|
63
|
+
})(ErrorCode || {});
|
|
64
|
+
var SphereSDKError = class extends Error {
|
|
65
|
+
constructor(code, message, details) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.details = details;
|
|
69
|
+
this.name = "SphereSDKError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/wallet/SphereWallet.ts
|
|
74
|
+
var SphereWallet = class {
|
|
75
|
+
constructor(clientOrKey, privateKeyOrNetwork, networkOrEndpoint, rpcEndpoint, indexerUrl) {
|
|
76
|
+
this.keylessConfig = null;
|
|
77
|
+
this._indexerUrl = null;
|
|
78
|
+
try {
|
|
79
|
+
if (clientOrKey instanceof Cedra) {
|
|
80
|
+
this.client = clientOrKey;
|
|
81
|
+
this.privateKeyHex = privateKeyOrNetwork;
|
|
82
|
+
const net = networkOrEndpoint;
|
|
83
|
+
this.network = this.mapNetwork(net || "testnet");
|
|
84
|
+
this._indexerUrl = rpcEndpoint || null;
|
|
85
|
+
const formattedKey = PrivateKey.formatPrivateKey(this.privateKeyHex, PrivateKeyVariants.Ed25519);
|
|
86
|
+
const privateKey = new Ed25519PrivateKey(formattedKey);
|
|
87
|
+
this.account = Account.fromPrivateKey({ privateKey });
|
|
88
|
+
} else {
|
|
89
|
+
this.privateKeyHex = clientOrKey;
|
|
90
|
+
const net = privateKeyOrNetwork;
|
|
91
|
+
this.network = this.mapNetwork(net || "testnet");
|
|
92
|
+
const endpoint = rpcEndpoint || networkOrEndpoint;
|
|
93
|
+
const config = new CedraConfig({
|
|
94
|
+
network: this.network,
|
|
95
|
+
fullnode: endpoint
|
|
96
|
+
});
|
|
97
|
+
this.client = new Cedra(config);
|
|
98
|
+
this._indexerUrl = indexerUrl || null;
|
|
99
|
+
const formattedKey = PrivateKey.formatPrivateKey(this.privateKeyHex, PrivateKeyVariants.Ed25519);
|
|
100
|
+
const privateKey = new Ed25519PrivateKey(formattedKey);
|
|
101
|
+
this.account = Account.fromPrivateKey({ privateKey });
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new SphereSDKError(
|
|
105
|
+
"WALLET_CREATION_FAILED" /* WALLET_CREATION_FAILED */,
|
|
106
|
+
"Failed to create wallet",
|
|
107
|
+
error
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getAddress() {
|
|
112
|
+
return this.account.accountAddress.toString();
|
|
113
|
+
}
|
|
114
|
+
getPublicKey() {
|
|
115
|
+
return this.account.publicKey.toString();
|
|
116
|
+
}
|
|
117
|
+
getIndexerUrl() {
|
|
118
|
+
return this._indexerUrl;
|
|
119
|
+
}
|
|
120
|
+
async existsOnChain() {
|
|
121
|
+
try {
|
|
122
|
+
await this.client.getAccountInfo({
|
|
123
|
+
accountAddress: this.account.accountAddress
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async getBalance(coinType) {
|
|
131
|
+
const defaultCoinType = "0x1::cedra_coin::CedraCoin";
|
|
132
|
+
const targetCoinType = coinType || defaultCoinType;
|
|
133
|
+
try {
|
|
134
|
+
if (targetCoinType.startsWith("0x") && !targetCoinType.includes("::")) {
|
|
135
|
+
const [balance] = await this.client.view({
|
|
136
|
+
payload: {
|
|
137
|
+
function: "0x1::primary_fungible_store::balance",
|
|
138
|
+
typeArguments: ["0x1::fungible_asset::Metadata"],
|
|
139
|
+
functionArguments: [this.getAddress(), targetCoinType]
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const [decimals] = await this.client.view({
|
|
143
|
+
payload: {
|
|
144
|
+
function: "0x1::fungible_asset::decimals",
|
|
145
|
+
typeArguments: ["0x1::fungible_asset::Metadata"],
|
|
146
|
+
functionArguments: [targetCoinType]
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
const amountVal = balance?.toString() || "0";
|
|
150
|
+
const decimalsVal = decimals ? Number(decimals) : 8;
|
|
151
|
+
return {
|
|
152
|
+
coinType: targetCoinType,
|
|
153
|
+
amount: amountVal,
|
|
154
|
+
decimals: decimalsVal,
|
|
155
|
+
formatted: (Number(amountVal) / Math.pow(10, decimalsVal)).toFixed(Math.min(decimalsVal, 8))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const amount = await this.client.getAccountCoinAmount({
|
|
159
|
+
accountAddress: this.account.accountAddress,
|
|
160
|
+
coinType: targetCoinType
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
coinType: targetCoinType,
|
|
164
|
+
amount: amount.toString(),
|
|
165
|
+
decimals: 8,
|
|
166
|
+
formatted: (Number(amount) / 1e8).toFixed(8)
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
coinType: targetCoinType,
|
|
171
|
+
amount: "0",
|
|
172
|
+
decimals: 8,
|
|
173
|
+
formatted: "0.00000000"
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async getAllBalances() {
|
|
178
|
+
return [await this.getBalance()];
|
|
179
|
+
}
|
|
180
|
+
async sendTransaction(options, simulate = true) {
|
|
181
|
+
try {
|
|
182
|
+
const { to, amount, coinType, maxGasAmount, gasUnitPrice } = options;
|
|
183
|
+
const transaction = await this.client.transaction.build.simple({
|
|
184
|
+
sender: this.account.accountAddress,
|
|
185
|
+
data: {
|
|
186
|
+
function: coinType?.startsWith("0x") && !coinType.includes("::") ? "0x1::primary_fungible_store::transfer" : "0x1::cedra_account::transfer",
|
|
187
|
+
typeArguments: coinType?.startsWith("0x") && !coinType.includes("::") ? ["0x1::fungible_asset::Metadata"] : [],
|
|
188
|
+
functionArguments: coinType?.startsWith("0x") && !coinType.includes("::") ? [coinType, to, amount] : [to, amount]
|
|
189
|
+
},
|
|
190
|
+
options: {
|
|
191
|
+
maxGasAmount: maxGasAmount || 2e4,
|
|
192
|
+
gasUnitPrice
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (simulate) {
|
|
196
|
+
const [sim] = await this.client.transaction.simulate.simple({
|
|
197
|
+
signerPublicKey: this.account.publicKey,
|
|
198
|
+
transaction
|
|
199
|
+
});
|
|
200
|
+
if (!sim.success) throw new Error(`Simulation failed: ${sim.vm_status}`);
|
|
201
|
+
}
|
|
202
|
+
const authenticator = await this.client.transaction.sign({
|
|
203
|
+
signer: this.account,
|
|
204
|
+
transaction
|
|
205
|
+
});
|
|
206
|
+
const pending = await this.client.transaction.submit.simple({
|
|
207
|
+
transaction,
|
|
208
|
+
senderAuthenticator: authenticator
|
|
209
|
+
});
|
|
210
|
+
return await this.waitForTransaction(pending.hash);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new SphereSDKError("TRANSACTION_FAILED" /* TRANSACTION_FAILED */, error.message, error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async waitForTransaction(hash) {
|
|
216
|
+
const tx = await this.client.waitForTransaction({ transactionHash: hash });
|
|
217
|
+
return {
|
|
218
|
+
hash,
|
|
219
|
+
success: tx.success,
|
|
220
|
+
vmStatus: tx.vm_status,
|
|
221
|
+
gasUsed: tx.gas_used,
|
|
222
|
+
version: tx.version
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async getGasPrice() {
|
|
226
|
+
const res = await this.client.getGasPriceEstimation();
|
|
227
|
+
return res.gas_estimate;
|
|
228
|
+
}
|
|
229
|
+
async getTransactionHistory() {
|
|
230
|
+
const txs = await this.client.getAccountTransactions({
|
|
231
|
+
accountAddress: this.account.accountAddress
|
|
232
|
+
});
|
|
233
|
+
return txs.map((tx) => ({
|
|
234
|
+
hash: tx.hash,
|
|
235
|
+
success: tx.success,
|
|
236
|
+
type: tx.type,
|
|
237
|
+
sender: tx.sender,
|
|
238
|
+
receiver: tx.payload?.functionArguments?.[0],
|
|
239
|
+
amount: tx.payload?.functionArguments?.[1],
|
|
240
|
+
timestamp: parseInt(tx.timestamp || "0") / 1e3,
|
|
241
|
+
version: tx.version
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
async simulateTransaction(options) {
|
|
245
|
+
const { to, amount, maxGasAmount, gasUnitPrice } = options;
|
|
246
|
+
const transaction = await this.client.transaction.build.simple({
|
|
247
|
+
sender: this.account.accountAddress,
|
|
248
|
+
data: {
|
|
249
|
+
function: "0x1::cedra_account::transfer",
|
|
250
|
+
functionArguments: [to, amount]
|
|
251
|
+
},
|
|
252
|
+
options: {
|
|
253
|
+
maxGasAmount: maxGasAmount || 2e4,
|
|
254
|
+
gasUnitPrice
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const [res] = await this.client.transaction.simulate.simple({
|
|
258
|
+
signerPublicKey: this.account.publicKey,
|
|
259
|
+
transaction
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
success: res.success,
|
|
263
|
+
gasUsed: res.gas_used,
|
|
264
|
+
gasUnitPrice: res.gas_unit_price,
|
|
265
|
+
totalGasCost: Number(res.gas_used) * Number(res.gas_unit_price),
|
|
266
|
+
vmStatus: res.vm_status
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async signMessage(message) {
|
|
270
|
+
const sig = this.account.sign(new TextEncoder().encode(message));
|
|
271
|
+
return sig.toString();
|
|
272
|
+
}
|
|
273
|
+
async getAccountInfo() {
|
|
274
|
+
const info = await this.client.getAccountInfo({ accountAddress: this.account.accountAddress });
|
|
275
|
+
return {
|
|
276
|
+
address: this.getAddress(),
|
|
277
|
+
publicKey: this.getPublicKey(),
|
|
278
|
+
existsOnChain: true,
|
|
279
|
+
sequenceNumber: info.sequence_number,
|
|
280
|
+
authKey: info.authentication_key
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async disconnect() {
|
|
284
|
+
this.account = null;
|
|
285
|
+
}
|
|
286
|
+
updateNetwork(network, rpcEndpoint) {
|
|
287
|
+
this.network = this.mapNetwork(network);
|
|
288
|
+
this.client = new Cedra(new CedraConfig({ network: this.network, fullnode: rpcEndpoint }));
|
|
289
|
+
}
|
|
290
|
+
mapNetwork(network) {
|
|
291
|
+
switch (network.toLowerCase()) {
|
|
292
|
+
case "mainnet":
|
|
293
|
+
return Network.MAINNET;
|
|
294
|
+
case "testnet":
|
|
295
|
+
return Network.TESTNET;
|
|
296
|
+
case "devnet":
|
|
297
|
+
return Network.DEVNET;
|
|
298
|
+
default:
|
|
299
|
+
return Network.TESTNET;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
getAccount() {
|
|
303
|
+
return this.account;
|
|
304
|
+
}
|
|
305
|
+
getClient() {
|
|
306
|
+
return this.client;
|
|
307
|
+
}
|
|
308
|
+
getPrivateKeyHex() {
|
|
309
|
+
return this.privateKeyHex;
|
|
310
|
+
}
|
|
311
|
+
setKeylessConfig(config) {
|
|
312
|
+
this.keylessConfig = config;
|
|
313
|
+
}
|
|
314
|
+
isKeyless() {
|
|
315
|
+
return !!this.keylessConfig;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var GoogleAuthProvider = class {
|
|
319
|
+
constructor(config) {
|
|
320
|
+
this.clientId = config.clientId;
|
|
321
|
+
this.redirectUri = config.redirectUri;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Initiate Google OAuth login flow
|
|
325
|
+
* Opens Google Sign-In popup
|
|
326
|
+
* @param nonce Optional OIDC nonce for keyless binding
|
|
327
|
+
*/
|
|
328
|
+
async login(nonce) {
|
|
329
|
+
try {
|
|
330
|
+
if (typeof window === "undefined") {
|
|
331
|
+
throw new SphereSDKError(
|
|
332
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
333
|
+
"Google OAuth requires a browser environment"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
this.loadGoogleIdentityScript().then(() => {
|
|
338
|
+
const uxMode = this.redirectUri ? "redirect" : "popup";
|
|
339
|
+
if (uxMode === "redirect") {
|
|
340
|
+
const nonce2 = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
341
|
+
const params = new URLSearchParams({
|
|
342
|
+
client_id: this.clientId,
|
|
343
|
+
redirect_uri: this.redirectUri,
|
|
344
|
+
response_type: "id_token",
|
|
345
|
+
scope: "openid email profile",
|
|
346
|
+
nonce: nonce2,
|
|
347
|
+
prompt: "select_account"
|
|
348
|
+
});
|
|
349
|
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
350
|
+
window.location.href = authUrl;
|
|
351
|
+
} else {
|
|
352
|
+
const client = google.accounts.oauth2.initTokenClient({
|
|
353
|
+
client_id: this.clientId,
|
|
354
|
+
scope: "email profile openid",
|
|
355
|
+
nonce,
|
|
356
|
+
// Bind the ephemeral key pair via nonce
|
|
357
|
+
callback: async (tokenResponse) => {
|
|
358
|
+
if (tokenResponse.error) {
|
|
359
|
+
reject(new SphereSDKError(
|
|
360
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
361
|
+
`Google login failed: ${tokenResponse.error_description || tokenResponse.error}`
|
|
362
|
+
));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (tokenResponse && tokenResponse.access_token) {
|
|
366
|
+
try {
|
|
367
|
+
const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
|
|
368
|
+
headers: {
|
|
369
|
+
"Authorization": `Bearer ${tokenResponse.access_token}`
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (!userInfoResponse.ok) {
|
|
373
|
+
throw new Error("Failed to fetch user info from Google");
|
|
374
|
+
}
|
|
375
|
+
const payload = await userInfoResponse.json();
|
|
376
|
+
const authResponse = {
|
|
377
|
+
idToken: tokenResponse.access_token,
|
|
378
|
+
// Use access token
|
|
379
|
+
email: payload.email,
|
|
380
|
+
name: payload.name,
|
|
381
|
+
picture: payload.picture,
|
|
382
|
+
sub: payload.sub
|
|
383
|
+
};
|
|
384
|
+
resolve(authResponse);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
reject(new SphereSDKError(
|
|
387
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
388
|
+
"Failed to retrieve user profile after login",
|
|
389
|
+
error
|
|
390
|
+
));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
client.requestAccessToken();
|
|
396
|
+
}
|
|
397
|
+
}).catch(reject);
|
|
398
|
+
});
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof SphereSDKError) {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
throw new SphereSDKError(
|
|
404
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
405
|
+
"Failed to initiate Google login",
|
|
406
|
+
error
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Initialize Google Identity Services
|
|
412
|
+
* @param callback Credential handler
|
|
413
|
+
* @param nonce Optional OIDC nonce for binding
|
|
414
|
+
*/
|
|
415
|
+
async initialize(callback, nonce) {
|
|
416
|
+
await this.loadGoogleIdentityScript();
|
|
417
|
+
google.accounts.id.initialize({
|
|
418
|
+
client_id: this.clientId,
|
|
419
|
+
callback,
|
|
420
|
+
nonce
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Handle the credential response from Google
|
|
425
|
+
*/
|
|
426
|
+
async handleCredentialResponse(credential) {
|
|
427
|
+
try {
|
|
428
|
+
const payload = decodeJwt(credential);
|
|
429
|
+
const authResponse = {
|
|
430
|
+
idToken: credential,
|
|
431
|
+
email: payload.email,
|
|
432
|
+
name: payload.name,
|
|
433
|
+
picture: payload.picture,
|
|
434
|
+
sub: payload.sub
|
|
435
|
+
};
|
|
436
|
+
if (!authResponse.email || !authResponse.sub) {
|
|
437
|
+
throw new SphereSDKError(
|
|
438
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
439
|
+
"Invalid Google credential: missing required fields"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return authResponse;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (error instanceof SphereSDKError) {
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
throw new SphereSDKError(
|
|
448
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
449
|
+
"Failed to process Google credential",
|
|
450
|
+
error
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Verify Google ID token (optional, for enhanced security)
|
|
456
|
+
* This should ideally be done server-side
|
|
457
|
+
*/
|
|
458
|
+
async verifyToken(idToken) {
|
|
459
|
+
try {
|
|
460
|
+
const JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
461
|
+
const response = await fetch(JWKS_URI);
|
|
462
|
+
const jwks = await response.json();
|
|
463
|
+
await jwtVerify(idToken, async () => {
|
|
464
|
+
return jwks.keys[0];
|
|
465
|
+
});
|
|
466
|
+
return true;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error("Token verification failed:", error);
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Load Google Identity Services script
|
|
474
|
+
*/
|
|
475
|
+
loadGoogleIdentityScript() {
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
if (typeof google !== "undefined" && google.accounts) {
|
|
478
|
+
resolve();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const script = document.createElement("script");
|
|
482
|
+
script.src = "https://accounts.google.com/gsi/client";
|
|
483
|
+
script.async = true;
|
|
484
|
+
script.defer = true;
|
|
485
|
+
script.onload = () => resolve();
|
|
486
|
+
script.onerror = () => reject(
|
|
487
|
+
new SphereSDKError(
|
|
488
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
489
|
+
"Failed to load Google Identity Services"
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
document.head.appendChild(script);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Sign out from Google
|
|
497
|
+
*/
|
|
498
|
+
async logout() {
|
|
499
|
+
try {
|
|
500
|
+
if (typeof google !== "undefined" && google.accounts) {
|
|
501
|
+
google.accounts.id.disableAutoSelect();
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error("Google logout error:", error);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Render Google Sign-In button
|
|
509
|
+
* @param elementId ID of the container element
|
|
510
|
+
* @param options Button customization options
|
|
511
|
+
* @param options Button customization options
|
|
512
|
+
*/
|
|
513
|
+
async renderButton(elementId, options) {
|
|
514
|
+
await this.loadGoogleIdentityScript();
|
|
515
|
+
if (options?.callback) {
|
|
516
|
+
google.accounts.id.initialize({
|
|
517
|
+
client_id: this.clientId,
|
|
518
|
+
callback: options.callback,
|
|
519
|
+
nonce: options.nonce
|
|
520
|
+
// Pass nonce to button initialization
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
google.accounts.id.renderButton(document.getElementById(elementId), {
|
|
524
|
+
theme: options?.theme || "outline",
|
|
525
|
+
size: options?.size || "large",
|
|
526
|
+
text: options?.text || "signin_with",
|
|
527
|
+
shape: options?.shape || "rectangular",
|
|
528
|
+
width: options?.width
|
|
529
|
+
// Google API accepts pixels as number or string
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
var SecureStorage = class {
|
|
534
|
+
constructor(encryptionKey, storagePrefix = "cedra_sdk_") {
|
|
535
|
+
this.encryptionKey = encryptionKey || this.generateEncryptionKey();
|
|
536
|
+
this.storagePrefix = storagePrefix;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get item from storage and decrypt
|
|
540
|
+
*/
|
|
541
|
+
async getItem(key) {
|
|
542
|
+
try {
|
|
543
|
+
const fullKey = this.storagePrefix + key;
|
|
544
|
+
const encryptedData = localStorage.getItem(fullKey);
|
|
545
|
+
if (!encryptedData) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
const bytes = CryptoJS2.AES.decrypt(encryptedData, this.encryptionKey);
|
|
549
|
+
try {
|
|
550
|
+
const decrypted = bytes.toString(CryptoJS2.enc.Utf8);
|
|
551
|
+
if (!decrypted) return null;
|
|
552
|
+
return decrypted;
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.warn(`[SecureStorage] Failed to decrypt ${key}. Key might have changed.`);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error("Storage get error:", error);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Encrypt and store item
|
|
564
|
+
*/
|
|
565
|
+
async setItem(key, value) {
|
|
566
|
+
try {
|
|
567
|
+
const fullKey = this.storagePrefix + key;
|
|
568
|
+
const encrypted = CryptoJS2.AES.encrypt(
|
|
569
|
+
value,
|
|
570
|
+
this.encryptionKey
|
|
571
|
+
).toString();
|
|
572
|
+
localStorage.setItem(fullKey, encrypted);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error("Storage set error:", error);
|
|
575
|
+
throw new SphereSDKError(
|
|
576
|
+
"STORAGE_ERROR" /* STORAGE_ERROR */,
|
|
577
|
+
"Failed to store data",
|
|
578
|
+
error
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Remove item from storage
|
|
584
|
+
*/
|
|
585
|
+
async removeItem(key) {
|
|
586
|
+
try {
|
|
587
|
+
const fullKey = this.storagePrefix + key;
|
|
588
|
+
localStorage.removeItem(fullKey);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error("Storage remove error:", error);
|
|
591
|
+
throw new SphereSDKError(
|
|
592
|
+
"STORAGE_ERROR" /* STORAGE_ERROR */,
|
|
593
|
+
"Failed to remove data from storage",
|
|
594
|
+
error
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Clear all SDK storage
|
|
600
|
+
*/
|
|
601
|
+
async clear() {
|
|
602
|
+
try {
|
|
603
|
+
const keys = Object.keys(localStorage);
|
|
604
|
+
for (const key of keys) {
|
|
605
|
+
if (key.startsWith(this.storagePrefix)) {
|
|
606
|
+
localStorage.removeItem(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} catch (error) {
|
|
610
|
+
console.error("Storage clear error:", error);
|
|
611
|
+
throw new SphereSDKError(
|
|
612
|
+
"STORAGE_ERROR" /* STORAGE_ERROR */,
|
|
613
|
+
"Failed to clear storage",
|
|
614
|
+
error
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Generate encryption key from browser fingerprint
|
|
620
|
+
* This provides basic encryption without requiring user input
|
|
621
|
+
* Note: Not cryptographically secure, but better than plaintext
|
|
622
|
+
*/
|
|
623
|
+
generateEncryptionKey() {
|
|
624
|
+
const fingerprint = [
|
|
625
|
+
navigator.userAgent,
|
|
626
|
+
navigator.language,
|
|
627
|
+
(/* @__PURE__ */ new Date()).getTimezoneOffset(),
|
|
628
|
+
screen.width,
|
|
629
|
+
screen.height,
|
|
630
|
+
"cedra-sdk-v1"
|
|
631
|
+
// Version salt
|
|
632
|
+
].join("|");
|
|
633
|
+
return CryptoJS2.SHA256(fingerprint).toString();
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Check if storage is available
|
|
637
|
+
*/
|
|
638
|
+
static isAvailable() {
|
|
639
|
+
try {
|
|
640
|
+
const test = "__storage_test__";
|
|
641
|
+
localStorage.setItem(test, test);
|
|
642
|
+
localStorage.removeItem(test);
|
|
643
|
+
return true;
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var SessionManager = class {
|
|
650
|
+
constructor(storage, sessionDurationHours = 24) {
|
|
651
|
+
this.sessionKey = "current_session";
|
|
652
|
+
// in milliseconds
|
|
653
|
+
this.cachedSession = null;
|
|
654
|
+
this.storage = storage;
|
|
655
|
+
this.sessionDuration = sessionDurationHours * 60 * 60 * 1e3;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Get cached session data (synchronous)
|
|
659
|
+
*/
|
|
660
|
+
getCachedSession() {
|
|
661
|
+
return this.cachedSession;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Save session data
|
|
665
|
+
*/
|
|
666
|
+
async saveSession(data) {
|
|
667
|
+
try {
|
|
668
|
+
const sessionData = {
|
|
669
|
+
...data,
|
|
670
|
+
expiresAt: Date.now() + this.sessionDuration
|
|
671
|
+
};
|
|
672
|
+
this.cachedSession = sessionData;
|
|
673
|
+
await this.storage.setItem(
|
|
674
|
+
this.sessionKey,
|
|
675
|
+
JSON.stringify(sessionData)
|
|
676
|
+
);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
throw new SphereSDKError(
|
|
679
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
680
|
+
`Google authentication failed: ${error.message}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Get current session
|
|
686
|
+
* Returns null if session doesn't exist or is expired
|
|
687
|
+
*/
|
|
688
|
+
async getSession() {
|
|
689
|
+
try {
|
|
690
|
+
const sessionStr = await this.storage.getItem(this.sessionKey);
|
|
691
|
+
if (!sessionStr) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
const session = JSON.parse(sessionStr);
|
|
695
|
+
this.cachedSession = session;
|
|
696
|
+
if (Date.now() > session.expiresAt) {
|
|
697
|
+
await this.clearSession();
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
return session;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
console.error("Failed to get session:", error);
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Clear current session
|
|
708
|
+
*/
|
|
709
|
+
async clearSession() {
|
|
710
|
+
this.cachedSession = null;
|
|
711
|
+
await this.storage.removeItem(this.sessionKey);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Check if session is valid
|
|
715
|
+
*/
|
|
716
|
+
async isSessionValid() {
|
|
717
|
+
const session = await this.getSession();
|
|
718
|
+
return session !== null;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Refresh session expiration
|
|
722
|
+
*/
|
|
723
|
+
async refreshSession() {
|
|
724
|
+
const session = await this.getSession();
|
|
725
|
+
if (session) {
|
|
726
|
+
await this.saveSession({
|
|
727
|
+
encryptedWallet: session.encryptedWallet,
|
|
728
|
+
userInfo: session.userInfo,
|
|
729
|
+
refreshToken: session.refreshToken
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get time until session expires (in milliseconds)
|
|
735
|
+
*/
|
|
736
|
+
async getTimeUntilExpiration() {
|
|
737
|
+
const session = await this.getSession();
|
|
738
|
+
if (!session) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const timeLeft = session.expiresAt - Date.now();
|
|
742
|
+
return timeLeft > 0 ? timeLeft : 0;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
var KeyDerivation = class {
|
|
746
|
+
// 256 bits for Ed25519
|
|
747
|
+
/**
|
|
748
|
+
* Derive a private key from user ID and salt
|
|
749
|
+
* @param options Key derivation options
|
|
750
|
+
* @returns Hex-encoded private key (64 characters)
|
|
751
|
+
*/
|
|
752
|
+
static derivePrivateKey(options) {
|
|
753
|
+
try {
|
|
754
|
+
const { userId, salt, iterations = this.DEFAULT_ITERATIONS } = options;
|
|
755
|
+
if (!userId || userId.length === 0) {
|
|
756
|
+
throw new SphereSDKError(
|
|
757
|
+
"KEY_DERIVATION_FAILED" /* KEY_DERIVATION_FAILED */,
|
|
758
|
+
"User ID is required for key derivation"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (!salt || salt.length < 16) {
|
|
762
|
+
throw new SphereSDKError(
|
|
763
|
+
"KEY_DERIVATION_FAILED" /* KEY_DERIVATION_FAILED */,
|
|
764
|
+
"Salt must be at least 16 characters"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const derivedKey = CryptoJS2.PBKDF2(userId, salt, {
|
|
768
|
+
keySize: this.KEY_LENGTH / 4,
|
|
769
|
+
// CryptoJS uses 32-bit words
|
|
770
|
+
iterations,
|
|
771
|
+
hasher: CryptoJS2.algo.SHA256
|
|
772
|
+
});
|
|
773
|
+
const privateKeyHex = derivedKey.toString(CryptoJS2.enc.Hex);
|
|
774
|
+
if (privateKeyHex.length !== 64) {
|
|
775
|
+
throw new SphereSDKError(
|
|
776
|
+
"KEY_DERIVATION_FAILED" /* KEY_DERIVATION_FAILED */,
|
|
777
|
+
"Derived key has invalid length"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return privateKeyHex;
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (error instanceof SphereSDKError) {
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
throw new SphereSDKError(
|
|
786
|
+
"KEY_DERIVATION_FAILED" /* KEY_DERIVATION_FAILED */,
|
|
787
|
+
"Failed to derive private key",
|
|
788
|
+
error
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Generate a deterministic salt from app name and user email
|
|
794
|
+
* This ensures the same user gets the same wallet across devices
|
|
795
|
+
* @param appName Application name
|
|
796
|
+
* @param userEmail User's email address
|
|
797
|
+
* @returns Deterministic salt string
|
|
798
|
+
*/
|
|
799
|
+
static generateDeterministicSalt(appName, userEmail) {
|
|
800
|
+
const normalizedApp = appName.trim().toLowerCase();
|
|
801
|
+
const normalizedEmail = userEmail.trim().toLowerCase();
|
|
802
|
+
const combined = `${normalizedApp}:${normalizedEmail}:sphere-wallet-v1`;
|
|
803
|
+
const hash = CryptoJS2.SHA256(combined);
|
|
804
|
+
return hash.toString(CryptoJS2.enc.Hex);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Validate a private key format
|
|
808
|
+
* @param privateKey Hex-encoded private key
|
|
809
|
+
* @returns True if valid
|
|
810
|
+
*/
|
|
811
|
+
static isValidPrivateKey(privateKey) {
|
|
812
|
+
const hexRegex = /^[0-9a-fA-F]{64}$/;
|
|
813
|
+
return hexRegex.test(privateKey);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Securely clear sensitive data from memory
|
|
817
|
+
* @param data Sensitive string to clear
|
|
818
|
+
*/
|
|
819
|
+
static clearSensitiveData(data) {
|
|
820
|
+
if (data) {
|
|
821
|
+
data = "0".repeat(data.length);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Generates a temporary ephemeral key pair for the session
|
|
826
|
+
* @returns A fresh Ed25519 account/keypair
|
|
827
|
+
*/
|
|
828
|
+
static generateEphemeralKeyPair() {
|
|
829
|
+
return Account.generate();
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Computes an OIDC-compliant nonce hash to bind the session key
|
|
833
|
+
* @param publicKey Ephemeral Public Key (hex or string)
|
|
834
|
+
* @param blinding A random blinding factor (hex)
|
|
835
|
+
* @param expiry Expiration timestamp (optional)
|
|
836
|
+
* @returns SHA256 hash of the nonce commitment
|
|
837
|
+
*/
|
|
838
|
+
static computeNonce(publicKey, blinding, expiry = 0) {
|
|
839
|
+
const combined = `${publicKey}:${blinding}:${expiry}`;
|
|
840
|
+
const hash = CryptoJS2.SHA256(combined);
|
|
841
|
+
return hash.toString(CryptoJS2.enc.Hex);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
KeyDerivation.DEFAULT_ITERATIONS = 1e5;
|
|
845
|
+
KeyDerivation.KEY_LENGTH = 32;
|
|
846
|
+
|
|
847
|
+
// src/core/constants.ts
|
|
848
|
+
var DEFAULT_APP_NAME = "Sphere Connect";
|
|
849
|
+
var DEFAULT_STORAGE_KEY = "sphere-connect-v1-secure-key";
|
|
850
|
+
var SphereAccountAbstraction = class extends EventEmitter {
|
|
851
|
+
constructor(config) {
|
|
852
|
+
super();
|
|
853
|
+
this.currentWallet = null;
|
|
854
|
+
this.initialized = false;
|
|
855
|
+
this.appName = DEFAULT_APP_NAME;
|
|
856
|
+
this.storageKey = DEFAULT_STORAGE_KEY;
|
|
857
|
+
if (!config.googleClientId) {
|
|
858
|
+
throw new SphereSDKError(
|
|
859
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
860
|
+
"Google Client ID is required for initializing the Sphere SDK"
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
this.config = {
|
|
864
|
+
network: config.network || "testnet",
|
|
865
|
+
googleClientId: config.googleClientId,
|
|
866
|
+
rpcEndpoint: config.rpcEndpoint,
|
|
867
|
+
indexerUrl: config.indexerUrl,
|
|
868
|
+
redirectUri: config.redirectUri
|
|
869
|
+
};
|
|
870
|
+
const rpc = this.config.rpcEndpoint;
|
|
871
|
+
const cedraConfig = new CedraConfig({
|
|
872
|
+
network: this.config.network,
|
|
873
|
+
fullnode: rpc
|
|
874
|
+
});
|
|
875
|
+
this.cedra = new Cedra(cedraConfig);
|
|
876
|
+
this.authProvider = new GoogleAuthProvider({
|
|
877
|
+
clientId: this.config.googleClientId,
|
|
878
|
+
redirectUri: this.config.redirectUri
|
|
879
|
+
});
|
|
880
|
+
this.storage = new SecureStorage(this.storageKey);
|
|
881
|
+
this.sessionManager = new SessionManager(this.storage, 24);
|
|
882
|
+
}
|
|
883
|
+
async initialize() {
|
|
884
|
+
if (this.initialized) return;
|
|
885
|
+
try {
|
|
886
|
+
await this.restoreSession();
|
|
887
|
+
this.initialized = true;
|
|
888
|
+
this.emit("initialized");
|
|
889
|
+
} catch (error) {
|
|
890
|
+
console.error("Sphere SDK initialization error:", error);
|
|
891
|
+
this.initialized = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async loginWithGoogle() {
|
|
895
|
+
try {
|
|
896
|
+
if (!this.initialized) await this.initialize();
|
|
897
|
+
const googleUser = await this.authProvider.login();
|
|
898
|
+
if (!googleUser || !googleUser.sub || !googleUser.email) {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
const salt = KeyDerivation.generateDeterministicSalt(this.appName, googleUser.email);
|
|
902
|
+
const privateKeyHex = KeyDerivation.derivePrivateKey({
|
|
903
|
+
userId: googleUser.sub,
|
|
904
|
+
salt,
|
|
905
|
+
iterations: 1e5
|
|
906
|
+
});
|
|
907
|
+
const wallet = new SphereWallet(
|
|
908
|
+
this.cedra,
|
|
909
|
+
privateKeyHex,
|
|
910
|
+
this.config.network,
|
|
911
|
+
this.config.rpcEndpoint,
|
|
912
|
+
this.config.indexerUrl
|
|
913
|
+
);
|
|
914
|
+
const encryptedWallet = this.encryptWalletData(privateKeyHex);
|
|
915
|
+
await this.sessionManager.saveSession({
|
|
916
|
+
encryptedWallet,
|
|
917
|
+
userInfo: {
|
|
918
|
+
email: googleUser.email,
|
|
919
|
+
name: googleUser.name,
|
|
920
|
+
picture: googleUser.picture
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
this.currentWallet = wallet;
|
|
924
|
+
this.emit("auth_success", { wallet, user: googleUser });
|
|
925
|
+
return wallet;
|
|
926
|
+
} catch (error) {
|
|
927
|
+
throw new SphereSDKError(
|
|
928
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
929
|
+
`Google authentication failed: ${error.message}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async restoreSession() {
|
|
934
|
+
try {
|
|
935
|
+
const session = await this.sessionManager.getSession();
|
|
936
|
+
if (session && session.expiresAt > Date.now()) {
|
|
937
|
+
const privateKeyHex = this.decryptWalletData(session.encryptedWallet);
|
|
938
|
+
if (!privateKeyHex) return null;
|
|
939
|
+
const wallet = new SphereWallet(
|
|
940
|
+
this.cedra,
|
|
941
|
+
privateKeyHex,
|
|
942
|
+
this.config.network,
|
|
943
|
+
this.config.rpcEndpoint,
|
|
944
|
+
this.config.indexerUrl
|
|
945
|
+
);
|
|
946
|
+
this.currentWallet = wallet;
|
|
947
|
+
this.emit("auth_success", { wallet, method: "restored" });
|
|
948
|
+
return wallet;
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
console.error("Session restoration failed:", error);
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
async handleGoogleResponse(credential) {
|
|
956
|
+
try {
|
|
957
|
+
if (!this.initialized) await this.initialize();
|
|
958
|
+
const googleUser = await this.authProvider.handleCredentialResponse(credential);
|
|
959
|
+
const salt = KeyDerivation.generateDeterministicSalt(this.appName, googleUser.email);
|
|
960
|
+
const privateKeyHex = KeyDerivation.derivePrivateKey({
|
|
961
|
+
userId: googleUser.sub,
|
|
962
|
+
salt,
|
|
963
|
+
iterations: 1e5
|
|
964
|
+
});
|
|
965
|
+
const wallet = new SphereWallet(
|
|
966
|
+
this.cedra,
|
|
967
|
+
privateKeyHex,
|
|
968
|
+
this.config.network,
|
|
969
|
+
this.config.rpcEndpoint,
|
|
970
|
+
this.config.indexerUrl
|
|
971
|
+
);
|
|
972
|
+
const encryptedWallet = this.encryptWalletData(privateKeyHex);
|
|
973
|
+
await this.sessionManager.saveSession({
|
|
974
|
+
encryptedWallet,
|
|
975
|
+
userInfo: {
|
|
976
|
+
email: googleUser.email,
|
|
977
|
+
name: googleUser.name,
|
|
978
|
+
picture: googleUser.picture
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
this.currentWallet = wallet;
|
|
982
|
+
this.emit("auth_success", { wallet, user: googleUser });
|
|
983
|
+
return wallet;
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw new SphereSDKError(
|
|
986
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
987
|
+
`Failed to handle Google response: ${error.message}`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async logout() {
|
|
992
|
+
await this.authProvider.logout();
|
|
993
|
+
await this.sessionManager.clearSession();
|
|
994
|
+
if (this.currentWallet) {
|
|
995
|
+
await this.currentWallet.disconnect();
|
|
996
|
+
}
|
|
997
|
+
this.currentWallet = null;
|
|
998
|
+
this.emit("logout");
|
|
999
|
+
}
|
|
1000
|
+
isAuthenticated() {
|
|
1001
|
+
return this.currentWallet !== null;
|
|
1002
|
+
}
|
|
1003
|
+
getCurrentWallet() {
|
|
1004
|
+
return this.currentWallet;
|
|
1005
|
+
}
|
|
1006
|
+
getUserEmail() {
|
|
1007
|
+
const session = this.sessionManager.getCachedSession();
|
|
1008
|
+
return session?.userInfo.email || null;
|
|
1009
|
+
}
|
|
1010
|
+
getEphemeralPublicKey() {
|
|
1011
|
+
return this.currentWallet?.getPublicKey() || null;
|
|
1012
|
+
}
|
|
1013
|
+
async updateNetwork(network, rpcEndpoint) {
|
|
1014
|
+
this.config.network = network;
|
|
1015
|
+
this.config.rpcEndpoint = rpcEndpoint;
|
|
1016
|
+
const cedraConfig = new CedraConfig({
|
|
1017
|
+
network,
|
|
1018
|
+
fullnode: rpcEndpoint
|
|
1019
|
+
});
|
|
1020
|
+
this.cedra = new Cedra(cedraConfig);
|
|
1021
|
+
if (this.currentWallet) {
|
|
1022
|
+
this.currentWallet.updateNetwork(network, rpcEndpoint);
|
|
1023
|
+
}
|
|
1024
|
+
this.emit("network_changed", { network, rpcEndpoint });
|
|
1025
|
+
}
|
|
1026
|
+
getIndexerUrl() {
|
|
1027
|
+
return this.config.indexerUrl;
|
|
1028
|
+
}
|
|
1029
|
+
// Internal helpers for encryption
|
|
1030
|
+
encryptWalletData(privateKeyHex) {
|
|
1031
|
+
return CryptoJS2.AES.encrypt(privateKeyHex, this.storageKey).toString();
|
|
1032
|
+
}
|
|
1033
|
+
decryptWalletData(encryptedData) {
|
|
1034
|
+
try {
|
|
1035
|
+
const bytes = CryptoJS2.AES.decrypt(encryptedData, this.storageKey);
|
|
1036
|
+
return bytes.toString(CryptoJS2.enc.Utf8);
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
console.warn("[Sphere] Failed to decrypt wallet data");
|
|
1039
|
+
return "";
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// EventEmitter overrides to satisfy interface
|
|
1043
|
+
on(event, listener) {
|
|
1044
|
+
return super.on(event, listener);
|
|
1045
|
+
}
|
|
1046
|
+
off(event, listener) {
|
|
1047
|
+
return super.off(event, listener);
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
var SphereContext = createContext(void 0);
|
|
1051
|
+
var SphereProvider = ({ children, config }) => {
|
|
1052
|
+
const [sdk] = useState(() => new SphereAccountAbstraction(config));
|
|
1053
|
+
const [wallet, setWallet] = useState(null);
|
|
1054
|
+
const [walletInfo, setWalletInfo] = useState(null);
|
|
1055
|
+
const [balance, setBalance] = useState(null);
|
|
1056
|
+
const [email, setEmail] = useState(null);
|
|
1057
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1058
|
+
const [error, setError] = useState(null);
|
|
1059
|
+
const refreshData = useCallback(async (activeWallet) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const [info, bal] = await Promise.all([
|
|
1062
|
+
activeWallet.getAccountInfo(),
|
|
1063
|
+
activeWallet.getBalance()
|
|
1064
|
+
]);
|
|
1065
|
+
setWalletInfo(info);
|
|
1066
|
+
setBalance(bal);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
console.error("Failed to refresh data:", err);
|
|
1069
|
+
}
|
|
1070
|
+
}, []);
|
|
1071
|
+
useEffect(() => {
|
|
1072
|
+
const onAuth = ({ wallet: wallet2 }) => {
|
|
1073
|
+
setWallet(wallet2);
|
|
1074
|
+
setEmail(sdk.getUserEmail());
|
|
1075
|
+
refreshData(wallet2);
|
|
1076
|
+
};
|
|
1077
|
+
const onLogout = () => {
|
|
1078
|
+
setWallet(null);
|
|
1079
|
+
setEmail(null);
|
|
1080
|
+
setWalletInfo(null);
|
|
1081
|
+
setBalance(null);
|
|
1082
|
+
};
|
|
1083
|
+
sdk.on("auth_success", onAuth);
|
|
1084
|
+
sdk.on("logout", onLogout);
|
|
1085
|
+
const init = async () => {
|
|
1086
|
+
try {
|
|
1087
|
+
await sdk.initialize();
|
|
1088
|
+
const restored = await sdk.restoreSession();
|
|
1089
|
+
if (restored) {
|
|
1090
|
+
setWallet(restored);
|
|
1091
|
+
setEmail(sdk.getUserEmail());
|
|
1092
|
+
await refreshData(restored);
|
|
1093
|
+
}
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
console.error("SDK Init error:", err);
|
|
1096
|
+
setError(err.message || "Initialization failed");
|
|
1097
|
+
} finally {
|
|
1098
|
+
setIsLoading(false);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
init();
|
|
1102
|
+
return () => {
|
|
1103
|
+
sdk.off("auth_success", onAuth);
|
|
1104
|
+
sdk.off("logout", onLogout);
|
|
1105
|
+
};
|
|
1106
|
+
}, [sdk, refreshData]);
|
|
1107
|
+
useEffect(() => {
|
|
1108
|
+
if (!wallet) return void 0;
|
|
1109
|
+
const pollInterval = setInterval(() => {
|
|
1110
|
+
refreshData(wallet);
|
|
1111
|
+
}, 1e4);
|
|
1112
|
+
return () => clearInterval(pollInterval);
|
|
1113
|
+
}, [wallet, refreshData]);
|
|
1114
|
+
const handleAuthSuccess = useCallback(async (newWallet) => {
|
|
1115
|
+
setWallet(newWallet);
|
|
1116
|
+
await refreshData(newWallet);
|
|
1117
|
+
}, [refreshData]);
|
|
1118
|
+
const login = async () => {
|
|
1119
|
+
setError(null);
|
|
1120
|
+
try {
|
|
1121
|
+
await sdk.loginWithGoogle();
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
console.error("Sphere Login Error:", err);
|
|
1124
|
+
setError(err.message || "Login failed");
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const logout = async () => {
|
|
1129
|
+
try {
|
|
1130
|
+
await sdk.logout();
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
setError(err.message || "Logout failed");
|
|
1133
|
+
throw err;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
return /* @__PURE__ */ jsx(
|
|
1137
|
+
SphereContext.Provider,
|
|
1138
|
+
{
|
|
1139
|
+
value: {
|
|
1140
|
+
sdk,
|
|
1141
|
+
wallet,
|
|
1142
|
+
walletInfo,
|
|
1143
|
+
balance,
|
|
1144
|
+
email,
|
|
1145
|
+
indexerUrl: sdk.getIndexerUrl(),
|
|
1146
|
+
isAuthenticated: !!wallet,
|
|
1147
|
+
isLoading,
|
|
1148
|
+
error,
|
|
1149
|
+
login,
|
|
1150
|
+
logout,
|
|
1151
|
+
setIsLoading,
|
|
1152
|
+
refreshData: async () => {
|
|
1153
|
+
if (wallet) await refreshData(wallet);
|
|
1154
|
+
},
|
|
1155
|
+
handleAuthSuccess
|
|
1156
|
+
},
|
|
1157
|
+
children
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
};
|
|
1161
|
+
var useSphere = () => {
|
|
1162
|
+
const context = useContext(SphereContext);
|
|
1163
|
+
if (context === void 0) {
|
|
1164
|
+
throw new Error("useSphere must be used within a SphereProvider");
|
|
1165
|
+
}
|
|
1166
|
+
return context;
|
|
1167
|
+
};
|
|
1168
|
+
var SphereModal = ({ isOpen, onClose }) => {
|
|
1169
|
+
const { login, isAuthenticated, isLoading, error } = useSphere();
|
|
1170
|
+
const [localLoading, setLocalLoading] = useState(false);
|
|
1171
|
+
useEffect(() => {
|
|
1172
|
+
if (isAuthenticated) {
|
|
1173
|
+
onClose();
|
|
1174
|
+
}
|
|
1175
|
+
}, [isAuthenticated, onClose]);
|
|
1176
|
+
const handleLogin = async () => {
|
|
1177
|
+
try {
|
|
1178
|
+
setLocalLoading(true);
|
|
1179
|
+
await login();
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
console.error("Sphere Login Error:", err);
|
|
1182
|
+
} finally {
|
|
1183
|
+
setLocalLoading(false);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
if (!isOpen) return null;
|
|
1187
|
+
return /* @__PURE__ */ jsx("div", { style: styles.overlay, onClick: (e) => e.target === e.currentTarget && onClose(), children: /* @__PURE__ */ jsxs("div", { style: styles.modal, children: [
|
|
1188
|
+
/* @__PURE__ */ jsxs("div", { style: styles.header, children: [
|
|
1189
|
+
/* @__PURE__ */ jsx("div", { style: { width: 32 } }),
|
|
1190
|
+
" ",
|
|
1191
|
+
/* @__PURE__ */ jsx("button", { style: styles.closeBtn, onClick: onClose, "aria-label": "Close", children: "\xD7" })
|
|
1192
|
+
] }),
|
|
1193
|
+
/* @__PURE__ */ jsxs("div", { style: styles.content, children: [
|
|
1194
|
+
/* @__PURE__ */ jsx("h2", { style: styles.title, children: "Connect to Sphere" }),
|
|
1195
|
+
/* @__PURE__ */ jsx("p", { style: styles.description, children: "Experience the next generation of account abstraction. Secure, fast, and effortless." }),
|
|
1196
|
+
error && /* @__PURE__ */ jsx("div", { style: styles.errorBanner, children: error }),
|
|
1197
|
+
/* @__PURE__ */ jsx("div", { style: styles.actionContainer, children: /* @__PURE__ */ jsxs(
|
|
1198
|
+
"button",
|
|
1199
|
+
{
|
|
1200
|
+
style: {
|
|
1201
|
+
...styles.customButton,
|
|
1202
|
+
opacity: isLoading || localLoading ? 0.7 : 1,
|
|
1203
|
+
cursor: isLoading || localLoading ? "not-allowed" : "pointer"
|
|
1204
|
+
},
|
|
1205
|
+
onClick: handleLogin,
|
|
1206
|
+
disabled: isLoading || localLoading,
|
|
1207
|
+
children: [
|
|
1208
|
+
isLoading || localLoading ? /* @__PURE__ */ jsx("div", { style: styles.spinner }) : /* @__PURE__ */ jsx(
|
|
1209
|
+
"img",
|
|
1210
|
+
{
|
|
1211
|
+
src: "https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png",
|
|
1212
|
+
alt: "Google",
|
|
1213
|
+
style: styles.googleIcon
|
|
1214
|
+
}
|
|
1215
|
+
),
|
|
1216
|
+
isLoading || localLoading ? "Connecting..." : "Continue with Google"
|
|
1217
|
+
]
|
|
1218
|
+
}
|
|
1219
|
+
) }),
|
|
1220
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
1221
|
+
@keyframes sphere-spin {
|
|
1222
|
+
0% { transform: rotate(0deg); }
|
|
1223
|
+
100% { transform: rotate(360deg); }
|
|
1224
|
+
}
|
|
1225
|
+
` }),
|
|
1226
|
+
/* @__PURE__ */ jsxs("div", { style: styles.footer, children: [
|
|
1227
|
+
"Securely powered by ",
|
|
1228
|
+
/* @__PURE__ */ jsx("strong", { children: "Sphere SDK" })
|
|
1229
|
+
] })
|
|
1230
|
+
] })
|
|
1231
|
+
] }) });
|
|
1232
|
+
};
|
|
1233
|
+
var styles = {
|
|
1234
|
+
overlay: {
|
|
1235
|
+
position: "fixed",
|
|
1236
|
+
top: 0,
|
|
1237
|
+
left: 0,
|
|
1238
|
+
right: 0,
|
|
1239
|
+
bottom: 0,
|
|
1240
|
+
backgroundColor: "transparent",
|
|
1241
|
+
display: "flex",
|
|
1242
|
+
alignItems: "center",
|
|
1243
|
+
justifyContent: "center",
|
|
1244
|
+
zIndex: 9999,
|
|
1245
|
+
padding: "20px"
|
|
1246
|
+
},
|
|
1247
|
+
modal: {
|
|
1248
|
+
backgroundColor: "#0c0c0c",
|
|
1249
|
+
color: "#ffffff",
|
|
1250
|
+
width: "90%",
|
|
1251
|
+
maxWidth: "400px",
|
|
1252
|
+
aspectRatio: "1/1",
|
|
1253
|
+
borderRadius: "0",
|
|
1254
|
+
position: "relative",
|
|
1255
|
+
boxShadow: "0 0 0 1px #222, 0 25px 60px rgba(0, 0, 0, 0.7)",
|
|
1256
|
+
overflow: "hidden",
|
|
1257
|
+
fontFamily: "Inter, -apple-system, system-ui, sans-serif",
|
|
1258
|
+
display: "flex",
|
|
1259
|
+
flexDirection: "column"
|
|
1260
|
+
},
|
|
1261
|
+
header: {
|
|
1262
|
+
display: "flex",
|
|
1263
|
+
justifyContent: "space-between",
|
|
1264
|
+
alignItems: "center",
|
|
1265
|
+
padding: "24px 24px 0 24px"
|
|
1266
|
+
},
|
|
1267
|
+
closeBtn: {
|
|
1268
|
+
background: "rgba(255, 255, 255, 0.05)",
|
|
1269
|
+
border: "none",
|
|
1270
|
+
color: "#888",
|
|
1271
|
+
fontSize: "24px",
|
|
1272
|
+
cursor: "pointer",
|
|
1273
|
+
lineHeight: 1,
|
|
1274
|
+
width: "32px",
|
|
1275
|
+
height: "32px",
|
|
1276
|
+
borderRadius: "50%",
|
|
1277
|
+
display: "flex",
|
|
1278
|
+
alignItems: "center",
|
|
1279
|
+
justifyContent: "center",
|
|
1280
|
+
transition: "all 0.2s"
|
|
1281
|
+
},
|
|
1282
|
+
content: {
|
|
1283
|
+
display: "flex",
|
|
1284
|
+
flexDirection: "column",
|
|
1285
|
+
padding: "20px",
|
|
1286
|
+
alignItems: "center",
|
|
1287
|
+
flex: 1,
|
|
1288
|
+
justifyContent: "center",
|
|
1289
|
+
width: "100%",
|
|
1290
|
+
boxSizing: "border-box"
|
|
1291
|
+
},
|
|
1292
|
+
title: {
|
|
1293
|
+
fontSize: "1.5rem",
|
|
1294
|
+
fontWeight: "700",
|
|
1295
|
+
color: "#ffffff",
|
|
1296
|
+
textAlign: "center",
|
|
1297
|
+
marginBottom: "10px",
|
|
1298
|
+
margin: 0
|
|
1299
|
+
},
|
|
1300
|
+
description: {
|
|
1301
|
+
fontSize: "0.7rem",
|
|
1302
|
+
color: "#94a3b8",
|
|
1303
|
+
textAlign: "center",
|
|
1304
|
+
lineHeight: "1.5",
|
|
1305
|
+
marginBottom: "32px",
|
|
1306
|
+
margin: 0
|
|
1307
|
+
},
|
|
1308
|
+
errorBanner: {
|
|
1309
|
+
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
1310
|
+
border: "1px solid rgba(239, 68, 68, 0.2)",
|
|
1311
|
+
color: "#f87171",
|
|
1312
|
+
padding: "12px 16px",
|
|
1313
|
+
borderRadius: "16px",
|
|
1314
|
+
fontSize: "13px",
|
|
1315
|
+
marginBottom: "24px",
|
|
1316
|
+
textAlign: "center",
|
|
1317
|
+
width: "100%"
|
|
1318
|
+
},
|
|
1319
|
+
actionContainer: {
|
|
1320
|
+
display: "flex",
|
|
1321
|
+
flexDirection: "column",
|
|
1322
|
+
width: "100%"
|
|
1323
|
+
},
|
|
1324
|
+
customButton: {
|
|
1325
|
+
display: "flex",
|
|
1326
|
+
alignItems: "center",
|
|
1327
|
+
justifyContent: "center",
|
|
1328
|
+
gap: "12px",
|
|
1329
|
+
width: "100%",
|
|
1330
|
+
height: "52px",
|
|
1331
|
+
backgroundColor: "#0c0c0c",
|
|
1332
|
+
border: "1px solid #333",
|
|
1333
|
+
borderRadius: "0",
|
|
1334
|
+
cursor: "pointer",
|
|
1335
|
+
fontSize: "15px",
|
|
1336
|
+
fontWeight: "600",
|
|
1337
|
+
color: "#ffffff",
|
|
1338
|
+
transition: "transform 0.1s, opacity 0.2s",
|
|
1339
|
+
boxSizing: "border-box"
|
|
1340
|
+
},
|
|
1341
|
+
googleIcon: {
|
|
1342
|
+
width: "20px",
|
|
1343
|
+
height: "20px"
|
|
1344
|
+
},
|
|
1345
|
+
spinner: {
|
|
1346
|
+
width: "20px",
|
|
1347
|
+
height: "20px",
|
|
1348
|
+
border: "2px solid rgba(255, 255, 255, 0.1)",
|
|
1349
|
+
borderTop: "2px solid #ffffff",
|
|
1350
|
+
borderRadius: "50%",
|
|
1351
|
+
animation: "sphere-spin 0.8s linear infinite"
|
|
1352
|
+
},
|
|
1353
|
+
footer: {
|
|
1354
|
+
marginTop: "32px",
|
|
1355
|
+
textAlign: "center",
|
|
1356
|
+
fontSize: "11px",
|
|
1357
|
+
color: "#475569",
|
|
1358
|
+
letterSpacing: "0.5px"
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
export { ErrorCode, GoogleAuthProvider, KeyDerivation, SecureStorage, SessionManager, SphereAccountAbstraction, SphereModal, SphereProvider, SphereSDKError, SphereWallet, useSphere };
|
|
1363
|
+
//# sourceMappingURL=index.mjs.map
|
|
1364
|
+
//# sourceMappingURL=index.mjs.map
|