web3 integration start
This commit is contained in:
parent
5fd83944fc
commit
667a6a780e
|
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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.)
|
||||
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"@chengsokdara/use-whisper": "^0.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@noble/curves": "^2.0.1",
|
||||
"@oddjs/odd": "^0.37.2",
|
||||
"@tldraw/assets": "^3.15.4",
|
||||
"@tldraw/tldraw": "^3.15.4",
|
||||
|
|
@ -52,6 +53,7 @@
|
|||
"tldraw": "^3.15.4",
|
||||
"use-whisper": "^0.0.1",
|
||||
"vercel": "^39.1.1",
|
||||
"viem": "^2.38.6",
|
||||
"webcola": "^3.4.0",
|
||||
"webnative": "^0.36.3"
|
||||
},
|
||||
|
|
@ -74,6 +76,12 @@
|
|||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
|
||||
|
|
@ -2530,6 +2538,45 @@
|
|||
"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": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
|
|
@ -4621,6 +4668,57 @@
|
|||
"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": {
|
||||
"version": "7.120.4",
|
||||
"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_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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
|
|
@ -11247,6 +11366,21 @@
|
|||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz",
|
||||
|
|
@ -13504,6 +13638,51 @@
|
|||
"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": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz",
|
||||
|
|
@ -16610,6 +16789,51 @@
|
|||
"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": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"@chengsokdara/use-whisper": "^0.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@noble/curves": "^2.0.1",
|
||||
"@oddjs/odd": "^0.37.2",
|
||||
"@tldraw/assets": "^3.15.4",
|
||||
"@tldraw/tldraw": "^3.15.4",
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
"tldraw": "^3.15.4",
|
||||
"use-whisper": "^0.0.1",
|
||||
"vercel": "^39.1.1",
|
||||
"viem": "^2.38.6",
|
||||
"webcola": "^3.4.0",
|
||||
"webnative": "^0.36.3"
|
||||
},
|
||||
|
|
|
|||
11
src/App.tsx
11
src/App.tsx
|
|
@ -28,6 +28,7 @@ import { useState, useEffect } from 'react';
|
|||
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||
import { FileSystemProvider } from './context/FileSystemContext';
|
||||
import { NotificationProvider } from './context/NotificationContext';
|
||||
import { BlockchainProvider } from './context/BlockchainContext';
|
||||
import NotificationsDisplay from './components/NotificationsDisplay';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
|
||||
|
|
@ -102,8 +103,9 @@ const AppWithProviders = () => {
|
|||
<AuthProvider>
|
||||
<FileSystemProvider>
|
||||
<NotificationProvider>
|
||||
<DailyProvider callObject={callObject}>
|
||||
<BrowserRouter>
|
||||
<BlockchainProvider>
|
||||
<DailyProvider callObject={callObject}>
|
||||
<BrowserRouter>
|
||||
{/* Display notifications */}
|
||||
<NotificationsDisplay />
|
||||
|
||||
|
|
@ -169,8 +171,9 @@ const AppWithProviders = () => {
|
|||
</OptionalAuthRoute>
|
||||
} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</DailyProvider>
|
||||
</BrowserRouter>
|
||||
</DailyProvider>
|
||||
</BlockchainProvider>
|
||||
</NotificationProvider>
|
||||
</FileSystemProvider>
|
||||
</AuthProvider>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useBlockchain } from '../../context/BlockchainContext';
|
||||
import { useNotifications } from '../../context/NotificationContext';
|
||||
import { unlinkAccount } from '../../lib/auth/blockchainLinking';
|
||||
|
||||
interface ProfileProps {
|
||||
onLogout?: () => void;
|
||||
|
|
@ -8,8 +11,11 @@ interface ProfileProps {
|
|||
|
||||
export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }) => {
|
||||
const { session, updateSession, clearSession } = useAuth();
|
||||
const { wallet, linkedAccount, isConnecting, connect, linkWebCryptoAccount, disconnect } = useBlockchain();
|
||||
const { addNotification } = useNotifications();
|
||||
const [vaultPath, setVaultPath] = useState(session.obsidianVaultPath || '');
|
||||
const [isEditingVault, setIsEditingVault] = useState(false);
|
||||
const [isLinking, setIsLinking] = useState(false);
|
||||
|
||||
const handleVaultPathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 = () => {
|
||||
// Clear the session
|
||||
clearSession();
|
||||
|
|
@ -148,6 +193,116 @@ export const Profile: React.FC<ProfileProps> = ({ onLogout, onOpenVaultBrowser }
|
|||
</details>
|
||||
</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">
|
||||
<button onClick={handleLogout} className="logout-button">
|
||||
Sign Out
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as crypto from './crypto';
|
||||
import { isBrowser } from '../utils/browser';
|
||||
import { storeKeyPairInMemory } from './keyStorage';
|
||||
|
||||
export interface CryptoAuthResult {
|
||||
success: boolean;
|
||||
|
|
@ -88,6 +89,10 @@ export class CryptoAuthService {
|
|||
crypto.addRegisteredUser(username);
|
||||
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)
|
||||
localStorage.setItem(`${username}_authData`, JSON.stringify({
|
||||
challenge,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// Blockchain integration exports
|
||||
|
||||
export * from './ethereum';
|
||||
export * from './walletIntegration';
|
||||
|
||||
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,6 +6,9 @@ import { useState, useEffect, useRef } from "react"
|
|||
import { useDialogs } from "tldraw"
|
||||
import { SettingsDialog } from "./SettingsDialog"
|
||||
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 StarBoardButton from "../components/StarBoardButton"
|
||||
import { ObsidianVaultBrowser } from "../components/ObsidianVaultBrowser"
|
||||
|
|
@ -25,7 +28,10 @@ export function CustomToolbar() {
|
|||
const { addDialog, removeDialog } = useDialogs()
|
||||
|
||||
const { session, setSession, clearSession } = useAuth()
|
||||
const { wallet, linkedAccount, isConnecting, connect, linkWebCryptoAccount, disconnect } = useBlockchain()
|
||||
const { addNotification } = useNotifications()
|
||||
const [showProfilePopup, setShowProfilePopup] = useState(false)
|
||||
const [isLinking, setIsLinking] = useState(false)
|
||||
const [showVaultBrowser, setShowVaultBrowser] = useState(false)
|
||||
const [showHolonBrowser, setShowHolonBrowser] = useState(false)
|
||||
const [vaultBrowserMode, setVaultBrowserMode] = useState<'keyboard' | 'button'>('keyboard')
|
||||
|
|
@ -771,6 +777,195 @@ export function CustomToolbar() {
|
|||
</button>
|
||||
</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
|
||||
href="/dashboard"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue