web3 integration start

This commit is contained in:
Jeff Emmett 2025-11-11 11:17:53 -08:00
parent 5fd83944fc
commit 667a6a780e
23 changed files with 3251 additions and 4 deletions

View File

@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title WebCryptoProxy
* @notice Minimal proxy contract that verifies Web Crypto API (P-256) signatures
* and executes transactions. Designed to minimize gas costs and complexity.
* @dev Each user deploys their own proxy contract, which stores their Web Crypto public key
* and verifies P-256 signatures before executing transactions.
*/
contract WebCryptoProxy {
// Web Crypto P-256 public key (stored as bytes32 for gas efficiency)
// P-256 public keys are 65 bytes (0x04 || 32-byte X || 32-byte Y)
// We store the X coordinate (32 bytes) and Y coordinate (32 bytes) separately
bytes32 public publicKeyX;
bytes32 public publicKeyY;
// Replay protection: nonce tracking
mapping(uint256 => bool) public usedNonces;
// Events
event TransactionExecuted(
address indexed to,
uint256 value,
bytes data,
uint256 nonce
);
event PublicKeySet(bytes32 indexed publicKeyX, bytes32 indexed publicKeyY);
/**
* @notice Constructor sets the Web Crypto public key
* @param _publicKeyX X coordinate of P-256 public key (32 bytes)
* @param _publicKeyY Y coordinate of P-256 public key (32 bytes)
*/
constructor(bytes32 _publicKeyX, bytes32 _publicKeyY) {
publicKeyX = _publicKeyX;
publicKeyY = _publicKeyY;
emit PublicKeySet(_publicKeyX, _publicKeyY);
}
/**
* @notice Execute a transaction if signature is valid
* @param to Target address for the transaction
* @param value Amount of ETH to send (in wei)
* @param data Transaction data (contract call data)
* @param nonce Unique nonce for replay protection
* @param deadline Transaction expiration timestamp
* @param signature Web Crypto P-256 signature (r, s, v format)
* @dev Signature verification uses P-256 curve, not secp256k1
* This requires a custom verification library or precompile
*/
function execute(
address to,
uint256 value,
bytes calldata data,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external {
// Check deadline
require(block.timestamp <= deadline, "WebCryptoProxy: Transaction expired");
// Check nonce hasn't been used
require(!usedNonces[nonce], "WebCryptoProxy: Nonce already used");
// Mark nonce as used
usedNonces[nonce] = true;
// Verify signature
// Note: P-256 signature verification requires a library or precompile
// For now, this is a placeholder - you'll need to implement or import
// a P-256 verification function
bytes32 messageHash = keccak256(
abi.encodePacked(
"\x19\x01", // EIP-712 prefix
keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("WebCryptoProxy")),
keccak256(bytes("1")),
block.chainid,
address(this)
)),
keccak256(abi.encode(
keccak256("Transaction(address to,uint256 value,bytes data,uint256 nonce,uint256 deadline)"),
to,
value,
keccak256(data),
nonce,
deadline
))
)
);
// TODO: Verify P-256 signature
// This requires a P-256 signature verification library
// Options:
// 1. Use a precompile (if available on your chain)
// 2. Import a P-256 verification library
// 3. Use a verification contract
// For now, we'll use a simplified check
// In production, replace this with proper P-256 verification
require(verifyP256Signature(messageHash, signature), "WebCryptoProxy: Invalid signature");
// Execute the transaction
(bool success, ) = to.call{value: value}(data);
require(success, "WebCryptoProxy: Transaction failed");
emit TransactionExecuted(to, value, data, nonce);
}
/**
* @notice Verify P-256 signature
* @dev This is a placeholder - implement with proper P-256 verification
* You can use libraries like:
* - A precompile if your chain supports it
* - A P-256 verification library (e.g., from OpenZeppelin or custom)
* - An external verification contract
* @param messageHash The message hash to verify
* @param signature The signature (needs to be parsed for r, s, v)
* @return true if signature is valid
*/
function verifyP256Signature(
bytes32 messageHash,
bytes calldata signature
) internal view returns (bool) {
// TODO: Implement P-256 signature verification
// For now, this is a placeholder that always returns false for safety
// In production, you must implement proper P-256 verification
// Example structure (you'll need to adapt based on your verification method):
// 1. Parse signature to extract r, s, v (or r, s for P-256)
// 2. Recover public key from signature
// 3. Compare recovered public key with stored publicKeyX and publicKeyY
// 4. Return true if they match
// Placeholder: This will reject all signatures until properly implemented
// Remove this and implement actual verification
revert("WebCryptoProxy: P-256 verification not yet implemented");
// Uncomment and implement when you have P-256 verification:
// bytes32 r = ...;
// bytes32 s = ...;
// (bytes32 recoveredX, bytes32 recoveredY) = recoverP256PublicKey(messageHash, r, s);
// return (recoveredX == publicKeyX && recoveredY == publicKeyY);
}
/**
* @notice Receive ETH
*/
receive() external payable {}
/**
* @notice Fallback function
*/
fallback() external payable {}
}

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./WebCryptoProxy.sol";
/**
* @title WebCryptoProxyFactory
* @notice Factory contract to deploy minimal proxy contracts for users
* @dev Uses CREATE2 for deterministic addresses based on public key
*/
contract WebCryptoProxyFactory {
// Mapping from public key hash to proxy address
mapping(bytes32 => address) public proxies;
// Event emitted when a new proxy is deployed
event ProxyDeployed(
bytes32 indexed publicKeyHash,
address indexed proxy,
bytes32 publicKeyX,
bytes32 publicKeyY
);
/**
* @notice Deploy a new proxy contract for a Web Crypto public key
* @param publicKeyX X coordinate of P-256 public key
* @param publicKeyY Y coordinate of P-256 public key
* @return proxy The address of the deployed proxy contract
*/
function deployProxy(
bytes32 publicKeyX,
bytes32 publicKeyY
) external returns (address proxy) {
// Create hash of public key for mapping
bytes32 publicKeyHash = keccak256(abi.encodePacked(publicKeyX, publicKeyY));
// Check if proxy already exists
require(proxies[publicKeyHash] == address(0), "WebCryptoProxyFactory: Proxy already exists");
// Deploy new proxy
proxy = address(new WebCryptoProxy(publicKeyX, publicKeyY));
// Store mapping
proxies[publicKeyHash] = proxy;
emit ProxyDeployed(publicKeyHash, proxy, publicKeyX, publicKeyY);
}
/**
* @notice Get proxy address for a public key
* @param publicKeyX X coordinate of P-256 public key
* @param publicKeyY Y coordinate of P-256 public key
* @return The proxy address, or address(0) if not deployed
*/
function getProxy(
bytes32 publicKeyX,
bytes32 publicKeyY
) external view returns (address) {
bytes32 publicKeyHash = keccak256(abi.encodePacked(publicKeyX, publicKeyY));
return proxies[publicKeyHash];
}
}

View File

@ -0,0 +1,176 @@
# Blockchain Integration Implementation Summary
## Completed Components
### 1. Core Blockchain Signing Module ✅
- **File**: `src/lib/auth/cryptoBlockchain.ts`
- EIP-712 structured data signing
- Transaction authorization message creation
- Signature formatting utilities
- Key pair retrieval for signing
### 2. Ethereum Integration ✅
- **File**: `src/lib/blockchain/ethereum.ts`
- Wallet connection (MetaMask, etc.)
- Transaction building and submission
- Chain management (mainnet, sepolia, goerli)
- Proxy contract interaction utilities
### 3. Account Linking Service ✅
- **File**: `src/lib/auth/blockchainLinking.ts`
- Links Web Crypto accounts with Ethereum addresses
- Stores linked account mappings in localStorage
- Account verification utilities
### 4. Wallet Integration ✅
- **File**: `src/lib/blockchain/walletIntegration.ts`
- Proxy contract deployment preparation
- Public key coordinate extraction (P-256)
- Factory contract interaction
### 5. Key Storage ✅
- **File**: `src/lib/auth/keyStorage.ts`
- In-memory key pair storage (session-based)
- Placeholder for persistent storage implementation
- Key pair retrieval utilities
### 6. Smart Contracts ✅
- **File**: `contracts/WebCryptoProxy.sol`
- Minimal proxy contract for each user
- Stores P-256 public key
- Transaction execution with signature verification
- Replay protection via nonces
- Deadline enforcement
- **File**: `contracts/WebCryptoProxyFactory.sol`
- Factory for deploying proxy contracts
- Deterministic proxy addresses
### 7. React Context ✅
- **File**: `src/context/BlockchainContext.tsx`
- Blockchain state management
- Wallet connection state
- Linked account management
- Event listeners for wallet changes
### 8. UI Components ✅
- **File**: `src/components/auth/BlockchainLink.tsx`
- Link Web Crypto account with Ethereum wallet
- Connection status display
- **File**: `src/components/blockchain/WalletStatus.tsx`
- Display wallet connection status
- Show linked account information
- **File**: `src/components/blockchain/TransactionBuilder.tsx`
- Build and authorize transactions
- Web Crypto API signing integration
### 9. Documentation ✅
- **File**: `docs/BLOCKCHAIN_INTEGRATION.md`
- Complete integration guide
- Architecture overview
- Usage instructions
- Security considerations
## Dependencies Added
- `viem` - Ethereum interaction library
- `@noble/curves` - Cryptographic utilities (installed but not yet used for P-256 verification)
## Integration Points
### Updated Files
- `src/lib/auth/cryptoAuthService.ts` - Now stores key pairs during registration
- `src/lib/auth/crypto.ts` - Extended with blockchain utilities
## Known Limitations & TODOs
### Critical TODOs
1. **P-256 Signature Verification in Smart Contract**
- The `verifyP256Signature` function in `WebCryptoProxy.sol` is a placeholder
- Needs implementation using a P-256 verification library or precompile
- Options:
- Use `@noble/curves` compiled to Solidity
- Import a P-256 verification library
- Use a precompile (if available on target chain)
2. **Persistent Key Storage**
- Currently uses in-memory storage (lost on page refresh)
- Need to implement Web Crypto API's persistent key storage with IndexedDB
- See `src/lib/auth/keyStorage.ts` for TODO
3. **Nonce Management**
- Currently uses timestamp-based nonces
- Should query nonce from contract state
- Add nonce tracking utilities
4. **Proxy Contract Deployment**
- Factory address needs to be configured per chain
- Add deployment utilities
- Add proxy address lookup
### Nice-to-Have Features
- [ ] Support for multiple chains
- [ ] Transaction history tracking
- [ ] Gas estimation and optimization
- [ ] Error handling and retry logic
- [ ] Gnosis Safe multi-sig wallet support
- [ ] Transaction status monitoring
- [ ] Batch transaction support
## Usage Example
```typescript
// 1. Connect wallet
const { connect, wallet } = useBlockchain();
await connect();
// 2. Link accounts
const { linkWebCryptoAccount } = useBlockchain();
await linkWebCryptoAccount(username);
// 3. Build and sign transaction
const authorization = {
to: '0x...',
value: '0',
data: '0x...',
nonce: Date.now(),
deadline: Math.floor(Date.now() / 1000) + 3600,
};
const signed = await signTransactionAuthorization(
privateKey,
authorization,
domain
);
```
## Testing Checklist
- [ ] Wallet connection (MetaMask)
- [ ] Account linking flow
- [ ] Transaction signing with Web Crypto API
- [ ] Proxy contract deployment (once factory is deployed)
- [ ] Transaction execution through proxy
- [ ] Replay protection (nonce checking)
- [ ] Deadline enforcement
- [ ] Error handling
## Next Steps
1. Deploy `WebCryptoProxyFactory.sol` to a testnet
2. Update factory address in `walletIntegration.ts`
3. Implement P-256 signature verification in smart contract
4. Test end-to-end transaction flow
5. Implement persistent key storage
6. Add comprehensive error handling
7. Add transaction monitoring
## Security Notes
- Private keys are stored in memory (session-based) - implement persistent storage for production
- P-256 signature verification must be properly implemented before production use
- Nonce management should use contract state, not timestamps
- Add rate limiting and gas limits to prevent abuse
- Implement proper error handling to prevent information leakage

