174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
// Cloudflare Worker relayer service for gasless transactions
|
|
// Verifies Web Crypto API signatures and submits transactions on behalf of users
|
|
|
|
import { type Address, type Hash, createWalletClient, createPublicClient, http, encodeFunctionData } from 'viem';
|
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
import { mainnet, sepolia } from 'viem/chains';
|
|
|
|
interface RelayerRequest {
|
|
authorization: {
|
|
to: string;
|
|
value: string;
|
|
data: string;
|
|
nonce: number;
|
|
deadline: number;
|
|
};
|
|
signature: string;
|
|
proxyContractAddress: Address;
|
|
chainId: number;
|
|
}
|
|
|
|
interface RelayerResponse {
|
|
success: boolean;
|
|
transactionHash?: Hash;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Relayer service that accepts signed transaction requests
|
|
* and submits them to the blockchain, paying for gas
|
|
*/
|
|
export default {
|
|
async fetch(request: Request, env: any): Promise<Response> {
|
|
// CORS headers
|
|
const corsHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
};
|
|
|
|
// Handle CORS preflight
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
// Health check endpoint
|
|
if (request.method === 'GET' && new URL(request.url).pathname === '/health') {
|
|
return new Response(JSON.stringify({ status: 'ok' }), {
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
// Only handle POST requests for transaction submission
|
|
if (request.method !== 'POST') {
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: 'Method not allowed' }),
|
|
{
|
|
status: 405,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
try {
|
|
const body: RelayerRequest = await request.json();
|
|
|
|
// Validate request
|
|
if (!body.authorization || !body.signature || !body.proxyContractAddress) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: 'Invalid request format' }),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Check deadline
|
|
if (Date.now() / 1000 > body.authorization.deadline) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: 'Transaction expired' }),
|
|
{
|
|
status: 400,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Get relayer private key from environment
|
|
// WARNING: This should be stored securely in Cloudflare Workers secrets
|
|
const relayerPrivateKey = env.RELAYER_PRIVATE_KEY;
|
|
if (!relayerPrivateKey) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, error: 'Relayer not configured' }),
|
|
{
|
|
status: 500,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Get chain configuration
|
|
const chain = body.chainId === 1 ? mainnet : sepolia;
|
|
|
|
// Create relayer account
|
|
const relayerAccount = privateKeyToAccount(relayerPrivateKey as `0x${string}`);
|
|
|
|
// Create wallet client for relayer
|
|
const walletClient = createWalletClient({
|
|
account: relayerAccount,
|
|
chain,
|
|
transport: http(),
|
|
});
|
|
|
|
// Build transaction to proxy contract
|
|
const proxyABI = [
|
|
{
|
|
name: 'execute',
|
|
type: 'function',
|
|
inputs: [
|
|
{ name: 'to', type: 'address' },
|
|
{ name: 'value', type: 'uint256' },
|
|
{ name: 'data', type: 'bytes' },
|
|
{ name: 'nonce', type: 'uint256' },
|
|
{ name: 'deadline', type: 'uint256' },
|
|
{ name: 'signature', type: 'bytes' },
|
|
],
|
|
},
|
|
] as const;
|
|
|
|
const signatureBytes = `0x${Buffer.from(body.signature, 'base64').toString('hex')}`;
|
|
|
|
const transactionData = encodeFunctionData({
|
|
abi: proxyABI,
|
|
functionName: 'execute',
|
|
args: [
|
|
body.authorization.to as Address,
|
|
BigInt(body.authorization.value),
|
|
body.authorization.data as `0x${string}`,
|
|
BigInt(body.authorization.nonce),
|
|
BigInt(body.authorization.deadline),
|
|
signatureBytes as `0x${string}`,
|
|
],
|
|
});
|
|
|
|
// Submit transaction (relayer pays for gas)
|
|
const hash = await walletClient.sendTransaction({
|
|
to: body.proxyContractAddress,
|
|
data: transactionData,
|
|
});
|
|
|
|
const response: RelayerResponse = {
|
|
success: true,
|
|
transactionHash: hash,
|
|
};
|
|
|
|
return new Response(JSON.stringify(response), {
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
});
|
|
} catch (error) {
|
|
console.error('Relayer error:', error);
|
|
const response: RelayerResponse = {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
};
|
|
|
|
return new Response(JSON.stringify(response), {
|
|
status: 500,
|
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|