kentucky-signer-viem 0.1.3 → 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.
@@ -0,0 +1,318 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import type { Address, Hex } from 'viem'
3
+ import type {
4
+ RelayerClient,
5
+ PaymentMode,
6
+ EstimateResponse,
7
+ RelayResponse,
8
+ StatusResponse,
9
+ TransactionStatus,
10
+ Authorization7702,
11
+ } from '../relayer-client'
12
+ import type { ExecutionIntent, SignedIntent } from '../intent'
13
+
14
+ /**
15
+ * Hook for relaying intents through the relayer
16
+ */
17
+ export interface UseRelayIntentResult {
18
+ /** Relay a signed intent */
19
+ relay: (
20
+ chainId: number,
21
+ accountAddress: Address,
22
+ signedIntent: SignedIntent,
23
+ paymentMode: PaymentMode,
24
+ authorization?: Authorization7702
25
+ ) => Promise<RelayResponse>
26
+ /** Whether a relay is in progress */
27
+ isRelaying: boolean
28
+ /** Last relay response */
29
+ response: RelayResponse | null
30
+ /** Last error */
31
+ error: Error | null
32
+ /** Reset state */
33
+ reset: () => void
34
+ }
35
+
36
+ /**
37
+ * Hook for relaying signed intents through the Kentucky Signer Relayer
38
+ *
39
+ * @param client - Relayer client instance
40
+ * @returns Relay functions and state
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const relayer = useRelayerClient('https://relayer.example.com')
45
+ * const { relay, isRelaying, response, error } = useRelayIntent(relayer)
46
+ *
47
+ * const handleSubmit = async () => {
48
+ * const result = await relay(42161, accountAddress, signedIntent, 'sponsored')
49
+ * if (result.success) {
50
+ * console.log('TX:', result.txHash)
51
+ * }
52
+ * }
53
+ * ```
54
+ */
55
+ export function useRelayIntent(client: RelayerClient): UseRelayIntentResult {
56
+ const [isRelaying, setIsRelaying] = useState(false)
57
+ const [response, setResponse] = useState<RelayResponse | null>(null)
58
+ const [error, setError] = useState<Error | null>(null)
59
+
60
+ const relay = useCallback(
61
+ async (
62
+ chainId: number,
63
+ accountAddress: Address,
64
+ signedIntent: SignedIntent,
65
+ paymentMode: PaymentMode,
66
+ authorization?: Authorization7702
67
+ ): Promise<RelayResponse> => {
68
+ setIsRelaying(true)
69
+ setError(null)
70
+
71
+ try {
72
+ const result = await client.relay(chainId, accountAddress, signedIntent, paymentMode, authorization)
73
+ setResponse(result)
74
+ return result
75
+ } catch (err) {
76
+ const error = err instanceof Error ? err : new Error('Relay failed')
77
+ setError(error)
78
+ return { success: false, error: error.message }
79
+ } finally {
80
+ setIsRelaying(false)
81
+ }
82
+ },
83
+ [client]
84
+ )
85
+
86
+ const reset = useCallback(() => {
87
+ setResponse(null)
88
+ setError(null)
89
+ }, [])
90
+
91
+ return { relay, isRelaying, response, error, reset }
92
+ }
93
+
94
+ /**
95
+ * Hook for tracking transaction status
96
+ */
97
+ export interface UseTransactionStatusResult {
98
+ /** Current transaction status */
99
+ status: TransactionStatus | null
100
+ /** Full status response */
101
+ statusResponse: StatusResponse | null
102
+ /** Whether status is being fetched */
103
+ isLoading: boolean
104
+ /** Last error */
105
+ error: Error | null
106
+ /** Manually refresh status */
107
+ refresh: () => void
108
+ }
109
+
110
+ /**
111
+ * Hook for tracking transaction status with polling
112
+ *
113
+ * @param client - Relayer client instance
114
+ * @param chainId - Chain ID
115
+ * @param txHash - Transaction hash to track (null to disable)
116
+ * @param pollInterval - Polling interval in ms (default: 3000)
117
+ * @returns Transaction status and state
118
+ *
119
+ * @example
120
+ * ```tsx
121
+ * const { status, statusResponse, isLoading } = useTransactionStatus(
122
+ * relayer,
123
+ * 42161,
124
+ * txHash
125
+ * )
126
+ *
127
+ * if (status === 'confirmed') {
128
+ * console.log('Transaction confirmed in block:', statusResponse?.blockNumber)
129
+ * }
130
+ * ```
131
+ */
132
+ export function useTransactionStatus(
133
+ client: RelayerClient,
134
+ chainId: number,
135
+ txHash: Hex | null,
136
+ pollInterval: number = 3000
137
+ ): UseTransactionStatusResult {
138
+ const [status, setStatus] = useState<TransactionStatus | null>(null)
139
+ const [statusResponse, setStatusResponse] = useState<StatusResponse | null>(null)
140
+ const [isLoading, setIsLoading] = useState(false)
141
+ const [error, setError] = useState<Error | null>(null)
142
+ const intervalRef = useRef<NodeJS.Timeout | null>(null)
143
+
144
+ const fetchStatus = useCallback(async () => {
145
+ if (!txHash) return
146
+
147
+ setIsLoading(true)
148
+ try {
149
+ const response = await client.getStatus(chainId, txHash)
150
+ setStatusResponse(response)
151
+ setStatus(response.status)
152
+ setError(null)
153
+
154
+ // Stop polling if transaction is finalized
155
+ if (response.status === 'confirmed' || response.status === 'failed') {
156
+ if (intervalRef.current) {
157
+ clearInterval(intervalRef.current)
158
+ intervalRef.current = null
159
+ }
160
+ }
161
+ } catch (err) {
162
+ setError(err instanceof Error ? err : new Error('Failed to fetch status'))
163
+ } finally {
164
+ setIsLoading(false)
165
+ }
166
+ }, [client, chainId, txHash])
167
+
168
+ // Start polling when txHash is set
169
+ useEffect(() => {
170
+ if (!txHash) {
171
+ setStatus(null)
172
+ setStatusResponse(null)
173
+ return
174
+ }
175
+
176
+ // Fetch immediately
177
+ fetchStatus()
178
+
179
+ // Start polling
180
+ intervalRef.current = setInterval(fetchStatus, pollInterval)
181
+
182
+ return () => {
183
+ if (intervalRef.current) {
184
+ clearInterval(intervalRef.current)
185
+ intervalRef.current = null
186
+ }
187
+ }
188
+ }, [txHash, pollInterval, fetchStatus])
189
+
190
+ return { status, statusResponse, isLoading, error, refresh: fetchStatus }
191
+ }
192
+
193
+ /**
194
+ * Hook for estimating gas and fees
195
+ */
196
+ export interface UseEstimateResult {
197
+ /** Estimate gas and fees */
198
+ estimate: (
199
+ chainId: number,
200
+ accountAddress: Address,
201
+ intent: ExecutionIntent
202
+ ) => Promise<EstimateResponse | null>
203
+ /** Last estimate response */
204
+ estimateResponse: EstimateResponse | null
205
+ /** Whether estimate is in progress */
206
+ isEstimating: boolean
207
+ /** Last error */
208
+ error: Error | null
209
+ }
210
+
211
+ /**
212
+ * Hook for estimating gas and token fees
213
+ *
214
+ * @param client - Relayer client instance
215
+ * @returns Estimate functions and state
216
+ *
217
+ * @example
218
+ * ```tsx
219
+ * const { estimate, estimateResponse, isEstimating } = useEstimate(relayer)
220
+ *
221
+ * useEffect(() => {
222
+ * estimate(42161, accountAddress, intent)
223
+ * }, [intent])
224
+ *
225
+ * if (estimateResponse) {
226
+ * console.log('Gas:', estimateResponse.gasEstimate)
227
+ * console.log('Tokens:', estimateResponse.tokenOptions)
228
+ * }
229
+ * ```
230
+ */
231
+ export function useEstimate(client: RelayerClient): UseEstimateResult {
232
+ const [estimateResponse, setEstimateResponse] = useState<EstimateResponse | null>(null)
233
+ const [isEstimating, setIsEstimating] = useState(false)
234
+ const [error, setError] = useState<Error | null>(null)
235
+
236
+ const estimate = useCallback(
237
+ async (
238
+ chainId: number,
239
+ accountAddress: Address,
240
+ intent: ExecutionIntent
241
+ ): Promise<EstimateResponse | null> => {
242
+ setIsEstimating(true)
243
+ setError(null)
244
+
245
+ try {
246
+ const response = await client.estimate(chainId, accountAddress, intent)
247
+ setEstimateResponse(response)
248
+ return response
249
+ } catch (err) {
250
+ const error = err instanceof Error ? err : new Error('Estimate failed')
251
+ setError(error)
252
+ return null
253
+ } finally {
254
+ setIsEstimating(false)
255
+ }
256
+ },
257
+ [client]
258
+ )
259
+
260
+ return { estimate, estimateResponse, isEstimating, error }
261
+ }
262
+
263
+ /**
264
+ * Hook for fetching account nonce
265
+ */
266
+ export interface UseNonceResult {
267
+ /** Current nonce */
268
+ nonce: bigint | null
269
+ /** Whether nonce is being fetched */
270
+ isLoading: boolean
271
+ /** Last error */
272
+ error: Error | null
273
+ /** Refresh nonce */
274
+ refresh: () => void
275
+ }
276
+
277
+ /**
278
+ * Hook for fetching account nonce
279
+ *
280
+ * @param client - Relayer client instance
281
+ * @param chainId - Chain ID
282
+ * @param address - Account address (null to disable)
283
+ * @returns Nonce and state
284
+ */
285
+ export function useNonce(
286
+ client: RelayerClient,
287
+ chainId: number,
288
+ address: Address | null
289
+ ): UseNonceResult {
290
+ const [nonce, setNonce] = useState<bigint | null>(null)
291
+ const [isLoading, setIsLoading] = useState(false)
292
+ const [error, setError] = useState<Error | null>(null)
293
+
294
+ const fetchNonce = useCallback(async () => {
295
+ if (!address) return
296
+
297
+ setIsLoading(true)
298
+ try {
299
+ const result = await client.getNonce(chainId, address)
300
+ setNonce(result)
301
+ setError(null)
302
+ } catch (err) {
303
+ setError(err instanceof Error ? err : new Error('Failed to fetch nonce'))
304
+ } finally {
305
+ setIsLoading(false)
306
+ }
307
+ }, [client, chainId, address])
308
+
309
+ useEffect(() => {
310
+ if (address) {
311
+ fetchNonce()
312
+ } else {
313
+ setNonce(null)
314
+ }
315
+ }, [address, fetchNonce])
316
+
317
+ return { nonce, isLoading, error, refresh: fetchNonce }
318
+ }
@@ -0,0 +1,305 @@
1
+ import type { Address, Hex } from 'viem'
2
+ import type { ExecutionIntent, SignedIntent } from './intent'
3
+
4
+ /**
5
+ * Payment mode for relaying
6
+ */
7
+ export type PaymentMode = 'sponsored' | { token: Address }
8
+
9
+ /**
10
+ * EIP-7702 Authorization for gasless onboarding
11
+ * When provided to relay(), allows users with 0 ETH to delegate their EOA
12
+ * to the smart account delegate in the same transaction as execution
13
+ */
14
+ export interface Authorization7702 {
15
+ /** Chain ID (0 for all chains) */
16
+ chainId: number
17
+ /** Contract address to delegate to */
18
+ contractAddress: Address
19
+ /** Nonce for the authorization */
20
+ nonce: bigint
21
+ /** Recovery identifier (0 or 1) */
22
+ yParity: number
23
+ /** Signature r component */
24
+ r: Hex
25
+ /** Signature s component */
26
+ s: Hex
27
+ }
28
+
29
+ /**
30
+ * Token payment option returned by estimate
31
+ */
32
+ export interface TokenOption {
33
+ /** Token address */
34
+ token: Address
35
+ /** Token symbol */
36
+ symbol: string
37
+ /** Estimated fee in token units */
38
+ estimatedFee: string
39
+ /** Fee percentage (e.g., 5 = 5%) */
40
+ feePercentage: number
41
+ }
42
+
43
+ /**
44
+ * Gas estimate response
45
+ */
46
+ export interface EstimateResponse {
47
+ /** Estimated gas units */
48
+ gasEstimate: string
49
+ /** Estimated gas cost in wei */
50
+ gasCostWei: string
51
+ /** Whether sponsored mode is available */
52
+ sponsoredAvailable: boolean
53
+ /** Available token payment options */
54
+ tokenOptions: TokenOption[]
55
+ }
56
+
57
+ /**
58
+ * Relay response
59
+ */
60
+ export interface RelayResponse {
61
+ /** Whether the relay was successful */
62
+ success: boolean
63
+ /** Transaction hash if successful */
64
+ txHash?: Hex
65
+ /** Error message if failed */
66
+ error?: string
67
+ }
68
+
69
+ /**
70
+ * Transaction status
71
+ */
72
+ export type TransactionStatus = 'pending' | 'confirmed' | 'failed'
73
+
74
+ /**
75
+ * Status response
76
+ */
77
+ export interface StatusResponse {
78
+ /** Current status */
79
+ status: TransactionStatus
80
+ /** Transaction hash */
81
+ txHash: Hex
82
+ /** Block number if confirmed */
83
+ blockNumber?: number
84
+ /** Gas used if confirmed */
85
+ gasUsed?: string
86
+ /** Token amount paid if applicable */
87
+ tokenPaid?: string
88
+ }
89
+
90
+ /**
91
+ * Relayer client options
92
+ */
93
+ export interface RelayerClientOptions {
94
+ /** Relayer API base URL */
95
+ baseUrl: string
96
+ /** Request timeout in ms (default: 30000) */
97
+ timeout?: number
98
+ }
99
+
100
+ /**
101
+ * Client for interacting with the Kentucky Signer Relayer API
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const relayer = new RelayerClient({ baseUrl: 'https://relayer.example.com' })
106
+ *
107
+ * // Get nonce
108
+ * const nonce = await relayer.getNonce(42161, accountAddress)
109
+ *
110
+ * // Create and sign intent
111
+ * const intent = createExecutionIntent({ nonce, target: '0x...' })
112
+ * const signed = await signIntent(account, intent)
113
+ *
114
+ * // Estimate fees
115
+ * const estimate = await relayer.estimate(42161, accountAddress, intent)
116
+ *
117
+ * // Relay transaction
118
+ * const result = await relayer.relay(42161, accountAddress, signed, 'sponsored')
119
+ * console.log('TX Hash:', result.txHash)
120
+ *
121
+ * // Check status
122
+ * const status = await relayer.getStatus(42161, result.txHash!)
123
+ * ```
124
+ */
125
+ export class RelayerClient {
126
+ private baseUrl: string
127
+ private timeout: number
128
+
129
+ constructor(options: RelayerClientOptions) {
130
+ this.baseUrl = options.baseUrl.replace(/\/$/, '') // Remove trailing slash
131
+ this.timeout = options.timeout ?? 30000
132
+ }
133
+
134
+ /**
135
+ * Check if the relayer is healthy
136
+ */
137
+ async health(): Promise<{ status: string; relayer: Address; timestamp: string }> {
138
+ const response = await this.fetch('/health')
139
+ return response
140
+ }
141
+
142
+ /**
143
+ * Get the current nonce for an account
144
+ *
145
+ * @param chainId - Chain ID
146
+ * @param address - Account address
147
+ * @returns Current nonce as bigint
148
+ */
149
+ async getNonce(chainId: number, address: Address): Promise<bigint> {
150
+ const response = await this.fetch(`/nonce/${chainId}/${address}`)
151
+ return BigInt(response.nonce)
152
+ }
153
+
154
+ /**
155
+ * Estimate gas and fees for an intent
156
+ *
157
+ * @param chainId - Chain ID
158
+ * @param accountAddress - Account address (the delegated EOA)
159
+ * @param intent - Execution intent
160
+ * @returns Estimate response
161
+ */
162
+ async estimate(
163
+ chainId: number,
164
+ accountAddress: Address,
165
+ intent: ExecutionIntent
166
+ ): Promise<EstimateResponse> {
167
+ const response = await this.fetch('/estimate', {
168
+ method: 'POST',
169
+ body: JSON.stringify({
170
+ chainId,
171
+ accountAddress,
172
+ intent: {
173
+ nonce: intent.nonce.toString(),
174
+ deadline: intent.deadline.toString(),
175
+ target: intent.target,
176
+ value: intent.value.toString(),
177
+ data: intent.data,
178
+ },
179
+ }),
180
+ })
181
+ return response
182
+ }
183
+
184
+ /**
185
+ * Relay a signed intent
186
+ *
187
+ * @param chainId - Chain ID
188
+ * @param accountAddress - Account address (the delegated EOA)
189
+ * @param signedIntent - Signed execution intent
190
+ * @param paymentMode - Payment mode ('sponsored' or { token: Address })
191
+ * @param authorization - Optional EIP-7702 authorization for gasless onboarding
192
+ * @returns Relay response with transaction hash
193
+ *
194
+ * @example Gasless onboarding (delegate + execute in one tx)
195
+ * ```typescript
196
+ * // Get current nonce for authorization
197
+ * const txNonce = await publicClient.getTransactionCount({ address: accountAddress })
198
+ *
199
+ * // Sign EIP-7702 authorization
200
+ * const authorization = await account.sign7702Authorization({
201
+ * contractAddress: delegateAddress,
202
+ * chainId: 42161,
203
+ * }, txNonce)
204
+ *
205
+ * // Relay with authorization
206
+ * const result = await relayer.relay(
207
+ * 42161,
208
+ * accountAddress,
209
+ * signedIntent,
210
+ * 'sponsored',
211
+ * authorization
212
+ * )
213
+ * ```
214
+ */
215
+ async relay(
216
+ chainId: number,
217
+ accountAddress: Address,
218
+ signedIntent: SignedIntent,
219
+ paymentMode: PaymentMode,
220
+ authorization?: Authorization7702
221
+ ): Promise<RelayResponse> {
222
+ const body: any = {
223
+ chainId,
224
+ accountAddress,
225
+ intent: {
226
+ nonce: signedIntent.intent.nonce.toString(),
227
+ deadline: signedIntent.intent.deadline.toString(),
228
+ target: signedIntent.intent.target,
229
+ value: signedIntent.intent.value.toString(),
230
+ data: signedIntent.intent.data,
231
+ },
232
+ ownerSignature: signedIntent.signature,
233
+ paymentMode,
234
+ }
235
+
236
+ // Include authorization for gasless onboarding if provided
237
+ if (authorization) {
238
+ body.authorization = {
239
+ chainId: authorization.chainId,
240
+ contractAddress: authorization.contractAddress,
241
+ nonce: authorization.nonce.toString(),
242
+ yParity: authorization.yParity,
243
+ r: authorization.r,
244
+ s: authorization.s,
245
+ }
246
+ }
247
+
248
+ const response = await this.fetch('/relay', {
249
+ method: 'POST',
250
+ body: JSON.stringify(body),
251
+ })
252
+ return response
253
+ }
254
+
255
+ /**
256
+ * Get transaction status
257
+ *
258
+ * @param chainId - Chain ID
259
+ * @param txHash - Transaction hash
260
+ * @returns Status response
261
+ */
262
+ async getStatus(chainId: number, txHash: Hex): Promise<StatusResponse> {
263
+ const response = await this.fetch(`/status/${chainId}/${txHash}`)
264
+ return response
265
+ }
266
+
267
+ /**
268
+ * Make a fetch request to the relayer API
269
+ */
270
+ private async fetch(path: string, options?: RequestInit): Promise<any> {
271
+ const controller = new AbortController()
272
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
273
+
274
+ try {
275
+ const response = await fetch(`${this.baseUrl}${path}`, {
276
+ ...options,
277
+ headers: {
278
+ 'Content-Type': 'application/json',
279
+ ...options?.headers,
280
+ },
281
+ signal: controller.signal,
282
+ })
283
+
284
+ const data = await response.json()
285
+
286
+ if (!response.ok) {
287
+ throw new Error(data.error || `Request failed: ${response.status}`)
288
+ }
289
+
290
+ return data
291
+ } finally {
292
+ clearTimeout(timeoutId)
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Create a relayer client
299
+ *
300
+ * @param baseUrl - Relayer API base URL
301
+ * @returns Relayer client instance
302
+ */
303
+ export function createRelayerClient(baseUrl: string): RelayerClient {
304
+ return new RelayerClient({ baseUrl })
305
+ }
@@ -296,9 +296,9 @@ export class SecureKentuckySignerClient {
296
296
  /**
297
297
  * Sign a raw hash for EVM (signed request)
298
298
  */
299
- async signHash(hash: Hex, chainId: number, token: string): Promise<Hex> {
299
+ async signHash(hash: Hex, token: string): Promise<Hex> {
300
300
  const response = await this.signEvmTransaction(
301
- { tx_hash: hash, chain_id: chainId },
301
+ { tx_hash: hash },
302
302
  token
303
303
  )
304
304
  return response.signature.full
package/src/types.ts CHANGED
@@ -66,16 +66,19 @@ export interface AccountInfoResponse {
66
66
 
67
67
  /**
68
68
  * EVM signature response from Kentucky Signer
69
+ *
70
+ * Note: v is always 27 or 28 (standard format).
71
+ * EIP-155 encoding should be applied by the caller when needed for legacy transactions.
69
72
  */
70
73
  export interface EvmSignatureResponse {
71
74
  success: boolean
72
75
  signature: {
73
76
  r: Hex
74
77
  s: Hex
78
+ /** v value: 27 or 28 (recovery_id + 27) */
75
79
  v: number
76
80
  full: Hex
77
81
  }
78
- chain_id: number
79
82
  signer_address: string
80
83
  }
81
84
 
@@ -163,8 +166,6 @@ export interface ClientOptions {
163
166
  export interface SignEvmRequest {
164
167
  /** Transaction hash to sign (32 bytes, hex encoded) */
165
168
  tx_hash: Hex
166
- /** Chain ID */
167
- chain_id: number
168
169
  }
169
170
 
170
171
  /**