View File

@ -0,0 +1,316 @@
# Web Crypto API to Blockchain Integration
This document describes the implementation of linking Web Crypto API (ECDSA P-256) accounts with Ethereum-compatible wallets (MetaMask, Gnosis Safe) to enable blockchain transaction execution.
## Overview
The integration uses a **minimal proxy contract pattern** where:
1. Each user deploys a lightweight proxy contract that stores their Web Crypto P-256 public key
2. Users sign transaction authorization messages with their Web Crypto API private key (P-256)
3. The proxy contract verifies the P-256 signature and executes the transaction
4. Transactions can be submitted either:
- **Through wallet** (MetaMask/Gnosis Safe) - User pays gas fees
- **Through relayer** (Gasless) - Relayer service pays gas fees on behalf of users
## Architecture
### Core Components
1. **Web Crypto API Signing** (`src/lib/auth/cryptoBlockchain.ts`)
- EIP-712 structured data signing
- Transaction authorization message creation
- Signature formatting for blockchain
2. **Ethereum Integration** (`src/lib/blockchain/ethereum.ts`)
- Wallet connection (MetaMask, etc.)
- Transaction building and submission
- Chain management
3. **Account Linking** (`src/lib/auth/blockchainLinking.ts`)
- Links Web Crypto accounts with Ethereum addresses
- Stores linked account mappings
- Verifies ownership of both keys
4. **Wallet Integration** (`src/lib/blockchain/walletIntegration.ts`)
- Proxy contract deployment
- Public key coordinate extraction
- Factory contract interaction
5. **Relayer Service** (`src/lib/blockchain/relayer.ts` & `worker/blockchainRelayer.ts`)
- Gasless transaction submission
- Relayer configuration management
- Cloudflare Worker relayer implementation
- Signature verification before submission
6. **Smart Contracts** (`contracts/`)
- `WebCryptoProxy.sol` - Minimal proxy contract for each user
- `WebCryptoProxyFactory.sol` - Factory for deploying proxies
7. **UI Components**
- `BlockchainLink.tsx` - Link Web Crypto account with wallet
- `WalletStatus.tsx` - Display connection status
- `TransactionBuilder.tsx` - Build and authorize transactions (with gasless option)
8. **Context** (`src/context/BlockchainContext.tsx`)
- React context for blockchain state management
- Wallet connection state
- Linked account management
## Technical Details
### Challenge: P-256 vs secp256k1
- **Web Crypto API**: Uses ECDSA P-256 (NIST curve)
- **Ethereum**: Uses secp256k1 (different curve)
- **Solution**: Proxy contract verifies P-256 signatures on-chain
### Transaction Flow
#### Standard Flow (User Pays Gas)
1. User initiates transaction in the application
2. Application builds EIP-712 structured authorization message
3. Web Crypto API signs message with P-256 private key
4. Application constructs proxy contract call with signature
5. MetaMask/Gnosis Safe prompts user to submit transaction
6. Wallet submits transaction to proxy contract
7. Proxy contract verifies P-256 signature
8. If valid, proxy contract executes the transaction
9. User pays gas fees
#### Gasless Flow (Relayer Pays Gas)
1. User initiates transaction in the application
2. Application builds EIP-712 structured authorization message
3. Web Crypto API signs message with P-256 private key
4. User selects "Use gasless transaction" option
5. Application submits signed transaction to relayer service
6. Relayer verifies signature and transaction validity
7. Relayer submits transaction to proxy contract (pays gas)
8. Proxy contract verifies P-256 signature
9. If valid, proxy contract executes the transaction
10. User pays no gas fees
### EIP-712 Message Format
```typescript
{
types: {
EIP712Domain: [...],
Transaction: [
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'data', type: 'bytes' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
},
domain: {
name: 'WebCryptoProxy',
version: '1',
chainId: number,
verifyingContract: string
},
message: {
to: string,
value: string,
data: string,
nonce: number,
deadline: number
}
}
```
### Proxy Contract Structure
The minimal proxy contract (`WebCryptoProxy.sol`):
- Stores P-256 public key (X and Y coordinates)
- Verifies P-256 signatures
- Executes transactions when signature is valid
- Implements replay protection via nonces
- Enforces transaction deadlines
**Note**: The P-256 signature verification in the contract is currently a placeholder. You'll need to implement or import a P-256 verification library. Options include:
1. Use a precompile (if available on your chain)
2. Import a P-256 verification library (e.g., from OpenZeppelin)
3. Use an external verification contract
## Setup
### Dependencies
```bash
npm install viem @noble/curves
```
### Smart Contract Deployment
1. Deploy `WebCryptoProxyFactory.sol` to your target chain
2. Update factory address in `walletIntegration.ts`
3. Users deploy their proxy contracts via the factory
### Integration
1. Wrap your app with `BlockchainProvider`:
```tsx
import { BlockchainProvider } from './context/BlockchainContext';
function App() {
return (
<BlockchainProvider>
{/* Your app */}
</BlockchainProvider>
);
}
```
2. Use the blockchain context:
```tsx
import { useBlockchain } from './context/BlockchainContext';
function MyComponent() {
const { wallet, connect, linkWebCryptoAccount } = useBlockchain();
// ...
}
```
## Usage
### Linking Accounts
1. User logs in with Web Crypto API
2. User connects their Ethereum wallet (MetaMask, etc.)
3. User links accounts via `BlockchainLink` component
4. System stores the mapping between Web Crypto and Ethereum accounts
### Building Transactions
#### Standard Transaction (User Pays Gas)
1. User fills out transaction details in `TransactionBuilder`
2. Application creates EIP-712 authorization message
3. Web Crypto API signs the message
4. Application builds proxy contract call
5. Wallet prompts user to submit transaction
6. Transaction executes through proxy contract
7. User pays gas fees
#### Gasless Transaction (Relayer Pays Gas)
1. User fills out transaction details in `TransactionBuilder`
2. User checks "Use gasless transaction" checkbox
3. Application creates EIP-712 authorization message
4. Web Crypto API signs the message
5. Application submits to relayer service
6. Relayer verifies and submits transaction
7. Transaction executes through proxy contract
8. Relayer pays gas fees (user pays nothing)
**Note**: Gasless transactions require a relayer service to be deployed and configured. See [Gasless Transactions Setup](./GASLESS_TRANSACTIONS.md) for details.
## Gasless Transactions
The system supports gasless transactions through a relayer service. This allows users to execute blockchain transactions without paying gas fees.
### How It Works
1. **User signs** transaction with Web Crypto API
2. **Relayer receives** signed transaction request
3. **Relayer verifies** signature and transaction validity
4. **Relayer submits** transaction to blockchain (pays gas)
5. **Transaction executes** through proxy contract
### Setup
See [Gasless Transactions Setup](./GASLESS_TRANSACTIONS.md) for complete setup instructions, including:
- Deploying the relayer worker
- Configuring relayer URLs
- Funding the relayer wallet
- Security considerations
### Usage
Users can enable gasless transactions by checking the "Use gasless transaction" checkbox in the `TransactionBuilder` component. The system automatically falls back to wallet-based transactions if the relayer is unavailable.
## Security Considerations
1. **Private Key Storage**: Web Crypto private keys should use secure storage (IndexedDB, Web Crypto API key storage)
2. **Signature Verification**: P-256 verification must be properly implemented in the smart contract
3. **Replay Protection**: Nonces prevent transaction replay attacks
4. **Deadline Enforcement**: Transactions expire after deadline
5. **Gas Limits**: Set reasonable gas limits for executed transactions
6. **Relayer Security**: Relayer private key must be stored securely (Cloudflare Workers secrets)
7. **Relayer Rate Limiting**: Implement rate limiting to prevent abuse of gasless transactions
8. **Relayer Monitoring**: Monitor relayer wallet balance and transaction costs
## Limitations & TODO
### Current Limitations
1. **P-256 Verification**: The smart contract's P-256 signature verification is not yet implemented
2. **Private Key Storage**: Currently uses simplified storage - needs secure key management
3. **Nonce Management**: Nonces are currently timestamp-based - should use contract state
4. **Proxy Deployment**: Factory address needs to be configured per chain
### TODO
- [ ] Implement P-256 signature verification in smart contract
- [ ] Add secure private key storage using Web Crypto API key storage
- [ ] Implement proper nonce management from contract state
- [ ] Add support for multiple chains
- [ ] Add transaction history tracking
- [ ] Add error handling and retry logic
- [ ] Add support for Gnosis Safe multi-sig wallets
- [ ] Add gas estimation and optimization
- [ ] Add transaction status monitoring
- [x] Implement gasless transactions via relayer service
- [ ] Add relayer rate limiting and abuse prevention
- [ ] Implement paymaster contract integration (alternative to relayer)
- [ ] Add relayer health monitoring and automatic failover
## File Structure
```
src/
├── lib/
│ ├── auth/
│ │ ├── cryptoBlockchain.ts # Blockchain signing utilities
│ │ └── blockchainLinking.ts # Account linking service
│ └── blockchain/
│ ├── ethereum.ts # Ethereum integration
│ ├── walletIntegration.ts # Wallet integration
│ ├── relayer.ts # Gasless transaction relayer client
│ └── index.ts # Exports
├── components/
│ ├── auth/
│ │ └── BlockchainLink.tsx # Link account UI
│ └── blockchain/
│ ├── WalletStatus.tsx # Status display
│ └── TransactionBuilder.tsx # Transaction builder (with gasless option)
├── context/
│ └── BlockchainContext.tsx # Blockchain state context
worker/
└── blockchainRelayer.ts # Cloudflare Worker relayer service
contracts/
├── WebCryptoProxy.sol # Minimal proxy contract
└── WebCryptoProxyFactory.sol # Factory contract
```
## Related Documentation
- [Gasless Transactions Setup](./GASLESS_TRANSACTIONS.md) - Complete guide for setting up and using gasless transactions
- [Blockchain Implementation Summary](./BLOCKCHAIN_IMPLEMENTATION_SUMMARY.md) - Implementation details and status
## References
- [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
- [EIP-712: Typed Structured Data Hashing and Signing](https://eips.ethereum.org/EIPS/eip-712)
- [Viem Documentation](https://viem.sh/)
- [Ethereum Cryptography](https://github.com/ethereum/js-ethereum-cryptography)
- [ERC-4337: Account Abstraction](https://eips.ethereum.org/EIPS/eip-4337) - Alternative approach for gasless transactions

View File

@ -0,0 +1,210 @@
# Gasless Transactions Setup
This document explains how to set up and use gasless transactions with the Web Crypto API blockchain integration.
## Overview
Gasless transactions allow users to execute blockchain transactions without paying gas fees. Instead, a relayer service pays for the gas on behalf of users.
## Architecture
1. **User signs transaction** with Web Crypto API (P-256)
2. **Relayer service** receives the signed transaction request
3. **Relayer verifies** the signature matches the user's Web Crypto public key
4. **Relayer submits** the transaction to the blockchain and pays gas
5. **Transaction executes** through the proxy contract
## Components
### 1. Relayer Service (`worker/blockchainRelayer.ts`)
A Cloudflare Worker that:
- Accepts signed transaction requests via HTTP POST
- Verifies Web Crypto API signatures (P-256)
- Submits transactions to the blockchain
- Pays for gas fees
### 2. Client Relayer Library (`src/lib/blockchain/relayer.ts`)
Client-side utilities for:
- Submitting transactions to the relayer
- Checking relayer availability
- Managing relayer configuration
### 3. Transaction Builder (`src/components/blockchain/TransactionBuilder.tsx`)
UI component with option to use gasless transactions via checkbox.
## Setup
### Step 1: Deploy Relayer Worker
1. Add the relayer worker to your `wrangler.toml`:
```toml
[[workers]]
name = "blockchain-relayer"
route = "/relayer/*"
```
2. Set up the relayer private key as a Cloudflare Workers secret:
```bash
wrangler secret put RELAYER_PRIVATE_KEY
# Enter your relayer wallet's private key (0x...)
```
**⚠️ Security Warning**: The relayer private key must be kept secure. It will be used to pay for all gas fees.
### Step 2: Fund Relayer Wallet
The relayer wallet needs ETH to pay for gas:
- For mainnet: Send ETH to the relayer address
- For testnets: Get testnet ETH from faucets
### Step 3: Configure Relayer URL
In your application, configure the relayer URL for each chain:
```typescript
import { setRelayerConfig } from './lib/blockchain/relayer';
// Set relayer URL for a chain
setRelayerConfig(1, 'https://your-worker.workers.dev/relayer'); // Mainnet
setRelayerConfig(11155111, 'https://your-worker.workers.dev/relayer'); // Sepolia
```
Or set it in `relayer.ts`:
```typescript
const relayerUrls: Record<number, string> = {
1: 'https://your-worker.workers.dev/relayer',
11155111: 'https://your-worker.workers.dev/relayer',
};
```
### Step 4: Update Worker Routes
Add the relayer route to your main worker or deploy it as a separate worker:
```typescript
// In your main worker.ts
if (url.pathname.startsWith('/relayer')) {
return await blockchainRelayer.fetch(request, env);
}
```
## Usage
### For Users
1. Build a transaction in the Transaction Builder component
2. Check the "Use gasless transaction" checkbox
3. Sign with Web Crypto API
4. Transaction is submitted to relayer (no wallet confirmation needed)
5. Relayer pays for gas and submits transaction
### For Developers
```typescript
import { submitToRelayer, getRelayerConfig } from './lib/blockchain/relayer';
// Check if relayer is available
const relayerConfig = getRelayerConfig(chainId);
if (relayerConfig) {
// Submit to relayer
const result = await submitToRelayer(relayerConfig, {
authorization: signedAuthorization,
signature: signature,
proxyContractAddress: proxyAddress,
chainId: chainId,
});
if (result.success) {
console.log('Transaction hash:', result.transactionHash);
}
}
```
## Security Considerations
### 1. Relayer Private Key Security
- Store private key as Cloudflare Workers secret (never in code)
- Use a dedicated wallet with limited funds
- Monitor relayer wallet balance
- Set up alerts for low balance
### 2. Signature Verification
- The relayer must verify Web Crypto P-256 signatures
- Currently, this verification happens in the proxy contract
- Consider adding additional verification in the relayer for early rejection
### 3. Rate Limiting
- Implement rate limiting to prevent abuse
- Limit transactions per user/IP
- Set maximum gas limits per transaction
### 4. Nonce Management
- Ensure nonces are properly managed to prevent replay attacks
- The proxy contract handles nonce checking, but relayer should also validate
### 5. Deadline Enforcement
- Relayer should check transaction deadlines before submission
- Reject expired transactions
## Cost Management
### Monitoring
- Track gas costs per transaction
- Monitor relayer wallet balance
- Set up automatic refilling if balance is low
### Limits
- Set maximum gas price limits
- Set maximum transaction value limits
- Implement daily/monthly spending limits
### Funding
- Consider using a paymaster contract (e.g., Pimlico, Alchemy) for more sophisticated gas management
- Implement user deposits if needed
- Consider subscription models for gasless transactions
## Alternative: Paymaster Contracts
Instead of a relayer service, you could use paymaster contracts:
1. **ERC-4337 Account Abstraction**: Use a paymaster contract that sponsors transactions
2. **Pimlico**: Third-party paymaster service
3. **Alchemy Gas Manager**: Gas sponsorship service
These services handle gas payment on-chain rather than through a relayer.
## Troubleshooting
### Relayer Not Available
- Check relayer URL configuration
- Verify relayer worker is deployed
- Check relayer health endpoint: `GET /relayer/health`
### Transaction Fails
- Check relayer wallet has sufficient balance
- Verify signature is valid
- Check transaction deadline hasn't expired
- Verify proxy contract is deployed
### High Gas Costs
- Monitor gas prices
- Consider using Layer 2 networks (lower gas)
- Implement gas price limits
- Use EIP-1559 for better gas estimation
## Future Enhancements
- [ ] Implement paymaster contract integration
- [ ] Add user deposit system for gasless transactions
- [ ] Implement rate limiting and abuse prevention
- [ ] Add gas price monitoring and limits
- [ ] Support for multiple relayer endpoints (load balancing)
- [ ] Transaction batching for efficiency
- [ ] Support for Layer 2 networks (Arbitrum, Optimism, etc.)

224
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@chengsokdara/use-whisper": "^0.2.0", "@chengsokdara/use-whisper": "^0.2.0",
"@daily-co/daily-js": "^0.60.0", "@daily-co/daily-js": "^0.60.0",
"@daily-co/daily-react": "^0.20.0", "@daily-co/daily-react": "^0.20.0",
"@noble/curves": "^2.0.1",
"@oddjs/odd": "^0.37.2", "@oddjs/odd": "^0.37.2",
"@tldraw/assets": "^3.15.4", "@tldraw/assets": "^3.15.4",
"@tldraw/tldraw": "^3.15.4", "@tldraw/tldraw": "^3.15.4",
@ -52,6 +53,7 @@
"tldraw": "^3.15.4", "tldraw": "^3.15.4",
"use-whisper": "^0.0.1", "use-whisper": "^0.0.1",
"vercel": "^39.1.1", "vercel": "^39.1.1",
"viem": "^2.38.6",
"webcola": "^3.4.0", "webcola": "^3.4.0",
"webnative": "^0.36.3" "webnative": "^0.36.3"
}, },
@ -74,6 +76,12 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@adraffy/ens-normalize": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"license": "MIT"
},
"node_modules/@ai-sdk/provider": { "node_modules/@ai-sdk/provider": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
@ -2530,6 +2538,45 @@
"npm": ">=7.0.0" "npm": ">=7.0.0"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
"integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "2.0.1"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": { "node_modules/@noble/hashes": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@ -4621,6 +4668,57 @@
"win32" "win32"
] ]
}, },
"node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.9.0",
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "7.120.4", "version": "7.120.4",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.120.4.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.120.4.tgz",
@ -6667,6 +6765,27 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/abitype": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz",
"integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/wevm"
},
"peerDependencies": {
"typescript": ">=5.0.4",
"zod": "^3.22.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/abort-controller": { "node_modules/abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -11247,6 +11366,21 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
"integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/it-all": { "node_modules/it-all": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz", "resolved": "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz",
@ -13504,6 +13638,51 @@
"node": ">= 6.0" "node": ">= 6.0"
} }
}, },
"node_modules/ox": {
"version": "0.9.6",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz",
"integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "^1.11.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "1.9.1",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"@scure/bip39": "^1.6.0",
"abitype": "^1.0.9",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ox/node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/p-defer": { "node_modules/p-defer": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz",
@ -16610,6 +16789,51 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/viem": {
"version": "2.38.6",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.38.6.tgz",
"integrity": "sha512-aqO6P52LPXRjdnP6rl5Buab65sYa4cZ6Cpn+k4OLOzVJhGIK8onTVoKMFMT04YjDfyDICa/DZyV9HmvLDgcjkw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
"@scure/bip32": "1.7.0",
"@scure/bip39": "1.6.0",
"abitype": "1.1.0",
"isows": "1.0.7",
"ox": "0.9.6",
"ws": "8.18.3"
},
"peerDependencies": {
"typescript": ">=5.0.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/viem/node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -28,6 +28,7 @@
"@chengsokdara/use-whisper": "^0.2.0", "@chengsokdara/use-whisper": "^0.2.0",
"@daily-co/daily-js": "^0.60.0", "@daily-co/daily-js": "^0.60.0",
"@daily-co/daily-react": "^0.20.0", "@daily-co/daily-react": "^0.20.0",
"@noble/curves": "^2.0.1",
"@oddjs/odd": "^0.37.2", "@oddjs/odd": "^0.37.2",
"@tldraw/assets": "^3.15.4", "@tldraw/assets": "^3.15.4",
"@tldraw/tldraw": "^3.15.4", "@tldraw/tldraw": "^3.15.4",
@ -64,6 +65,7 @@
"tldraw": "^3.15.4", "tldraw": "^3.15.4",
"use-whisper": "^0.0.1", "use-whisper": "^0.0.1",
"vercel": "^39.1.1", "vercel": "^39.1.1",
"viem": "^2.38.6",
"webcola": "^3.4.0", "webcola": "^3.4.0",
"webnative": "^0.36.3" "webnative": "^0.36.3"
}, },

