143 lines
4.4 KiB
TypeScript
143 lines
4.4 KiB
TypeScript
/**
|
|
* Passkey-backed x402 Signer
|
|
*
|
|
* Client-side module that uses the EncryptID passkey (WebAuthn PRF) to
|
|
* derive an EOA and sign x402 micropayments. Replaces the EVM_PRIVATE_KEY
|
|
* env var approach with hardware-backed passkey signing.
|
|
*
|
|
* Flow:
|
|
* 1. Authenticate with passkey (gets PRF output)
|
|
* 2. Derive EOA via deriveEOAFromPRF()
|
|
* 3. Create x402 client with EOA as signer
|
|
* 4. Return wrapped fetch that auto-handles 402 responses
|
|
* 5. Zero the private key bytes after signing
|
|
*/
|
|
|
|
import { deriveEOAFromPRF, zeroPrivateKey } from '../../src/encryptid/eoa-derivation';
|
|
import type { DerivedKeys } from '../../src/encryptid/key-derivation';
|
|
|
|
/**
|
|
* Options for creating a passkey-backed x402 fetch wrapper.
|
|
*/
|
|
export interface PasskeySignerOptions {
|
|
/** Network identifier (default: 'eip155:84532' for Base Sepolia) */
|
|
network?: string;
|
|
/** PRF output from passkey authentication */
|
|
prfOutput: Uint8Array;
|
|
}
|
|
|
|
/**
|
|
* Result from creating a passkey signer, including cleanup.
|
|
*/
|
|
export interface PasskeySignerResult {
|
|
/** The fetch function with x402 payment handling */
|
|
paidFetch: typeof fetch;
|
|
/** The derived EOA address */
|
|
eoaAddress: string;
|
|
/** Call this to zero out the private key from memory */
|
|
cleanup: () => void;
|
|
}
|
|
|
|
/**
|
|
* Create a passkey-backed x402 payment signer.
|
|
*
|
|
* Derives an EOA from the passkey PRF output and creates a fetch wrapper
|
|
* that automatically handles 402 Payment Required responses by signing
|
|
* USDC transfers on the specified network.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const { paidFetch, eoaAddress, cleanup } = await createPasskeySigner({
|
|
* prfOutput: prfOutputBytes,
|
|
* network: 'eip155:84532',
|
|
* });
|
|
*
|
|
* try {
|
|
* const res = await paidFetch('https://example.com/paid-endpoint', {
|
|
* method: 'POST',
|
|
* body: JSON.stringify({ data: 'test' }),
|
|
* });
|
|
* console.log('Payment + response:', await res.json());
|
|
* } finally {
|
|
* cleanup(); // Zero private key
|
|
* }
|
|
* ```
|
|
*/
|
|
export async function createPasskeySigner(
|
|
options: PasskeySignerOptions,
|
|
): Promise<PasskeySignerResult> {
|
|
const network = options.network || 'eip155:84532';
|
|
|
|
// Derive EOA from PRF output
|
|
const eoa = deriveEOAFromPRF(options.prfOutput);
|
|
|
|
// Dynamic imports for x402 client libraries (only loaded when needed)
|
|
const [{ x402Client, wrapFetchWithPayment }, { ExactEvmScheme }, { privateKeyToAccount }] =
|
|
await Promise.all([
|
|
import('@x402/fetch'),
|
|
import('@x402/evm/exact/client'),
|
|
import('viem/accounts'),
|
|
]);
|
|
|
|
// Convert raw private key bytes to viem account
|
|
const hexKey = ('0x' + Array.from(eoa.privateKey).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
|
|
const account = privateKeyToAccount(hexKey);
|
|
|
|
// Set up x402 client with the derived EOA
|
|
// Type cast needed: viem's LocalAccount doesn't include readContract,
|
|
// but ExactEvmScheme only uses signing methods at runtime
|
|
const client = new x402Client();
|
|
client.register(network as `${string}:${string}`, new ExactEvmScheme(account as any));
|
|
|
|
const paidFetch = wrapFetchWithPayment(fetch as any, client) as unknown as typeof fetch;
|
|
|
|
return {
|
|
paidFetch,
|
|
eoaAddress: eoa.address,
|
|
cleanup: () => {
|
|
zeroPrivateKey(eoa.privateKey);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a passkey signer directly from EncryptID DerivedKeys.
|
|
*
|
|
* Convenience wrapper when keys are already available from the key manager.
|
|
*/
|
|
export async function createPasskeySignerFromKeys(
|
|
keys: DerivedKeys,
|
|
network?: string,
|
|
): Promise<PasskeySignerResult | null> {
|
|
if (!keys.eoaPrivateKey || !keys.eoaAddress) {
|
|
return null;
|
|
}
|
|
|
|
const net = network || 'eip155:84532';
|
|
|
|
const [{ x402Client, wrapFetchWithPayment }, { ExactEvmScheme }, { privateKeyToAccount }] =
|
|
await Promise.all([
|
|
import('@x402/fetch'),
|
|
import('@x402/evm/exact/client'),
|
|
import('viem/accounts'),
|
|
]);
|
|
|
|
const hexKey = ('0x' + Array.from(keys.eoaPrivateKey).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
|
|
const account = privateKeyToAccount(hexKey);
|
|
|
|
const client = new x402Client();
|
|
client.register(net as `${string}:${string}`, new ExactEvmScheme(account as any));
|
|
|
|
const paidFetch = wrapFetchWithPayment(fetch as any, client) as unknown as typeof fetch;
|
|
|
|
return {
|
|
paidFetch,
|
|
eoaAddress: keys.eoaAddress,
|
|
cleanup: () => {
|
|
if (keys.eoaPrivateKey) {
|
|
zeroPrivateKey(keys.eoaPrivateKey);
|
|
}
|
|
},
|
|
};
|
|
}
|