decane-connect-kit 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +527 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +526 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,7 @@ __export(errors_exports, {
|
|
|
44
44
|
NewDeviceError: () => NewDeviceError,
|
|
45
45
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
46
46
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
47
|
+
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
47
48
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
48
49
|
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
49
50
|
SessionExpiredError: () => SessionExpiredError,
|
|
@@ -53,7 +54,7 @@ __export(errors_exports, {
|
|
|
53
54
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
54
55
|
WrongPasswordError: () => WrongPasswordError
|
|
55
56
|
});
|
|
56
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
57
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
57
58
|
var init_errors = __esm({
|
|
58
59
|
"src/social/errors.ts"() {
|
|
59
60
|
"use strict";
|
|
@@ -85,6 +86,15 @@ var init_errors = __esm({
|
|
|
85
86
|
super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
|
|
86
87
|
}
|
|
87
88
|
};
|
|
89
|
+
ReconnectRequiredError = class extends DecaneConnectError {
|
|
90
|
+
constructor() {
|
|
91
|
+
super(
|
|
92
|
+
"RECONNECT_REQUIRED",
|
|
93
|
+
"Your session ended \u2014 sign in again to unlock this wallet",
|
|
94
|
+
true
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
88
98
|
UserCancelledError = class extends DecaneConnectError {
|
|
89
99
|
constructor() {
|
|
90
100
|
super("USER_CANCELLED", "User cancelled the operation", true);
|
|
@@ -181,6 +191,7 @@ __export(index_exports, {
|
|
|
181
191
|
NewDeviceError: () => NewDeviceError,
|
|
182
192
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
183
193
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
194
|
+
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
184
195
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
185
196
|
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
186
197
|
SessionExpiredError: () => SessionExpiredError,
|
|
@@ -2228,6 +2239,13 @@ var TeeClient = class {
|
|
|
2228
2239
|
await verifyTeeAttestation(this.teeUrl, nonceHex);
|
|
2229
2240
|
this.attested = true;
|
|
2230
2241
|
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Opens a signing session and returns the addresses the TEE derived from the
|
|
2244
|
+
* key it actually reconstructed — authoritative, unlike the backend's
|
|
2245
|
+
* recorded copy, which nothing else can verify. Returns null if the TEE
|
|
2246
|
+
* couldn't derive them (older signer build, or a transient share-fetch
|
|
2247
|
+
* failure); callers fall back to the backend's addresses in that case.
|
|
2248
|
+
*/
|
|
2231
2249
|
async openSession(deviceShare, jwt) {
|
|
2232
2250
|
await this.ensureAttested();
|
|
2233
2251
|
const res = await fetch(`${this.teeUrl}/signer/session`, {
|
|
@@ -2236,7 +2254,9 @@ var TeeClient = class {
|
|
|
2236
2254
|
body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
|
|
2237
2255
|
});
|
|
2238
2256
|
if (!res.ok) throw new Error("session open failed");
|
|
2239
|
-
|
|
2257
|
+
const body = await res.json();
|
|
2258
|
+
this.handle = body.handle;
|
|
2259
|
+
return body.addresses ?? null;
|
|
2240
2260
|
}
|
|
2241
2261
|
/**
|
|
2242
2262
|
* Flow B: exchange a recovery share (from a recovery-share file) for a
|
|
@@ -2369,9 +2389,24 @@ var SessionManager = class {
|
|
|
2369
2389
|
window.addEventListener("pagehide", () => this.end());
|
|
2370
2390
|
}
|
|
2371
2391
|
}
|
|
2372
|
-
|
|
2392
|
+
/**
|
|
2393
|
+
* Opens the TEE session and resolves which addresses this wallet really has.
|
|
2394
|
+
*
|
|
2395
|
+
* `fallback` is whatever the backend recorded; the TEE's answer is derived
|
|
2396
|
+
* from the signing key itself and therefore outranks it. Returns the
|
|
2397
|
+
* resolved addresses so callers can propagate the corrected values rather
|
|
2398
|
+
* than continuing to use a stale record.
|
|
2399
|
+
*/
|
|
2400
|
+
async start(deviceShare, jwt, fallback) {
|
|
2373
2401
|
this.tee = new TeeClient(this.teeUrl);
|
|
2374
|
-
await this.tee.openSession(deviceShare, jwt);
|
|
2402
|
+
const verified = await this.tee.openSession(deviceShare, jwt);
|
|
2403
|
+
const addresses = verified ?? fallback;
|
|
2404
|
+
if (!addresses) {
|
|
2405
|
+
this.tee = null;
|
|
2406
|
+
throw new Error(
|
|
2407
|
+
"No addresses available for this wallet \u2014 the signer could not derive them and none are recorded"
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2375
2410
|
this.evmAddress = addresses.evm;
|
|
2376
2411
|
this.solanaAddress = addresses.solana;
|
|
2377
2412
|
this.expiresAt = Date.now() + this.durationMs;
|
|
@@ -2381,6 +2416,7 @@ var SessionManager = class {
|
|
|
2381
2416
|
this.events.emit("session-expired");
|
|
2382
2417
|
}, this.durationMs);
|
|
2383
2418
|
this.events.emit("session-started");
|
|
2419
|
+
return addresses;
|
|
2384
2420
|
}
|
|
2385
2421
|
end() {
|
|
2386
2422
|
void this.tee?.closeSession();
|
|
@@ -2431,6 +2467,11 @@ async function splitMaster(master, version = 1) {
|
|
|
2431
2467
|
if (parts.length !== 3) throw new Error("unexpected share count");
|
|
2432
2468
|
return { device: parts[0], server: parts[1], recovery: parts[2], version };
|
|
2433
2469
|
}
|
|
2470
|
+
async function combineShares(a, b) {
|
|
2471
|
+
const master = await (0, import_shamir_secret_sharing.combine)([a, b]);
|
|
2472
|
+
if (master.length !== 32) throw new Error("reconstruction produced wrong length");
|
|
2473
|
+
return master;
|
|
2474
|
+
}
|
|
2434
2475
|
|
|
2435
2476
|
// src/social/crypto/wrap.ts
|
|
2436
2477
|
function buf(u) {
|
|
@@ -2875,6 +2916,18 @@ async function createDecaneConnect(config) {
|
|
|
2875
2916
|
}
|
|
2876
2917
|
};
|
|
2877
2918
|
}
|
|
2919
|
+
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
2920
|
+
const stored = await storage.getShare(userId);
|
|
2921
|
+
if (!stored) {
|
|
2922
|
+
throw new CallSequenceError(
|
|
2923
|
+
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
const wrappingKey = await getWrappingKey(stored);
|
|
2927
|
+
const iv = generateRandomBytes(12);
|
|
2928
|
+
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
2929
|
+
await storage.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
2930
|
+
}
|
|
2878
2931
|
async function getWrappingKey(stored) {
|
|
2879
2932
|
if (stored.unlockMethod === "passkey") {
|
|
2880
2933
|
const prfOutput = await getPrfOutput({
|
|
@@ -3010,8 +3063,15 @@ async function createDecaneConnect(config) {
|
|
|
3010
3063
|
console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
|
|
3011
3064
|
}
|
|
3012
3065
|
}
|
|
3013
|
-
await session.start(shareSet.device, jwt, addresses);
|
|
3066
|
+
const confirmed = await session.start(shareSet.device, jwt, addresses);
|
|
3014
3067
|
zeroize(shareSet.device);
|
|
3068
|
+
if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3069
|
+
console.warn(
|
|
3070
|
+
`decane-connect-kit: signer derived a different EVM address (${confirmed.evm}) than this device did (${addresses.evm}) during setup. Using the signer's.`
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
addresses.evm = evmAddress = confirmed.evm;
|
|
3074
|
+
addresses.solana = solanaAddress = confirmed.solana;
|
|
3015
3075
|
if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
|
|
3016
3076
|
try {
|
|
3017
3077
|
const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
|
|
@@ -3035,15 +3095,15 @@ async function createDecaneConnect(config) {
|
|
|
3035
3095
|
}
|
|
3036
3096
|
async function returningUserUnlock(userId, jwt) {
|
|
3037
3097
|
const rawAddresses = await api.getAddresses(jwt);
|
|
3038
|
-
|
|
3039
|
-
throw new CallSequenceError(
|
|
3040
|
-
"No addresses are recorded for this user \u2014 inconsistent server state"
|
|
3041
|
-
);
|
|
3042
|
-
}
|
|
3043
|
-
const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
|
|
3098
|
+
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
|
|
3044
3099
|
const stored = await storage.getShare(userId);
|
|
3045
3100
|
let deviceShare;
|
|
3046
3101
|
if (!stored) {
|
|
3102
|
+
if (!config.onRecoveryRotated) {
|
|
3103
|
+
throw new CallSequenceError(
|
|
3104
|
+
"Recovery rotates the share set and issues a new recovery file, but no onRecoveryRotated callback is configured to save it. Configure onRecoveryRotated before allowing recovery."
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3047
3107
|
deviceShare = await recoverDeviceShare({
|
|
3048
3108
|
userId,
|
|
3049
3109
|
jwt,
|
|
@@ -3054,13 +3114,11 @@ async function createDecaneConnect(config) {
|
|
|
3054
3114
|
onProgress: config.onRecoveryProgress,
|
|
3055
3115
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
3056
3116
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
3057
|
-
if (!config.onRecoveryRotated)
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const file = await buildRecoveryShareFile(recoveryShare, password, addresses, shareSetVersion, passwordHint);
|
|
3063
|
-
await config.onRecoveryFileReady?.(file, recoveryShareFileName(addresses.evm));
|
|
3117
|
+
if (!config.onRecoveryRotated) return;
|
|
3118
|
+
if (!recordedAddresses) return;
|
|
3119
|
+
const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: recordedAddresses.evm, solanaAddress: recordedAddresses.solana });
|
|
3120
|
+
const file = await buildRecoveryShareFile(recoveryShare, password, recordedAddresses, shareSetVersion, passwordHint);
|
|
3121
|
+
await config.onRecoveryFileReady?.(file, recoveryShareFileName(recordedAddresses.evm));
|
|
3064
3122
|
await api.markRecoveryShareSaved(jwt);
|
|
3065
3123
|
}
|
|
3066
3124
|
});
|
|
@@ -3079,8 +3137,13 @@ async function createDecaneConnect(config) {
|
|
|
3079
3137
|
throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
|
|
3080
3138
|
}
|
|
3081
3139
|
}
|
|
3082
|
-
await session.start(deviceShare, jwt,
|
|
3140
|
+
const addresses = await session.start(deviceShare, jwt, recordedAddresses);
|
|
3083
3141
|
zeroize(deviceShare);
|
|
3142
|
+
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3143
|
+
console.warn(
|
|
3144
|
+
`decane-connect-kit: recorded EVM address (${recordedAddresses.evm}) does not match the signing key (${addresses.evm}). Using the signing key's address; the record has been corrected.`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3084
3147
|
return addresses;
|
|
3085
3148
|
}
|
|
3086
3149
|
async function handleAuthResult(result) {
|
|
@@ -3142,14 +3205,24 @@ async function createDecaneConnect(config) {
|
|
|
3142
3205
|
return handleAuthResult(result);
|
|
3143
3206
|
},
|
|
3144
3207
|
async unlock() {
|
|
3145
|
-
if (!auth)
|
|
3146
|
-
throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3208
|
+
if (!auth) {
|
|
3209
|
+
throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3210
|
+
}
|
|
3147
3211
|
if (session.isActive()) return;
|
|
3148
3212
|
await returningUserUnlock(auth.userId, auth.jwt);
|
|
3149
3213
|
},
|
|
3150
3214
|
isUnlocked() {
|
|
3151
3215
|
return session.isActive();
|
|
3152
3216
|
},
|
|
3217
|
+
/**
|
|
3218
|
+
* True when we have a persisted identity but no live auth state, i.e.
|
|
3219
|
+
* unlock() cannot succeed and the user must sign in again. Exposed on the
|
|
3220
|
+
* client (not just the React context) so non-React consumers can branch on
|
|
3221
|
+
* it without having to catch ReconnectRequiredError.
|
|
3222
|
+
*/
|
|
3223
|
+
needsReconnect() {
|
|
3224
|
+
return !auth && !!persisted;
|
|
3225
|
+
},
|
|
3153
3226
|
sessionExpiresAt() {
|
|
3154
3227
|
return session.expiresAtTimestamp();
|
|
3155
3228
|
},
|
|
@@ -3242,10 +3315,12 @@ async function createDecaneConnect(config) {
|
|
|
3242
3315
|
const tee = session.requireActive();
|
|
3243
3316
|
const result = await tee.rotateShares(auth.jwt);
|
|
3244
3317
|
try {
|
|
3318
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3319
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3245
3320
|
const file = await buildRecoveryShareFile(
|
|
3246
3321
|
result.recoveryShare,
|
|
3247
3322
|
opts.password,
|
|
3248
|
-
|
|
3323
|
+
addresses,
|
|
3249
3324
|
result.shareSetVersion,
|
|
3250
3325
|
opts.passwordHint
|
|
3251
3326
|
);
|
|
@@ -3261,11 +3336,13 @@ async function createDecaneConnect(config) {
|
|
|
3261
3336
|
const tee = session.requireActive();
|
|
3262
3337
|
const result = await tee.exportPortableBackup(auth.jwt);
|
|
3263
3338
|
try {
|
|
3339
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3340
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3264
3341
|
return await buildPortableBackup(
|
|
3265
3342
|
result.deviceShare,
|
|
3266
3343
|
result.recoveryShare,
|
|
3267
3344
|
opts.password,
|
|
3268
|
-
|
|
3345
|
+
addresses,
|
|
3269
3346
|
result.shareSetVersion,
|
|
3270
3347
|
opts.passwordHint
|
|
3271
3348
|
);
|
|
@@ -3337,6 +3414,17 @@ function b64uToBuffer(b64u) {
|
|
|
3337
3414
|
function toHex(bytes) {
|
|
3338
3415
|
return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3339
3416
|
}
|
|
3417
|
+
async function deriveAddressesFromShares(a, b) {
|
|
3418
|
+
const master = await combineShares(a, b);
|
|
3419
|
+
try {
|
|
3420
|
+
return {
|
|
3421
|
+
evm: new import_ethers2.Wallet(toHex(master)).address,
|
|
3422
|
+
solana: import_web32.Keypair.fromSeed(master).publicKey.toBase58()
|
|
3423
|
+
};
|
|
3424
|
+
} finally {
|
|
3425
|
+
zeroize(master);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3340
3428
|
|
|
3341
3429
|
// src/social/index.ts
|
|
3342
3430
|
init_errors();
|
|
@@ -3498,7 +3586,7 @@ function WalletIcon({ src, name, size = 36 }) {
|
|
|
3498
3586
|
fontWeight: 800,
|
|
3499
3587
|
color: "var(--dc-accent)",
|
|
3500
3588
|
flexShrink: 0,
|
|
3501
|
-
fontFamily: "var(--dc-font-sans)",
|
|
3589
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)",
|
|
3502
3590
|
letterSpacing: "-0.02em"
|
|
3503
3591
|
}
|
|
3504
3592
|
},
|
|
@@ -3843,7 +3931,10 @@ function WalletSelector({
|
|
|
3843
3931
|
alignItems: "center",
|
|
3844
3932
|
justifyContent: "center",
|
|
3845
3933
|
padding: "16px",
|
|
3846
|
-
|
|
3934
|
+
// Same fallback chain as SocialWalletModal — without it, this modal
|
|
3935
|
+
// drops to the browser default serif whenever the token block hasn't
|
|
3936
|
+
// been injected, while the social modal still lands on system-ui.
|
|
3937
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)"
|
|
3847
3938
|
}
|
|
3848
3939
|
},
|
|
3849
3940
|
/* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -4076,7 +4167,7 @@ function WalletSelector({
|
|
|
4076
4167
|
fontSize: 13,
|
|
4077
4168
|
padding: "8px 12px 8px 32px",
|
|
4078
4169
|
outline: "none",
|
|
4079
|
-
fontFamily: "var(--dc-font-sans)",
|
|
4170
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)",
|
|
4080
4171
|
transition: "border-color 0.15s",
|
|
4081
4172
|
boxSizing: "border-box"
|
|
4082
4173
|
},
|
|
@@ -4262,6 +4353,15 @@ function countdown(expiresAt) {
|
|
|
4262
4353
|
const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
4263
4354
|
return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
4264
4355
|
}
|
|
4356
|
+
function downloadJson(data, filename) {
|
|
4357
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
4358
|
+
const url = URL.createObjectURL(blob);
|
|
4359
|
+
const a = document.createElement("a");
|
|
4360
|
+
a.href = url;
|
|
4361
|
+
a.download = filename;
|
|
4362
|
+
a.click();
|
|
4363
|
+
URL.revokeObjectURL(url);
|
|
4364
|
+
}
|
|
4265
4365
|
function CopyButton({ value }) {
|
|
4266
4366
|
const [copied, setCopied] = (0, import_react3.useState)(false);
|
|
4267
4367
|
async function copy() {
|
|
@@ -4289,7 +4389,54 @@ function CopyButton({ value }) {
|
|
|
4289
4389
|
copied ? "\u2713" : "copy"
|
|
4290
4390
|
);
|
|
4291
4391
|
}
|
|
4292
|
-
function
|
|
4392
|
+
function useBalance(kind, address, chain, rpcUrls) {
|
|
4393
|
+
const [state, setState] = (0, import_react3.useState)({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4394
|
+
(0, import_react3.useEffect)(() => {
|
|
4395
|
+
if (!address || !chain) return;
|
|
4396
|
+
let cancelled = false;
|
|
4397
|
+
setState((s) => ({ ...s, loading: true }));
|
|
4398
|
+
(async () => {
|
|
4399
|
+
try {
|
|
4400
|
+
if (kind === "evm") {
|
|
4401
|
+
const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
|
|
4402
|
+
if (!rpcUrl) {
|
|
4403
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
const { JsonRpcProvider, formatEther } = await import("ethers");
|
|
4407
|
+
const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
|
|
4408
|
+
if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
|
|
4409
|
+
} else {
|
|
4410
|
+
const cluster = chain.slice("solana:".length);
|
|
4411
|
+
const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
|
|
4412
|
+
const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
|
|
4413
|
+
const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
|
|
4414
|
+
if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
|
|
4415
|
+
}
|
|
4416
|
+
} catch {
|
|
4417
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4418
|
+
}
|
|
4419
|
+
})();
|
|
4420
|
+
return () => {
|
|
4421
|
+
cancelled = true;
|
|
4422
|
+
};
|
|
4423
|
+
}, [kind, address, chain, rpcUrls]);
|
|
4424
|
+
return state;
|
|
4425
|
+
}
|
|
4426
|
+
function formatBalance(value) {
|
|
4427
|
+
if (value == null) return "\u2014";
|
|
4428
|
+
const n = Number(value);
|
|
4429
|
+
if (!Number.isFinite(n)) return value;
|
|
4430
|
+
return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
|
|
4431
|
+
}
|
|
4432
|
+
function AddressRow({
|
|
4433
|
+
label,
|
|
4434
|
+
address,
|
|
4435
|
+
kind,
|
|
4436
|
+
chain,
|
|
4437
|
+
rpcUrls
|
|
4438
|
+
}) {
|
|
4439
|
+
const balance = useBalance(kind, address, chain, rpcUrls);
|
|
4293
4440
|
const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
|
|
4294
4441
|
return /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4295
4442
|
background: "var(--dc-surface)",
|
|
@@ -4297,7 +4444,233 @@ function AddressRow({ label, address }) {
|
|
|
4297
4444
|
borderRadius: 10,
|
|
4298
4445
|
padding: "10px 14px",
|
|
4299
4446
|
marginBottom: 8
|
|
4300
|
-
} }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase",
|
|
4447
|
+
} }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 } }, /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase" } }, label), /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, fontFamily: "monospace", color: "var(--dc-text)" } }, balance.loading ? "\u2026" : `${formatBalance(balance.value)} ${balance.symbol}`)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ import_react3.default.createElement(CopyButton, { value: address })));
|
|
4448
|
+
}
|
|
4449
|
+
function SignInView({
|
|
4450
|
+
onGoogleSignIn,
|
|
4451
|
+
onEmailStart,
|
|
4452
|
+
onEmailVerify,
|
|
4453
|
+
authMethods,
|
|
4454
|
+
resumeHint
|
|
4455
|
+
}) {
|
|
4456
|
+
const [googleLoading, setGoogleLoading] = (0, import_react3.useState)(false);
|
|
4457
|
+
const [email, setEmail] = (0, import_react3.useState)("");
|
|
4458
|
+
const [code, setCode] = (0, import_react3.useState)("");
|
|
4459
|
+
const [emailSent, setEmailSent] = (0, import_react3.useState)(false);
|
|
4460
|
+
const [emailLoading, setEmailLoading] = (0, import_react3.useState)(false);
|
|
4461
|
+
const [error, setError] = (0, import_react3.useState)("");
|
|
4462
|
+
async function handleGoogle() {
|
|
4463
|
+
setError("");
|
|
4464
|
+
setGoogleLoading(true);
|
|
4465
|
+
try {
|
|
4466
|
+
await onGoogleSignIn();
|
|
4467
|
+
} catch (e) {
|
|
4468
|
+
setError(e.message);
|
|
4469
|
+
setGoogleLoading(false);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
async function handleEmailStart(e) {
|
|
4473
|
+
e.preventDefault();
|
|
4474
|
+
setError("");
|
|
4475
|
+
setEmailLoading(true);
|
|
4476
|
+
try {
|
|
4477
|
+
await onEmailStart(email);
|
|
4478
|
+
setEmailSent(true);
|
|
4479
|
+
} catch (e2) {
|
|
4480
|
+
setError(e2.message);
|
|
4481
|
+
} finally {
|
|
4482
|
+
setEmailLoading(false);
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
async function handleEmailVerify(e) {
|
|
4486
|
+
e.preventDefault();
|
|
4487
|
+
setError("");
|
|
4488
|
+
setEmailLoading(true);
|
|
4489
|
+
try {
|
|
4490
|
+
await onEmailVerify(email, code);
|
|
4491
|
+
} catch (e2) {
|
|
4492
|
+
setError(e2.message);
|
|
4493
|
+
} finally {
|
|
4494
|
+
setEmailLoading(false);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
const inputStyle = {
|
|
4498
|
+
width: "100%",
|
|
4499
|
+
background: "var(--dc-surface)",
|
|
4500
|
+
border: "1px solid var(--dc-border)",
|
|
4501
|
+
borderRadius: 8,
|
|
4502
|
+
color: "var(--dc-text)",
|
|
4503
|
+
padding: "9px 12px",
|
|
4504
|
+
fontSize: 13,
|
|
4505
|
+
outline: "none",
|
|
4506
|
+
fontFamily: "inherit",
|
|
4507
|
+
boxSizing: "border-box"
|
|
4508
|
+
};
|
|
4509
|
+
return /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px 16px" } }, resumeHint && /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 12, color: "var(--dc-text-muted)", marginBottom: 12, textAlign: "center" } }, "Welcome back \u2014 sign in again to resume ", resumeHint), authMethods.includes("google") && /* @__PURE__ */ import_react3.default.createElement(
|
|
4510
|
+
"button",
|
|
4511
|
+
{
|
|
4512
|
+
onClick: handleGoogle,
|
|
4513
|
+
disabled: googleLoading,
|
|
4514
|
+
style: {
|
|
4515
|
+
width: "100%",
|
|
4516
|
+
padding: "10px 0",
|
|
4517
|
+
marginBottom: 10,
|
|
4518
|
+
background: "var(--dc-accent)",
|
|
4519
|
+
color: "var(--dc-accent-text)",
|
|
4520
|
+
border: "none",
|
|
4521
|
+
borderRadius: 10,
|
|
4522
|
+
fontSize: 13,
|
|
4523
|
+
fontWeight: 700,
|
|
4524
|
+
cursor: googleLoading ? "not-allowed" : "pointer",
|
|
4525
|
+
opacity: googleLoading ? 0.7 : 1,
|
|
4526
|
+
fontFamily: "inherit"
|
|
4527
|
+
}
|
|
4528
|
+
},
|
|
4529
|
+
googleLoading ? "Redirecting\u2026" : "Continue with Google"
|
|
4530
|
+
), authMethods.includes("email") && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
|
|
4531
|
+
"input",
|
|
4532
|
+
{
|
|
4533
|
+
type: "email",
|
|
4534
|
+
required: true,
|
|
4535
|
+
placeholder: "your@email.com",
|
|
4536
|
+
value: email,
|
|
4537
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4538
|
+
style: { ...inputStyle, flex: 1 }
|
|
4539
|
+
}
|
|
4540
|
+
), /* @__PURE__ */ import_react3.default.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4541
|
+
padding: "0 16px",
|
|
4542
|
+
background: "var(--dc-surface)",
|
|
4543
|
+
border: "1px solid var(--dc-border)",
|
|
4544
|
+
borderRadius: 8,
|
|
4545
|
+
color: "var(--dc-text)",
|
|
4546
|
+
fontSize: 13,
|
|
4547
|
+
cursor: "pointer",
|
|
4548
|
+
fontFamily: "inherit"
|
|
4549
|
+
} }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
|
|
4550
|
+
"input",
|
|
4551
|
+
{
|
|
4552
|
+
placeholder: "6-digit code",
|
|
4553
|
+
value: code,
|
|
4554
|
+
maxLength: 6,
|
|
4555
|
+
autoFocus: true,
|
|
4556
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
|
|
4557
|
+
style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
|
|
4558
|
+
}
|
|
4559
|
+
), /* @__PURE__ */ import_react3.default.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4560
|
+
padding: "0 16px",
|
|
4561
|
+
background: "var(--dc-accent)",
|
|
4562
|
+
color: "var(--dc-accent-text)",
|
|
4563
|
+
border: "none",
|
|
4564
|
+
borderRadius: 8,
|
|
4565
|
+
fontSize: 13,
|
|
4566
|
+
fontWeight: 700,
|
|
4567
|
+
cursor: "pointer",
|
|
4568
|
+
fontFamily: "inherit"
|
|
4569
|
+
} }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
|
|
4570
|
+
}
|
|
4571
|
+
function RecoverySection({
|
|
4572
|
+
addresses,
|
|
4573
|
+
onRotateShares,
|
|
4574
|
+
onExportPortableBackup
|
|
4575
|
+
}) {
|
|
4576
|
+
const [expanded, setExpanded] = (0, import_react3.useState)(false);
|
|
4577
|
+
const [mode, setMode] = (0, import_react3.useState)(null);
|
|
4578
|
+
const [password, setPassword] = (0, import_react3.useState)("");
|
|
4579
|
+
const [passwordHint, setPasswordHint] = (0, import_react3.useState)("");
|
|
4580
|
+
const [confirmed, setConfirmed] = (0, import_react3.useState)(false);
|
|
4581
|
+
const [busy, setBusy] = (0, import_react3.useState)(false);
|
|
4582
|
+
const [result, setResult] = (0, import_react3.useState)(null);
|
|
4583
|
+
const [error, setError] = (0, import_react3.useState)("");
|
|
4584
|
+
function reset() {
|
|
4585
|
+
setMode(null);
|
|
4586
|
+
setPassword("");
|
|
4587
|
+
setPasswordHint("");
|
|
4588
|
+
setConfirmed(false);
|
|
4589
|
+
setError("");
|
|
4590
|
+
}
|
|
4591
|
+
async function handleGenerate() {
|
|
4592
|
+
if (!addresses?.evm || !mode) return;
|
|
4593
|
+
if (password.length < 8) {
|
|
4594
|
+
setError("Password must be at least 8 characters.");
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
if (mode === "portable" && !confirmed) {
|
|
4598
|
+
setError("Confirm you understand the risk first.");
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
setBusy(true);
|
|
4602
|
+
setError("");
|
|
4603
|
+
try {
|
|
4604
|
+
if (mode === "share") {
|
|
4605
|
+
const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
|
|
4606
|
+
downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4607
|
+
setResult({ kind: "share", version: file.shareSetVersion });
|
|
4608
|
+
} else {
|
|
4609
|
+
const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
|
|
4610
|
+
downloadJson(file, `decane-full-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4611
|
+
setResult({ kind: "portable", version: file.shareSetVersion });
|
|
4612
|
+
}
|
|
4613
|
+
reset();
|
|
4614
|
+
} catch (e) {
|
|
4615
|
+
setError(e.message);
|
|
4616
|
+
} finally {
|
|
4617
|
+
setBusy(false);
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
const inputStyle = {
|
|
4621
|
+
width: "100%",
|
|
4622
|
+
background: "var(--dc-bg)",
|
|
4623
|
+
border: "1px solid var(--dc-border)",
|
|
4624
|
+
borderRadius: 8,
|
|
4625
|
+
color: "var(--dc-text)",
|
|
4626
|
+
padding: "8px 10px",
|
|
4627
|
+
fontSize: 12,
|
|
4628
|
+
outline: "none",
|
|
4629
|
+
fontFamily: "inherit",
|
|
4630
|
+
boxSizing: "border-box"
|
|
4631
|
+
};
|
|
4632
|
+
const btnStyle = {
|
|
4633
|
+
padding: "8px 12px",
|
|
4634
|
+
borderRadius: 8,
|
|
4635
|
+
fontSize: 12,
|
|
4636
|
+
fontWeight: 700,
|
|
4637
|
+
cursor: "pointer",
|
|
4638
|
+
fontFamily: "inherit",
|
|
4639
|
+
border: "none"
|
|
4640
|
+
};
|
|
4641
|
+
return /* @__PURE__ */ import_react3.default.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ import_react3.default.createElement(
|
|
4642
|
+
"button",
|
|
4643
|
+
{
|
|
4644
|
+
onClick: () => setExpanded((v) => !v),
|
|
4645
|
+
style: {
|
|
4646
|
+
width: "100%",
|
|
4647
|
+
padding: "10px 14px",
|
|
4648
|
+
background: "var(--dc-surface)",
|
|
4649
|
+
border: "none",
|
|
4650
|
+
display: "flex",
|
|
4651
|
+
alignItems: "center",
|
|
4652
|
+
justifyContent: "space-between",
|
|
4653
|
+
cursor: "pointer",
|
|
4654
|
+
fontFamily: "inherit"
|
|
4655
|
+
}
|
|
4656
|
+
},
|
|
4657
|
+
/* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
|
|
4658
|
+
/* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
|
|
4659
|
+
), expanded && /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "12px 14px", borderTop: "1px solid var(--dc-border)" } }, /* @__PURE__ */ import_react3.default.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5, margin: "0 0 10px" } }, "Your wallet is split into three shares \u2014 device, server, and one that's yours to keep. Any two reconstruct it; one alone reveals nothing."), !mode && /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => {
|
|
4660
|
+
reset();
|
|
4661
|
+
setMode("share");
|
|
4662
|
+
}, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => {
|
|
4663
|
+
reset();
|
|
4664
|
+
setMode("portable");
|
|
4665
|
+
}, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate full recovery file")), mode === "share" && /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", margin: 0 } }, "One share \u2014 useless alone, safe to keep in a password manager or cloud drive. Generating a new one invalidates any previous recovery-share file."), /* @__PURE__ */ import_react3.default.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ import_react3.default.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: handleGenerate, disabled: busy, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), mode === "portable" && /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4666
|
+
background: "rgba(255,85,51,0.08)",
|
|
4667
|
+
border: "1px solid rgba(255,85,51,0.3)",
|
|
4668
|
+
borderRadius: 8,
|
|
4669
|
+
padding: "8px 10px",
|
|
4670
|
+
fontSize: 11,
|
|
4671
|
+
color: "#ff9977",
|
|
4672
|
+
lineHeight: 1.5
|
|
4673
|
+
} }, /* @__PURE__ */ import_react3.default.createElement("strong", { style: { display: "block", marginBottom: 3, color: "#ff5533" } }, "This file controls your wallet"), "Contains two shares \u2014 file plus password gives complete control. Keep it offline, not a cloud drive. This also invalidates your current recovery-share file."), /* @__PURE__ */ import_react3.default.createElement("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--dc-text)" } }, /* @__PURE__ */ import_react3.default.createElement("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), style: { width: "auto" } }), "I understand this is a bearer secret"), /* @__PURE__ */ import_react3.default.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ import_react3.default.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: handleGenerate, disabled: busy || !confirmed, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), result && /* @__PURE__ */ import_react3.default.createElement("div", { style: { marginTop: 10, fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5 } }, result.kind === "share" ? "Recovery-share file" : "Full recovery file", " downloaded \u2014 share set is now version ", result.version, ". Any previous recovery file no longer works.")));
|
|
4301
4674
|
}
|
|
4302
4675
|
function SocialWalletModal({
|
|
4303
4676
|
open,
|
|
@@ -4305,7 +4678,15 @@ function SocialWalletModal({
|
|
|
4305
4678
|
theme = "auto",
|
|
4306
4679
|
socialAuth,
|
|
4307
4680
|
onUnlock,
|
|
4308
|
-
onDisconnect
|
|
4681
|
+
onDisconnect,
|
|
4682
|
+
onGoogleSignIn,
|
|
4683
|
+
onEmailStart,
|
|
4684
|
+
onEmailVerify,
|
|
4685
|
+
authMethods = ["google", "email"],
|
|
4686
|
+
onRotateShares,
|
|
4687
|
+
onExportPortableBackup,
|
|
4688
|
+
rpcUrls,
|
|
4689
|
+
chains
|
|
4309
4690
|
}) {
|
|
4310
4691
|
const [timer, setTimer] = (0, import_react3.useState)("");
|
|
4311
4692
|
const [unlocking, setUnlocking] = (0, import_react3.useState)(false);
|
|
@@ -4354,6 +4735,9 @@ function SocialWalletModal({
|
|
|
4354
4735
|
setDisconnecting(false);
|
|
4355
4736
|
}
|
|
4356
4737
|
}
|
|
4738
|
+
const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
|
|
4739
|
+
const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
4740
|
+
const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
|
|
4357
4741
|
const modal = /* @__PURE__ */ import_react3.default.createElement(
|
|
4358
4742
|
"div",
|
|
4359
4743
|
{
|
|
@@ -4391,10 +4775,14 @@ function SocialWalletModal({
|
|
|
4391
4775
|
borderRadius: 20,
|
|
4392
4776
|
width: "100%",
|
|
4393
4777
|
maxWidth: 380,
|
|
4394
|
-
|
|
4395
|
-
|
|
4778
|
+
maxHeight: "90vh",
|
|
4779
|
+
overflowY: "auto",
|
|
4780
|
+
boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
4396
4781
|
} }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4397
4782
|
height: 2,
|
|
4783
|
+
position: "sticky",
|
|
4784
|
+
top: 0,
|
|
4785
|
+
zIndex: 1,
|
|
4398
4786
|
background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
|
|
4399
4787
|
} }), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4400
4788
|
width: 34,
|
|
@@ -4423,7 +4811,7 @@ function SocialWalletModal({
|
|
|
4423
4811
|
strokeLinecap: "round",
|
|
4424
4812
|
strokeLinejoin: "round"
|
|
4425
4813
|
}
|
|
4426
|
-
))), /* @__PURE__ */ import_react3.default.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split
|
|
4814
|
+
))), /* @__PURE__ */ import_react3.default.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split three ways")), /* @__PURE__ */ import_react3.default.createElement(
|
|
4427
4815
|
"button",
|
|
4428
4816
|
{
|
|
4429
4817
|
onClick: onClose,
|
|
@@ -4443,7 +4831,16 @@ function SocialWalletModal({
|
|
|
4443
4831
|
}
|
|
4444
4832
|
},
|
|
4445
4833
|
/* @__PURE__ */ import_react3.default.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ import_react3.default.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
|
|
4446
|
-
)), /* @__PURE__ */ import_react3.default.createElement(
|
|
4834
|
+
)), showSignIn ? /* @__PURE__ */ import_react3.default.createElement(
|
|
4835
|
+
SignInView,
|
|
4836
|
+
{
|
|
4837
|
+
onGoogleSignIn,
|
|
4838
|
+
onEmailStart,
|
|
4839
|
+
onEmailVerify,
|
|
4840
|
+
authMethods,
|
|
4841
|
+
resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
|
|
4842
|
+
}
|
|
4843
|
+
) : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4447
4844
|
margin: "0 16px 14px",
|
|
4448
4845
|
padding: "8px 14px",
|
|
4449
4846
|
background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
|
|
@@ -4463,7 +4860,14 @@ function SocialWalletModal({
|
|
|
4463
4860
|
fontSize: 12,
|
|
4464
4861
|
fontWeight: 700,
|
|
4465
4862
|
color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
|
|
4466
|
-
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px
|
|
4863
|
+
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px 14px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm, kind: "evm", chain: evmChain, rpcUrls }), socialAuth.addresses?.solana && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana, kind: "solana", chain: solChain, rpcUrls })), socialAuth.isUnlocked && /* @__PURE__ */ import_react3.default.createElement(
|
|
4864
|
+
RecoverySection,
|
|
4865
|
+
{
|
|
4866
|
+
addresses: socialAuth.addresses,
|
|
4867
|
+
onRotateShares,
|
|
4868
|
+
onExportPortableBackup
|
|
4869
|
+
}
|
|
4870
|
+
), /* @__PURE__ */ import_react3.default.createElement("div", { style: {
|
|
4467
4871
|
padding: "14px 16px",
|
|
4468
4872
|
borderTop: "1px solid var(--dc-border)",
|
|
4469
4873
|
display: "flex",
|
|
@@ -4508,7 +4912,7 @@ function SocialWalletModal({
|
|
|
4508
4912
|
}
|
|
4509
4913
|
},
|
|
4510
4914
|
disconnecting ? "Signing out\u2026" : "Sign out"
|
|
4511
|
-
)))
|
|
4915
|
+
))))
|
|
4512
4916
|
);
|
|
4513
4917
|
return (0, import_react_dom2.createPortal)(modal, document.body);
|
|
4514
4918
|
}
|
|
@@ -4519,7 +4923,8 @@ var DEFAULT_SOCIAL = {
|
|
|
4519
4923
|
addresses: null,
|
|
4520
4924
|
isNewUser: false,
|
|
4521
4925
|
isUnlocked: false,
|
|
4522
|
-
sessionExpiresAt: null
|
|
4926
|
+
sessionExpiresAt: null,
|
|
4927
|
+
needsReconnect: false
|
|
4523
4928
|
};
|
|
4524
4929
|
var DecaneContext = (0, import_react4.createContext)(null);
|
|
4525
4930
|
function hasSocial(mode) {
|
|
@@ -4579,7 +4984,7 @@ function DecaneKit({ config, children }) {
|
|
|
4579
4984
|
sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
|
|
4580
4985
|
const saved = sdk.getAddresses();
|
|
4581
4986
|
if (saved) {
|
|
4582
|
-
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
|
|
4987
|
+
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
|
|
4583
4988
|
}
|
|
4584
4989
|
const params = new URLSearchParams(window.location.search);
|
|
4585
4990
|
if (params.has("decane_jwt") || params.has("decane_error")) {
|
|
@@ -4593,7 +4998,8 @@ function DecaneKit({ config, children }) {
|
|
|
4593
4998
|
addresses: result.addresses,
|
|
4594
4999
|
isNewUser: result.isNewUser,
|
|
4595
5000
|
isUnlocked: sdk?.isUnlocked() ?? false,
|
|
4596
|
-
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
|
|
5001
|
+
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
|
|
5002
|
+
needsReconnect: false
|
|
4597
5003
|
});
|
|
4598
5004
|
}
|
|
4599
5005
|
const connectWithGoogle = (0, import_react4.useCallback)(async () => {
|
|
@@ -4620,7 +5026,8 @@ function DecaneKit({ config, children }) {
|
|
|
4620
5026
|
setSocialAuth((p) => ({
|
|
4621
5027
|
...p,
|
|
4622
5028
|
isUnlocked: sdk.isUnlocked(),
|
|
4623
|
-
sessionExpiresAt: sdk.sessionExpiresAt()
|
|
5029
|
+
sessionExpiresAt: sdk.sessionExpiresAt(),
|
|
5030
|
+
needsReconnect: false
|
|
4624
5031
|
}));
|
|
4625
5032
|
}, []);
|
|
4626
5033
|
const socialDisconnect = (0, import_react4.useCallback)(async () => {
|
|
@@ -4644,6 +5051,31 @@ function DecaneKit({ config, children }) {
|
|
|
4644
5051
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
4645
5052
|
return sdk.signSolanaTransaction(tx);
|
|
4646
5053
|
}, []);
|
|
5054
|
+
const socialRotateShares = (0, import_react4.useCallback)(async (opts) => {
|
|
5055
|
+
const sdk = socialRef.current;
|
|
5056
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5057
|
+
return sdk.rotateShares(opts);
|
|
5058
|
+
}, []);
|
|
5059
|
+
const socialExportPortableBackup = (0, import_react4.useCallback)(async (opts) => {
|
|
5060
|
+
const sdk = socialRef.current;
|
|
5061
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5062
|
+
return sdk.exportPortableBackup(opts);
|
|
5063
|
+
}, []);
|
|
5064
|
+
const socialHasRecoveryShare = (0, import_react4.useCallback)(async () => {
|
|
5065
|
+
const sdk = socialRef.current;
|
|
5066
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5067
|
+
return sdk.hasRecoveryShare();
|
|
5068
|
+
}, []);
|
|
5069
|
+
const socialShouldPromptRecovery = (0, import_react4.useCallback)(async () => {
|
|
5070
|
+
const sdk = socialRef.current;
|
|
5071
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5072
|
+
return sdk.shouldPromptRecovery();
|
|
5073
|
+
}, []);
|
|
5074
|
+
const socialDismissRecoveryPrompt = (0, import_react4.useCallback)(async () => {
|
|
5075
|
+
const sdk = socialRef.current;
|
|
5076
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5077
|
+
return sdk.dismissRecoveryPrompt();
|
|
5078
|
+
}, []);
|
|
4647
5079
|
const handleWalletConnect = (0, import_react4.useCallback)(
|
|
4648
5080
|
(wallet, preference) => {
|
|
4649
5081
|
walletRef.current.connect(wallet, preference).catch(console.error);
|
|
@@ -4671,6 +5103,11 @@ function DecaneKit({ config, children }) {
|
|
|
4671
5103
|
socialSignMessage,
|
|
4672
5104
|
socialSendTransaction,
|
|
4673
5105
|
socialSignSolanaTransaction,
|
|
5106
|
+
socialRotateShares,
|
|
5107
|
+
socialExportPortableBackup,
|
|
5108
|
+
socialHasRecoveryShare,
|
|
5109
|
+
socialShouldPromptRecovery,
|
|
5110
|
+
socialDismissRecoveryPrompt,
|
|
4674
5111
|
socialModalOpen,
|
|
4675
5112
|
openSocialModal,
|
|
4676
5113
|
closeSocialModal
|
|
@@ -4701,7 +5138,15 @@ function DecaneKit({ config, children }) {
|
|
|
4701
5138
|
theme,
|
|
4702
5139
|
socialAuth,
|
|
4703
5140
|
onUnlock: socialUnlock,
|
|
4704
|
-
onDisconnect: socialDisconnect
|
|
5141
|
+
onDisconnect: socialDisconnect,
|
|
5142
|
+
onGoogleSignIn: connectWithGoogle,
|
|
5143
|
+
onEmailStart: connectWithEmail,
|
|
5144
|
+
onEmailVerify: verifyEmailCode2,
|
|
5145
|
+
authMethods: config?.social?.authMethods ?? ["google", "email"],
|
|
5146
|
+
onRotateShares: socialRotateShares,
|
|
5147
|
+
onExportPortableBackup: socialExportPortableBackup,
|
|
5148
|
+
rpcUrls: config?.social?.rpcUrls,
|
|
5149
|
+
chains: config?.social?.chains
|
|
4705
5150
|
}
|
|
4706
5151
|
)
|
|
4707
5152
|
);
|
|
@@ -4789,6 +5234,11 @@ function useSocialAuth() {
|
|
|
4789
5234
|
isNewUser: socialAuth.isNewUser,
|
|
4790
5235
|
addresses: socialAuth.addresses,
|
|
4791
5236
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5237
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5238
|
+
// live session — unlock() will always throw in this state (every
|
|
5239
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5240
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5241
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4792
5242
|
googleLoading,
|
|
4793
5243
|
emailLoading,
|
|
4794
5244
|
error,
|
|
@@ -4808,6 +5258,11 @@ function useSocialWallet() {
|
|
|
4808
5258
|
socialSignMessage,
|
|
4809
5259
|
socialSendTransaction,
|
|
4810
5260
|
socialSignSolanaTransaction,
|
|
5261
|
+
socialRotateShares,
|
|
5262
|
+
socialExportPortableBackup,
|
|
5263
|
+
socialHasRecoveryShare,
|
|
5264
|
+
socialShouldPromptRecovery,
|
|
5265
|
+
socialDismissRecoveryPrompt,
|
|
4811
5266
|
openSocialModal,
|
|
4812
5267
|
closeSocialModal,
|
|
4813
5268
|
socialModalOpen
|
|
@@ -4857,6 +5312,26 @@ function useSocialWallet() {
|
|
|
4857
5312
|
throw e;
|
|
4858
5313
|
}
|
|
4859
5314
|
}, [socialDisconnect]);
|
|
5315
|
+
const rotateShares = (0, import_react5.useCallback)(
|
|
5316
|
+
(opts) => run(() => socialRotateShares(opts)),
|
|
5317
|
+
[socialRotateShares]
|
|
5318
|
+
);
|
|
5319
|
+
const exportPortableBackup = (0, import_react5.useCallback)(
|
|
5320
|
+
(opts) => run(() => socialExportPortableBackup(opts)),
|
|
5321
|
+
[socialExportPortableBackup]
|
|
5322
|
+
);
|
|
5323
|
+
const hasRecoveryShare = (0, import_react5.useCallback)(
|
|
5324
|
+
() => run(() => socialHasRecoveryShare()),
|
|
5325
|
+
[socialHasRecoveryShare]
|
|
5326
|
+
);
|
|
5327
|
+
const shouldPromptRecovery = (0, import_react5.useCallback)(
|
|
5328
|
+
() => run(() => socialShouldPromptRecovery()),
|
|
5329
|
+
[socialShouldPromptRecovery]
|
|
5330
|
+
);
|
|
5331
|
+
const dismissRecoveryPrompt = (0, import_react5.useCallback)(
|
|
5332
|
+
() => run(() => socialDismissRecoveryPrompt()),
|
|
5333
|
+
[socialDismissRecoveryPrompt]
|
|
5334
|
+
);
|
|
4860
5335
|
return {
|
|
4861
5336
|
// State
|
|
4862
5337
|
isConnected: !!socialAuth.addresses,
|
|
@@ -4864,6 +5339,11 @@ function useSocialWallet() {
|
|
|
4864
5339
|
isNewUser: socialAuth.isNewUser,
|
|
4865
5340
|
addresses: socialAuth.addresses,
|
|
4866
5341
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5342
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5343
|
+
// live session — unlock() will always throw in this state (every
|
|
5344
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5345
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5346
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4867
5347
|
// Modal
|
|
4868
5348
|
modalOpen: socialModalOpen,
|
|
4869
5349
|
openModal: openSocialModal,
|
|
@@ -4875,6 +5355,12 @@ function useSocialWallet() {
|
|
|
4875
5355
|
signMessage,
|
|
4876
5356
|
sendTransaction,
|
|
4877
5357
|
signSolanaTransaction: signSolanaTransaction2,
|
|
5358
|
+
// Recovery (2-of-3 Shamir share set)
|
|
5359
|
+
rotateShares,
|
|
5360
|
+
exportPortableBackup,
|
|
5361
|
+
hasRecoveryShare,
|
|
5362
|
+
shouldPromptRecovery,
|
|
5363
|
+
dismissRecoveryPrompt,
|
|
4878
5364
|
loading,
|
|
4879
5365
|
error,
|
|
4880
5366
|
clearError: () => setError(null)
|
|
@@ -5121,6 +5607,7 @@ function useSignTypedData() {
|
|
|
5121
5607
|
NewDeviceError,
|
|
5122
5608
|
OriginNotAllowedError,
|
|
5123
5609
|
PromptPinNotConfiguredError,
|
|
5610
|
+
ReconnectRequiredError,
|
|
5124
5611
|
RecoveryFailedError,
|
|
5125
5612
|
RecoveryWalletMismatchError,
|
|
5126
5613
|
SessionExpiredError,
|