kentucky-signer-viem 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kentucky-signer-viem",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Custom Viem account integration for Kentucky Signer with passkey authentication",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/account.ts CHANGED
@@ -8,11 +8,13 @@ import {
8
8
  type TransactionSerializable,
9
9
  type TypedData,
10
10
  type TypedDataDefinition,
11
+ type SignedAuthorizationList,
11
12
  hashMessage,
12
13
  hashTypedData,
13
14
  keccak256,
14
15
  serializeTransaction,
15
16
  toHex,
17
+ toRlp,
16
18
  concat,
17
19
  numberToHex,
18
20
  } from 'viem'
@@ -42,6 +44,66 @@ export type TwoFactorCallback = (requirements: {
42
44
  pinLength: number
43
45
  }) => Promise<TwoFactorCodes | null | undefined>
44
46
 
47
+ /**
48
+ * EIP-7702 Magic byte used in authorization hash
49
+ * @see https://eips.ethereum.org/EIPS/eip-7702
50
+ */
51
+ const EIP7702_MAGIC = '0x05' as const
52
+
53
+ /**
54
+ * Parameters for signing an EIP-7702 authorization
55
+ */
56
+ export interface SignAuthorizationParameters {
57
+ /** The contract address to delegate to */
58
+ contractAddress: Address
59
+ /** Chain ID (0 for all chains, defaults to client chain) */
60
+ chainId?: number
61
+ /** Nonce for the authorization (defaults to account's current nonce) */
62
+ nonce?: bigint
63
+ /**
64
+ * Whether the authorization will be executed by the signer themselves.
65
+ * If 'self', the nonce in the authorization will be incremented by 1
66
+ * over the transaction nonce.
67
+ */
68
+ executor?: 'self'
69
+ }
70
+
71
+ /**
72
+ * Signed EIP-7702 authorization
73
+ * Compatible with viem's SignedAuthorizationList
74
+ */
75
+ export interface SignedAuthorization {
76
+ /** Chain ID (0 for all chains) */
77
+ chainId: number
78
+ /** Contract address to delegate to */
79
+ contractAddress: Address
80
+ /** Nonce for the authorization */
81
+ nonce: bigint
82
+ /** Recovery identifier (0 or 1) */
83
+ yParity: number
84
+ /** Signature r component */
85
+ r: Hex
86
+ /** Signature s component */
87
+ s: Hex
88
+ }
89
+
90
+ /**
91
+ * Compute the hash for an EIP-7702 authorization
92
+ * Hash = keccak256(0x05 || rlp([chain_id, address, nonce]))
93
+ */
94
+ function hashAuthorization(params: {
95
+ contractAddress: Address
96
+ chainId: number
97
+ nonce: bigint
98
+ }): Hex {
99
+ const rlpEncoded = toRlp([
100
+ params.chainId === 0 ? '0x' : numberToHex(params.chainId),
101
+ params.contractAddress,
102
+ params.nonce === 0n ? '0x' : numberToHex(params.nonce),
103
+ ])
104
+ return keccak256(concat([EIP7702_MAGIC, rlpEncoded]))
105
+ }
106
+
45
107
  /**
46
108
  * Options for creating a Kentucky Signer account
47
109
  */