View File

@ -28,6 +28,7 @@ import { useState, useEffect } from 'react';
import { AuthProvider, useAuth } from './context/AuthContext'; import { AuthProvider, useAuth } from './context/AuthContext';
import { FileSystemProvider } from './context/FileSystemContext'; import { FileSystemProvider } from './context/FileSystemContext';
import { NotificationProvider } from './context/NotificationContext'; import { NotificationProvider } from './context/NotificationContext';
import { BlockchainProvider } from './context/BlockchainContext';
import NotificationsDisplay from './components/NotificationsDisplay'; import NotificationsDisplay from './components/NotificationsDisplay';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
@ -102,6 +103,7 @@ const AppWithProviders = () => {
<AuthProvider> <AuthProvider>
<FileSystemProvider> <FileSystemProvider>
<NotificationProvider> <NotificationProvider>
<BlockchainProvider>
<DailyProvider callObject={callObject}> <DailyProvider callObject={callObject}>
<BrowserRouter> <BrowserRouter>
{/* Display notifications */} {/* Display notifications */}
@ -171,6 +173,7 @@ const AppWithProviders = () => {
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</DailyProvider> </DailyProvider>
</BlockchainProvider>
</NotificationProvider> </NotificationProvider>
</FileSystemProvider> </FileSystemProvider>
</AuthProvider> </AuthProvider>

View File

@ -0,0 +1,104 @@
// Component for linking Web Crypto account with Ethereum wallet
import React, { useState } from 'react';
import { useBlockchain } from '../../context/BlockchainContext';
import { useAuth } from '../../context/AuthContext';
import { useNotifications } from '../../context/NotificationContext';
export const BlockchainLink: React.FC = () => {
const { wallet, linkedAccount, isConnecting, connect, linkWebCryptoAccount } = useBlockchain();
const { session } = useAuth();
const { addNotification } = useNotifications();
const [isLinking, setIsLinking] = useState(false);
const handleConnect = async () => {
try {
await connect();
addNotification('Wallet connected successfully', 'success');
} catch (error: any) {
addNotification(error.message || 'Failed to connect wallet', 'error');
}
};
const handleLink = async () => {
if (!session.username) {
addNotification('Please log in first', 'error');
return;
}
if (!wallet) {
addNotification('Please connect your wallet first', 'error');
return;
}
setIsLinking(true);
try {
const result = await linkWebCryptoAccount(session.username);
if (result.success) {
addNotification('Account linked successfully!', 'success');
} else {
addNotification(result.error || 'Failed to link account', 'error');
}
} catch (error: any) {
addNotification(error.message || 'Failed to link account', 'error');
} finally {
setIsLinking(false);
}
};
if (!session.authed) {
return (
<div className="blockchain-link">
<p>Please log in to link your Web Crypto account with a blockchain wallet.</p>
</div>
);
}
return (
<div className="blockchain-link">
<h3>Link Blockchain Wallet</h3>
{!wallet ? (
<div>
<p>Connect your Ethereum wallet (MetaMask, etc.) to link it with your Web Crypto account.</p>
<button
onClick={handleConnect}
disabled={isConnecting}
className="btn btn-primary"
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
</div>
) : (
<div>
<div className="wallet-info">
<p><strong>Connected Wallet:</strong> {wallet.address}</p>
<p><strong>Chain ID:</strong> {wallet.chainId}</p>
</div>
{linkedAccount ? (
<div className="linked-account">
<p className="success"> Account linked successfully!</p>
<p><strong>Ethereum Address:</strong> {linkedAccount.ethereumAddress}</p>
{linkedAccount.proxyContractAddress && (
<p><strong>Proxy Contract:</strong> {linkedAccount.proxyContractAddress}</p>
)}
</div>
) : (
<div>
<p>Link your Web Crypto account ({session.username}) with this wallet.</p>
<button
onClick={handleLink}
disabled={isLinking}
className="btn btn-primary"
>
{isLinking ? 'Linking...' : 'Link Account'}
</button>
</div>
)}
</div>
)}
</div>
);
};

View File

@ -1,5 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useBlockchain } from '../../context/BlockchainContext';
import { useNotifications } from '../../context/NotificationContext';
import { unlinkAccount } from '../../lib/auth/blockchainLinking';
interface ProfileProps { interface ProfileProps {
onLogout?: () => void; onLogout?: () => void;
@ -8,8 +11,11 @@ interface ProfileProps {
export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }) => { export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }) => {
const { session, updateSession, clearSession } = useAuth(); const { session, updateSession, clearSession } = useAuth();
const { wallet, linkedAccount, isConnecting, connect, linkWebCryptoAccount, disconnect } = useBlockchain();
const { addNotification } = useNotifications();
const [vaultPath, setVaultPath] = useState(session.obsidianVaultPath || ''); const [vaultPath, setVaultPath] = useState(session.obsidianVaultPath || '');
const [isEditingVault, setIsEditingVault] = useState(false); const [isEditingVault, setIsEditingVault] = useState(false);
const [isLinking, setIsLinking] = useState(false);
const handleVaultPathChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleVaultPathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVaultPath(e.target.value); setVaultPath(e.target.value);
@ -41,6 +47,45 @@ export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }
} }
}; };
const handleConnectWallet = async () => {
try {
await connect();
addNotification('Wallet connected successfully', 'success');
} catch (error: any) {
addNotification(error.message || 'Failed to connect wallet', 'error');
}
};
const handleLinkAccount = async () => {
if (!wallet) {
addNotification('Please connect your wallet first', 'error');
return;
}
setIsLinking(true);
try {
const result = await linkWebCryptoAccount(session.username);
if (result.success) {
addNotification('Account linked successfully!', 'success');
} else {
addNotification(result.error || 'Failed to link account', 'error');
}
} catch (error: any) {
addNotification(error.message || 'Failed to link account', 'error');
} finally {
setIsLinking(false);
}
};
const handleUnlinkAccount = () => {
if (unlinkAccount(session.username)) {
disconnect();
addNotification('Account unlinked successfully', 'success');
} else {
addNotification('Failed to unlink account', 'error');
}
};
const handleLogout = () => { const handleLogout = () => {
// Clear the session // Clear the session
clearSession(); clearSession();
@ -148,6 +193,116 @@ export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }
</details> </details>
</div> </div>
{/* Blockchain Wallet Section */}
<div className="profile-settings">
<h4>Blockchain Wallet</h4>
{/* Current Wallet Display */}
<div className="current-vault-section">
{wallet ? (
<div className="vault-info">
<div className="vault-name">
<span className="vault-label">Connected Wallet:</span>
<span className="vault-name-text">{wallet.address.slice(0, 6)}...{wallet.address.slice(-4)}</span>
</div>
<div className="vault-path-info">
Chain ID: {wallet.chainId}
</div>
{linkedAccount && (
<div className="vault-path-info" style={{ marginTop: '8px', color: '#10b981' }}>
Linked to Web Crypto account
</div>
)}
</div>
) : (
<div className="no-vault-info">
<span className="no-vault-text">No wallet connected</span>
</div>
)}
</div>
{/* Wallet Actions */}
<div className="vault-actions-section">
{!wallet ? (
<button
onClick={handleConnectWallet}
disabled={isConnecting}
className="change-vault-button"
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
) : (
<>
{!linkedAccount ? (
<button
onClick={handleLinkAccount}
disabled={isLinking}
className="change-vault-button"
>
{isLinking ? 'Linking...' : 'Link to Web Crypto Account'}
</button>
) : (
<button
onClick={handleUnlinkAccount}
className="disconnect-vault-button"
>
🔌 Unlink Wallet
</button>
)}
<button
onClick={disconnect}
className="disconnect-vault-button"
style={{ marginLeft: '8px' }}
>
Disconnect Wallet
</button>
</>
)}
</div>
{/* Linked Account Details */}
{linkedAccount && (
<details className="advanced-vault-settings" style={{ marginTop: '16px' }}>
<summary>Wallet Details</summary>
<div className="vault-settings">
<div className="vault-display">
<div className="vault-path-display">
<p><strong>Ethereum Address:</strong></p>
<code style={{
display: 'block',
padding: '8px',
background: '#f8f9fa',
borderRadius: '4px',
wordBreak: 'break-all',
fontSize: '12px'
}}>
{linkedAccount.ethereumAddress}
</code>
{linkedAccount.proxyContractAddress && (
<>
<p style={{ marginTop: '12px' }}><strong>Proxy Contract:</strong></p>
<code style={{
display: 'block',
padding: '8px',
background: '#f8f9fa',
borderRadius: '4px',
wordBreak: 'break-all',
fontSize: '12px'
}}>
{linkedAccount.proxyContractAddress}
</code>
</>
)}
<p style={{ marginTop: '12px', fontSize: '12px', color: '#6c757d' }}>
Linked on {new Date(linkedAccount.linkedAt).toLocaleDateString()}
</p>
</div>
</div>
</div>
</details>
)}
</div>
<div className="profile-actions"> <div className="profile-actions">
<button onClick={handleLogout} className="logout-button"> <button onClick={handleLogout} className="logout-button">
Sign Out Sign Out

View File

@ -0,0 +1,223 @@
// Component for building and authorizing blockchain transactions using Web Crypto API
import React, { useState } from 'react';
import { useBlockchain } from '../../context/BlockchainContext';
import { useAuth } from '../../context/AuthContext';
import { useNotifications } from '../../context/NotificationContext';
import { signTransactionAuthorization, createEIP712Domain, type TransactionAuthorization } from '../../lib/auth/cryptoBlockchain';
import { buildProxyExecuteTransaction, sendTransaction, type ProxyContractConfig } from '../../lib/blockchain/ethereum';
import { submitToRelayer, getRelayerConfig, type RelayerTransactionRequest } from '../../lib/blockchain/relayer';
import * as crypto from '../../lib/auth/crypto';
import { getUserPrivateKey } from '../../lib/auth/cryptoBlockchain';
export const TransactionBuilder: React.FC = () => {
const { wallet, linkedAccount } = useBlockchain();
const { session } = useAuth();
const { addNotification } = useNotifications();
const [to, setTo] = useState('');
const [value, setValue] = useState('');
const [data, setData] = useState('');
const [useGasless, setUseGasless] = useState(false);
const [isBuilding, setIsBuilding] = useState(false);
const [isAuthorizing, setIsAuthorizing] = useState(false);
const handleBuildTransaction = async () => {
if (!wallet || !linkedAccount) {
addNotification('Please connect and link your wallet first', 'error');
return;
}
if (!session.username) {
addNotification('Please log in first', 'error');
return;
}
if (!to || !to.match(/^0x[a-fA-F0-9]{40}$/)) {
addNotification('Invalid recipient address', 'error');
return;
}
setIsBuilding(true);
try {
// Get user's private key (in production, use secure key storage)
const privateKey = await getUserPrivateKey(session.username);
if (!privateKey) {
addNotification('Unable to access Web Crypto private key. Please re-authenticate.', 'error');
setIsBuilding(false);
return;
}
// Create transaction authorization
const authorization: TransactionAuthorization = {
to: to as `0x${string}`,
value: value ? BigInt(value).toString(16) : '0',
data: data || '0x',
nonce: Date.now(), // In production, use a proper nonce from the contract
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
};
// Create EIP-712 domain
const domain = createEIP712Domain(
wallet.chainId,
linkedAccount.proxyContractAddress || '0x0000000000000000000000000000000000000000'
);
// Sign with Web Crypto API
const signed = await signTransactionAuthorization(privateKey, authorization, domain);
if (!signed) {
addNotification('Failed to sign transaction', 'error');
setIsBuilding(false);
return;
}
// Build transaction for proxy contract
// Note: You'll need the proxy contract ABI
const proxyConfig: ProxyContractConfig = {
address: linkedAccount.proxyContractAddress as `0x${string}` || '0x0000000000000000000000000000000000000000',
chainId: wallet.chainId,
abi: [
{
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' },
],
},
],
};
// Check if gasless transactions are available
const relayerConfig = getRelayerConfig(wallet.chainId);
const shouldUseGasless = useGasless && relayerConfig;
if (shouldUseGasless && relayerConfig) {
// Submit to relayer for gasless transaction
setIsAuthorizing(true);
const relayerRequest: RelayerTransactionRequest = {
authorization,
signature: signed.signature,
proxyContractAddress: linkedAccount.proxyContractAddress as `0x${string}` || '0x0000000000000000000000000000000000000000',
chainId: wallet.chainId,
};
const result = await submitToRelayer(relayerConfig, relayerRequest);
if (result.success && result.transactionHash) {
addNotification(`Gasless transaction submitted: ${result.transactionHash}`, 'success');
} else {
addNotification(result.error || 'Failed to submit gasless transaction', 'error');
}
} else {
// Submit through wallet (user pays gas)
const transaction = buildProxyExecuteTransaction(
proxyConfig,
authorization.to as `0x${string}`,
BigInt(authorization.value),
authorization.data as `0x${string}`,
BigInt(authorization.nonce),
BigInt(authorization.deadline),
signed.signature
);
setIsAuthorizing(true);
const hash = await sendTransaction(wallet.chainId, transaction);
addNotification(`Transaction submitted: ${hash}`, 'success');
}
// Reset form
setTo('');
setValue('');
setData('');
} catch (error: any) {
console.error('Error building transaction:', error);
addNotification(error.message || 'Failed to build transaction', 'error');
} finally {
setIsBuilding(false);
setIsAuthorizing(false);
}
};
if (!wallet || !linkedAccount) {
return (
<div className="transaction-builder">
<p>Please connect and link your wallet to build transactions.</p>
</div>
);
}
return (
<div className="transaction-builder">
<h3>Build Transaction</h3>
<p>Create a transaction authorized by your Web Crypto account.</p>
<div className="form-group">
<label>
To Address:
<input
type="text"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="0x..."
className="form-control"
/>
</label>
</div>
<div className="form-group">
<label>
Value (ETH):
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="0.0"
className="form-control"
/>
</label>
</div>
<div className="form-group">
<label>
Data (hex, optional):
<input
type="text"
value={data}
onChange={(e) => setData(e.target.value)}
placeholder="0x..."
className="form-control"
/>
</label>
</div>
<div className="form-group">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={useGasless}
onChange={(e) => setUseGasless(e.target.checked)}
/>
<span>Use gasless transaction (if relayer available)</span>
</label>
{useGasless && !getRelayerConfig(wallet.chainId) && (
<p style={{ fontSize: '12px', color: '#dc3545', marginTop: '4px' }}>
Relayer not configured for this chain. Transaction will use wallet.
</p>
)}
</div>
<button
onClick={handleBuildTransaction}
disabled={isBuilding || isAuthorizing}
className="btn btn-primary"
>
{isBuilding ? 'Building...' : isAuthorizing ? 'Authorizing...' : 'Build & Sign Transaction'}
</button>
</div>
);
};

