Files
managing-apps/src/Managing.WebApp/src/pages/adminPage/account/PrivyDelegationModal.tsx
2025-11-08 00:09:28 +07:00

467 lines
15 KiB
TypeScript

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<Uint8Array> {
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<any> {
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<PrivyDelegationModalProps> = ({
isOpen,
onClose,
account,
}) => {
const [isDelegated, setIsDelegated] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const [signatureResult, setSignatureResult] = useState<{
payload: string,
signature: string,
body?: any,
canonicalizedBodyString?: string
} | null>(null)
const [isSigningMessage, setIsSigningMessage] = useState<boolean>(false)
const [isSendingRequest, setIsSendingRequest] = useState<boolean>(false)
const [messageToSign, setMessageToSign] = useState<string>('Hello, Ethereum')
const [apiResponse, setApiResponse] = useState<any>(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 ? (
<div className="text-center py-4">
<span className="loading loading-spinner loading-md"></span>
<p>Checking delegation status...</p>
</div>
) : error ? (
<div className="alert alert-error mb-4">
<p>{error}</p>
</div>
) : (
<>
<div className="mb-4">
<p className="mb-2">
<strong>Wallet Address:</strong> {account.key}
</p>
<p className="mb-2">
<strong>Delegation Status:</strong>{' '}
<span className={isDelegated ? 'text-success' : 'text-error'}>
{isDelegated ? 'Enabled' : 'Disabled'}
</span>
</p>
</div>
<div className="flex justify-center mb-4">
{isDelegated ? (
<button
onClick={handleRevokeDelegation}
className="btn btn-error"
disabled={isLoading}
>
Revoke Delegation
</button>
) : (
<button
onClick={handleRequestDelegation}
className="btn btn-primary"
disabled={isLoading}
>
Enable Delegation
</button>
)}
</div>
<div className="divider">Message Signing</div>
<div className="mb-4">
<label className="form-control w-full">
<div className="label">
<span className="label-text">Message to sign</span>
</div>
<input
type="text"
value={messageToSign}
onChange={(e) => setMessageToSign(e.target.value)}
className="input input-bordered w-full"
disabled={isSigningMessage}
/>
</label>
</div>
<div className="flex flex-col gap-2 mb-4">
<button
onClick={handleSignMessage}
className="btn btn-secondary"
disabled={isSigningMessage || !messageToSign}
>
{isSigningMessage ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Generating Signature...
</>
) : (
'Generate Signature'
)}
</button>
{signatureResult && (
<button
onClick={handleSendRequest}
className="btn btn-primary"
disabled={isSendingRequest || !signatureResult}
>
{isSendingRequest ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Sending Request...
</>
) : (
'Send Request Using Signature'
)}
</button>
)}
</div>
{signatureResult && (
<div className="mb-4">
<div className="label">
<span className="label-text font-bold">Generated Signature:</span>
</div>
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
<pre className="text-xs break-all whitespace-pre-wrap">{signatureResult.signature}</pre>
</div>
</div>
)}
{apiResponse && (
<div className="mb-4">
<div className="label">
<span className="label-text font-bold">API Response:</span>
</div>
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
<pre className="text-xs break-all whitespace-pre-wrap">
{JSON.stringify(apiResponse, null, 2)}
</pre>
</div>
</div>
)}
<div className="mt-4">
<div className="alert alert-info">
<p className="text-sm">
<strong>Note:</strong> This component ensures that the exact same canonicalized payload is used
for both signature generation and request sending, maintaining signature integrity.
</p>
</div>
</div>
</>
)}
</>
)
return (
<Modal
showModal={isOpen}
onClose={onClose}
titleHeader="Wallet Delegation"
>
{modalContent}
</Modal>
)
}
export default PrivyDelegationModal