@@ -63,13 +125,44 @@ export interface KentuckySignerAccountOptions {
63
125
  /**
64
126
  * Extended account type with Kentucky Signer specific properties
65
127
  */
66
- export interface KentuckySignerAccount extends LocalAccount<'kentuckySigner'> {
128
+ export interface KentuckySignerAccount extends Omit<LocalAccount<'kentuckySigner'>, 'signAuthorization'> {
67
129
  /** Account ID */
68
130
  accountId: string
69
131
  /** Current session */
70
132
  session: AuthSession
71
133
  /** Update the session (e.g., after refresh) */
72
134
  updateSession: (session: AuthSession) => void
135
+ /**
136
+ * Sign an EIP-7702 authorization to delegate code to this account
137
+ *
138
+ * @param params - Authorization parameters
139
+ * @param nonce - Current account nonce (required if params.nonce not specified)
140
+ * @returns Signed authorization compatible with viem's authorizationList
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * // Get current nonce
145
+ * const nonce = await publicClient.getTransactionCount({ address: account.address })
146
+ *
147
+ * // Sign authorization
148
+ * const authorization = await account.sign7702Authorization({
149
+ * contractAddress: '0x69007702764179f14F51cdce752f4f775d74E139', // Alchemy MA v2
150
+ * chainId: 1,
151
+ * executor: 'self', // Account will execute the tx
152
+ * }, nonce)
153
+ *
154
+ * // Send EIP-7702 transaction
155
+ * const hash = await walletClient.sendTransaction({
156
+ * authorizationList: [authorization],
157
+ * to: account.address,
158
+ * data: initializeCalldata,
159
+ * })
160
+ * ```
161
+ */
162
+ sign7702Authorization: (
163
+ params: SignAuthorizationParameters,
164
+ nonce: bigint
165
+ ) => Promise<SignedAuthorization>
73
166
  }
74
167
 
75
168
  /**
@@ -132,20 +225,30 @@ export function createKentuckySignerAccount(
132
225
  return session.token
133
226
  }
134
227
 
228
+ /**
229
+ * Ensure a value is properly formatted as a Hex string with 0x prefix
230
+ */
231
+ function ensureHexPrefix(value: string): Hex {
232
+ return (value.startsWith('0x') ? value : `0x${value}`) as Hex
233
+ }
234
+
135
235
  /**
136
236
  * Sign a hash using Kentucky Signer and return full signature
137
237
  * Handles 2FA by detecting the error and calling the callback
238
+ *
239
+ * Note: v in returned signature is always 27 or 28 (standard format)
138
240
  */
139
- async function signHash(hash: Hex, chainId: number): Promise<Hex> {
241
+ async function signHash(hash: Hex): Promise<Hex> {
140
242
  const token = await getToken()
141
243
 
142
244
  // First attempt without 2FA codes
143
245
  try {
144
246
  const response = await client.signEvmTransaction(
145
- { tx_hash: hash, chain_id: chainId },
247
+ { tx_hash: hash },
146
248
  token
147
249
  )
148
- return response.signature.full
250
+ // Ensure the signature has 0x prefix
251
+ return ensureHexPrefix(response.signature.full)
149
252
  } catch (err) {
150
253
  // Check if 2FA is required
151
254
  if (err instanceof KentuckySignerError && err.code === '2FA_REQUIRED' && on2FARequired) {
@@ -163,10 +266,11 @@ export function createKentuckySignerAccount(
163
266
 
164
267
  // Retry with 2FA codes
165
268
  const response = await client.signEvmTransactionWith2FA(
166
- { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
269
+ { tx_hash: hash, totp_code: codes.totpCode, pin: codes.pin },
167
270
  token
168
271
  )
169
- return response.signature.full
272
+ // Ensure the signature has 0x prefix
273
+ return ensureHexPrefix(response.signature.full)
170
274
  }
171
275
  throw err
172
276
  }
@@ -175,19 +279,21 @@ export function createKentuckySignerAccount(
175
279
  /**
176
280
  * Sign a hash using Kentucky Signer and return signature components
177
281
  * Handles 2FA by detecting the error and calling the callback
282
+ *
283
+ * Note: v is always 27 or 28 (standard format, recovery_id + 27)
178
284
  */
179
- async function signHashWithComponents(hash: Hex, chainId: number): Promise<{ r: Hex; s: Hex; v: number }> {
285
+ async function signHashWithComponents(hash: Hex): Promise<{ r: Hex; s: Hex; v: number }> {
180
286
  const token = await getToken()
181
287
 
182
288
  // First attempt without 2FA codes
183
289
  try {
184
290
  const response = await client.signEvmTransaction(
185
- { tx_hash: hash, chain_id: chainId },
291
+ { tx_hash: hash },
186
292
  token
187
293
  )
188
294
  return {
189
- r: response.signature.r,
190
- s: response.signature.s,
295
+ r: ensureHexPrefix(response.signature.r),
296
+ s: ensureHexPrefix(response.signature.s),
191
297
  v: response.signature.v,
192
298
  }
193
299
  } catch (err) {
@@ -207,12 +313,12 @@ export function createKentuckySignerAccount(
207
313
 
208
314
  // Retry with 2FA codes
209
315
  const response = await client.signEvmTransactionWith2FA(
210
- { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
316
+ { tx_hash: hash, totp_code: codes.totpCode, pin: codes.pin },
211
317
  token
212
318
  )
213
319
  return {
214
- r: response.signature.r,
215
- s: response.signature.s,
320
+ r: ensureHexPrefix(response.signature.r),
321
+ s: ensureHexPrefix(response.signature.s),
216
322
  v: response.signature.v,
217
323
  }
218
324
  }
@@ -230,7 +336,7 @@ export function createKentuckySignerAccount(
230
336
  */
231
337
  async signMessage({ message }: { message: SignableMessage }): Promise<Hex> {
232
338
  const messageHash = hashMessage(message)
233
- return signHash(messageHash, defaultChainId)
339
+ return signHash(messageHash)
234
340
  },
235
341
 
236
342
  /**
@@ -238,6 +344,9 @@ export function createKentuckySignerAccount(
238
344
  *
239
345
  * Serializes the transaction, hashes it, signs via Kentucky Signer,
240
346
  * and returns the signed serialized transaction.
347
+ *
348
+ * For legacy transactions, applies EIP-155 encoding (v = chainId * 2 + 35 + recoveryId)
349
+ * For modern transactions (EIP-1559, EIP-2930, etc.), uses yParity (0 or 1)
241
350
  */
242
351
  async signTransaction(
243
352
  transaction: TransactionSerializable
@@ -251,29 +360,37 @@ export function createKentuckySignerAccount(
251
360
  // Hash the serialized transaction
252
361
  const txHash = keccak256(serializedUnsigned)
253
362
 
254
- // Sign the hash and get components directly from API
255
- const { r, s, v } = await signHashWithComponents(txHash, chainId)
363
+ // Sign the hash - v is always 27 or 28 (recovery_id + 27)
364
+ const { r, s, v } = await signHashWithComponents(txHash)
365
+
366
+ // Convert v (27/28) to recovery ID (0/1)
367
+ const recoveryId = v - 27
256
368
 
257
- // For EIP-1559 and EIP-2930 transactions, v is 0 or 1
258
- // For legacy transactions, v is chainId * 2 + 35 + recovery
369
+ // Determine signature format based on transaction type
370
+ let signatureV: bigint
259
371
  let yParity: number
372
+
260
373
  if (
261
374
  transaction.type === 'eip1559' ||
262
375
  transaction.type === 'eip2930' ||
263
376
  transaction.type === 'eip4844' ||
264
377
  transaction.type === 'eip7702'
265
378
  ) {
266
- yParity = v >= 27 ? v - 27 : v // Convert from 27/28 to 0/1 if needed
379
+ // Modern transactions use yParity directly (0 or 1)
380
+ yParity = recoveryId
381
+ signatureV = BigInt(yParity)
267
382
  } else {
268
- // Legacy transaction - v already includes chain ID
269
- yParity = v
383
+ // Legacy transaction - apply EIP-155 encoding
384
+ // v = chainId * 2 + 35 + recoveryId
385
+ signatureV = BigInt(chainId * 2 + 35 + recoveryId)
386
+ yParity = recoveryId
270
387
  }
271
388
 
272
389
  // Serialize with signature
273
390
  const serializedSigned = serializeTransaction(transaction, {
274
391
  r,
275
392
  s,
276
- v: BigInt(yParity),
393
+ v: signatureV,
277
394
  yParity,
278
395
  } as any)
279
396
 
@@ -290,7 +407,7 @@ export function createKentuckySignerAccount(
290
407
  typedData: TypedDataDefinition<TTypedData, TPrimaryType>
291
408
  ): Promise<Hex> {
292
409
  const hash = hashTypedData(typedData)
293
- return signHash(hash, defaultChainId)
410
+ return signHash(hash)
294
411
  },
295
412
  }) as KentuckySignerAccount
296
413
 
@@ -306,6 +423,49 @@ export function createKentuckySignerAccount(
306
423
  }
307
424
  }
308
425
 
426
+ /**
427
+ * Sign an EIP-7702 authorization
428
+ *
429
+ * This allows the EOA to delegate its code to a smart contract,
430
+ * enabling smart account features like batching and gas sponsorship.
431
+ */
432
+ account.sign7702Authorization = async (
433
+ params: SignAuthorizationParameters,
434
+ currentNonce: bigint
435
+ ): Promise<SignedAuthorization> => {
436
+ // Determine the nonce for the authorization
437
+ // If executor is 'self', increment by 1 because the tx will use currentNonce
438
+ const authNonce = params.executor === 'self'
439
+ ? currentNonce + 1n
440
+ : (params.nonce ?? currentNonce)
441
+
442
+ // Use provided chainId or default
443
+ const chainId = params.chainId ?? defaultChainId
444
+
445
+ // Compute the authorization hash
446
+ const authHash = hashAuthorization({
447
+ contractAddress: params.contractAddress,
448
+ chainId,
449
+ nonce: authNonce,
450
+ })
451
+
452
+ // Sign the hash using Kentucky Signer
453
+ // v is always 27 or 28 (recovery_id + 27)
454
+ const { r, s, v } = await signHashWithComponents(authHash)
455
+
456
+ // Convert v (27/28) to yParity (0/1) for EIP-7702
457
+ const yParity = v - 27
458
+
459
+ return {
460
+ chainId,
461
+ contractAddress: params.contractAddress,
462
+ nonce: authNonce,
463
+ yParity,
464
+ r,
465
+ s,
466
+ }
467
+ }
468
+
309
469
  return account
310
470
  }
311
471
 
package/src/client.ts CHANGED
@@ -218,9 +218,9 @@ export class KentuckySignerClient {
218
218
  /**
219
219
  * Sign an EVM transaction hash
220
220
  *
221
- * @param request - Sign request with tx_hash and chain_id
221
+ * @param request - Sign request with tx_hash
222
222
  * @param token - JWT token
223
- * @returns Signature response with r, s, v components
223
+ * @returns Signature response with r, s, v components (v is always 27 or 28)
224
224
  */
225
225
  async signEvmTransaction(
226
226
  request: SignEvmRequest,
@@ -239,13 +239,12 @@ export class KentuckySignerClient {
239
239
  * Convenience method that wraps signEvmTransaction.
240
240
  *
241
241
  * @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
242
- * @param chainId - Chain ID
243
242
  * @param token - JWT token
244
243
  * @returns Full signature (hex encoded with 0x prefix)
245
244
  */
246
- async signHash(hash: Hex, chainId: number, token: string): Promise<Hex> {
245
+ async signHash(hash: Hex, token: string): Promise<Hex> {
247
246
  const response = await this.signEvmTransaction(
248
- { tx_hash: hash, chain_id: chainId },
247
+ { tx_hash: hash },
249
248
  token
250
249
  )
251
250
  return response.signature.full
package/src/index.ts CHANGED
@@ -15,8 +15,15 @@ export {
15
15
  type KentuckySignerAccountOptions,
16
16
  type TwoFactorCodes,
17
17
  type TwoFactorCallback,
18
+ // EIP-7702 types
19
+ type SignAuthorizationParameters,
20
+ type SignedAuthorization,
18
21
  } from './account'
19
22
 
23
+ // EIP-7702 Constants
24
+ /** Alchemy's SemiModularAccount7702 implementation address (same across all EVM chains) */
25
+ export const ALCHEMY_SEMI_MODULAR_ACCOUNT_7702 = '0x69007702764179f14F51cdce752f4f775d74E139' as const
26
+
20
27
  // Client
21
28
  export {
22
29
  KentuckySignerClient,
@@ -127,3 +134,28 @@ export {
127
134
  formatError,
128
135
  withRetry,
129
136
  } from './utils'
137
+
138
+ // Intent signing for relayer integration
139
+ export {
140
+ createExecutionIntent,
141
+ signIntent,
142
+ signBatchIntents,
143
+ hashIntent,
144
+ hashBatchIntents,
145
+ type ExecutionIntent,
146
+ type SignedIntent,
147
+ type CreateIntentParams,
148
+ } from './intent'
149
+
150
+ // Relayer client
151
+ export {
152
+ RelayerClient,
153
+ createRelayerClient,
154
+ type PaymentMode,
155
+ type TokenOption,
156
+ type EstimateResponse,
157
+ type RelayResponse,
158
+ type TransactionStatus,
159
+ type StatusResponse,
160
+ type RelayerClientOptions,
161
+ } from './relayer-client'
package/src/intent.ts ADDED
@@ -0,0 +1,167 @@
1
+ import {
2
+ type Address,
3
+ type Hex,
4
+ keccak256,
5
+ encodeAbiParameters,
6
+ parseAbiParameters,
7
+ encodePacked,
8
+ } from 'viem'
9
+ import type { KentuckySignerAccount } from './account'
10
+
11
+ /**
12
+ * Execution intent to be signed by the EOA owner
13
+ */
14
+ export interface ExecutionIntent {
15
+ /** Replay protection nonce */
16
+ nonce: bigint
17
+ /** Expiration timestamp (unix seconds) */
18
+ deadline: bigint
19
+ /** Contract to call */
20
+ target: Address
21
+ /** ETH value to send */
22
+ value: bigint
23
+ /** Calldata for the call */
24
+ data: Hex
25
+ }
26
+
27
+ /**
28
+ * Signed execution intent
29
+ */
30
+ export interface SignedIntent {
31
+ /** The execution intent */
32
+ intent: ExecutionIntent
33
+ /** Owner's signature on the intent */
34
+ signature: Hex
35
+ }
36
+
37
+ /**
38
+ * Parameters for creating an execution intent
39
+ */
40
+ export interface CreateIntentParams {
41
+ /** Account nonce (fetch from contract) */
42
+ nonce: bigint
43
+ /** Expiration timestamp (unix seconds) */
44
+ deadline?: bigint
45
+ /** Contract to call */
46
+ target: Address
47
+ /** ETH value to send */
48
+ value?: bigint
49
+ /** Calldata for the call */
50
+ data?: Hex
51
+ }
52
+
53
+ // EIP-712 type hash for ExecutionIntent
54
+ const INTENT_TYPEHASH = keccak256(
55
+ encodePacked(
56
+ ['string'],
57
+ ['ExecutionIntent(uint256 nonce,uint256 deadline,address target,uint256 value,bytes data)']
58
+ )
59
+ )
60
+
61
+ /**
62
+ * Create an execution intent
63
+ *
64
+ * @param params - Intent parameters
65
+ * @returns Execution intent
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const intent = createExecutionIntent({
70
+ * nonce: 0n,
71
+ * target: '0x...',
72
+ * value: parseEther('0.1'),
73
+ * data: '0x',
74
+ * })
75
+ * ```
76
+ */
77
+ export function createExecutionIntent(params: CreateIntentParams): ExecutionIntent {
78
+ return {
79
+ nonce: params.nonce,
80
+ deadline: params.deadline ?? BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour default
81
+ target: params.target,
82
+ value: params.value ?? 0n,
83
+ data: params.data ?? '0x',
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Compute the hash of an execution intent
89
+ *
90
+ * @param intent - The execution intent
91
+ * @returns Intent hash
92
+ */
93
+ export function hashIntent(intent: ExecutionIntent): Hex {
94
+ const dataHash = keccak256(intent.data)
95
+ return keccak256(
96
+ encodeAbiParameters(
97
+ parseAbiParameters('bytes32, uint256, uint256, address, uint256, bytes32'),
98
+ [INTENT_TYPEHASH, intent.nonce, intent.deadline, intent.target, intent.value, dataHash]
99
+ )
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Sign an execution intent using a Kentucky Signer account
105
+ *
106
+ * @param account - Kentucky Signer account
107
+ * @param intent - The execution intent to sign
108
+ * @returns Signed intent
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const account = useKentuckySignerAccount()
113
+ * const intent = createExecutionIntent({ nonce: 0n, target: '0x...' })
114
+ * const signedIntent = await signIntent(account, intent)
115
+ * ```
116
+ */
117
+ export async function signIntent(
118
+ account: KentuckySignerAccount,
119
+ intent: ExecutionIntent
120
+ ): Promise<SignedIntent> {
121
+ // Compute intent hash
122
+ const intentHash = hashIntent(intent)
123
+
124
+ // Sign the hash as a message (will be recovered with ECDSA)
125
+ // Kentucky Signer returns signatures with v = 27 or 28 (standard format)
126
+ // which is directly compatible with OpenZeppelin's ECDSA.recover
127
+ const signature = await account.signMessage({
128
+ message: { raw: intentHash },
129
+ })
130
+
131
+ return {
132
+ intent,
133
+ signature,
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Compute combined hash for batch intents
139
+ *
140
+ * @param intents - Array of execution intents
141
+ * @returns Combined hash
142
+ */
143
+ export function hashBatchIntents(intents: ExecutionIntent[]): Hex {
144
+ const intentHashes = intents.map(hashIntent)
145
+ return keccak256(encodePacked(['bytes32[]'], [intentHashes]))
146
+ }
147
+
148
+ /**
149
+ * Sign multiple execution intents for batch execution
150
+ *
151
+ * @param account - Kentucky Signer account
152
+ * @param intents - Array of execution intents
153
+ * @returns Array of signed intents
154
+ */
155
+ export async function signBatchIntents(
156
+ account: KentuckySignerAccount,
157
+ intents: ExecutionIntent[]
158
+ ): Promise<SignedIntent[]> {
159
+ const signedIntents: SignedIntent[] = []
160
+
161
+ for (const intent of intents) {
162
+ const signed = await signIntent(account, intent)
163
+ signedIntents.push(signed)
164
+ }
165
+
166
+ return signedIntents
167
+ }
@@ -30,3 +30,36 @@ export {
30
30
  useAddress,
31
31
  type UseWalletClientOptions,
32
32
  } from './hooks'
33
+
34
+ // Relayer Hooks
35
+ export {
36
+ useRelayIntent,
37
+ useTransactionStatus,
38
+ useEstimate,
39
+ useNonce,
40
+ type UseRelayIntentResult,
41
+ type UseTransactionStatusResult,
42
+ type UseEstimateResult,
43
+ type UseNonceResult,
44
+ } from './relayer-hooks'
45
+
46
+ // Re-export relayer types needed by hooks
47
+ export {
48
+ RelayerClient,
49
+ createRelayerClient,
50
+ type PaymentMode,
51
+ type TokenOption,
52
+ type EstimateResponse,
53
+ type RelayResponse,
54
+ type TransactionStatus,
55
+ type StatusResponse,
56
+ type Authorization7702,
57
+ } from '../relayer-client'
58
+
59
+ // Re-export intent types needed by hooks
60
+ export {
61
+ createExecutionIntent,
62
+ signIntent,
63
+ type ExecutionIntent,
64
+ type SignedIntent,
65
+ } from '../intent'