From e6e0c8e3ea0b4e90a83b6da89db058a7ae4b2c7e Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 20 Apr 2026 22:44:10 -0500 Subject: [PATCH 01/27] feat(side-panel): port approval UI from popup (Phase A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prep for killing the separate approval popup. Copy every tx-type view (evm/utxo/tendermint/other plus Transaction / TxidPage / AwaitingApproval) under side-panel/src/approval, subscribe SidePanel to requestStorage, and render the Transaction overlay full-bleed whenever a pending dApp request exists — matches the popup's singular-focus UX without the window. Adapted from popup: window.close() becomes an onDismiss prop, and the OPEN_SIDEBAR round-trip is dropped because the sidebar *is* the surface now. Popup still works in parallel — Phase B will flip the background off it; Phase C deletes it. Co-Authored-By: Claude Opus 4.7 (1M context) --- pages/side-panel/package.json | 5 +- pages/side-panel/src/SidePanel.tsx | 53 +++ .../src/approval/AwaitingApproval.tsx | 35 ++ pages/side-panel/src/approval/Transaction.tsx | 303 +++++++++++++ pages/side-panel/src/approval/TxidPage.tsx | 117 +++++ .../src/approval/evm/ChainSelect.tsx | 0 .../src/approval/evm/ContractDetailsCard.tsx | 174 ++++++++ .../src/approval/evm/HarpyDetailsCard.tsx | 258 +++++++++++ .../src/approval/evm/ProjectInfoCard.tsx | 94 ++++ .../src/approval/evm/RequestDataCard.tsx | 84 ++++ .../src/approval/evm/RequestDetailsCard.tsx | 59 +++ .../src/approval/evm/RequestFeeCard.tsx | 368 ++++++++++++++++ .../src/approval/evm/RequestMethodCard.tsx | 92 ++++ .../src/approval/evm/ThreatPrompt.tsx | 45 ++ pages/side-panel/src/approval/evm/index.tsx | 81 ++++ .../src/approval/evm/txTypes/eip712.tsx | 70 +++ .../src/approval/evm/txTypes/legacy.tsx | 148 +++++++ .../src/approval/evm/txTypes/personalSign.tsx | 137 ++++++ .../src/approval/other/ProjectInfoCard.tsx | 67 +++ .../src/approval/other/RequestDataCard.tsx | 84 ++++ .../src/approval/other/RequestDetailsCard.tsx | 71 +++ .../src/approval/other/RequestMethodCard.tsx | 51 +++ pages/side-panel/src/approval/other/index.tsx | 100 +++++ .../approval/tendermint/ProjectInfoCard.tsx | 67 +++ .../approval/tendermint/RequestDataCard.tsx | 84 ++++ .../tendermint/RequestDetailsCard.tsx | 134 ++++++ .../approval/tendermint/RequestMethodCard.tsx | 51 +++ .../src/approval/tendermint/index.tsx | 95 ++++ .../src/approval/utxo/CoinControlCard.tsx | 146 +++++++ .../src/approval/utxo/ProjectFeeCard.tsx | 255 +++++++++++ .../src/approval/utxo/ProjectInfoCard.tsx | 67 +++ .../src/approval/utxo/RequestDataCard.tsx | 88 ++++ .../src/approval/utxo/RequestDetailsCard.tsx | 156 +++++++ .../src/approval/utxo/RequestMethodCard.tsx | 51 +++ pages/side-panel/src/approval/utxo/index.tsx | 164 +++++++ pages/side-panel/src/approval/utxo/utils.ts | 106 +++++ .../side-panel/src/assets/sounds/chaching.mp3 | Bin 0 -> 37648 bytes pages/side-panel/src/assets/sounds/fail.mp3 | Bin 0 -> 10448 bytes pages/side-panel/src/assets/sounds/send.mp3 | Bin 0 -> 48144 bytes .../src/assets/svg/connect-keepkey.svg | 31 ++ .../src/assets/svg/hold-and-connect.svg | 408 ++++++++++++++++++ .../src/assets/svg/hold-and-release.svg | 26 ++ pnpm-lock.yaml | 9 + 43 files changed, 4433 insertions(+), 1 deletion(-) create mode 100644 pages/side-panel/src/approval/AwaitingApproval.tsx create mode 100644 pages/side-panel/src/approval/Transaction.tsx create mode 100644 pages/side-panel/src/approval/TxidPage.tsx create mode 100644 pages/side-panel/src/approval/evm/ChainSelect.tsx create mode 100644 pages/side-panel/src/approval/evm/ContractDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/evm/HarpyDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/evm/ProjectInfoCard.tsx create mode 100644 pages/side-panel/src/approval/evm/RequestDataCard.tsx create mode 100644 pages/side-panel/src/approval/evm/RequestDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/evm/RequestFeeCard.tsx create mode 100644 pages/side-panel/src/approval/evm/RequestMethodCard.tsx create mode 100644 pages/side-panel/src/approval/evm/ThreatPrompt.tsx create mode 100644 pages/side-panel/src/approval/evm/index.tsx create mode 100644 pages/side-panel/src/approval/evm/txTypes/eip712.tsx create mode 100644 pages/side-panel/src/approval/evm/txTypes/legacy.tsx create mode 100644 pages/side-panel/src/approval/evm/txTypes/personalSign.tsx create mode 100644 pages/side-panel/src/approval/other/ProjectInfoCard.tsx create mode 100644 pages/side-panel/src/approval/other/RequestDataCard.tsx create mode 100644 pages/side-panel/src/approval/other/RequestDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/other/RequestMethodCard.tsx create mode 100644 pages/side-panel/src/approval/other/index.tsx create mode 100644 pages/side-panel/src/approval/tendermint/ProjectInfoCard.tsx create mode 100644 pages/side-panel/src/approval/tendermint/RequestDataCard.tsx create mode 100644 pages/side-panel/src/approval/tendermint/RequestDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/tendermint/RequestMethodCard.tsx create mode 100644 pages/side-panel/src/approval/tendermint/index.tsx create mode 100644 pages/side-panel/src/approval/utxo/CoinControlCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/ProjectFeeCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/ProjectInfoCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/RequestDataCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/RequestDetailsCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/RequestMethodCard.tsx create mode 100644 pages/side-panel/src/approval/utxo/index.tsx create mode 100644 pages/side-panel/src/approval/utxo/utils.ts create mode 100644 pages/side-panel/src/assets/sounds/chaching.mp3 create mode 100644 pages/side-panel/src/assets/sounds/fail.mp3 create mode 100644 pages/side-panel/src/assets/sounds/send.mp3 create mode 100644 pages/side-panel/src/assets/svg/connect-keepkey.svg create mode 100644 pages/side-panel/src/assets/svg/hold-and-connect.svg create mode 100644 pages/side-panel/src/assets/svg/hold-and-release.svg diff --git a/pages/side-panel/package.json b/pages/side-panel/package.json index 342a733..5cefc0c 100644 --- a/pages/side-panel/package.json +++ b/pages/side-panel/package.json @@ -30,7 +30,10 @@ "date-fns": "^4.1.0", "framer-motion": "^11.5.4", "qrcode": "^1.5.4", - "react-icons": "^5.5.0" + "react-code-blocks": "^0.1.6", + "react-confetti": "^6.1.0", + "react-icons": "^5.5.0", + "react-json-view": "^1.21.3" }, "devDependencies": { "@extension/tailwindcss-config": "workspace:*", diff --git a/pages/side-panel/src/SidePanel.tsx b/pages/side-panel/src/SidePanel.tsx index a8207ba..67ee5cf 100644 --- a/pages/side-panel/src/SidePanel.tsx +++ b/pages/side-panel/src/SidePanel.tsx @@ -23,6 +23,7 @@ import { } from '@chakra-ui/react'; import { ArrowUpIcon, ArrowDownIcon, ChevronLeftIcon } from '@chakra-ui/icons'; import { withErrorBoundary, withSuspense } from '@extension/shared'; +import { requestStorage } from '@extension/storage'; import Connect from './components/Connect'; import Loading from './components/Loading'; @@ -34,6 +35,11 @@ import { Receive } from './components/Receive'; import AssetDetail from './components/AssetDetail'; import DonutChart from './components/DonutChart'; import NetworkAccountHeader from './components/NetworkAccountHeader'; +import Transaction from './approval/Transaction'; + +// Events older than this are dropped on load — an abandoned-tab pending +// request shouldn't hijack the sidebar forever. +const MAX_EVENT_AGE_MINUTES = 10; const HEADER_HEIGHT = '60px'; @@ -46,6 +52,7 @@ const SidePanel = () => { const [isRefreshing, setIsRefreshing] = useState(false); const [selectedAsset, setSelectedAsset] = useState(null); const [balancesInitialLoading, setBalancesInitialLoading] = useState(true); + const [pendingEvent, setPendingEvent] = useState(null); // Disclosures for drawers/modals const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure(); @@ -142,6 +149,41 @@ const SidePanel = () => { } }; + // Subscribe to requestStorage so any dApp-triggered approval request shown + // here takes over the panel as an overlay. Abandoned events beyond the age + // window are evicted on load so a stuck request can't wedge the UI. + const fetchPendingEvent = useCallback(async () => { + try { + const events = (await requestStorage.getEvents()) || []; + const now = Date.now(); + const fresh: any[] = []; + for (const ev of events) { + const ageMs = now - new Date(ev.timestamp).getTime(); + if (ageMs <= MAX_EVENT_AGE_MINUTES * 60_000) { + fresh.push(ev); + } else { + void requestStorage.removeEventById(ev.id); + } + } + // Newest-first — matches popup behavior; user sees the freshest request. + fresh.reverse(); + setPendingEvent(fresh[0] ?? null); + } catch (e) { + console.error('SidePanel: fetchPendingEvent failed', e); + setPendingEvent(null); + } + }, []); + + useEffect(() => { + fetchPendingEvent(); + const unsubscribe = requestStorage.subscribe?.(() => { + fetchPendingEvent(); + }); + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; + }, [fetchPendingEvent]); + // Listen for state changes and external asset context updates (e.g. dApp wallet_addEthereumChain) useEffect(() => { const messageListener = (message: any) => { @@ -255,6 +297,17 @@ const SidePanel = () => { } }; + // Pending dApp approval takes over the panel. We intentionally skip rendering + // the usual header/balances below so the user can't accidentally navigate + // while an approval is live — matches the old popup's singular-focus UX. + if (pendingEvent) { + return ( + + + + ); + } + return ( {/* Sticky header — floats above drawers */} diff --git a/pages/side-panel/src/approval/AwaitingApproval.tsx b/pages/side-panel/src/approval/AwaitingApproval.tsx new file mode 100644 index 0000000..bff7590 --- /dev/null +++ b/pages/side-panel/src/approval/AwaitingApproval.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Flex, Card, CardBody, Image, Heading, Button, CloseButton } from '@chakra-ui/react'; +import holdAndReleaseIcon from '../assets/svg/hold-and-release.svg'; + +const AwaitingApproval = ({ onCancel }: { onCancel: () => void }) => { + return ( + + + {/* Close button in the top-right corner */} + + + + + + Device Signing Request + + KeepKey - Approve on device + + Please approve the transaction on your KeepKey + +
+ or.... +
+
+ +
+
+
+
+ ); +}; + +export default AwaitingApproval; diff --git a/pages/side-panel/src/approval/Transaction.tsx b/pages/side-panel/src/approval/Transaction.tsx new file mode 100644 index 0000000..73cc5f5 --- /dev/null +++ b/pages/side-panel/src/approval/Transaction.tsx @@ -0,0 +1,303 @@ +import React, { useEffect, useState } from 'react'; +import EvmTransaction from './evm'; +import UtxoTransaction from './utxo'; +import OtherTransaction from './other'; +import TendermintTransaction from './tendermint'; +import { approvalStorage, requestStorage } from '@extension/storage/dist/lib'; +import { Flex, Spinner, Alert, AlertIcon, Button, Icon } from '@chakra-ui/react'; +import { WarningIcon } from '@chakra-ui/icons'; +import AwaitingApproval from './AwaitingApproval'; +import TxidPage from './TxidPage'; +import sendSoundFile from '../assets/sounds/send.mp3'; + +// Function to request asset context from background script +const requestAssetContext = () => { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type: 'GET_ASSET_CONTEXT' }, response => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + }); + }); +}; + +const Transaction = ({ + event, + reloadEvents, + onDismiss, +}: { + event: any; + reloadEvents: () => void; + onDismiss: () => void; +}) => { + const [transactionType, setTransactionType] = useState(null); + const [txHash, setTxHash] = useState(null); + const [awaitingDeviceApproval, setAwaitingDeviceApproval] = useState(false); + const [transactionInProgress, setTransactionInProgress] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [showTxidPage, setShowTxidPage] = useState(false); + const [assetContext, setAssetContext] = useState(null); // Local state for asset context + const [explorerUrl, setExplorerUrl] = useState(null); + const [showRefreshWarning, setShowRefreshWarning] = useState(false); + + // Fetch the assetContext on component mount + useEffect(() => { + requestAssetContext() + .then((context: any) => { + setAssetContext(context); // Set asset context state + console.log('assetContext: ', context); + }) + .catch(error => { + console.error('Failed to fetch asset context:', error); + }); + }, []); + + useEffect(() => { + // Explorer URL is now provided directly in the transaction_complete message + // No need to construct it here + }, []); + + const cancelRequest = () => { + chrome.runtime.sendMessage({ type: 'RESET_APP' }, response => { + if (response?.success) { + console.log('Sidebar reset successfully'); + } else { + console.error('Failed to reset the app:', response?.error); + } + }); + + setShowRefreshWarning(true); + }; + + const handleResponse = async (decision: 'accept' | 'reject') => { + try { + chrome.runtime.sendMessage({ action: 'eth_sign_response', response: { decision, eventId: event.id } }); + + if (decision === 'reject') { + await requestStorage.removeEventById(event.id); + reloadEvents(); + } else if (decision === 'accept') { + setAwaitingDeviceApproval(true); + } + } catch (error) { + console.error('Error handling response:', error); + } + }; + + useEffect(() => { + const handleMessage = (message: any) => { + console.log('message received:', message); + // Only handle messages addressed to THIS event. Messages without an + // eventId are legacy/unscoped; accept them for backward compatibility + // so nothing hangs if an older handler is still in flight. + if (message?.eventId && message.eventId !== event.id) return; + if (message.action === 'transaction_complete') { + // Play success sound after device signs transaction + try { + const sendSound = new Audio(sendSoundFile); + sendSound.play(); + } catch (e) { + console.error('Error playing sound:', e); + } + + setShowTxidPage(true); + setTxHash(message.txHash); // Set the txHash from the event + + // Set explorer URL if provided in the message + if (message.explorerTxLink && message.txHash) { + const finalUrl = message.explorerTxLink + message.txHash; + setExplorerUrl(finalUrl); + console.log('Explorer URL from message:', finalUrl); + } + + setAwaitingDeviceApproval(false); + setTransactionInProgress(false); + } else if (message.action === 'signature_complete') { + // Play success sound after device signs message + try { + const sendSound = new Audio(sendSoundFile); + sendSound.play(); + } catch (e) { + console.error('Error playing sound:', e); + } + + // For signatures (not transactions), clean up and dismiss the overlay + console.log('Signature complete, dismissing approval overlay'); + setAwaitingDeviceApproval(false); + setTransactionInProgress(false); + + // Remove the event from storage and dismiss overlay + requestStorage + .removeEventById(event.id) + .then(() => { + setTimeout(() => { + onDismiss(); + }, 500); + }) + .catch(error => { + console.error('Error removing event:', error); + setTimeout(() => { + onDismiss(); + }, 500); + }); + } else if (message.action === 'transaction_error') { + const errorDetails = message.error || message.e?.message || JSON.stringify(message.e); + const errorText = 'Transaction failed: ' + errorDetails; + + // If user denied the transaction, just close the popup + if ( + errorDetails.includes('User denied') || + errorDetails.includes('user rejected') || + errorDetails.includes('User rejected') + ) { + console.log('User denied transaction, dismissing approval overlay'); + requestStorage + .removeEventById(event.id) + .then(() => { + setTimeout(() => { + onDismiss(); + }, 1000); + }) + .catch(() => { + setTimeout(() => { + onDismiss(); + }, 1000); + }); + } else { + // Show error for other types of failures + setErrorMessage(errorText); + setTransactionInProgress(false); + } + } + }; + + chrome.runtime.onMessage.addListener(handleMessage); + + return () => { + chrome.runtime.onMessage.removeListener(handleMessage); + }; + }, [event.id]); + + useEffect(() => { + console.log('event:', event); + if (event?.networkId) { + if (event.networkId.includes('eip155')) { + setTransactionType('evm'); + } else { + switch (event.chain) { + case 'bitcoin': + case 'bitcoincash': + case 'dogecoin': + case 'litecoin': + case 'dash': + setTransactionType('utxo'); + break; + case 'cosmos': + case 'thorchain': + case 'osmosis': + case 'mayachain': + setTransactionType('tendermint'); + break; + case 'ripple': + setTransactionType('other'); + break; + case 'solana': + setTransactionType('other'); + break; + default: + setTransactionType('unknown'); + } + } + } + }, [event]); + + const handleCloseTab = async () => { + if (txHash) { + try { + const updatedEvent = { ...event, status: 'approval', txHash }; + await requestStorage.removeEventById(event.id); + await approvalStorage.addEvent(updatedEvent); + reloadEvents(); + onDismiss(); + } catch (error) { + console.error('Error closing tab and storing event:', error); + } + } + }; + + const handleCancel = () => { + cancelRequest(); + setAwaitingDeviceApproval(false); + setTransactionInProgress(false); + reloadEvents(); + }; + + const renderTransaction = () => { + console.log('transactionType:', transactionType); + switch (transactionType) { + case 'evm': + return ; + case 'tendermint': + return ; + case 'utxo': + return ; + case 'other': + return ; + default: + return
Unknown Transaction Type {transactionType}
; + } + }; + + if (errorMessage) { + return ( + + + + +

Error Occurred

+

{errorMessage}

+ + + +
+
+
+ ); + } + + if (showTxidPage && txHash) { + // Show the txid page if the transaction is complete + // explorerUrl is optional - TxidPage can handle it being undefined + return ; + } + + return ( +
+ {transactionInProgress && } + + {!awaitingDeviceApproval && renderTransaction()} + + {awaitingDeviceApproval && } + + {showRefreshWarning && ( + + +

You must refresh the dApp page to reconnect your wallet.

+
+ )} +
+ ); +}; + +export default Transaction; diff --git a/pages/side-panel/src/approval/TxidPage.tsx b/pages/side-panel/src/approval/TxidPage.tsx new file mode 100644 index 0000000..5d24423 --- /dev/null +++ b/pages/side-panel/src/approval/TxidPage.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Text, + Icon, + Button, + Card, + CardBody, + Divider, + IconButton, + Tooltip, + useClipboard, + Flex, + Link, +} from '@chakra-ui/react'; +import { CheckCircleIcon, CopyIcon, ExternalLinkIcon } from '@chakra-ui/icons'; +import Confetti from 'react-confetti'; + +const TxidPage = ({ txHash, explorerUrl, onClose }: { txHash: string; explorerUrl?: string; onClose?: () => void }) => { + const [showConfetti, setShowConfetti] = useState(true); + const { hasCopied, onCopy } = useClipboard(txHash); + + useEffect(() => { + const confettiTimer = setTimeout(() => { + setShowConfetti(false); + }, 5000); + return () => clearTimeout(confettiTimer); + }, []); + + const handleExplorerClick = () => { + if (explorerUrl) { + window.open(explorerUrl, '_blank', 'noopener,noreferrer'); + } + }; + + const handleClose = () => { + onClose?.(); + }; + + const truncatedHash = txHash.length > 20 ? `${txHash.slice(0, 10)}...${txHash.slice(-10)}` : txHash; + + return ( + + + + {showConfetti && } + + + + + Transaction Complete + + + + + + Transaction Hash + + + + {explorerUrl ? ( + + {truncatedHash} + + + ) : ( + + {truncatedHash} + + )} + + + } + onClick={onCopy} + size="xs" + colorScheme={hasCopied ? 'green' : 'gray'} + variant="ghost" + /> + + + + + {explorerUrl && ( + + )} + + + + + + + ); +}; + +export default TxidPage; diff --git a/pages/side-panel/src/approval/evm/ChainSelect.tsx b/pages/side-panel/src/approval/evm/ChainSelect.tsx new file mode 100644 index 0000000..e69de29 diff --git a/pages/side-panel/src/approval/evm/ContractDetailsCard.tsx b/pages/side-panel/src/approval/evm/ContractDetailsCard.tsx new file mode 100644 index 0000000..4a1cf3a --- /dev/null +++ b/pages/side-panel/src/approval/evm/ContractDetailsCard.tsx @@ -0,0 +1,174 @@ +import { + Box, + Heading, + Text, + Avatar, + Flex, + Alert, + AlertIcon, + Spinner, + Card, + CardHeader, + CardBody, + Button, + Collapse, +} from '@chakra-ui/react'; +import { CheckIcon, WarningIcon } from '@chakra-ui/icons'; +import { useState } from 'react'; + +interface TransactionRequest { + from: string; + to: string; + data: string; +} + +interface Transaction { + request: TransactionRequest; +} + +interface ContractDetailsCardProps { + transaction: Transaction; +} + +// Fixing the requestSmartInsight to pass correct transaction details +const requestSmartInsight = (transaction: Transaction) => { + return new Promise((resolve, reject) => { + //TODO handle 712 data + chrome.runtime.sendMessage( + { tx: transaction.request, source: transaction?.request?.from, type: 'GET_TX_INSIGHT' }, + response => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + }, + ); + }); +}; + +export default function ContractDetailsCard({ transaction }: ContractDetailsCardProps) { + const [apiResponse, setApiResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + + const handleGetInsight = async () => { + try { + if (!transaction) { + throw new Error('Transaction data is missing.'); + } + + setIsLoading(true); + setApiResponse(null); + + const response: any = await requestSmartInsight(transaction); + + if (!response) { + setApiResponse(null); + } else { + setApiResponse(response); + } + } catch (error: any) { + console.error('Error fetching API response:', error); + + let errorMessage = 'Failed to fetch API response.'; + if (error.message) { + errorMessage = error.message; + } + + setApiResponse({ error: errorMessage }); + } finally { + setIsLoading(false); + } + }; + + // Determine recommended action + const recommendedAction = apiResponse?.pioneer?.recommendation?.toUpperCase() || 'UNKNOWN'; + + // Function to render the summary properly + const renderSummary = (summary: string) => { + return ( + + + Summary + + {summary} + + ); + }; + + return ( + + + + + + Pioneer Summary: + + + + + {!apiResponse && !isLoading && ( + + )} + {isLoading ? ( + + + + ) : apiResponse?.error ? ( + + + {apiResponse.error} + + ) : apiResponse ? ( + <> + {/* Recommended Action Box */} + {recommendedAction === 'ALLOW' ? ( + + + + + ALLOW + + + + ) : recommendedAction === 'REJECT' ? ( + + + + + REJECT + + + + ) : ( + + + Warning: Potential issues detected. + + )} + + {/* Summary */} + {apiResponse?.pioneer?.summary && renderSummary(apiResponse.pioneer.summary)} + + {/* Advanced Section */} + + + + + Raw API Response + +
+                  {JSON.stringify(apiResponse, null, 2)}
+                
+
+
+ + ) : null} +
+
+ ); +} diff --git a/pages/side-panel/src/approval/evm/HarpyDetailsCard.tsx b/pages/side-panel/src/approval/evm/HarpyDetailsCard.tsx new file mode 100644 index 0000000..630fda4 --- /dev/null +++ b/pages/side-panel/src/approval/evm/HarpyDetailsCard.tsx @@ -0,0 +1,258 @@ +import { + Box, + Heading, + Text, + Avatar, + Flex, + Alert, + AlertIcon, + Spinner, + Card, + CardHeader, + CardBody, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Badge, + VStack, + HStack, +} from '@chakra-ui/react'; +import { CheckIcon } from '@chakra-ui/icons'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; + +interface TransactionRequest { + from: string; + to: string; + data: string; +} + +interface Transaction { + request: TransactionRequest; +} + +interface ContractDetailsCardProps { + transaction: Transaction; +} + +export default function ContractDetailsCard({ transaction }: ContractDetailsCardProps) { + const [apiResponse, setApiResponse] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // const harpieLogoUrl = + // 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTWg0ouWVCbQHXGmOxH2pMnL9B0S8DA9pnapogVb3JxicS1sni0pwLWQO0M5UO4hiVjr9c&usqp=CAU'; + // + // useEffect(() => { + // const fetchApiResponse = async () => { + // try { + // if (!transaction) { + // throw new Error('Transaction data is missing.'); + // } + // + // // Ensure transaction.request is used as per your requirement + // const tx = { ...transaction.request }; + // tx.from = '0x141D9959cAe3853b035000490C03991eB70Fc4aC'; + // console.log('tx: ', tx); + // + // const response = await axios.post( + // 'http://127.0.0.1:9001/api/v1/checkTx', + // { tx }, + // { + // headers: { + // 'Content-Type': 'application/json', + // Authorization: `Bearer keepkey-client-v1`, + // }, + // validateStatus: status => status < 500, // Resolve only if the status code is less than 500 + // }, + // ); + // + // console.log('response: ', response); + // + // if (response.status === 204) { + // // No Content + // setApiResponse(null); + // } else { + // const data = response.data; + // console.log('data: ', data); + // setApiResponse(data); + // } + // } catch (error: any) { + // console.error('Error fetching API response:', error); + // + // let errorMessage = 'Failed to fetch API response.'; + // if (error.response) { + // errorMessage = `API error: ${error.response.status} ${error.response.statusText}`; + // } else if (error.request) { + // errorMessage = 'No response received from the server.'; + // } else if (error.message) { + // errorMessage = error.message; + // } + // + // setApiResponse({ error: errorMessage }); + // } finally { + // setIsLoading(false); + // } + // }; + // fetchApiResponse(); + // }, [transaction]); + + // Determine if there are any alert flags + const flags = apiResponse?.addressDetails?.tags || {}; + const hasAlerts = Object.keys(flags).length > 0 && Object.values(flags).some(value => value === true); + + // Determine recommended action + const recommendedAction = apiResponse?.recommendedAction || 'UNKNOWN'; + + // Recursive function to render nested objects safely + const renderResponseFields = (obj: any) => { + if (typeof obj !== 'object' || obj === null) { + return null; + } + + return ( + + {Object.entries(obj).map(([key, value]) => { + const isObject = typeof value === 'object' && value !== null; + const displayValue = typeof value === 'boolean' ? (value ? 'Yes' : 'No') : String(value); + + return ( + + {isObject ? ( + + + {key.charAt(0).toUpperCase() + key.slice(1)} + + {renderResponseFields(value)} + + ) : ( + + + {key}: + + {displayValue} + + )} + + ); + })} + + ); + }; + + // Function to render the summary properly + const renderSummary = (summary: string) => { + return ( + + + Summary + + {summary} + + ); + }; + + return ( + + + + + + Harpie Analysis + + + + + {isLoading ? ( + + + + ) : apiResponse?.error ? ( + + + {apiResponse.error} + + ) : ( + <> + {/* Recommended Action Box or Warning Alert */} + {recommendedAction === 'ALLOW' ? ( + + + + + ALLOW + + + + ) : hasAlerts ? ( + + + Warning: Potential issues detected. + + ) : null} + + {apiResponse ? ( + + {/* Summary Section */} + {apiResponse.summary && typeof apiResponse.summary === 'string' && ( + + {renderSummary(apiResponse.summary)} + + )} + + {/*/!* Address Details Section *!/*/} + {/*{apiResponse.addressDetails && (*/} + {/* */} + {/* */} + {/* */} + {/* Address Details*/} + {/* */} + {/* */} + {/* */} + {/* {renderResponseFields(apiResponse.addressDetails)}*/} + {/* */} + {/* */} + {/*)}*/} + + {/* Tags Section */} + {apiResponse.addressDetails?.tags && ( + + + + Tags + + + + + + + + + + + + {Object.entries(apiResponse.addressDetails.tags).map(([tag, status]) => ( + + + + + ))} + +
TagStatus
{tag} + {status ? Yes : No} +
+
+
+ )} +
+ ) : ( + No data available. + )} + + )} +
+
+ ); +} diff --git a/pages/side-panel/src/approval/evm/ProjectInfoCard.tsx b/pages/side-panel/src/approval/evm/ProjectInfoCard.tsx new file mode 100644 index 0000000..9970a24 --- /dev/null +++ b/pages/side-panel/src/approval/evm/ProjectInfoCard.tsx @@ -0,0 +1,94 @@ +import { useMemo, useEffect, useState } from 'react'; +import { Avatar, Box, Text, VStack, Stack, Badge, Image } from '@chakra-ui/react'; + +const KEEPKEY_LOGO = '/kk-logo.png'; +const KEEPKEY_LOGO_FALLBACK = '/icon-128.png'; + +interface IProps { + transaction: any; +} + +export default function ProjectInfoCard({ transaction }: IProps) { + const [faviconUrl, setFaviconUrl] = useState(null); + const [logoError, setLogoError] = useState(false); + const isKeepKeyExtension = transaction?.siteUrl === 'KeepKey Browser Extension'; + + // Clean the URL to extract the domain + const cleanUrl = useMemo(() => { + try { + const urlObj = new URL(transaction?.siteUrl); + return `${urlObj.protocol}//${urlObj.hostname}`; + } catch (error) { + console.error('Invalid URL', error); + return null; + } + }, [transaction?.siteUrl]); + + // Attempt to fetch the favicon from the cleaned URL or handle the KeepKey Browser Extension case + useEffect(() => { + setLogoError(false); // Reset error state for new URL + if (isKeepKeyExtension) { + setFaviconUrl(KEEPKEY_LOGO); + } else if (cleanUrl) { + const favicon = `${cleanUrl}/favicon.ico`; + setFaviconUrl(favicon); + } + }, [cleanUrl, isKeepKeyExtension]); + + // Get the appropriate logo src + const getLogoSrc = () => { + if (logoError) return KEEPKEY_LOGO_FALLBACK; + return faviconUrl || KEEPKEY_LOGO; + }; + + return ( + + + {/* Logo - square for KeepKey, round avatar for dApps */} + {isKeepKeyExtension ? ( + KeepKey setLogoError(true)} + /> + ) : ( + setLogoError(true)} /> + )} + + {/* Sub Avatar for KeepKey logo - only show if not already KeepKey */} + {!isKeepKeyExtension && ( + + )} + + + {/* For KeepKey extension, just show "wants to" since logo has the name */} + {isKeepKeyExtension ? ( + + wants to{' '} + + {transaction.type} + + + ) : ( + + {cleanUrl} + + wants to {transaction.type} + + + )} + + + ); +} diff --git a/pages/side-panel/src/approval/evm/RequestDataCard.tsx b/pages/side-panel/src/approval/evm/RequestDataCard.tsx new file mode 100644 index 0000000..ce60cb4 --- /dev/null +++ b/pages/side-panel/src/approval/evm/RequestDataCard.tsx @@ -0,0 +1,84 @@ +import { Box, Heading, IconButton, Button, Spinner } from '@chakra-ui/react'; +import { CodeBlock, codepen } from 'react-code-blocks'; +import { useState } from 'react'; +import { ChevronDownIcon, ChevronRightIcon } from '@chakra-ui/icons'; +import { requestStorage } from '@extension/storage'; // Import the requestStorage + +/** + * Component + */ +export default function RequestDataCard({ transaction }: any) { + const [isOpen, setIsOpen] = useState(false); + const [fetchedTransaction, setFetchedTransaction] = useState(transaction.unsignedTx); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Toggle visibility + const toggleVisibility = () => { + setIsOpen(!isOpen); + }; + + // Function to fetch event data from storage + const fetchEventData = async () => { + setLoading(true); + setError(null); + + try { + // Fetch the transaction from storage using its ID + const response = await requestStorage.getEventById(transaction.id); + if (response) { + setFetchedTransaction(response.unsignedTx); // Update the transaction data with the fetched data + } else { + setError('Transaction not found in storage'); + } + } catch (err) { + console.error('Error fetching event data:', err); + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + + return ( + + + : } + aria-label="Toggle data visibility" + variant="ghost" + size="sm" + mr={2} + /> + + Data + + + + {isOpen && ( + + {/* Fetch Data Button */} + + + {/* Display error if any */} + {error && ( + + {error} + + )} + + {/* Conditionally render the code block */} + + + + + )} + + ); +} diff --git a/pages/side-panel/src/approval/evm/RequestDetailsCard.tsx b/pages/side-panel/src/approval/evm/RequestDetailsCard.tsx new file mode 100644 index 0000000..a48b814 --- /dev/null +++ b/pages/side-panel/src/approval/evm/RequestDetailsCard.tsx @@ -0,0 +1,59 @@ +import { useState, useEffect } from 'react'; +import { Box, Spinner, Flex } from '@chakra-ui/react'; +import React, { Fragment } from 'react'; +import LegacyTx from './txTypes/legacy'; +import Eip712Tx from './txTypes/eip712'; + +// Function to request asset context from background script +const requestAssetContext = () => { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type: 'GET_ASSET_CONTEXT' }, response => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + }); + }); +}; + +export default function RequestDetailsCard({ transaction }: any) { + const [price, setPrice] = useState(null); + const [isNative, setIsNative] = useState(true); // Toggle for hex/native + const [usdValue, setUsdValue] = useState(''); + + useEffect(() => { + // Request the asset context from the background script + requestAssetContext() + .then((assetContext: any) => { + console.log('assetContext: ', assetContext); + setPrice(assetContext?.assets?.priceUsd); // Assume priceUsd is the key for USD price + }) + .catch(err => console.error(err)); + }, []); + + const toggleHexNative = () => { + setIsNative(!isNative); + }; + + const renderTx = () => { + switch (transaction?.type) { + case 'eth_signTypedData_v4': + case 'eth_signTypedData_v3': + case 'eth_signTypedData': + return ; + default: + return ; + } + }; + + if (!transaction?.unsignedTx) { + // Show spinner if transaction.unsignedTx is not set + return ( + + + + ); + } + + return {renderTx()}; +} diff --git a/pages/side-panel/src/approval/evm/RequestFeeCard.tsx b/pages/side-panel/src/approval/evm/RequestFeeCard.tsx new file mode 100644 index 0000000..a4e6e1b --- /dev/null +++ b/pages/side-panel/src/approval/evm/RequestFeeCard.tsx @@ -0,0 +1,368 @@ +import React, { useState, useEffect, Fragment } from 'react'; + +import { + FormControl, + RadioGroup, + Radio, + Text, + Alert, + Button, + AlertTitle, + Box, + Switch, + Heading, + Input, + Spinner, + Badge, + InputGroup, + InputLeftAddon, +} from '@chakra-ui/react'; +import { requestStorage } from '@extension/storage'; +const TAG = ' | RequestFeeCard | '; + +const requestFeeData = () => { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type: 'GET_GAS_ESTIMATE' }, response => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + }); + }); +}; + +const requestAssetContext = () => { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type: 'GET_ASSET_CONTEXT' }, response => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(response); + }); + }); +}; + +// const updateEventById = async (id, updatedTransaction) => { +// return new Promise((resolve, reject) => { +// chrome.runtime.sendMessage( +// { type: 'UPDATE_EVENT_BY_ID', payload: { id, updatedEvent: updatedTransaction } }, +// response => { +// if (chrome.runtime.lastError) { +// return reject(chrome.runtime.lastError); +// } +// resolve(response); +// }, +// ); +// }); +// }; + +const hexToDecimal = hex => { + return parseInt(hex, 16); +}; + +const decimalToHex = decimal => { + return '0x' + BigInt(decimal).toString(16); +}; + +const RequestFeeCard = ({ transaction }) => { + const [selectedFee, setSelectedFee] = useState(''); + const [customFee, setCustomFee] = useState(''); + const [dappProvidedFee, setDappProvidedFee] = useState(false); + const [displayFee, setDisplayFee] = useState(''); + const [feeWarning, setFeeWarning] = useState(false); + const [isEIP1559, setIsEIP1559] = useState(false); + const [fees, setFees] = useState({ + dappSuggested: '', + low: '', + medium: '', + high: '', + }); + const [loading, setLoading] = useState(true); + const [usdFee, setUsdFee] = useState(''); + const [assetContext, setAssetContext] = useState(null); + + const gasLimit = transaction.request.gasLimit ? hexToDecimal(transaction.request.gasLimit) : 21000; + + useEffect(() => { + const fetchAssetContext = async () => { + try { + const context = await requestAssetContext(); + console.log('RequestFeeCard - Full asset context:', context); + console.log('RequestFeeCard - Assets:', context?.assets); + console.log('RequestFeeCard - Price USD:', context?.assets?.priceUsd); + setAssetContext(context.assets); + } catch (error) { + console.error('Error fetching asset context:', error); + } + }; + fetchAssetContext(); + }, []); + + const calculateUsdValue = gweiFee => { + if (!assetContext || !assetContext.priceUsd) { + console.error('assetContext: ', assetContext); + console.error('Missing Price Data for Native gas asset!'); + return '0.00'; + } + + const feeInETH = parseFloat(gweiFee) * gasLimit * 1e-9; + const feeInUSD = feeInETH * parseFloat(assetContext.priceUsd); + return feeInUSD.toFixed(2); + }; + + const getFee = async () => { + const tag = TAG + ' | getFee | '; + setLoading(true); + try { + const feeData = await requestFeeData(); + console.log(tag, ' feeData: ', feeData); + + // feeData.gasPrice appears to already be in wei (not gwei) + const networkGasPriceWei = BigInt(feeData.gasPrice); + console.log(tag, ' feeData: ', feeData); + console.log(tag, ' networkGasPriceWei: ', networkGasPriceWei); + + // Calculate low, medium, and high gas prices in wei + const lowGasPriceWei = (networkGasPriceWei * BigInt(80)) / BigInt(100); // 80% + const mediumGasPriceWei = networkGasPriceWei; + const highGasPriceWei = (networkGasPriceWei * BigInt(120)) / BigInt(100); // 120% + console.log(tag, ' lowGasPriceWei: ', lowGasPriceWei); + console.log(tag, ' mediumGasPriceWei: ', mediumGasPriceWei); + console.log(tag, ' highGasPriceWei: ', highGasPriceWei); + + // Convert from wei to gwei for display (divide by 1e9) + // If the values are small (< 1000000), they might already be in gwei + const isAlreadyGwei = Number(networkGasPriceWei) < 1000000; + const divider = isAlreadyGwei ? 1 : 1e9; + + const feeSettings = { + dappSuggested: fees.dappSuggested, + low: Math.floor(Number(lowGasPriceWei) / divider).toString(), + medium: Math.floor(Number(mediumGasPriceWei) / divider).toString(), + high: Math.floor(Number(highGasPriceWei) / divider).toString(), + }; + console.log(tag, ' feeSettings: ', feeSettings); + setFees(feeSettings); + + setFeeWarning(false); + } catch (e) { + console.error('Error fetching fee data:', e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const isEthereumMainnet = transaction.networkId === 'eip155:1'; + setIsEIP1559(isEthereumMainnet); + + if ( + !transaction.request.maxPriorityFeePerGas && + !transaction.request.maxFeePerGas && + !transaction.request.gasPrice + ) { + getFee(); + setDappProvidedFee(false); + setSelectedFee('medium'); + } else { + const dappGasPrice = BigInt(hexToDecimal(transaction.request.gasPrice || '0x0')); + const dappGasPriceGwei = (dappGasPrice / BigInt(1e9)).toString(); + + setDappProvidedFee(true); + setFees(prevFees => ({ + ...prevFees, + dappSuggested: dappGasPriceGwei, + })); + + if (!fees.medium) { + getFee(); + } + + setSelectedFee('dappSuggested'); + } + }, [transaction, assetContext]); + + useEffect(() => { + let feeInGwei = ''; + if (selectedFee === 'custom') { + feeInGwei = customFee; + } else { + feeInGwei = fees[selectedFee] || ''; + } + + setDisplayFee(feeInGwei); + + if (feeInGwei) { + console.log('feeInGwei: ', feeInGwei); + const feeInUsd = calculateUsdValue(feeInGwei); + console.log('feeInUsd: ', feeInUsd); + + setUsdFee(feeInUsd); + } else { + setUsdFee(''); + } + + if (feeInGwei && selectedFee !== 'custom') { + handleUpdateTransaction(feeInGwei); + } + }, [selectedFee, customFee, fees, assetContext]); + + const handleFeeChange = value => { + setSelectedFee(value); + }; + + const handleCustomFeeChange = event => { + setCustomFee(event.target.value); + }; + + const handleSubmit = () => { + const feeInGwei = selectedFee === 'custom' ? customFee : fees[selectedFee]; + handleUpdateTransaction(feeInGwei); + }; + + const handleUpdateTransaction = async feeInGwei => { + let selectedFeeData = {}; + if (isEIP1559) { + const baseFeeInWei = BigInt(feeInGwei) * BigInt(1e9); + const priorityFeeInWei = BigInt(2 * 1e9); + const maxFeeInWei = baseFeeInWei + priorityFeeInWei; + + selectedFeeData = { + maxFeePerGas: decimalToHex(maxFeeInWei), + maxPriorityFeePerGas: decimalToHex(priorityFeeInWei), + }; + + // Remove gasPrice from request and requestInfo.params[0] + delete transaction.request.gasPrice; + delete transaction.requestInfo.params[0].gasPrice; + + // Set maxFeePerGas and maxPriorityFeePerGas in request and requestInfo.params[0] + transaction.request.maxFeePerGas = selectedFeeData.maxFeePerGas; + transaction.request.maxPriorityFeePerGas = selectedFeeData.maxPriorityFeePerGas; + + transaction.requestInfo.params[0].maxFeePerGas = selectedFeeData.maxFeePerGas; + transaction.requestInfo.params[0].maxPriorityFeePerGas = selectedFeeData.maxPriorityFeePerGas; + + // Remove gasPrice from top-level transaction + delete transaction.gasPrice; + } else { + const gasPriceInWei = BigInt(feeInGwei) * BigInt(1e9); + + selectedFeeData = { + gasPrice: decimalToHex(gasPriceInWei), + }; + + // Set gasPrice in request and requestInfo.params[0] + transaction.request.gasPrice = selectedFeeData.gasPrice; + transaction.requestInfo.params[0].gasPrice = selectedFeeData.gasPrice; + + // Remove maxFeePerGas and maxPriorityFeePerGas from request and requestInfo.params[0] + delete transaction.request.maxFeePerGas; + delete transaction.request.maxPriorityFeePerGas; + + delete transaction.requestInfo.params[0].maxFeePerGas; + delete transaction.requestInfo.params[0].maxPriorityFeePerGas; + + // Set gasPrice in top-level transaction + transaction.request.gasPrice = selectedFeeData.gasPrice; + + // Remove maxFeePerGas and maxPriorityFeePerGas from top-level transaction + delete transaction.maxFeePerGas; + delete transaction.maxPriorityFeePerGas; + } + + // + requestStorage.updateEventById(transaction.id, transaction); + }; + + return ( + + {(loading || !assetContext) && } + + {!loading && ( + + Current Fee: {displayFee ? `${displayFee} Gwei ($${usdFee} USD)` : 'No fee selected'} + Current Limit: {gasLimit} + + )} + + {!loading && !dappProvidedFee && ( + + Please select a fee option below: + + )} + + {feeWarning && ( + + Warning + DApp suggested fee is lower than the network recommended fee. + + )} + + {!loading && assetContext && ( + + + {dappProvidedFee && fees.dappSuggested && ( + + DApp Suggested Fee ({fees.dappSuggested} Gwei){' '} + ${calculateUsdValue(fees.dappSuggested)} USD + + )} + + Low ({fees.low} Gwei) ${calculateUsdValue(fees.low)} USD + + + Medium ({fees.medium} Gwei) ${calculateUsdValue(fees.medium)} USD + + + High ({fees.high} Gwei) ${calculateUsdValue(fees.high)} USD + +
+ + Custom Fee + +
+ {selectedFee === 'custom' && ( + + + + + )} +
+ )} + + {selectedFee === 'custom' && ( + + + + )} + + {/**/} + {/* */} + {/* */} + {/* Use EIP-1559:*/} + {/* */} + {/* setIsEIP1559(!isEIP1559)}*/} + {/* colorScheme={isEIP1559 ? 'blue' : 'gray'}*/} + {/* />*/} + {/* */} + {/**/} +
+ ); +}; + +export default RequestFeeCard; diff --git a/pages/side-panel/src/approval/evm/RequestMethodCard.tsx b/pages/side-panel/src/approval/evm/RequestMethodCard.tsx new file mode 100644 index 0000000..5e55641 --- /dev/null +++ b/pages/side-panel/src/approval/evm/RequestMethodCard.tsx @@ -0,0 +1,92 @@ +import { Box, Flex, Text, Heading, Icon } from '@chakra-ui/react'; +import { CheckCircleIcon, WarningIcon, InfoIcon, QuestionIcon } from '@chakra-ui/icons'; + +const getMethodInfo = (txType: string, hasSmartContractExecution: boolean) => { + switch (txType) { + case 'eth_sign': + return { + title: 'Legacy Method', + description: 'Sign data', + icon: , + color: 'gray.500', + }; + + case 'personal_sign': + return { + title: 'Safe Method', + description: 'Does not move funds', + icon: , + color: 'green.400', + }; + + case 'transfer': + return { + title: 'Transaction', + description: 'Simple transfer - no smart contract interaction', + icon: , + color: 'green.400', + }; + case 'eth_sendTransaction': + case 'eth_signTransaction': + return { + title: hasSmartContractExecution ? 'Smart Contract' : 'Transaction', + description: hasSmartContractExecution + ? 'Interacts with smart contract - review carefully' + : 'Simple transfer - no smart contract interaction', + icon: hasSmartContractExecution ? ( + + ) : ( + + ), + color: hasSmartContractExecution ? 'yellow.400' : 'green.400', + }; + + case 'eth_signTypedData': + case 'eth_signTypedData_v3': + case 'eth_signTypedData_v4': + return { + title: 'Typed Data Transaction', + description: 'This transaction has smart contract execution, requires extended validation', + icon: , + color: 'yellow.400', + }; + + default: + return { + title: 'Unknown Method', + description: 'Verify before proceeding', + icon: , + color: 'red.500', + }; + } +}; + +/** + * Component + */ +export default function RequestMethodCard({ transaction }: any) { + const hasSmartContractExecution = + transaction.request?.data && transaction.request.data.length > 0 && transaction.request.data !== '0x'; + + const { title, description, icon, color } = getMethodInfo(transaction.type, hasSmartContractExecution); + + return ( + + + {icon && ( + + {icon} + + )} + + {title} + + + + + {description} + + + + ); +} diff --git a/pages/side-panel/src/approval/evm/ThreatPrompt.tsx b/pages/side-panel/src/approval/evm/ThreatPrompt.tsx new file mode 100644 index 0000000..1cee5da --- /dev/null +++ b/pages/side-panel/src/approval/evm/ThreatPrompt.tsx @@ -0,0 +1,45 @@ +import { Box, Divider, Link, Text, Flex, Button } from '@chakra-ui/react'; +import { WarningIcon } from '@chakra-ui/icons'; + +import RequestModalContainer from './RequestModalContainer'; + +interface IProps { + metadata: { + icons: string[]; + name: string; + url: string; + }; + onApprove: () => void; + onReject: () => void; +} + +export default function ThreatPrompt({ metadata, onApprove, onReject }: IProps) { + const { url } = metadata; + + return ( + + + + + + + Website flagged + + + {url} + + + + This website you`re trying to connect is flagged as malicious by multiple security providers. Approving may + lead to loss of funds. + + + + + + ); +} diff --git a/pages/side-panel/src/approval/evm/index.tsx b/pages/side-panel/src/approval/evm/index.tsx new file mode 100644 index 0000000..898d8ed --- /dev/null +++ b/pages/side-panel/src/approval/evm/index.tsx @@ -0,0 +1,81 @@ +import { + Avatar, + Button, + Card, + CardHeader, + Flex, + Spinner, + Stack, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + Divider, +} from '@chakra-ui/react'; +import React, { useEffect, useState } from 'react'; +import RequestFeeCard from './RequestFeeCard'; +import RequestDataCard from './RequestDataCard'; +import RequestDetailsCard from './RequestDetailsCard'; +import ContractDetailsCard from './ContractDetailsCard'; +import RequestMethodCard from './RequestMethodCard'; +import ProjectInfoCard from './ProjectInfoCard'; + +export function EvmTransaction({ transaction, reloadEvents, handleResponse }: any) { + return ( + + + + + + + + + {/*Insight*/} + Details + Fees + Raw + + + + {/* Contract Tab */} + {/**/} + {/* */} + {/**/} + + {/* Review Tab */} + + + + + {/* Fees Tab */} + + {transaction.type !== 'personal_sign' && ( + <> + + + )} + + + {/* Raw Data Tab */} + + + + + + + + + + + + + + ); +} + +export default EvmTransaction; diff --git a/pages/side-panel/src/approval/evm/txTypes/eip712.tsx b/pages/side-panel/src/approval/evm/txTypes/eip712.tsx new file mode 100644 index 0000000..08a9896 --- /dev/null +++ b/pages/side-panel/src/approval/evm/txTypes/eip712.tsx @@ -0,0 +1,70 @@ +import { Badge, Box, Divider, Flex, HStack, Switch, Table, Tbody, Td, Text, Textarea, Tr } from '@chakra-ui/react'; +import React, { useState } from 'react'; + +export default function Eip712Tx({ transaction }: any) { + const [isNative, setIsNative] = useState(true); + const toggleHexNative = () => setIsNative(!isNative); + + // Assuming the EIP-712 typed data is in transaction.requestInfo.params[1] + const typedData = JSON.parse(transaction?.requestInfo?.params[1]); + + // Example logic for handling the contract address and signer in EIP-712 + const contractAddress = typedData?.domain?.verifyingContract; + const signer = transaction?.requestInfo?.params[0]; // Typically the signer is in the first param of `eth_signTypedData_v4` + const spender = typedData?.message?.spender || 'Unknown'; + + // Handle the native value from the typed data (if applicable) + const ethValue = typedData?.message?.details?.amount; + const nativeValue = parseFloat(parseInt(ethValue || '0', 10).toString()) / 1e18; + + return ( + + + + + + + + + + + + + + + + + + +
+ Signer: + {signer || 'Unknown'}
+ Contract: + {contractAddress || 'Unknown'}
+ Spender: + {spender}
+ Data: + + {/* Display a non-editable Textarea for large data payloads */} +