kentucky-signer-viem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +428 -0
- package/dist/index.d.mts +603 -0
- package/dist/index.d.ts +603 -0
- package/dist/index.js +760 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +714 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +288 -0
- package/dist/react/index.d.ts +288 -0
- package/dist/react/index.js +815 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +791 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +60 -0
- package/src/account.ts +255 -0
- package/src/auth.ts +427 -0
- package/src/client.ts +308 -0
- package/src/index.ts +73 -0
- package/src/react/context.tsx +317 -0
- package/src/react/hooks.ts +287 -0
- package/src/react/index.ts +31 -0
- package/src/types.ts +237 -0
- package/src/utils.ts +197 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
// src/react/context.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// src/client.ts
|
|
12
|
+
var KentuckySignerError = class extends Error {
|
|
13
|
+
constructor(message, code, details) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.details = details;
|
|
17
|
+
this.name = "KentuckySignerError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var KentuckySignerClient = class {
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
23
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
24
|
+
this.timeout = options.timeout ?? 3e4;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Make an authenticated request to the API
|
|
28
|
+
*/
|
|
29
|
+
async request(path, options = {}) {
|
|
30
|
+
const { token, ...fetchOptions } = options;
|
|
31
|
+
const headers = {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...options.headers
|
|
34
|
+
};
|
|
35
|
+
if (token) {
|
|
36
|
+
;
|
|
37
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
38
|
+
}
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
41
|
+
try {
|
|
42
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
43
|
+
...fetchOptions,
|
|
44
|
+
headers,
|
|
45
|
+
signal: controller.signal
|
|
46
|
+
});
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
if (!response.ok || data.success === false) {
|
|
49
|
+
const error = data;
|
|
50
|
+
throw new KentuckySignerError(
|
|
51
|
+
error.error?.message ?? "Unknown error",
|
|
52
|
+
error.error?.code ?? "UNKNOWN_ERROR",
|
|
53
|
+
error.error?.details
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timeoutId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get a challenge for passkey authentication
|
|
63
|
+
*
|
|
64
|
+
* @param accountId - Account ID to authenticate
|
|
65
|
+
* @returns Challenge response with 32-byte challenge
|
|
66
|
+
*/
|
|
67
|
+
async getChallenge(accountId) {
|
|
68
|
+
return this.request("/api/auth/challenge", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ account_id: accountId })
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Authenticate with a passkey credential
|
|
75
|
+
*
|
|
76
|
+
* @param accountId - Account ID to authenticate
|
|
77
|
+
* @param credential - WebAuthn credential from navigator.credentials.get()
|
|
78
|
+
* @returns Authentication response with JWT token
|
|
79
|
+
*/
|
|
80
|
+
async authenticatePasskey(accountId, credential) {
|
|
81
|
+
return this.request("/api/auth/passkey", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
account_id: accountId,
|
|
85
|
+
credential_id: credential.credentialId,
|
|
86
|
+
client_data_json: credential.clientDataJSON,
|
|
87
|
+
authenticator_data: credential.authenticatorData,
|
|
88
|
+
signature: credential.signature,
|
|
89
|
+
user_handle: credential.userHandle
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Refresh an authentication token
|
|
95
|
+
*
|
|
96
|
+
* @param token - Current JWT token
|
|
97
|
+
* @returns New authentication response with fresh token
|
|
98
|
+
*/
|
|
99
|
+
async refreshToken(token) {
|
|
100
|
+
return this.request("/api/auth/refresh", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
token
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Logout and invalidate token
|
|
107
|
+
*
|
|
108
|
+
* @param token - JWT token to invalidate
|
|
109
|
+
*/
|
|
110
|
+
async logout(token) {
|
|
111
|
+
await this.request("/api/auth/logout", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
token
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get account information
|
|
118
|
+
*
|
|
119
|
+
* @param accountId - Account ID
|
|
120
|
+
* @param token - JWT token
|
|
121
|
+
* @returns Account info with addresses and passkeys
|
|
122
|
+
*/
|
|
123
|
+
async getAccountInfo(accountId, token) {
|
|
124
|
+
return this.request(`/api/accounts/${accountId}`, {
|
|
125
|
+
method: "GET",
|
|
126
|
+
token
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if an account exists
|
|
131
|
+
*
|
|
132
|
+
* @param accountId - Account ID
|
|
133
|
+
* @param token - JWT token
|
|
134
|
+
* @returns True if account exists
|
|
135
|
+
*/
|
|
136
|
+
async accountExists(accountId, token) {
|
|
137
|
+
try {
|
|
138
|
+
await this.request(`/api/accounts/${accountId}`, {
|
|
139
|
+
method: "HEAD",
|
|
140
|
+
token
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sign an EVM transaction hash
|
|
149
|
+
*
|
|
150
|
+
* @param request - Sign request with tx_hash and chain_id
|
|
151
|
+
* @param token - JWT token
|
|
152
|
+
* @returns Signature response with r, s, v components
|
|
153
|
+
*/
|
|
154
|
+
async signEvmTransaction(request, token) {
|
|
155
|
+
return this.request("/api/sign/evm", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
token,
|
|
158
|
+
body: JSON.stringify(request)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sign a raw hash for EVM
|
|
163
|
+
*
|
|
164
|
+
* Convenience method that wraps signEvmTransaction.
|
|
165
|
+
*
|
|
166
|
+
* @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
|
|
167
|
+
* @param chainId - Chain ID
|
|
168
|
+
* @param token - JWT token
|
|
169
|
+
* @returns Full signature (hex encoded with 0x prefix)
|
|
170
|
+
*/
|
|
171
|
+
async signHash(hash, chainId, token) {
|
|
172
|
+
const response = await this.signEvmTransaction(
|
|
173
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
174
|
+
token
|
|
175
|
+
);
|
|
176
|
+
return response.signature.full;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Create a new account with passkey authentication
|
|
180
|
+
*
|
|
181
|
+
* @param credential - WebAuthn credential from navigator.credentials.create()
|
|
182
|
+
* @returns Account creation response with account ID and addresses
|
|
183
|
+
*/
|
|
184
|
+
async createAccountWithPasskey(credential) {
|
|
185
|
+
return this.request("/api/accounts/create/passkey", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
credential_id: credential.credentialId,
|
|
189
|
+
public_key: credential.publicKey,
|
|
190
|
+
client_data_json: credential.clientDataJSON,
|
|
191
|
+
authenticator_data: credential.authenticatorData
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Create a new account with password authentication
|
|
197
|
+
*
|
|
198
|
+
* @param request - Password and confirmation
|
|
199
|
+
* @returns Account creation response with account ID and addresses
|
|
200
|
+
*/
|
|
201
|
+
async createAccountWithPassword(request) {
|
|
202
|
+
return this.request("/api/accounts/create/password", {
|
|
203
|
+
method: "POST",
|
|
204
|
+
body: JSON.stringify(request)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Authenticate with password
|
|
209
|
+
*
|
|
210
|
+
* @param request - Account ID and password
|
|
211
|
+
* @returns Authentication response with JWT token
|
|
212
|
+
*/
|
|
213
|
+
async authenticatePassword(request) {
|
|
214
|
+
return this.request("/api/auth/password", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify(request)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Health check
|
|
221
|
+
*
|
|
222
|
+
* @returns True if the API is healthy
|
|
223
|
+
*/
|
|
224
|
+
async healthCheck() {
|
|
225
|
+
try {
|
|
226
|
+
const response = await this.request("/api/health", {
|
|
227
|
+
method: "GET"
|
|
228
|
+
});
|
|
229
|
+
return response.status === "ok";
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get API version
|
|
236
|
+
*
|
|
237
|
+
* @returns Version string
|
|
238
|
+
*/
|
|
239
|
+
async getVersion() {
|
|
240
|
+
const response = await this.request("/api/version", {
|
|
241
|
+
method: "GET"
|
|
242
|
+
});
|
|
243
|
+
return response.version;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/utils.ts
|
|
248
|
+
function base64UrlEncode(data) {
|
|
249
|
+
let base64;
|
|
250
|
+
if (typeof Buffer !== "undefined") {
|
|
251
|
+
base64 = Buffer.from(data).toString("base64");
|
|
252
|
+
} else {
|
|
253
|
+
const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
|
|
254
|
+
base64 = btoa(binary);
|
|
255
|
+
}
|
|
256
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
257
|
+
}
|
|
258
|
+
function base64UrlDecode(str) {
|
|
259
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
260
|
+
const padding = (4 - base64.length % 4) % 4;
|
|
261
|
+
base64 += "=".repeat(padding);
|
|
262
|
+
if (typeof Buffer !== "undefined") {
|
|
263
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
264
|
+
} else {
|
|
265
|
+
const binary = atob(base64);
|
|
266
|
+
const bytes = new Uint8Array(binary.length);
|
|
267
|
+
for (let i = 0; i < binary.length; i++) {
|
|
268
|
+
bytes[i] = binary.charCodeAt(i);
|
|
269
|
+
}
|
|
270
|
+
return bytes;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/auth.ts
|
|
275
|
+
function isWebAuthnAvailable() {
|
|
276
|
+
return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
|
|
277
|
+
}
|
|
278
|
+
var LocalStorageTokenStorage = class {
|
|
279
|
+
constructor(keyPrefix = "kentucky_signer") {
|
|
280
|
+
this.keyPrefix = keyPrefix;
|
|
281
|
+
}
|
|
282
|
+
async getToken() {
|
|
283
|
+
if (typeof localStorage === "undefined") return null;
|
|
284
|
+
const token = localStorage.getItem(`${this.keyPrefix}_token`);
|
|
285
|
+
const expiresAt = localStorage.getItem(`${this.keyPrefix}_expires`);
|
|
286
|
+
if (token && expiresAt && Date.now() < parseInt(expiresAt, 10)) {
|
|
287
|
+
return token;
|
|
288
|
+
}
|
|
289
|
+
await this.clearToken();
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
async setToken(token, expiresAt) {
|
|
293
|
+
if (typeof localStorage === "undefined") return;
|
|
294
|
+
localStorage.setItem(`${this.keyPrefix}_token`, token);
|
|
295
|
+
localStorage.setItem(`${this.keyPrefix}_expires`, expiresAt.toString());
|
|
296
|
+
}
|
|
297
|
+
async clearToken() {
|
|
298
|
+
if (typeof localStorage === "undefined") return;
|
|
299
|
+
localStorage.removeItem(`${this.keyPrefix}_token`);
|
|
300
|
+
localStorage.removeItem(`${this.keyPrefix}_expires`);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
function credentialToPasskey(credential) {
|
|
304
|
+
const response = credential.response;
|
|
305
|
+
return {
|
|
306
|
+
credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
|
|
307
|
+
clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
|
|
308
|
+
authenticatorData: base64UrlEncode(new Uint8Array(response.authenticatorData)),
|
|
309
|
+
signature: base64UrlEncode(new Uint8Array(response.signature)),
|
|
310
|
+
userHandle: response.userHandle ? base64UrlEncode(new Uint8Array(response.userHandle)) : void 0
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
async function authenticateWithPasskey(options) {
|
|
314
|
+
if (!isWebAuthnAvailable()) {
|
|
315
|
+
throw new KentuckySignerError(
|
|
316
|
+
"WebAuthn is not available in this environment",
|
|
317
|
+
"WEBAUTHN_NOT_AVAILABLE",
|
|
318
|
+
"Use authenticateWithToken() for server-side authentication"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
|
|
322
|
+
const challengeResponse = await client.getChallenge(options.accountId);
|
|
323
|
+
const challengeBytes = base64UrlDecode(challengeResponse.challenge);
|
|
324
|
+
const credentialRequestOptions = {
|
|
325
|
+
publicKey: {
|
|
326
|
+
challenge: challengeBytes.buffer,
|
|
327
|
+
timeout: 6e4,
|
|
328
|
+
rpId: options.rpId ?? window.location.hostname,
|
|
329
|
+
userVerification: "preferred",
|
|
330
|
+
allowCredentials: options.allowCredentials?.map((id) => ({
|
|
331
|
+
type: "public-key",
|
|
332
|
+
id: base64UrlDecode(id).buffer
|
|
333
|
+
}))
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const credential = await navigator.credentials.get(
|
|
337
|
+
credentialRequestOptions
|
|
338
|
+
);
|
|
339
|
+
if (!credential) {
|
|
340
|
+
throw new KentuckySignerError(
|
|
341
|
+
"User cancelled passkey authentication",
|
|
342
|
+
"USER_CANCELLED"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const passkeyCredential = credentialToPasskey(credential);
|
|
346
|
+
const authResponse = await client.authenticatePasskey(
|
|
347
|
+
options.accountId,
|
|
348
|
+
passkeyCredential
|
|
349
|
+
);
|
|
350
|
+
const accountInfo = await client.getAccountInfo(
|
|
351
|
+
options.accountId,
|
|
352
|
+
authResponse.token
|
|
353
|
+
);
|
|
354
|
+
const expiresAt = Date.now() + authResponse.expires_in * 1e3;
|
|
355
|
+
return {
|
|
356
|
+
token: authResponse.token,
|
|
357
|
+
accountId: options.accountId,
|
|
358
|
+
evmAddress: accountInfo.addresses.evm,
|
|
359
|
+
btcAddress: accountInfo.addresses.bitcoin,
|
|
360
|
+
solAddress: accountInfo.addresses.solana,
|
|
361
|
+
expiresAt
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function isSessionValid(session, bufferMs = 6e4) {
|
|
365
|
+
return Date.now() + bufferMs < session.expiresAt;
|
|
366
|
+
}
|
|
367
|
+
async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
|
|
368
|
+
if (isSessionValid(session, bufferMs)) {
|
|
369
|
+
return session;
|
|
370
|
+
}
|
|
371
|
+
const client = new KentuckySignerClient({ baseUrl });
|
|
372
|
+
const authResponse = await client.refreshToken(session.token);
|
|
373
|
+
return {
|
|
374
|
+
...session,
|
|
375
|
+
token: authResponse.token,
|
|
376
|
+
expiresAt: Date.now() + authResponse.expires_in * 1e3
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/account.ts
|
|
381
|
+
import {
|
|
382
|
+
hashMessage,
|
|
383
|
+
hashTypedData,
|
|
384
|
+
keccak256,
|
|
385
|
+
serializeTransaction
|
|
386
|
+
} from "viem";
|
|
387
|
+
import { toAccount } from "viem/accounts";
|
|
388
|
+
function createKentuckySignerAccount(options) {
|
|
389
|
+
const { config, defaultChainId = 1, onSessionExpired } = options;
|
|
390
|
+
let session = options.session;
|
|
391
|
+
const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
|
|
392
|
+
async function getToken() {
|
|
393
|
+
if (Date.now() + 6e4 >= session.expiresAt) {
|
|
394
|
+
if (onSessionExpired) {
|
|
395
|
+
session = await onSessionExpired();
|
|
396
|
+
} else {
|
|
397
|
+
throw new KentuckySignerError(
|
|
398
|
+
"Session expired",
|
|
399
|
+
"SESSION_EXPIRED",
|
|
400
|
+
"Please re-authenticate with your passkey"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return session.token;
|
|
405
|
+
}
|
|
406
|
+
async function signHash(hash, chainId) {
|
|
407
|
+
const token = await getToken();
|
|
408
|
+
const response = await client.signEvmTransaction(
|
|
409
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
410
|
+
token
|
|
411
|
+
);
|
|
412
|
+
return response.signature.full;
|
|
413
|
+
}
|
|
414
|
+
function parseSignature(signature) {
|
|
415
|
+
const r = `0x${signature.slice(2, 66)}`;
|
|
416
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
417
|
+
const v = BigInt(`0x${signature.slice(130, 132)}`);
|
|
418
|
+
return { r, s, v };
|
|
419
|
+
}
|
|
420
|
+
const account = toAccount({
|
|
421
|
+
address: session.evmAddress,
|
|
422
|
+
/**
|
|
423
|
+
* Sign a message
|
|
424
|
+
*
|
|
425
|
+
* Supports string messages, hex messages, and raw bytes.
|
|
426
|
+
*/
|
|
427
|
+
async signMessage({ message }) {
|
|
428
|
+
const messageHash = hashMessage(message);
|
|
429
|
+
return signHash(messageHash, defaultChainId);
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
432
|
+
* Sign a transaction
|
|
433
|
+
*
|
|
434
|
+
* Serializes the transaction, hashes it, signs via Kentucky Signer,
|
|
435
|
+
* and returns the signed serialized transaction.
|
|
436
|
+
*/
|
|
437
|
+
async signTransaction(transaction) {
|
|
438
|
+
const chainId = transaction.chainId ?? defaultChainId;
|
|
439
|
+
const serializedUnsigned = serializeTransaction(transaction);
|
|
440
|
+
const txHash = keccak256(serializedUnsigned);
|
|
441
|
+
const signature = await signHash(txHash, chainId);
|
|
442
|
+
const { r, s, v } = parseSignature(signature);
|
|
443
|
+
let yParity;
|
|
444
|
+
if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
|
|
445
|
+
yParity = Number(v) - 27;
|
|
446
|
+
} else {
|
|
447
|
+
yParity = Number(v);
|
|
448
|
+
}
|
|
449
|
+
const serializedSigned = serializeTransaction(transaction, {
|
|
450
|
+
r,
|
|
451
|
+
s,
|
|
452
|
+
v: BigInt(yParity),
|
|
453
|
+
yParity
|
|
454
|
+
});
|
|
455
|
+
return serializedSigned;
|
|
456
|
+
},
|
|
457
|
+
/**
|
|
458
|
+
* Sign typed data (EIP-712)
|
|
459
|
+
*/
|
|
460
|
+
async signTypedData(typedData) {
|
|
461
|
+
const hash = hashTypedData(typedData);
|
|
462
|
+
return signHash(hash, defaultChainId);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
account.source = "kentuckySigner";
|
|
466
|
+
account.accountId = config.accountId;
|
|
467
|
+
account.session = session;
|
|
468
|
+
account.updateSession = (newSession) => {
|
|
469
|
+
session = newSession;
|
|
470
|
+
if (newSession.evmAddress !== account.address) {
|
|
471
|
+
;
|
|
472
|
+
account.address = newSession.evmAddress;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
return account;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/react/context.tsx
|
|
479
|
+
import { jsx } from "react/jsx-runtime";
|
|
480
|
+
var KentuckySignerContext = createContext(null);
|
|
481
|
+
function KentuckySignerProvider({
|
|
482
|
+
baseUrl,
|
|
483
|
+
defaultChainId = 1,
|
|
484
|
+
storageKeyPrefix = "kentucky_signer",
|
|
485
|
+
persistSession = true,
|
|
486
|
+
children
|
|
487
|
+
}) {
|
|
488
|
+
const [state, setState] = useState({
|
|
489
|
+
isAuthenticating: false,
|
|
490
|
+
isAuthenticated: false,
|
|
491
|
+
session: null,
|
|
492
|
+
account: null,
|
|
493
|
+
error: null
|
|
494
|
+
});
|
|
495
|
+
const client = useMemo(
|
|
496
|
+
() => new KentuckySignerClient({ baseUrl }),
|
|
497
|
+
[baseUrl]
|
|
498
|
+
);
|
|
499
|
+
const storage = useMemo(
|
|
500
|
+
() => persistSession ? new LocalStorageTokenStorage(storageKeyPrefix) : null,
|
|
501
|
+
[persistSession, storageKeyPrefix]
|
|
502
|
+
);
|
|
503
|
+
const createAccount = useCallback(
|
|
504
|
+
(session) => {
|
|
505
|
+
return createKentuckySignerAccount({
|
|
506
|
+
config: { baseUrl, accountId: session.accountId },
|
|
507
|
+
session,
|
|
508
|
+
defaultChainId,
|
|
509
|
+
onSessionExpired: async () => {
|
|
510
|
+
const newSession = await refreshSessionIfNeeded(session, baseUrl, 0);
|
|
511
|
+
setState((s) => ({
|
|
512
|
+
...s,
|
|
513
|
+
session: newSession,
|
|
514
|
+
account: s.account ? { ...s.account, session: newSession } : null
|
|
515
|
+
}));
|
|
516
|
+
return newSession;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
[baseUrl, defaultChainId]
|
|
521
|
+
);
|
|
522
|
+
useEffect(() => {
|
|
523
|
+
async function restoreSession() {
|
|
524
|
+
if (!storage) return;
|
|
525
|
+
try {
|
|
526
|
+
const savedSession = localStorage.getItem(`${storageKeyPrefix}_session`);
|
|
527
|
+
if (!savedSession) return;
|
|
528
|
+
const session = JSON.parse(savedSession);
|
|
529
|
+
if (!isSessionValid(session)) {
|
|
530
|
+
try {
|
|
531
|
+
const refreshed = await refreshSessionIfNeeded(session, baseUrl, 0);
|
|
532
|
+
const account2 = createAccount(refreshed);
|
|
533
|
+
localStorage.setItem(
|
|
534
|
+
`${storageKeyPrefix}_session`,
|
|
535
|
+
JSON.stringify(refreshed)
|
|
536
|
+
);
|
|
537
|
+
setState({
|
|
538
|
+
isAuthenticating: false,
|
|
539
|
+
isAuthenticated: true,
|
|
540
|
+
session: refreshed,
|
|
541
|
+
account: account2,
|
|
542
|
+
error: null
|
|
543
|
+
});
|
|
544
|
+
} catch {
|
|
545
|
+
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const account = createAccount(session);
|
|
550
|
+
setState({
|
|
551
|
+
isAuthenticating: false,
|
|
552
|
+
isAuthenticated: true,
|
|
553
|
+
session,
|
|
554
|
+
account,
|
|
555
|
+
error: null
|
|
556
|
+
});
|
|
557
|
+
} catch {
|
|
558
|
+
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
restoreSession();
|
|
562
|
+
}, [storage, storageKeyPrefix, baseUrl, createAccount]);
|
|
563
|
+
const authenticate = useCallback(
|
|
564
|
+
async (accountId, options) => {
|
|
565
|
+
setState((s) => ({ ...s, isAuthenticating: true, error: null }));
|
|
566
|
+
try {
|
|
567
|
+
const session = await authenticateWithPasskey({
|
|
568
|
+
baseUrl,
|
|
569
|
+
accountId,
|
|
570
|
+
rpId: options?.rpId
|
|
571
|
+
});
|
|
572
|
+
const account = createAccount(session);
|
|
573
|
+
if (storage) {
|
|
574
|
+
localStorage.setItem(
|
|
575
|
+
`${storageKeyPrefix}_session`,
|
|
576
|
+
JSON.stringify(session)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
setState({
|
|
580
|
+
isAuthenticating: false,
|
|
581
|
+
isAuthenticated: true,
|
|
582
|
+
session,
|
|
583
|
+
account,
|
|
584
|
+
error: null
|
|
585
|
+
});
|
|
586
|
+
} catch (error) {
|
|
587
|
+
setState((s) => ({
|
|
588
|
+
...s,
|
|
589
|
+
isAuthenticating: false,
|
|
590
|
+
error
|
|
591
|
+
}));
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
[baseUrl, createAccount, storage, storageKeyPrefix]
|
|
596
|
+
);
|
|
597
|
+
const logout = useCallback(async () => {
|
|
598
|
+
try {
|
|
599
|
+
if (state.session) {
|
|
600
|
+
await client.logout(state.session.token);
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
if (storage) {
|
|
605
|
+
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
606
|
+
}
|
|
607
|
+
setState({
|
|
608
|
+
isAuthenticating: false,
|
|
609
|
+
isAuthenticated: false,
|
|
610
|
+
session: null,
|
|
611
|
+
account: null,
|
|
612
|
+
error: null
|
|
613
|
+
});
|
|
614
|
+
}, [client, state.session, storage, storageKeyPrefix]);
|
|
615
|
+
const refreshSession = useCallback(async () => {
|
|
616
|
+
if (!state.session) return;
|
|
617
|
+
try {
|
|
618
|
+
const refreshed = await refreshSessionIfNeeded(state.session, baseUrl);
|
|
619
|
+
if (refreshed !== state.session) {
|
|
620
|
+
const account = createAccount(refreshed);
|
|
621
|
+
if (storage) {
|
|
622
|
+
localStorage.setItem(
|
|
623
|
+
`${storageKeyPrefix}_session`,
|
|
624
|
+
JSON.stringify(refreshed)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
setState((s) => ({
|
|
628
|
+
...s,
|
|
629
|
+
session: refreshed,
|
|
630
|
+
account
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
setState((s) => ({ ...s, error }));
|
|
635
|
+
}
|
|
636
|
+
}, [state.session, baseUrl, createAccount, storage, storageKeyPrefix]);
|
|
637
|
+
const clearError = useCallback(() => {
|
|
638
|
+
setState((s) => ({ ...s, error: null }));
|
|
639
|
+
}, []);
|
|
640
|
+
const value = useMemo(
|
|
641
|
+
() => ({
|
|
642
|
+
...state,
|
|
643
|
+
authenticate,
|
|
644
|
+
logout,
|
|
645
|
+
refreshSession,
|
|
646
|
+
clearError
|
|
647
|
+
}),
|
|
648
|
+
[state, authenticate, logout, refreshSession, clearError]
|
|
649
|
+
);
|
|
650
|
+
return /* @__PURE__ */ jsx(KentuckySignerContext.Provider, { value, children });
|
|
651
|
+
}
|
|
652
|
+
function useKentuckySignerContext() {
|
|
653
|
+
const context = useContext(KentuckySignerContext);
|
|
654
|
+
if (!context) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
"useKentuckySignerContext must be used within a KentuckySignerProvider"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return context;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/react/hooks.ts
|
|
663
|
+
import { useMemo as useMemo2, useCallback as useCallback2, useState as useState2 } from "react";
|
|
664
|
+
import { createWalletClient, http } from "viem";
|
|
665
|
+
function useKentuckySigner() {
|
|
666
|
+
const context = useKentuckySignerContext();
|
|
667
|
+
return {
|
|
668
|
+
isAuthenticated: context.isAuthenticated,
|
|
669
|
+
isAuthenticating: context.isAuthenticating,
|
|
670
|
+
session: context.session,
|
|
671
|
+
account: context.account,
|
|
672
|
+
error: context.error,
|
|
673
|
+
authenticate: context.authenticate,
|
|
674
|
+
logout: context.logout,
|
|
675
|
+
refreshSession: context.refreshSession,
|
|
676
|
+
clearError: context.clearError
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function useKentuckySignerAccount() {
|
|
680
|
+
const { account } = useKentuckySignerContext();
|
|
681
|
+
return account;
|
|
682
|
+
}
|
|
683
|
+
function useWalletClient(options) {
|
|
684
|
+
const { account } = useKentuckySignerContext();
|
|
685
|
+
const { chain, rpcUrl } = options;
|
|
686
|
+
return useMemo2(() => {
|
|
687
|
+
if (!account) return null;
|
|
688
|
+
return createWalletClient({
|
|
689
|
+
account,
|
|
690
|
+
chain,
|
|
691
|
+
transport: http(rpcUrl)
|
|
692
|
+
});
|
|
693
|
+
}, [account, chain, rpcUrl]);
|
|
694
|
+
}
|
|
695
|
+
function usePasskeyAuth() {
|
|
696
|
+
const { authenticate, isAuthenticating, error, clearError } = useKentuckySignerContext();
|
|
697
|
+
const login = useCallback2(
|
|
698
|
+
async (accountId, options) => {
|
|
699
|
+
clearError();
|
|
700
|
+
await authenticate(accountId, options);
|
|
701
|
+
},
|
|
702
|
+
[authenticate, clearError]
|
|
703
|
+
);
|
|
704
|
+
return {
|
|
705
|
+
login,
|
|
706
|
+
isLoading: isAuthenticating,
|
|
707
|
+
error,
|
|
708
|
+
clearError
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function useSignMessage() {
|
|
712
|
+
const { account } = useKentuckySignerContext();
|
|
713
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
714
|
+
const [error, setError] = useState2(null);
|
|
715
|
+
const signMessage = useCallback2(
|
|
716
|
+
async (message) => {
|
|
717
|
+
if (!account) {
|
|
718
|
+
throw new Error("Not authenticated");
|
|
719
|
+
}
|
|
720
|
+
setIsLoading(true);
|
|
721
|
+
setError(null);
|
|
722
|
+
try {
|
|
723
|
+
const signature = await account.signMessage({ message });
|
|
724
|
+
return signature;
|
|
725
|
+
} catch (err) {
|
|
726
|
+
setError(err);
|
|
727
|
+
throw err;
|
|
728
|
+
} finally {
|
|
729
|
+
setIsLoading(false);
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
[account]
|
|
733
|
+
);
|
|
734
|
+
return {
|
|
735
|
+
signMessage,
|
|
736
|
+
isLoading,
|
|
737
|
+
error,
|
|
738
|
+
isAvailable: !!account
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function useSignTypedData() {
|
|
742
|
+
const { account } = useKentuckySignerContext();
|
|
743
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
744
|
+
const [error, setError] = useState2(null);
|
|
745
|
+
const signTypedData = useCallback2(
|
|
746
|
+
async (typedData) => {
|
|
747
|
+
if (!account) {
|
|
748
|
+
throw new Error("Not authenticated");
|
|
749
|
+
}
|
|
750
|
+
setIsLoading(true);
|
|
751
|
+
setError(null);
|
|
752
|
+
try {
|
|
753
|
+
const signature = await account.signTypedData(typedData);
|
|
754
|
+
return signature;
|
|
755
|
+
} catch (err) {
|
|
756
|
+
setError(err);
|
|
757
|
+
throw err;
|
|
758
|
+
} finally {
|
|
759
|
+
setIsLoading(false);
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
[account]
|
|
763
|
+
);
|
|
764
|
+
return {
|
|
765
|
+
signTypedData,
|
|
766
|
+
isLoading,
|
|
767
|
+
error,
|
|
768
|
+
isAvailable: !!account
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function useIsReady() {
|
|
772
|
+
const { isAuthenticated, account } = useKentuckySignerContext();
|
|
773
|
+
return isAuthenticated && account !== null;
|
|
774
|
+
}
|
|
775
|
+
function useAddress() {
|
|
776
|
+
const { account } = useKentuckySignerContext();
|
|
777
|
+
return account?.address;
|
|
778
|
+
}
|
|
779
|
+
export {
|
|
780
|
+
KentuckySignerProvider,
|
|
781
|
+
useAddress,
|
|
782
|
+
useIsReady,
|
|
783
|
+
useKentuckySigner,
|
|
784
|
+
useKentuckySignerAccount,
|
|
785
|
+
useKentuckySignerContext,
|
|
786
|
+
usePasskeyAuth,
|
|
787
|
+
useSignMessage,
|
|
788
|
+
useSignTypedData,
|
|
789
|
+
useWalletClient
|
|
790
|
+
};
|
|
791
|
+
//# sourceMappingURL=index.mjs.map
|