kentucky-signer-viem
v0.3.0
Published
Custom Viem account integration for Kentucky Signer with passkey authentication
Downloads
50
Maintainers
Readme
kentucky-signer-viem
A custom Viem account integration for the Kentucky Signer service, enabling EVM transaction signing using passkey (WebAuthn) or password authentication with optional two-factor authentication (TOTP/PIN).
Features
- Custom Viem Account - Full Viem compatibility for signing transactions, messages, and typed data
- Multiple Authentication Methods
- Passkey (WebAuthn) for browser environments
- Password authentication for browser and Node.js
- JWT token authentication for server environments
- Secure Mode - Ephemeral key signing with client-side key generation
- Two-Factor Authentication - TOTP (authenticator app) and PIN support
- Guardian Recovery - Social recovery with trusted guardians
- React Integration - Hooks and context for easy React app integration
- TypeScript Support - Full type definitions included
- Session Management - Automatic refresh and persistence options
- EIP-7702 Support - Sign authorizations for EOA code delegation
- Relayer Integration - Client for gasless transactions via relayer service
- Intent Signing - Sign execution intents for smart account operations
Installation
npm install kentucky-signer-viem viem
# or
yarn add kentucky-signer-viem viem
# or
pnpm add kentucky-signer-viem viemQuick Start
Browser (with Passkey)
import { createWalletClient, http, parseEther } from 'viem'
import { mainnet } from 'viem/chains'
import {
createKentuckySignerAccount,
authenticateWithPasskey,
} from 'kentucky-signer-viem'
// 1. Authenticate with passkey
const session = await authenticateWithPasskey({
baseUrl: 'https://signer.example.com',
accountId: '0123456789abcdef...', // 64-char hex account ID
})
// 2. Create Kentucky Signer account
const account = createKentuckySignerAccount({
config: {
baseUrl: 'https://signer.example.com',
accountId: session.accountId,
},
session,
defaultChainId: 1,
})
// 3. Create Viem wallet client
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
})
// 4. Sign and send transaction
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('0.1'),
})Password Authentication
import {
authenticateWithPassword,
createAccountWithPassword,
} from 'kentucky-signer-viem'
// Create a new account
const newAccount = await createAccountWithPassword({
baseUrl: 'https://signer.example.com',
password: 'your-secure-password',
confirmation: 'your-secure-password',
})
// Or authenticate with existing account
const session = await authenticateWithPassword({
baseUrl: 'https://signer.example.com',
accountId: 'existing_account_id',
password: 'your-secure-password',
})Node.js (with JWT Token)
import { createServerAccount } from 'kentucky-signer-viem'
const account = createServerAccount(
'https://signer.example.com',
'account_id_hex',
'jwt_token',
'0xYourEvmAddress',
1 // chainId
)React Integration
Setup Provider
import { KentuckySignerProvider } from 'kentucky-signer-viem/react'
function App() {
return (
<KentuckySignerProvider
baseUrl="https://signer.example.com"
defaultChainId={1}
persistSession={true}
useEphemeralKeys={true} // Enable secure mode
>
<YourApp />
</KentuckySignerProvider>
)
}Authentication Hook
import { useKentuckySigner } from 'kentucky-signer-viem/react'
function LoginButton() {
const { isAuthenticated, account, authenticate, logout } = useKentuckySigner()
if (isAuthenticated && account) {
return (
<div>
<span>Connected: {account.address}</span>
<button onClick={logout}>Logout</button>
</div>
)
}
return (
<button onClick={() => authenticate('account_id')}>
Login with Passkey
</button>
)
}Wallet Client Hook
import { useWalletClient, useIsReady } from 'kentucky-signer-viem/react'
import { mainnet } from 'viem/chains'
function SendTransaction() {
const isReady = useIsReady()
const walletClient = useWalletClient({ chain: mainnet })
async function send() {
if (!walletClient) return
const hash = await walletClient.sendTransaction({
to: '0x...',
value: parseEther('0.1'),
})
}
return <button onClick={send} disabled={!isReady}>Send</button>
}Secure Mode (Ephemeral Keys)
Secure mode adds an extra layer of security by requiring client-side ephemeral key signatures for all operations:
import { SecureKentuckySignerClient, EphemeralKeyManager } from 'kentucky-signer-viem'
// Create ephemeral key manager
const keyManager = new EphemeralKeyManager()
// Create secure client
const secureClient = new SecureKentuckySignerClient({
baseUrl: 'https://signer.example.com',
ephemeralKeyManager: keyManager,
})
// Authenticate with ephemeral key binding
const session = await authenticateWithPasskey({
baseUrl: 'https://signer.example.com',
accountId: 'account_id',
ephemeralPublicKey: await keyManager.getPublicKey(),
})
// Create account with secure client
const account = createKentuckySignerAccount({
config: { baseUrl, accountId },
session,
secureClient, // Uses ephemeral key signing
})EIP-7702 Authorization
Sign authorizations to delegate your EOA's code to a smart contract, enabling features like batching and gas sponsorship.
import { createPublicClient, http } from 'viem'
import { arbitrum } from 'viem/chains'
const publicClient = createPublicClient({
chain: arbitrum,
transport: http(),
})
// Get current transaction count for the account
const txNonce = await publicClient.getTransactionCount({
address: account.address,
})
// Sign EIP-7702 authorization
const authorization = await account.sign7702Authorization({
contractAddress: '0x...', // Smart account delegate contract
chainId: 42161, // Arbitrum
}, BigInt(txNonce))
// Result:
// {
// chainId: 42161,
// contractAddress: '0x...',
// nonce: 0n,
// yParity: 0,
// r: '0x...',
// s: '0x...'
// }The authorization can be included in an EIP-7702 transaction's authorizationList to delegate the EOA.
Intent Signing for Relayed Execution
Sign execution intents for gasless transactions via a relayer.
import {
createExecutionIntent,
signIntent,
RelayerClient,
} from 'kentucky-signer-viem'
import { encodeFunctionData, parseEther } from 'viem'
// Create relayer client
const relayer = new RelayerClient({
baseUrl: 'https://relayer.example.com',
})
// Get current nonce from the delegate contract
const nonce = await relayer.getNonce(42161, account.address)
// Create an execution intent
const intent = createExecutionIntent({
nonce,
target: '0x...', // Contract to call
value: parseEther('0.1'), // ETH to send (optional)
data: encodeFunctionData({
abi: [...],
functionName: 'transfer',
args: ['0x...', 1000n],
}),
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour (optional)
})
// Sign the intent
const signedIntent = await signIntent(account, intent)Relayer Client
The RelayerClient communicates with a relayer service to submit transactions on behalf of users.
Basic Usage
import { RelayerClient, createRelayerClient } from 'kentucky-signer-viem'
// Create client
const relayer = new RelayerClient({
baseUrl: 'https://relayer.example.com',
timeout: 30000, // Optional, default 30s
})
// Or use the factory function
const relayer = createRelayerClient('https://relayer.example.com')
// Check health
const health = await relayer.health()
// { status: 'ok', relayer: '0x...', timestamp: '2024-...' }Get Nonce
const nonce = await relayer.getNonce(42161, account.address)
// Returns bigint nonce from the delegate contractEstimate Gas
const estimate = await relayer.estimate(42161, account.address, intent)
// {
// gasEstimate: '150000',
// gasCostWei: '30000000000000',
// sponsoredAvailable: true,
// tokenOptions: [
// { token: '0x...', symbol: 'USDC', estimatedFee: '0.05', feePercentage: 5 }
// ]
// }Relay Transaction
// Sponsored (relayer pays gas)
const result = await relayer.relay(
42161,
account.address,
signedIntent,
'sponsored'
)
// Pay with ERC20 token
const result = await relayer.relay(
42161,
account.address,
signedIntent,
{ token: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' } // USDC on Arbitrum
)
if (result.success) {
console.log('TX Hash:', result.txHash)
} else {
console.error('Failed:', result.error)
}Gasless Onboarding
For users with zero ETH, combine EIP-7702 authorization with relay to delegate and execute in a single transaction:
import { createPublicClient, http } from 'viem'
import { arbitrum } from 'viem/chains'
const publicClient = createPublicClient({
chain: arbitrum,
transport: http(),
})
// Get current nonce
const txNonce = await publicClient.getTransactionCount({
address: account.address,
})
// Sign EIP-7702 authorization to delegate EOA
const authorization = await account.sign7702Authorization({
contractAddress: delegateAddress,
chainId: 42161,
}, BigInt(txNonce))
// Create and sign execution intent
const nonce = await relayer.getNonce(42161, account.address)
const intent = createExecutionIntent({
nonce,
target: '0x...',
data: '0x...',
})
const signedIntent = await signIntent(account, intent)
// Relay with authorization - delegates EOA and executes in one tx
const result = await relayer.relay(
42161,
account.address,
signedIntent,
'sponsored',
authorization // Include EIP-7702 authorization
)Check Transaction Status
const status = await relayer.getStatus(42161, txHash)
// {
// status: 'confirmed', // 'pending' | 'confirmed' | 'failed'
// txHash: '0x...',
// blockNumber: 12345678,
// gasUsed: '120000'
// }Two-Factor Authentication
Setup TOTP (Authenticator App)
import { KentuckySignerClient } from 'kentucky-signer-viem'
const client = new KentuckySignerClient({ baseUrl })
// Start TOTP setup - returns QR code URI
const setup = await client.setupTOTP(token)
console.log('Scan this QR code:', setup.uri)
console.log('Or enter manually:', setup.secret)
// Enable TOTP with verification code
await client.enableTOTP('123456', token)
// Check 2FA status
const status = await client.get2FAStatus(token)
// { totp_enabled: true, pin_enabled: false, pin_length: 0 }Setup PIN
// Setup 4 or 6 digit PIN
await client.setupPIN('123456', token)
// Disable PIN (requires current PIN)
await client.disablePIN('123456', token)Signing with 2FA
When 2FA is enabled, signing operations will automatically prompt for codes:
import { useKentuckySigner } from 'kentucky-signer-viem/react'
function App() {
const { twoFactorPrompt, submit2FA, cancel2FA } = useKentuckySigner()
// The 2FA prompt appears automatically when signing requires it
if (twoFactorPrompt.isVisible) {
return (
<TwoFactorModal
totpRequired={twoFactorPrompt.totpRequired}
pinRequired={twoFactorPrompt.pinRequired}
pinLength={twoFactorPrompt.pinLength}
onSubmit={(codes) => submit2FA(codes)}
onCancel={() => cancel2FA()}
/>
)
}
return <YourApp />
}Guardian Recovery
Set up trusted guardians for account recovery:
const client = new KentuckySignerClient({ baseUrl })
// Add a guardian (requires WebAuthn attestation from guardian's device)
const { guardian_index, guardian_count } = await client.addGuardian({
attestation_object: guardianAttestationBase64url,
label: 'My Friend',
}, token)
// List guardians
const { guardians } = await client.getGuardians(token)
// guardians: [{ index: 1, label: 'My Friend' }, ...]
// Initiate recovery (when locked out - register new passkey first)
const recovery = await client.initiateRecovery(
accountId,
newPasskeyAttestationObject,
'New Owner Passkey'
)
// Returns: { challenges, guardian_count, threshold, timelock_seconds }
// Guardian signs their challenge with their passkey
await client.verifyGuardian({
account_id: accountId,
guardian_index: 1,
authenticator_data: authDataBase64url,
client_data_json: clientDataBase64url,
signature: signatureBase64url,
})
// Check recovery status
const status = await client.getRecoveryStatus(accountId)
// { verified_count, threshold, can_complete, timelock_remaining }
// Complete recovery after threshold met and timelock expired
await client.completeRecovery(accountId)API Reference
Core Functions
| Function | Description |
|----------|-------------|
| createKentuckySignerAccount(options) | Create a Viem-compatible account |
| createServerAccount(...) | Create account with JWT token (Node.js) |
| authenticateWithPasskey(options) | Authenticate using WebAuthn |
| authenticateWithPassword(options) | Authenticate using password |
| createAccountWithPassword(options) | Create new account with password |
| authenticateWithToken(...) | Create session from JWT token |
Intent & Relayer Functions
| Function | Description |
|----------|-------------|
| createExecutionIntent(params) | Create an execution intent for relayed execution |
| signIntent(account, intent) | Sign an execution intent |
| signBatchIntents(account, intents) | Sign multiple intents for batch execution |
| hashIntent(intent) | Compute the hash of an execution intent |
| createRelayerClient(baseUrl) | Create a relayer client |
React Hooks
| Hook | Description |
|------|-------------|
| useKentuckySigner() | Access auth state, actions, and 2FA |
| useKentuckySignerAccount() | Get the current account |
| useWalletClient(options) | Create a Viem WalletClient |
| usePasskeyAuth() | Authentication flow with loading state |
| useSignMessage() | Sign messages with loading state |
| useSignTypedData() | Sign EIP-712 typed data |
| useIsReady() | Check if signer is ready |
| useAddress() | Get connected address |
Client Methods
Authentication
getChallenge(accountId)- Get WebAuthn challengeauthenticatePasskey(accountId, credential, ephemeralPublicKey?)- Authenticate with passkeyauthenticatePassword(request)- Authenticate with password ({ account_id, password })refreshToken(token)- Refresh JWT tokenlogout(token)- Invalidate session
Signing
signEvmTransaction(request, token)- Sign EVM transaction hashsignEvmTransactionWith2FA(request, token)- Sign with 2FA codes
Account Management
getAccountInfo(accountId, token)- Get account infogetAccountInfoExtended(accountId, token)- Get account info with auth configaddPassword(accountId, request, token)- Add password authaddPasskey(accountId, request, token)- Add passkeyremovePasskey(accountId, passkeyIndex, token)- Remove passkey by index
Two-Factor Authentication
get2FAStatus(token)- Get 2FA statussetupTOTP(token)- Start TOTP setupenableTOTP(code, token)- Enable TOTPdisableTOTP(code, token)- Disable TOTPsetupPIN(pin, token)- Setup PINdisablePIN(pin, token)- Disable PIN
Account Methods
EIP-7702 Authorization
account.sign7702Authorization(params, nonce)- Sign authorization to delegate EOA code
Relayer Client Methods
health()- Check relayer healthgetNonce(chainId, address)- Get account nonce from delegate contractestimate(chainId, address, intent)- Estimate gas and feesrelay(chainId, address, signedIntent, paymentMode, authorization?)- Submit transactiongetStatus(chainId, txHash)- Get transaction status
Guardian Recovery
addGuardian(request, token)- Add guardian passkeyremoveGuardian(guardianIndex, token)- Remove guardiangetGuardians(token)- List guardiansinitiateRecovery(accountId, attestationObject, label?)- Start recoveryverifyGuardian(request)- Submit guardian signaturegetRecoveryStatus(accountId)- Check recovery statuscompleteRecovery(accountId)- Complete recoverycancelRecovery(token)- Cancel recovery (owner only)
Error Handling
import { KentuckySignerError } from 'kentucky-signer-viem'
try {
await authenticate(accountId)
} catch (error) {
if (error instanceof KentuckySignerError) {
switch (error.code) {
case 'WEBAUTHN_NOT_AVAILABLE':
// WebAuthn not supported
break
case 'USER_CANCELLED':
// User cancelled authentication
break
case 'SESSION_EXPIRED':
// JWT token expired
break
case '2FA_REQUIRED':
// 2FA verification needed
break
case '2FA_CANCELLED':
// User cancelled 2FA input
break
// ... handle other codes
}
}
}Documentation
For detailed documentation, see the docs folder:
- Authentication - Passkey, password, and token auth
- Secure Mode - Ephemeral key signing
- Two-Factor Authentication - TOTP and PIN setup
- Guardian Recovery - Social recovery setup
- React Integration - Hooks and context usage
- API Reference - Complete API documentation
License
MIT