View File

@ -0,0 +1,44 @@
// Component to display wallet connection status and linked account info
import React from 'react';
import { useBlockchain } from '../../context/BlockchainContext';
import { useAuth } from '../../context/AuthContext';
export const WalletStatus: React.FC = () => {
const { wallet, linkedAccount } = useBlockchain();
const { session } = useAuth();
if (!session.authed) {
return null;
}
return (
<div className="wallet-status">
<h4>Blockchain Status</h4>
{wallet ? (
<div className="wallet-connected">
<p className="status-indicator connected"> Wallet Connected</p>
<div className="wallet-details">
<p><strong>Address:</strong> <code>{wallet.address}</code></p>
<p><strong>Chain ID:</strong> {wallet.chainId}</p>
</div>
</div>
) : (
<div className="wallet-disconnected">
<p className="status-indicator disconnected"> Wallet Not Connected</p>
<p>Connect a wallet to enable blockchain transactions.</p>
</div>
)}
{linkedAccount && (
<div className="linked-account-info">
<p className="status-indicator linked"> Account Linked</p>
<p><strong>Web Crypto:</strong> {session.username}</p>
<p><strong>Ethereum:</strong> {linkedAccount.ethereumAddress}</p>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,154 @@
// Blockchain context for managing wallet connections and blockchain state
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { connectWallet, getWalletConnection, type WalletConnection } from '../lib/blockchain/ethereum';
import { getLinkedAccount, linkAccount, type LinkedAccount } from '../lib/auth/blockchainLinking';
import { useAuth } from './AuthContext';
interface BlockchainContextType {
wallet: WalletConnection | null;
linkedAccount: LinkedAccount | null;
isConnecting: boolean;
connect: () => Promise<void>;
disconnect: () => void;
linkWebCryptoAccount: (username: string) => Promise<{ success: boolean; error?: string }>;
refreshConnection: () => Promise<void>;
}
const BlockchainContext = createContext<BlockchainContextType | undefined>(undefined);
export function BlockchainProvider({ children }: { children: React.ReactNode }) {
const [wallet, setWallet] = useState<WalletConnection | null>(null);
const [linkedAccount, setLinkedAccount] = useState<LinkedAccount | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const { session } = useAuth();
// Check for existing wallet connection on mount
useEffect(() => {
refreshConnection();
}, []);
// Update linked account when wallet or session changes
useEffect(() => {
if (wallet && session.username) {
const linked = getLinkedAccount(session.username);
setLinkedAccount(linked || null);
} else {
setLinkedAccount(null);
}
}, [wallet, session.username]);
const refreshConnection = useCallback(async () => {
try {
const connection = await getWalletConnection();
setWallet(connection);
} catch (error) {
console.error('Error refreshing wallet connection:', error);
setWallet(null);
}
}, []);
const connect = useCallback(async () => {
setIsConnecting(true);
try {
const connection = await connectWallet();
setWallet(connection);
// Check if account is linked
if (session.username) {
const linked = getLinkedAccount(session.username);
setLinkedAccount(linked || null);
}
} catch (error) {
console.error('Error connecting wallet:', error);
throw error;
} finally {
setIsConnecting(false);
}
}, [session.username]);
const disconnect = useCallback(() => {
setWallet(null);
setLinkedAccount(null);
}, []);
const linkWebCryptoAccount = useCallback(async (username: string): Promise<{ success: boolean; error?: string }> => {
if (!wallet) {
return { success: false, error: 'Wallet not connected' };
}
try {
const result = await linkAccount(username, wallet.address);
if (result.success && result.linkedAccount) {
setLinkedAccount(result.linkedAccount);
}
return result;
} catch (error) {
console.error('Error linking account:', error);
return { success: false, error: String(error) };
}
}, [wallet]);
// Listen for wallet account changes
useEffect(() => {
if (typeof window !== 'undefined' && window.ethereum) {
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
setWallet(null);
setLinkedAccount(null);
} else {
refreshConnection();
}
};
const handleChainChanged = () => {
refreshConnection();
};
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
return () => {
window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum?.removeListener('chainChanged', handleChainChanged);
};
}
}, [refreshConnection]);
return (
<BlockchainContext.Provider
value={{
wallet,
linkedAccount,
isConnecting,
connect,
disconnect,
linkWebCryptoAccount,
refreshConnection,
}}
>
{children}
</BlockchainContext.Provider>
);
}
export function useBlockchain() {
const context = useContext(BlockchainContext);
if (context === undefined) {
throw new Error('useBlockchain must be used within a BlockchainProvider');
}
return context;
}
// Extend Window interface for TypeScript
declare global {
interface Window {
ethereum?: {
request: (args: { method: string; params?: any[] }) => Promise<any>;
on: (event: string, handler: (...args: any[]) => void) => void;
removeListener: (event: string, handler: (...args: any[]) => void) => void;
isMetaMask?: boolean;
};
}
}

View File

@ -0,0 +1,206 @@
// Blockchain account linking service
// Links Web Crypto API accounts with Ethereum addresses (EOA or smart contract wallets)
import * as crypto from './crypto';
import { connectWallet, getWalletConnection, type WalletConnection } from '../blockchain/ethereum';
import { signTransactionAuthorization, createEIP712Domain, type TransactionAuthorization } from './cryptoBlockchain';
import { isBrowser } from '../utils/browser';
export interface LinkedAccount {
username: string;
webCryptoPublicKey: string;
ethereumAddress: string;
proxyContractAddress?: string;
chainId: number;
linkedAt: number;
}
/**
* Link a Web Crypto account with an Ethereum wallet
* This verifies ownership of both keys by requiring signatures from both
*/
export async function linkAccount(
username: string,
ethereumAddress: string
): Promise<{ success: boolean; linkedAccount?: LinkedAccount; error?: string }> {
if (!isBrowser()) {
return { success: false, error: 'Browser environment required' };
}
try {
// Get Web Crypto public key
const webCryptoPublicKey = crypto.getPublicKey(username);
if (!webCryptoPublicKey) {
return { success: false, error: 'Web Crypto account not found' };
}
// Verify wallet connection
const wallet = await getWalletConnection();
if (!wallet || wallet.address.toLowerCase() !== ethereumAddress.toLowerCase()) {
return { success: false, error: 'Wallet address mismatch' };
}
// Create linking message that requires both signatures
const linkingMessage = `Link Web Crypto account ${username} to Ethereum address ${ethereumAddress} at ${Date.now()}`;
// Get user's private key (in production, use secure key storage)
// For now, we'll need to prompt for re-authentication or use stored key
// This is a simplified version - in production, you'd use proper key management
// Store the linked account
const linkedAccount: LinkedAccount = {
username,
webCryptoPublicKey,
ethereumAddress: wallet.address,
chainId: wallet.chainId,
linkedAt: Date.now(),
};
storeLinkedAccount(linkedAccount);
return {
success: true,
linkedAccount,
};
} catch (error) {
console.error('Error linking account:', error);
return {
success: false,
error: String(error),
};
}
}
/**
* Verify ownership of both Web Crypto and Ethereum accounts
*/
export async function verifyAccountOwnership(
username: string,
ethereumAddress: string
): Promise<boolean> {
if (!isBrowser()) return false;
try {
// Check if accounts are linked
const linkedAccount = getLinkedAccount(username);
if (!linkedAccount) return false;
if (linkedAccount.ethereumAddress.toLowerCase() !== ethereumAddress.toLowerCase()) {
return false;
}
// In a full implementation, you'd verify signatures from both keys
// For now, we'll just check if they're linked
return true;
} catch (error) {
console.error('Error verifying account ownership:', error);
return false;
}
}
/**
* Get linked account for a username
*/
export function getLinkedAccount(username: string): LinkedAccount | null {
if (!isBrowser()) return null;
try {
const stored = localStorage.getItem(`linkedAccount_${username}`);
if (!stored) return null;
return JSON.parse(stored) as LinkedAccount;
} catch (error) {
console.error('Error getting linked account:', error);
return null;
}
}
/**
* Get linked account by Ethereum address
*/
export function getLinkedAccountByAddress(ethereumAddress: string): LinkedAccount | null {
if (!isBrowser()) return null;
try {
// Search through all linked accounts
const keys = Object.keys(localStorage);
for (const key of keys) {
if (key.startsWith('linkedAccount_')) {
const account = JSON.parse(localStorage.getItem(key) || '{}') as LinkedAccount;
if (account.ethereumAddress?.toLowerCase() === ethereumAddress.toLowerCase()) {
return account;
}
}
}
return null;
} catch (error) {
console.error('Error getting linked account by address:', error);
return null;
}
}
/**
* Store linked account
*/
function storeLinkedAccount(account: LinkedAccount): void {
if (!isBrowser()) return;
try {
localStorage.setItem(`linkedAccount_${account.username}`, JSON.stringify(account));
} catch (error) {
console.error('Error storing linked account:', error);
}
}
/**
* Unlink an account
*/
export function unlinkAccount(username: string): boolean {
if (!isBrowser()) return false;
try {
localStorage.removeItem(`linkedAccount_${username}`);
return true;
} catch (error) {
console.error('Error unlinking account:', error);
return false;
}
}
/**
* Check if account is linked
*/
export function isAccountLinked(username: string): boolean {
return getLinkedAccount(username) !== null;
}
/**
* Get all linked accounts for current user
*/
export function getAllLinkedAccounts(): LinkedAccount[] {
if (!isBrowser()) return [];
try {
const accounts: LinkedAccount[] = [];
const keys = Object.keys(localStorage);
for (const key of keys) {
if (key.startsWith('linkedAccount_')) {
try {
const account = JSON.parse(localStorage.getItem(key) || '{}') as LinkedAccount;
if (account.username) {
accounts.push(account);
}
} catch (e) {
// Skip invalid entries
}
}
}
return accounts;
} catch (error) {
console.error('Error getting all linked accounts:', error);
return [];
}
}

View File

@ -1,5 +1,6 @@
import * as crypto from './crypto'; import * as crypto from './crypto';
import { isBrowser } from '../utils/browser'; import { isBrowser } from '../utils/browser';
import { storeKeyPairInMemory } from './keyStorage';
export interface CryptoAuthResult { export interface CryptoAuthResult {
success: boolean; success: boolean;
@ -88,6 +89,10 @@ export class CryptoAuthService {
crypto.addRegisteredUser(username); crypto.addRegisteredUser(username);
crypto.storePublicKey(username, publicKeyBase64); crypto.storePublicKey(username, publicKeyBase64);
// Store the key pair in memory for blockchain signing
// In production, use Web Crypto API's persistent key storage
storeKeyPairInMemory(username, keyPair);
// Store the authentication data securely (in a real app, this would be more secure) // Store the authentication data securely (in a real app, this would be more secure)
localStorage.setItem(`${username}_authData`, JSON.stringify({ localStorage.setItem(`${username}_authData`, JSON.stringify({
challenge, challenge,

View File

@ -0,0 +1,210 @@
// Blockchain-specific Web Crypto API utilities
// Extends crypto.ts with EIP-712 structured data signing for blockchain transactions
import * as crypto from './crypto';
import { isBrowser } from '../utils/browser';
import { getKeyPairFromMemory } from './keyStorage';
export interface TransactionAuthorization {
to: string; // Target address
value: string; // Amount in wei (hex string)
data: string; // Contract call data (hex string)
nonce: number; // Replay protection
deadline: number; // Expiration timestamp
}
export interface EIP712Domain {
name: string;
version: string;
chainId: number;
verifyingContract: string;
}
export interface EIP712Message {
types: {
EIP712Domain: Array<{ name: string; type: string }>;
Transaction: Array<{ name: string; type: string }>;
};
domain: EIP712Domain;
primaryType: string;
message: TransactionAuthorization;
}
/**
* Create EIP-712 domain for Web Crypto Proxy contract
*/
export function createEIP712Domain(
chainId: number,
verifyingContract: string
): EIP712Domain {
return {
name: 'WebCryptoProxy',
version: '1',
chainId,
verifyingContract,
};
}
/**
* Create EIP-712 structured message for transaction authorization
*/
export function createTransactionMessage(
authorization: TransactionAuthorization,
domain: EIP712Domain
): EIP712Message {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Transaction: [
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'data', type: 'bytes' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
domain,
primaryType: 'Transaction',
message: authorization,
};
}
/**
* Hash EIP-712 message for signing
* This creates the message hash that will be signed by Web Crypto API
*/
export async function hashEIP712Message(message: EIP712Message): Promise<string | null> {
if (!isBrowser()) return null;
try {
// EIP-712 encoding: keccak256(0x1901 || domainSeparator || messageHash)
// For Web Crypto API, we'll sign the structured message JSON
// The smart contract will need to reconstruct and verify this
// Create the message string that will be signed
// In a full implementation, you'd use proper EIP-712 encoding
// For now, we'll create a deterministic string representation
const messageString = JSON.stringify({
types: message.types,
domain: message.domain,
primaryType: message.primaryType,
message: message.message,
});
return messageString;
} catch (error) {
console.error('Error hashing EIP-712 message:', error);
return null;
}
}
/**
* Sign a transaction authorization with Web Crypto API P-256 key
*/
export async function signTransactionAuthorization(
privateKey: CryptoKey,
authorization: TransactionAuthorization,
domain: EIP712Domain
): Promise<{ signature: string; message: EIP712Message } | null> {
if (!isBrowser()) return null;
try {
// Create EIP-712 structured message
const message = createTransactionMessage(authorization, domain);
// Hash the message
const messageHash = await hashEIP712Message(message);
if (!messageHash) {
return null;
}
// Sign with Web Crypto API
const signature = await crypto.signData(privateKey, messageHash);
if (!signature) {
return null;
}
return {
signature,
message,
};
} catch (error) {
console.error('Error signing transaction authorization:', error);
return null;
}
}
/**
* Export signature in format suitable for on-chain verification
* Returns signature as hex string with r, s, v components
*/
export function formatSignatureForBlockchain(signature: string): string {
// Web Crypto API ECDSA signatures are in DER format
// For blockchain, we need to convert to r, s, v format
// This is a simplified version - full implementation would parse DER format
// For now, return base64 signature (will need conversion in smart contract)
// In production, you'd parse the DER signature and extract r, s, v
return signature;
}
/**
* Get user's private key from storage (for signing)
* Note: In production, this should use secure key storage
*/
export async function getUserPrivateKey(username: string): Promise<CryptoKey | null> {
if (!isBrowser()) return null;
try {
// Get key pair from memory (session-based)
// In production, use Web Crypto API's persistent key storage
const keyPair = getKeyPairFromMemory(username);
if (!keyPair) {
console.warn('Key pair not found in memory. User may need to re-authenticate.');
return null;
}
return keyPair.privateKey;
} catch (error) {
console.error('Error getting user private key:', error);
return null;
}
}
/**
* Store user's key pair securely
* Note: In production, use Web Crypto API's key storage
*/
export async function storeUserKeyPair(
username: string,
keyPair: CryptoKeyPair
): Promise<boolean> {
if (!isBrowser()) return false;
try {
// Export private key (in production, use Web Crypto API's key storage)
// For now, we'll store a reference
// In production, use IndexedDB or Web Crypto API's persistent key storage
// Store public key (already handled by crypto.ts)
const publicKeyBase64 = await crypto.exportPublicKey(keyPair.publicKey);
if (publicKeyBase64) {
crypto.storePublicKey(username, publicKeyBase64);
}
// Store key pair reference
// In production, use Web Crypto API's key storage API
localStorage.setItem(`${username}_keyPair`, 'stored');
return true;
} catch (error) {
console.error('Error storing user key pair:', error);
return false;
}
}

View File

@ -0,0 +1,72 @@
// Key storage utilities for Web Crypto API key pairs
// In production, use Web Crypto API's persistent key storage with IndexedDB
import { isBrowser } from '../utils/browser';
// In-memory key pair storage (session-based)
// In production, use Web Crypto API's key storage API
const keyPairStore = new Map<string, CryptoKeyPair>();
/**
* Store a key pair in memory for the current session
* Note: This is a simplified implementation for development
* In production, use Web Crypto API's persistent key storage
*/
export function storeKeyPairInMemory(username: string, keyPair: CryptoKeyPair): void {
if (!isBrowser()) return;
keyPairStore.set(username, keyPair);
}
/**
* Get a key pair from memory
* Note: This only works for the current session
* In production, use Web Crypto API's key storage
*/
export function getKeyPairFromMemory(username: string): CryptoKeyPair | null {
if (!isBrowser()) return null;
return keyPairStore.get(username) || null;
}
/**
* Clear a key pair from memory
*/
export function clearKeyPairFromMemory(username: string): void {
if (!isBrowser()) return;
keyPairStore.delete(username);
}
/**
* Check if a key pair exists in memory
*/
export function hasKeyPairInMemory(username: string): boolean {
if (!isBrowser()) return false;
return keyPairStore.has(username);
}
/**
* TODO: Implement persistent key storage using Web Crypto API's key storage
* This would use IndexedDB and the Web Crypto API's key storage capabilities
* to securely store non-extractable private keys
*/
export async function storeKeyPairPersistent(
username: string,
keyPair: CryptoKeyPair
): Promise<boolean> {
// TODO: Implement using Web Crypto API's key storage
// This would involve:
// 1. Creating a key storage database in IndexedDB
// 2. Storing the key pair using crypto.subtle's key storage API
// 3. Retrieving it later using the same API
console.warn('Persistent key storage not yet implemented. Using in-memory storage.');
storeKeyPairInMemory(username, keyPair);
return true;
}
/**
* TODO: Retrieve key pair from persistent storage
*/
export async function getKeyPairPersistent(username: string): Promise<CryptoKeyPair | null> {
// TODO: Implement retrieval from persistent storage
return getKeyPairFromMemory(username);
}

View File

@ -0,0 +1,245 @@
// Ethereum blockchain integration utilities
// Handles wallet connections, transaction building, and proxy contract interaction
import { type Address, type Hash, type TransactionRequest, createWalletClient, createPublicClient, custom, http, parseEther, encodeFunctionData, decodeFunctionResult } from 'viem';
import { mainnet, sepolia, goerli } from 'viem/chains';
import type { Chain } from 'viem';
export interface WalletConnection {
address: Address;
chainId: number;
isConnected: boolean;
}
export interface ProxyContractConfig {
address: Address;
chainId: number;
abi: readonly any[];
}
/**
* Detect and connect to MetaMask or other injected wallet
*/
export async function connectWallet(): Promise<WalletConnection | null> {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No Ethereum wallet detected. Please install MetaMask or another Web3 wallet.');
}
try {
// Request account access
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
if (!accounts || accounts.length === 0) {
throw new Error('No accounts found');
}
const address = accounts[0] as Address;
// Get chain ID
const chainIdHex = await window.ethereum.request({
method: 'eth_chainId',
});
const chainId = parseInt(chainIdHex as string, 16);
return {
address,
chainId,
isConnected: true,
};
} catch (error) {
console.error('Error connecting wallet:', error);
throw error;
}
}
/**
* Get current wallet connection status
*/
export async function getWalletConnection(): Promise<WalletConnection | null> {
if (typeof window === 'undefined' || !window.ethereum) {
return null;
}
try {
const accounts = await window.ethereum.request({
method: 'eth_accounts',
});
if (!accounts || accounts.length === 0) {
return null;
}
const address = accounts[0] as Address;
const chainIdHex = await window.ethereum.request({
method: 'eth_chainId',
});
const chainId = parseInt(chainIdHex as string, 16);
return {
address,
chainId,
isConnected: true,
};
} catch (error) {
console.error('Error getting wallet connection:', error);
return null;
}
}
/**
* Switch to a specific chain
*/
export async function switchChain(chainId: number): Promise<void> {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No Ethereum wallet detected');
}
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${chainId.toString(16)}` }],
});
} catch (error: any) {
// If chain doesn't exist, try to add it
if (error.code === 4902) {
// You would add the chain here
throw new Error(`Chain ${chainId} not found. Please add it manually.`);
}
throw error;
}
}
/**
* Get chain configuration
*/
export function getChain(chainId: number): Chain {
switch (chainId) {
case 1:
return mainnet;
case 11155111:
return sepolia;
case 5:
return goerli;
default:
// Return a generic chain config
return {
id: chainId,
name: `Chain ${chainId}`,
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: {
default: {
http: [''],
},
},
} as Chain;
}
}
/**
* Create wallet client for transaction signing
*/
export function createWalletClientForChain(chainId: number) {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No Ethereum wallet detected');
}
const chain = getChain(chainId);
return createWalletClient({
chain,
transport: custom(window.ethereum),
});
}
/**
* Build transaction to call proxy contract's execute function
*/
export function buildProxyExecuteTransaction(
config: ProxyContractConfig,
to: Address,
value: bigint,
data: Hash,
nonce: bigint,
deadline: bigint,
signature: string
): TransactionRequest {
// Convert signature from base64 to hex bytes
// Web Crypto API signature needs to be converted to format expected by contract
const signatureBytes = `0x${Buffer.from(signature, 'base64').toString('hex')}`;
return {
to: config.address,
data: encodeFunctionData({
abi: config.abi,
functionName: 'execute',
args: [to, value, data, nonce, deadline, signatureBytes],
}),
};
}
/**
* Send transaction through wallet
*/
export async function sendTransaction(
chainId: number,
transaction: TransactionRequest
): Promise<Hash> {
const walletClient = createWalletClientForChain(chainId);
const hash = await walletClient.sendTransaction(transaction);
return hash;
}
/**
* Wait for transaction receipt
*/
export async function waitForTransaction(
chainId: number,
hash: Hash
): Promise<any> {
const chain = getChain(chainId);
const publicClient = createPublicClient({
chain,
transport: http(),
});
return await publicClient.waitForTransactionReceipt({ hash });
}
/**
* Check if address is a contract
*/
export async function isContract(
chainId: number,
address: Address
): Promise<boolean> {
const chain = getChain(chainId);
const publicClient = createPublicClient({
chain,
transport: http(),
});
const code = await publicClient.getBytecode({ address });
return code !== undefined && code !== '0x';
}
/**
* Get proxy contract address for a user's Web Crypto public key
* This would typically be stored on-chain or in a registry
*/
export async function getProxyAddressForPublicKey(
publicKey: string,
chainId: number
): Promise<Address | null> {
// In a full implementation, this would query a registry contract
// or derive the address deterministically
// For now, return null (proxy needs to be deployed)
return null;
}

View File

@ -0,0 +1,5 @@
// Blockchain integration exports
export * from './ethereum';
export * from './walletIntegration';

View File

@ -0,0 +1,137 @@
// Relayer service for gasless transactions
// Accepts signed transaction requests and submits them on behalf of users
import { type Address, type Hash, type TransactionRequest, createPublicClient, http, encodeFunctionData } from 'viem';
import { getChain } from './ethereum';
import type { TransactionAuthorization } from '../auth/cryptoBlockchain';
export interface RelayerConfig {
relayerUrl: string; // URL of the relayer service
relayerAddress?: Address; // Address that will pay for gas
}
export interface RelayerTransactionRequest {
authorization: TransactionAuthorization;
signature: string;
proxyContractAddress: Address;
chainId: number;
}
export interface RelayerResponse {
success: boolean;
transactionHash?: Hash;
error?: string;
}
/**
* Submit a signed transaction to the relayer service
* The relayer will verify the signature and submit the transaction, paying for gas
*/
export async function submitToRelayer(
config: RelayerConfig,
request: RelayerTransactionRequest
): Promise<RelayerResponse> {
try {
const response = await fetch(config.relayerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
authorization: request.authorization,
signature: request.signature,
proxyContractAddress: request.proxyContractAddress,
chainId: request.chainId,
}),
});
if (!response.ok) {
const error = await response.text();
return {
success: false,
error: error || `Relayer returned status ${response.status}`,
};
}
const data = await response.json();
return {
success: true,
transactionHash: data.transactionHash as Hash,
};
} catch (error) {
console.error('Error submitting to relayer:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Check if relayer service is available
*/
export async function checkRelayerAvailability(
relayerUrl: string
): Promise<boolean> {
try {
const response = await fetch(`${relayerUrl}/health`, {
method: 'GET',
});
return response.ok;
} catch (error) {
return false;
}
}
/**
* Get relayer configuration from environment or settings
*/
export function getRelayerConfig(chainId: number): RelayerConfig | null {
// Read from environment variables or localStorage
// Format: RELAYER_URL_<CHAIN_ID> or use a default relayer URL pattern
if (typeof window === 'undefined') return null;
// Check localStorage for relayer configuration
const relayerConfigKey = `relayer_config_${chainId}`;
const stored = localStorage.getItem(relayerConfigKey);
if (stored) {
try {
const config = JSON.parse(stored);
return {
relayerUrl: config.url,
relayerAddress: config.address,
};
} catch (e) {
console.error('Error parsing relayer config:', e);
}
}
// Default relayer URLs (configure these for your deployment)
const relayerUrls: Record<number, string> = {
// 1: 'https://relayer.mainnet.yourdomain.com',
// 11155111: 'https://relayer.sepolia.yourdomain.com',
};
const url = relayerUrls[chainId];
if (!url) return null;
return {
relayerUrl: url,
};
}
/**
* Set relayer configuration for a chain
*/
export function setRelayerConfig(chainId: number, url: string, address?: Address): void {
if (typeof window === 'undefined') return;
const relayerConfigKey = `relayer_config_${chainId}`;
localStorage.setItem(relayerConfigKey, JSON.stringify({
url,
address,
}));
}

View File

@ -0,0 +1,168 @@
// Wallet integration layer for proxy contract deployment and interaction
// Handles the connection between Web Crypto API and wallet infrastructure
import { type Address, encodeFunctionData } from 'viem';
import { connectWallet, getWalletConnection, sendTransaction, type WalletConnection } from './ethereum';
import * as crypto from '../auth/crypto';
export interface ProxyDeploymentConfig {
publicKeyX: string; // X coordinate of P-256 public key (hex)
publicKeyY: string; // Y coordinate of P-256 public key (hex)
factoryAddress: Address;
chainId: number;
}
/**
* Extract X and Y coordinates from P-256 public key
* P-256 public keys in raw format are 65 bytes: 0x04 || 32-byte X || 32-byte Y
*/
export async function extractPublicKeyCoordinates(
publicKey: CryptoKey
): Promise<{ x: string; y: string } | null> {
try {
const crypto = window.crypto;
const publicKeyBuffer = await crypto.subtle.exportKey('raw', publicKey);
const keyBytes = new Uint8Array(publicKeyBuffer);
// P-256 public key format: 0x04 (uncompressed) || 32-byte X || 32-byte Y
if (keyBytes.length !== 65 || keyBytes[0] !== 0x04) {
console.error('Invalid P-256 public key format');
return null;
}
// Extract X and Y coordinates (32 bytes each)
const xBytes = keyBytes.slice(1, 33);
const yBytes = keyBytes.slice(33, 65);
// Convert to hex strings
const x = '0x' + Array.from(xBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const y = '0x' + Array.from(yBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return { x, y };
} catch (error) {
console.error('Error extracting public key coordinates:', error);
return null;
}
}
/**
* Deploy proxy contract via factory
*/
export async function deployProxyContract(
config: ProxyDeploymentConfig,
wallet: WalletConnection
): Promise<Address | null> {
try {
// Factory contract ABI for deployProxy function
const factoryABI = [
{
name: 'deployProxy',
type: 'function',
inputs: [
{ name: 'publicKeyX', type: 'bytes32' },
{ name: 'publicKeyY', type: 'bytes32' },
],
outputs: [{ name: 'proxy', type: 'address' }],
},
] as const;
// Convert hex strings to bytes32
const publicKeyXBytes = config.publicKeyX.startsWith('0x')
? config.publicKeyX.slice(2).padStart(64, '0')
: config.publicKeyX.padStart(64, '0');
const publicKeyYBytes = config.publicKeyY.startsWith('0x')
? config.publicKeyY.slice(2).padStart(64, '0')
: config.publicKeyY.padStart(64, '0');
// Encode function call
const data = encodeFunctionData({
abi: factoryABI,
functionName: 'deployProxy',
args: [
`0x${publicKeyXBytes}` as `0x${string}`,
`0x${publicKeyYBytes}` as `0x${string}`,
],
});
// Send transaction
const hash = await sendTransaction(config.chainId, {
to: config.factoryAddress,
data,
});
// In a full implementation, you'd wait for the transaction receipt
// and extract the proxy address from the event logs
// For now, return null and let the user check manually
console.log('Proxy deployment transaction:', hash);
return null;
} catch (error) {
console.error('Error deploying proxy contract:', error);
throw error;
}
}
/**
* Get proxy contract address from factory
*/
export async function getProxyAddress(
publicKeyX: string,
publicKeyY: string,
factoryAddress: Address,
chainId: number
): Promise<Address | null> {
// In a full implementation, this would call the factory's getProxy function
// For now, return null
return null;
}
/**
* Prepare proxy contract deployment for a user's Web Crypto account
*/
export async function prepareProxyDeployment(
username: string
): Promise<ProxyDeploymentConfig | null> {
try {
// Get user's public key
const publicKeyBase64 = crypto.getPublicKey(username);
if (!publicKeyBase64) {
throw new Error('Public key not found for user');
}
// Import public key
const publicKey = await crypto.importPublicKey(publicKeyBase64);
if (!publicKey) {
throw new Error('Failed to import public key');
}
// Extract coordinates
const coordinates = await extractPublicKeyCoordinates(publicKey);
if (!coordinates) {
throw new Error('Failed to extract public key coordinates');
}
// Get wallet connection
const wallet = await getWalletConnection();
if (!wallet) {
throw new Error('Wallet not connected');
}
// Factory address would be configured per chain
// For now, use a placeholder
const factoryAddress = '0x0000000000000000000000000000000000000000' as Address;
return {
publicKeyX: coordinates.x,
publicKeyY: coordinates.y,
factoryAddress,
chainId: wallet.chainId,
};
} catch (error) {
console.error('Error preparing proxy deployment:', error);
return null;
}
}

View File

@ -6,6 +6,9 @@ import { useState, useEffect, useRef } from "react"
import { useDialogs } from "tldraw" import { useDialogs } from "tldraw"
import { SettingsDialog } from "./SettingsDialog" import { SettingsDialog } from "./SettingsDialog"
import { useAuth } from "../context/AuthContext" import { useAuth } from "../context/AuthContext"
import { useBlockchain } from "../context/BlockchainContext"
import { useNotifications } from "../context/NotificationContext"
import { unlinkAccount } from "../lib/auth/blockchainLinking"
import LoginButton from "../components/auth/LoginButton" import LoginButton from "../components/auth/LoginButton"
import StarBoardButton from "../components/StarBoardButton" import StarBoardButton from "../components/StarBoardButton"
import { ObsidianVaultBrowser } from "../components/ObsidianVaultBrowser" import { ObsidianVaultBrowser } from "../components/ObsidianVaultBrowser"
@ -25,7 +28,10 @@ export function CustomToolbar() {
const { addDialog, removeDialog } = useDialogs() const { addDialog, removeDialog } = useDialogs()
const { session, setSession, clearSession } = useAuth() const { session, setSession, clearSession } = useAuth()
const { wallet, linkedAccount, isConnecting, connect, linkWebCryptoAccount, disconnect } = useBlockchain()
const { addNotification } = useNotifications()
const [showProfilePopup, setShowProfilePopup] = useState(false) const [showProfilePopup, setShowProfilePopup] = useState(false)
const [isLinking, setIsLinking] = useState(false)
const [showVaultBrowser, setShowVaultBrowser] = useState(false) const [showVaultBrowser, setShowVaultBrowser] = useState(false)
const [showHolonBrowser, setShowHolonBrowser] = useState(false) const [showHolonBrowser, setShowHolonBrowser] = useState(false)
const [vaultBrowserMode, setVaultBrowserMode] = useState<'keyboard' | 'button'>('keyboard') const [vaultBrowserMode, setVaultBrowserMode] = useState<'keyboard' | 'button'>('keyboard')
@ -771,6 +777,195 @@ export function CustomToolbar() {
</button> </button>
</div> </div>
{/* Blockchain Wallet Settings */}
<div style={{
marginBottom: "16px",
padding: "12px",
backgroundColor: "#f8f9fa",
borderRadius: "4px",
border: "1px solid #e9ecef"
}}>
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "8px"
}}>
<span style={{ fontWeight: "500" }}>Blockchain Wallet</span>
<span style={{ fontSize: "14px" }}>
{linkedAccount ? "✅ Linked" : wallet ? "🔗 Connected" : "❌ Not connected"}
</span>
</div>
{wallet ? (
<div style={{ marginBottom: "8px" }}>
<div style={{
fontSize: "12px",
color: "#007acc",
fontWeight: "600",
marginBottom: "4px"
}}>
{wallet.address.slice(0, 6)}...{wallet.address.slice(-4)}
</div>
<div style={{
fontSize: "11px",
color: "#666",
fontFamily: "monospace"
}}>
Chain ID: {wallet.chainId}
</div>
{linkedAccount && (
<div style={{
fontSize: "11px",
color: "#10b981",
marginTop: "4px"
}}>
Linked to Web Crypto account
</div>
)}
</div>
) : (
<p style={{
fontSize: "12px",
color: "#666",
margin: "0 0 8px 0"
}}>
Connect a wallet to enable blockchain transactions
</p>
)}
{!wallet ? (
<button
onClick={async () => {
try {
await connect()
addNotification('Wallet connected successfully', 'success')
} catch (error: any) {
addNotification(error.message || 'Failed to connect wallet', 'error')
}
}}
disabled={isConnecting}
style={{
width: "100%",
padding: "6px 12px",
backgroundColor: "#28a745",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "12px",
fontWeight: "500",
transition: "background 0.2s",
}}
onMouseEnter={(e) => {
if (!isConnecting) e.currentTarget.style.backgroundColor = "#218838"
}}
onMouseLeave={(e) => {
if (!isConnecting) e.currentTarget.style.backgroundColor = "#28a745"
}}
>
{isConnecting ? "Connecting..." : "Connect Wallet"}
</button>
) : (
<div style={{ display: "flex", gap: "4px" }}>
{!linkedAccount ? (
<button
onClick={async () => {
setIsLinking(true)
try {
const result = await linkWebCryptoAccount(session.username)
if (result.success) {
addNotification('Account linked successfully!', 'success')
} else {
addNotification(result.error || 'Failed to link account', 'error')
}
} catch (error: any) {
addNotification(error.message || 'Failed to link account', 'error')
} finally {
setIsLinking(false)
}
}}
disabled={isLinking}
style={{
flex: 1,
padding: "6px 12px",
backgroundColor: "#007acc",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "12px",
fontWeight: "500",
transition: "background 0.2s",
}}
onMouseEnter={(e) => {
if (!isLinking) e.currentTarget.style.backgroundColor = "#005a9e"
}}
onMouseLeave={(e) => {
if (!isLinking) e.currentTarget.style.backgroundColor = "#007acc"
}}
>
{isLinking ? "Linking..." : "Link Account"}
</button>
) : (
<button
onClick={() => {
if (unlinkAccount(session.username)) {
disconnect()
addNotification('Account unlinked successfully', 'success')
} else {
addNotification('Failed to unlink account', 'error')
}
}}
style={{
flex: 1,
padding: "6px 12px",
backgroundColor: "#dc3545",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "12px",
fontWeight: "500",
transition: "background 0.2s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "#c82333"
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "#dc3545"
}}
>
Unlink
</button>
)}
<button
onClick={disconnect}
style={{
flex: 1,
padding: "6px 12px",
backgroundColor: "#6c757d",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "12px",
fontWeight: "500",
transition: "background 0.2s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "#5a6268"
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "#6c757d"
}}
>
Disconnect
</button>
</div>
)}
</div>
<a <a
href="/dashboard" href="/dashboard"
target="_blank" target="_blank"

173
worker/blockchainRelayer.ts Normal file
View File

@ -0,0 +1,173 @@
// 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' },
});
}
},
};