import React, {useEffect, useState} from 'react' import {useDelegatedActions, usePrivy, useWallets, WalletWithMetadata} from '@privy-io/react-auth' import {Account} from '../../../generated/ManagingApi' import canonicalize from 'canonicalize' // Support JSON canonicalization import elliptic from 'elliptic' // Elliptic curve cryptography for browsers import * as base64 from 'base64-js' // Base64 encoding/decoding import Modal from '../../../components/mollecules/Modal/Modal' // Replace with your app configuration - these should come from environment variables in a real app const PRIVY_APP_ID = 'insert-your-app-id' const PRIVY_APP_SECRET = 'insert-your-app-secret' // Don't hardcode in production! const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here' // Don't hardcode in production! // Initialize elliptic curve - P-256 is the curve used by Privy (NIST P-256) const EC = new elliptic.ec('p256') interface PrivyDelegationModalProps { isOpen: boolean onClose: () => void account: Account } // Use Web Crypto API to hash the message with SHA-256 async function sha256Hash(message: Uint8Array): Promise { const hashBuffer = await crypto.subtle.digest('SHA-256', message) return new Uint8Array(hashBuffer) } async function generateAuthorizationSignature({url, body}: {url: string; body: object}) { try { // First, canonicalize the request body - this is crucial for consistent signatures const canonicalizedBody = JSON.parse(canonicalize(body) as string); // Create the payload object that follows Privy's specification const payload = { version: 1, method: 'POST', url, body: canonicalizedBody, // Use the canonicalized body here headers: { 'privy-app-id': PRIVY_APP_ID } }; // JSON-canonicalize the payload to ensure deterministic serialization const serializedPayload = canonicalize(payload) as string; console.log('Canonicalized payload:', serializedPayload); // Extract private key from the authorization key by removing the prefix const privateKeyBase64 = PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', ''); // Decode the base64 private key to get the raw bytes const privateKeyBytes = base64.toByteArray(privateKeyBase64); // Import the private key into elliptic const privateKey = EC.keyFromPrivate(privateKeyBytes); // Convert the serialized payload to UTF-8 bytes const messageBytes = new TextEncoder().encode(serializedPayload); // Hash the message using SHA-256 with Web Crypto API const messageHash = await sha256Hash(messageBytes); // Sign the hash with the private key - use Uint8Array instead of Buffer // Using the array directly instead of creating a Buffer const signature = privateKey.sign(Array.from(messageHash)); // Convert signature to the DER format and then to base64 const signatureDer = signature.toDER(); const signatureBase64 = base64.fromByteArray(new Uint8Array(signatureDer)); return { payload: serializedPayload, signature: signatureBase64, body: canonicalizedBody, // Return the canonicalized body canonicalizedBodyString: canonicalize(canonicalizedBody) as string // Return the string form too }; } catch (err) { console.error('Error generating signature:', err); throw err; } } // Function to send the actual request with the generated signature async function sendPrivyRequest(url: string, body: any, signature: string): Promise { try { // Create basic auth token for Privy API const authToken = btoa(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`); // Set up the request headers const headers = { 'Authorization': `Basic ${authToken}`, 'privy-app-id': PRIVY_APP_ID, 'privy-authorization-signature': signature, 'Content-Type': 'application/json' }; console.log('Sending request with headers:', headers); console.log('Body:', body); // Make the API call const response = await fetch(url, { method: 'POST', headers: headers, body: body // Use the exact same canonicalized body string }); // Get the response body const responseText = await response.text(); // Try to parse as JSON if possible try { const responseJson = JSON.parse(responseText); if (!response.ok) { throw new Error(`API error (${response.status}): ${JSON.stringify(responseJson)}`); } return responseJson; } catch (parseError) { // If not valid JSON, return as text if (!response.ok) { throw new Error(`API error (${response.status}): ${responseText}`); } return { text: responseText }; } } catch (error) { console.error('Error sending Privy request:', error); throw error; } } const PrivyDelegationModal: React.FC = ({ isOpen, onClose, account, }) => { const [isDelegated, setIsDelegated] = useState(false) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [signatureResult, setSignatureResult] = useState<{ payload: string, signature: string, body?: any, canonicalizedBodyString?: string } | null>(null) const [isSigningMessage, setIsSigningMessage] = useState(false) const [isSendingRequest, setIsSendingRequest] = useState(false) const [messageToSign, setMessageToSign] = useState('Hello, Ethereum') const [apiResponse, setApiResponse] = useState(null) const { delegateWallet, revokeWallets } = useDelegatedActions() const { user } = usePrivy() const { ready, wallets } = useWallets() // Find the embedded wallet to delegate from the array of the user's wallets const walletToDelegate = wallets.find( (wallet) => wallet.walletClientType === 'privy' && wallet.address === account.key ) useEffect(() => { if (user && ready) { checkDelegationStatus() } }, [user, ready]) const checkDelegationStatus = () => { setIsLoading(true) setError(null) try { // Check if the wallet is already delegated by inspecting the user's linked accounts const delegatedWallet = user?.linkedAccounts.find( (linkedAccount): linkedAccount is WalletWithMetadata => linkedAccount.type === 'wallet' && linkedAccount.delegated && linkedAccount.address.toLowerCase() === account.key?.toLowerCase() ) setIsDelegated(!!delegatedWallet) } catch (err: any) { console.error('Error checking delegation status:', err) setError(`Failed to check delegation status: ${err.message}`) } finally { setIsLoading(false) } } const handleRequestDelegation = async () => { try { setIsLoading(true) setError(null) if (!walletToDelegate) { throw new Error('No embedded wallet found to delegate') } // Fixes the linter error by using the correct parameter type await delegateWallet({ address: walletToDelegate.address, chainType: 'ethereum' }) setIsDelegated(true) } catch (err: any) { console.error('Error requesting delegation:', err) setError(`Failed to request delegation: ${err.message}`) } finally { setIsLoading(false) } } const handleRevokeDelegation = async () => { try { setIsLoading(true) setError(null) if (!walletToDelegate) { throw new Error('No embedded wallet found to revoke delegation') } // Fix for linter error - revokeWallets doesn't take parameters await revokeWallets() setIsDelegated(false) } catch (err: any) { console.error('Error revoking delegation:', err) setError(`Failed to revoke delegation: ${err.message}`) } finally { setIsLoading(false) } } const handleSignMessage = async () => { try { setIsSigningMessage(true) setSignatureResult(null) setApiResponse(null) setError(null) if (!walletToDelegate) { throw new Error('No embedded wallet found to sign with') } // Prepare the request body const body = { address: walletToDelegate.address, chain_type: 'ethereum', method: 'personal_sign', params: { message: messageToSign, encoding: 'utf-8' } } // Generate the signature using browser crypto const result = await generateAuthorizationSignature({ url: 'https://auth.privy.io/api/v1/wallets/rpc', body }) setSignatureResult(result) // For debugging in the console console.log("Generated Authorization Signature:", result.signature); console.log("Payload:", result.payload); console.log("Body:", result.body); console.log("Canonicalized Body String:", result.canonicalizedBodyString); // Create a curl command that could be used to make the request // Note: We're using the canonicalized body from the result to ensure it matches what was signed const curlCommand = `curl --request POST https://auth.privy.io/api/v1/wallets/rpc \\ -u "${PRIVY_APP_ID}:YOUR_APP_SECRET" \\ -H "privy-app-id: ${PRIVY_APP_ID}" \\ -H "privy-authorization-signature: ${result.signature}" \\ -H 'Content-Type: application/json' \\ -d '${result.canonicalizedBodyString}'`; console.log("Curl command for testing:"); console.log(curlCommand); } catch (err: any) { console.error('Error signing message:', err) setError(`Failed to sign message: ${err.message}`) } finally { setIsSigningMessage(false) } } const handleSendRequest = async () => { if (!signatureResult?.canonicalizedBodyString || !signatureResult?.signature) { setError('No signature available. Please generate a signature first.'); return; } try { setIsSendingRequest(true); setApiResponse(null); setError(null); // Send the actual request using the same canonicalized body that was used for signature const response = await sendPrivyRequest( 'https://auth.privy.io/api/v1/wallets/rpc', signatureResult.canonicalizedBodyString, signatureResult.signature ); setApiResponse(response); console.log('API Response:', response); } catch (err: any) { console.error('Error sending request:', err); setError(`Failed to send request: ${err.message}`); } finally { setIsSendingRequest(false); } }; // Modal content to be passed to the Modal component const modalContent = ( <> {isLoading ? (

Checking delegation status...

) : error ? (

{error}

) : ( <>

Wallet Address: {account.key}

Delegation Status:{' '} {isDelegated ? 'Enabled' : 'Disabled'}

{isDelegated ? ( ) : ( )}
Message Signing
{signatureResult && ( )}
{signatureResult && (
Generated Signature:
{signatureResult.signature}
)} {apiResponse && (
API Response:
                  {JSON.stringify(apiResponse, null, 2)}
                
)}

Note: This component ensures that the exact same canonicalized payload is used for both signature generation and request sending, maintaining signature integrity.

)} ) return ( {modalContent} ) } export default PrivyDelegationModal