diff --git a/.gitignore b/.gitignore index da8df9f..f984719 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ node_modules .vscode/ + +# Build output +build/ +dist/ diff --git a/src/App.css b/src/App.css index 1240ef6..2936f4f 100644 --- a/src/App.css +++ b/src/App.css @@ -1,4 +1,4 @@ -/* Global Styles */ +/* ===== GLOBAL STYLES ===== */ html, body { height: 100vh; overflow: hidden; @@ -26,13 +26,12 @@ html, body { box-sizing: border-box; } -/* Tab Navigation */ +/* ===== TAB NAVIGATION ===== */ .tab-navigation { display: flex; justify-content: center; gap: 0; - margin-bottom: 40px; - margin-top: 80px; + margin: 80px 0 40px 0; } .tab-button { @@ -65,15 +64,55 @@ html, body { color: #282c34; } - -/* Tab Content */ .tab-content { width: 100%; max-width: 600px; padding: 20px; } -/* Shared Component Styles */ +/* ===== SIMULATION TAB NAVIGATION ===== */ +.simulation-tab-navigation { + display: flex; + justify-content: center; + gap: 0; + margin-bottom: 30px; +} + +.simulation-tab-button { + padding: 12px 40px; + font-size: 16px; + font-weight: bold; + background-color: #555; + color: #ffffff; + border: none; + cursor: pointer; + transition: all 0.3s ease; + border-radius: 0; + min-width: 150px; +} + +.simulation-tab-button:first-child { + border-radius: 8px 0 0 8px; +} + +.simulation-tab-button:last-child { + border-radius: 0 8px 8px 0; +} + +.simulation-tab-button:hover { + background-color: #666; +} + +.simulation-tab-button.active { + background-color: #61dafb; + color: #282c34; +} + +.simulation-tab-content { + width: 100%; +} + +/* ===== SHARED COMPONENTS ===== */ .component-container { width: 600px; max-width: 100%; @@ -81,7 +120,112 @@ html, body { text-align: center; } -/* Textarea styles */ +/* ===== BUTTONS ===== */ +.component-button { + padding: 15px 40px; + font-size: 16px; + background-color: #61dafb; + color: #282c34; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.3s ease; + width: 180px; + white-space: nowrap; +} + +.component-button:hover { + background-color: #21a1c4; +} + +.button-row { + display: flex; + gap: 12px; + align-items: center; + justify-content: center; + margin: 16px 0; +} + +/* Loading Button States */ +.loading-button { + position: relative; + overflow: hidden; + transition: all 0.3s ease; +} + +.loading-button.loading { + background-color: #666 !important; + cursor: not-allowed; +} + +.loading-button.success { + background-color: #4CAF50 !important; + animation: successPulse 0.6s ease-out; + min-width: 180px; + width: 180px; +} + +.loading-button.error { + background-color: #f44336 !important; + color: white !important; + animation: errorPulse 0.6s ease-out; + min-width: 180px; + width: 180px; +} + +.loading-button.disabled { + background-color: #444 !important; + cursor: not-allowed; + opacity: 0.6; +} + +.button-content { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + min-height: 24px; +} + +.loading-content .spinner { + width: 16px; + height: 16px; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.success-content .success-icon { + animation: bounceIn 0.6s ease-out; +} + +/* Copy Button */ +.copy-button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + color: #4CAF50; + cursor: pointer; + padding: 8px 12px; + font-size: 14px; + transition: all 0.2s ease; + position: relative; +} + +.copy-button:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); +} + +.copy-button:active { + transform: translateY(0); +} + +/* ===== TEXTAREAS & INPUTS ===== */ .base-textarea { padding: 15px; font-size: 14px; @@ -103,25 +247,32 @@ html, body { max-height: 15vh; } -.component-button { - padding: 15px 40px; - font-size: 16px; - background-color: #61dafb; - color: #282c34; - border: none; +.encode-textarea { + min-height: 50vh; + max-height: 70vh; +} + +.foundry-input-white { + width: 100%; + padding: 12px; + font-size: 14px; border-radius: 8px; - cursor: pointer; - font-weight: bold; - transition: background-color 0.3s ease; - width: 180px; - white-space: nowrap; + border: 1px solid #555; + background-color: #ffffff; + color: #333; + font-family: monospace; + transition: border-color 0.3s ease; + height: 40px; + box-sizing: border-box; } -.component-button:hover { - background-color: #21a1c4; +.foundry-input-white:focus { + outline: none; + border-color: #61dafb; + box-shadow: 0 0 8px rgba(97, 218, 251, 0.3); } -/* Result container styles */ +/* ===== RESULT DISPLAYS ===== */ .base-result-container { margin-top: 20px; padding: 15px; @@ -139,8 +290,10 @@ html, body { overflow-y: auto; } +.encode-result-container { + min-height: 60px; +} -/* Result content styles */ .base-result-content { color: #ffffff; font-size: 11px; @@ -155,76 +308,65 @@ html, body { overflow: visible; } -/* Encode Component Styles */ -.encode-textarea { - min-height: 50vh; - max-height: 70vh; -} - -.encode-result-container { - min-height: 60px; -} - .encode-result-content { max-height: 120px; overflow: auto; } -/* Toast Notifications */ -.toast { - position: fixed; - top: 30px; - left: 50%; - transform: translateX(-50%); - color: white; - padding: 12px 20px; - border-radius: 8px; - font-size: 14px; +.result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1px solid #333; +} + +.result-title { + color: #61dafb; font-weight: bold; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - z-index: 1000; - animation: slideDown 0.3s ease-out; + font-size: 14px; } -.toast.success { - background-color: #4CAF50; +.result-actions { + display: flex; + gap: 8px; + align-items: center; } -.toast.error { - background-color: #f44336; - border: 2px solid #d32f2f; - box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); - font-weight: bold; +.result-actions .copy-button, +.result-actions .edit-button, +.result-actions .simulate-button { + min-width: 60px; + height: 32px; + padding: 6px 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; } -/* Encode Layout */ -.encode-layout { - position: relative; - width: 100%; - height: 100%; +.result-actions .copy-button { + color: #4CAF50; + border-color: rgba(76, 175, 80, 0.3); } -.encode-examples-section { - position: fixed; - left: 1%; - top: 50%; - transform: translateY(-50%); - width: 25%; - max-height: calc(100vh - 100px); - overflow-y: auto; - z-index: 10; +.result-actions .copy-button:hover { + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); } -.encode-main-section { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; +.result-actions .edit-button, +.result-actions .simulate-button { + color: #FF9800; + border-color: rgba(255, 152, 0, 0.3); } +.result-actions .edit-button:hover, +.result-actions .simulate-button:hover { + box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3); +} -/* Form Components */ +/* ===== FORMS ===== */ .form-row { display: flex; gap: 20px; @@ -254,32 +396,10 @@ html, body { font-weight: bold; } -.foundry-input-white { - width: 100%; - padding: 12px; - font-size: 14px; - border-radius: 8px; - border: 1px solid #555; - background-color: #ffffff; - color: #333; - font-family: monospace; - transition: border-color 0.3s ease; - height: 40px; - box-sizing: border-box; -} - -.foundry-input-white:focus { - outline: none; - border-color: #61dafb; - box-shadow: 0 0 8px rgba(97, 218, 251, 0.3); -} - -/* Simulation Components */ .submit-section { margin-top: 20px; } - .api-key-info { font-size: 12px; color: #6c757d; @@ -298,6 +418,33 @@ html, body { text-decoration: underline; } +/* ===== ENCODE LAYOUT ===== */ +.encode-layout { + position: relative; + width: 100%; + height: 100%; +} + +.encode-examples-section { + position: fixed; + left: 1%; + top: 50%; + transform: translateY(-50%); + width: 25%; + max-height: calc(100vh - 100px); + overflow-y: auto; + z-index: 10; +} + +.encode-main-section { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +/* ===== SIMULATION COMPONENTS ===== */ .simulation-success-card { margin-top: 20px; } @@ -361,50 +508,34 @@ html, body { text-decoration: none; } -/* Responsive Design */ -@media (max-width: 1200px) { - .encode-layout { - display: flex; - flex-direction: column; - gap: 16px; - } - - .encode-examples-section { - position: static; - transform: none; - width: 100%; - max-width: 100%; - min-width: auto; - max-height: 400px; - left: auto; - top: auto; - order: 2; - } - - .encode-main-section { - order: 1; - display: block; - } +/* ===== TOAST NOTIFICATIONS ===== */ +.toast { + position: fixed; + top: 30px; + left: 50%; + transform: translateX(-50%); + color: white; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: bold; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + animation: slideDown 0.3s ease-out; } -@media (max-width: 1024px) { - .encode-examples-section { - max-height: 350px; - } +.toast.success { + background-color: #4CAF50; } -@media (max-width: 768px) { - .encode-examples-section { - max-height: 300px; - } - - .form-row { - flex-direction: column; - gap: 0; - } +.toast.error { + background-color: #f44336; + border: 2px solid #d32f2f; + box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); + font-weight: bold; } - +/* ===== ANIMATIONS ===== */ @keyframes slideDown { from { transform: translateX(-50%) translateY(-20px); @@ -416,148 +547,6 @@ html, body { } } - -/* Loading Button Styles */ -.loading-button { - position: relative; - overflow: hidden; - transition: all 0.3s ease; -} - -.loading-button.loading { - background-color: #666 !important; - cursor: not-allowed; -} - -.loading-button.success { - background-color: #4CAF50 !important; - animation: successPulse 0.6s ease-out; - min-width: 180px; - width: 180px; -} - -.loading-button.error { - background-color: #f44336 !important; - color: white !important; - animation: errorPulse 0.6s ease-out; - min-width: 180px; - width: 180px; -} - -.loading-button.disabled { - background-color: #444 !important; - cursor: not-allowed; - opacity: 0.6; -} - -.button-content { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 100%; - min-height: 24px; -} - -.loading-content .spinner { - width: 16px; - height: 16px; - border: 2px solid transparent; - border-top: 2px solid currentColor; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -.success-content .success-icon { - animation: bounceIn 0.6s ease-out; -} - -/* Copy Button Styles */ -.copy-button { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 6px; - color: #4CAF50; - cursor: pointer; - padding: 8px 12px; - font-size: 14px; - transition: all 0.2s ease; - position: relative; -} - -.copy-button:hover { - background: rgba(255, 255, 255, 0.15); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); -} - -.copy-button:active { - transform: translateY(0); -} - - -/* Button Row Styles */ -.button-row { - display: flex; - gap: 12px; - align-items: center; - justify-content: center; - margin: 16px 0; -} - - -/* Result Header Styles */ -.result-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 4px; - padding-bottom: 4px; - border-bottom: 1px solid #333; -} - -.result-title { - color: #61dafb; - font-weight: bold; - font-size: 14px; -} - -.result-actions { - display: flex; - gap: 8px; - align-items: center; -} - -.result-actions .copy-button, -.result-actions .edit-button { - min-width: 60px; - height: 32px; - padding: 6px 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -.result-actions .copy-button { - color: #4CAF50; - border-color: rgba(76, 175, 80, 0.3); -} - -.result-actions .copy-button:hover { - box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); -} - -.result-actions .edit-button { - color: #FF9800; - border-color: rgba(255, 152, 0, 0.3); -} - -.result-actions .edit-button:hover { - box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3); -} - -/* Button Animations */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } @@ -581,11 +570,50 @@ html, body { 100% { transform: scale(1); opacity: 1; } } -/* Responsive adjustments for new components */ +/* ===== RESPONSIVE DESIGN ===== */ +@media (max-width: 1200px) { + .encode-layout { + display: flex; + flex-direction: column; + gap: 16px; + } + + .encode-examples-section { + position: static; + transform: none; + width: 100%; + max-width: 100%; + min-width: auto; + max-height: 400px; + left: auto; + top: auto; + order: 2; + } + + .encode-main-section { + order: 1; + display: block; + } +} + +@media (max-width: 1024px) { + .encode-examples-section { + max-height: 350px; + } +} + @media (max-width: 768px) { + .encode-examples-section { + max-height: 300px; + } + + .form-row { + flex-direction: column; + gap: 0; + } + .button-row { flex-direction: column; gap: 8px; } - } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 22fd202..97e23bc 100644 --- a/src/App.js +++ b/src/App.js @@ -6,7 +6,7 @@ import { validateEncodedCalldata, validateDecodedJson } from './utils/core/round import { createDecodeOperation, createEncodeOperation, formatJSON } from './utils/componentUtils.js'; import DecodeCalldata from './components/forms/DecodeCalldata'; import EncodeCalldata from './components/forms/EncodeCalldata'; -import SimulateTX from './components/SimulateTX'; +import SimulateTX from './components/simulate/SimulateTX'; function App() { const [activeTab, setActiveTab] = useState('decode'); @@ -66,6 +66,13 @@ function App() { showToast('Switched to Encode tab with decoded result!', 'success'); }; + const handleSimulateFromEncode = (calldata) => { + // Navigate to simulate tab and populate with encoded calldata + setActiveTab('simulate'); + updateSimulationFormData('calldata', calldata); + showToast('Switched to Simulate TX tab with encoded calldata!', 'success'); + }; + return (
@@ -127,6 +134,7 @@ function App() { )} result={encodeResult} showToast={showToast} + onSimulate={handleSimulateFromEncode} /> )} diff --git a/src/components/SimulateTX.js b/src/components/SimulateTX.js deleted file mode 100644 index c6f6763..0000000 --- a/src/components/SimulateTX.js +++ /dev/null @@ -1,320 +0,0 @@ -import React, { useEffect } from 'react'; -import axios from 'axios'; -import LoadingButton from './ui/LoadingButton'; - -const SELECT_STYLES = { - appearance: 'none', - backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', - backgroundRepeat: 'no-repeat', - backgroundPosition: 'right 12px center', - backgroundSize: '16px', - paddingRight: '40px', - cursor: 'pointer' -}; - -const FormField = ({ label, type = "text", placeholder, value, onChange, options = null, autoComplete, name, required = false }) => ( -
- - {options ? ( - - ) : ( - onChange(e.target.value)} - autoComplete={autoComplete} - name={name} - /> - )} -
-); - -const SimulationSuccessCard = ({ accountSlug, projectSlug, simulationResult }) => ( -
- - -
-
-
🔍
-
-

View in Tenderly Dashboard

-

Analyze your transaction simulation with detailed traces and logs

-
-
- - - 🚀 Open Tenderly Simulation → - -
-
-); - -const SimulateTX = ({ - simulationState, - updateSimulationFormData, - updateSimulationStatus, - showToast -}) => { - const { formData, isSimulating, simulationResult } = simulationState; - useEffect(() => { - const savedProjectSlug = localStorage.getItem('tenderly_project_slug'); - - if (savedProjectSlug && !formData.projectSlug) { - updateSimulationFormData('projectSlug', savedProjectSlug); - } - }, [formData.projectSlug, updateSimulationFormData]); - - const handleInputChange = (field, value) => { - updateSimulationFormData(field, value); - - if (field === 'chainId') { - if (value === 'other') { - updateSimulationFormData('customChainId', ''); - } else { - updateSimulationFormData('customChainId', value); - } - } - - if (field === 'customChainId') { - const matchingNetwork = chainOptions.find(option => option.id === value && option.id !== 'other'); - - if (matchingNetwork) { - updateSimulationFormData('chainId', matchingNetwork.id); - } else { - updateSimulationFormData('chainId', 'other'); - } - } - - if (field === 'projectSlug') { - localStorage.setItem('tenderly_project_slug', value); - } - }; - - const chainOptions = [ - { id: 'other', name: 'Custom Chain ID'}, - { id: '1', name: 'Ethereum Mainnet'}, - { id: '56', name: 'BSC'}, - { id: '8453', name: 'Base'}, - { id: '42161', name: 'Arbitrum One'}, - { id: '43114', name: 'Avalanche'} - ]; - - - const simulateTransaction = async () => { - // Validate required fields - if (!formData.accountSlug || !formData.projectSlug || !formData.tenderlyApiKey) { - showToast('Please fill in Account Slug, Project Slug, and Tenderly API Key', 'error'); - return; - } - - if (!formData.from || !formData.to || !formData.calldata || !formData.customChainId) { - showToast('Please fill in all transaction fields (From, To, Calldata, Chain ID)', 'error'); - return; - } - - updateSimulationStatus('isSimulating', true); - updateSimulationStatus('simulationResult', null); - - try { - const simulationPayload = { - 'network_id': formData.customChainId, - 'from': formData.from, - 'to': formData.to, - 'input': formData.calldata, - 'gas': 100000000, - 'value': formData.msgValue ? (parseFloat(formData.msgValue) * 1e18).toString() : '0', - 'save': true, - 'save_if_fails': true, - 'simulation_type': 'full' - }; - - if (formData.blockHeight && formData.blockHeight !== 'latest' && formData.blockHeight !== '') { - simulationPayload.block_number = parseInt(formData.blockHeight); - } - - const response = await axios.post( - `https://api.tenderly.co/api/v1/account/${formData.accountSlug}/project/${formData.projectSlug}/simulate`, - simulationPayload, - { - headers: { - 'X-Access-Key': formData.tenderlyApiKey, - 'content-type': 'application/json' - } - } - ); - - updateSimulationStatus('simulationResult', response.data); - showToast('Transaction simulation completed successfully! Click the link below to view in Tenderly.', 'success'); - - } catch (error) { - console.error('Simulation error:', error); - const errorMessage = error.response?.data?.error?.message || - error.response?.data?.message || - error.message || - 'Unknown error occurred'; - showToast(`Simulation failed: ${errorMessage}`, 'error'); - } finally { - updateSimulationStatus('isSimulating', false); - } - }; - - return ( -
- {/* Transaction Details */} -
- handleInputChange('from', value)} - required={true} - /> - - handleInputChange('to', value)} - required={true} - /> -
- - {/* Network Configuration */} -
- handleInputChange('chainId', value)} - options={chainOptions} - /> - - handleInputChange('customChainId', value)} - /> -
- - {/* Transaction Parameters */} -
- handleInputChange('msgValue', value)} - /> - - handleInputChange('blockHeight', value)} - /> -
- - {/* Tenderly Configuration */} -
e.preventDefault()}> -
- handleInputChange('accountSlug', value)} - autoComplete="username" - name="tenderly-account-slug" - required={true} - /> - - handleInputChange('projectSlug', value)} - required={true} - /> -
- -
-
- handleInputChange('tenderlyApiKey', value)} - autoComplete="current-password" - name="tenderly-api-key" - required={true} - /> -
- â„šī¸ - You will need a Tenderly API key to use this service. - - Get API Key → - -
-
-
- - handleInputChange('calldata', value)} - required={true} - /> - -
- - Simulate Transaction - -
- - - {simulationResult && ( - - )} -
- ); -}; - -export default SimulateTX; \ No newline at end of file diff --git a/src/components/forms/EncodeCalldata.js b/src/components/forms/EncodeCalldata.js index 1a91291..4262354 100644 --- a/src/components/forms/EncodeCalldata.js +++ b/src/components/forms/EncodeCalldata.js @@ -17,7 +17,8 @@ const EncodeCalldata = ({ onChange, onButtonClick, result, - showToast + showToast, + onSimulate }) => { const buttonState = useButtonState(); const { isLoading, showSuccess, showError, lastProcessedValue, resetButtonStates, setButtonState } = buttonState; @@ -66,6 +67,14 @@ const EncodeCalldata = ({ const handleInputChange = createInputChangeHandler(onChange, lastProcessedValue, resetButtonStates); const handleCopy = createCopyHandler(showToast); + + const handleSimulate = () => { + if (!result) { + showToast('Please encode calldata first', 'error'); + return; + } + onSimulate(result); + }; return (
@@ -102,6 +111,7 @@ const EncodeCalldata = ({ result={result} title="Encoded Calldata" onCopy={handleCopy} + onSimulate={onSimulate ? handleSimulate : undefined} className="encode-result-container" contentClassName="encode-result-content" /> diff --git a/src/components/simulate/SimulateFoundry.js b/src/components/simulate/SimulateFoundry.js new file mode 100644 index 0000000..1e9e598 --- /dev/null +++ b/src/components/simulate/SimulateFoundry.js @@ -0,0 +1,188 @@ +import React from 'react'; +import LoadingButton from '../ui/LoadingButton'; +import ResultDisplay from '../ui/ResultDisplay'; +import { rpcUrlOptions } from '../../utils/networkConfig'; + +const SELECT_STYLES = { + appearance: 'none', + backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', + backgroundRepeat: 'no-repeat', + backgroundPosition: 'right 12px center', + backgroundSize: '16px', + paddingRight: '40px', + cursor: 'pointer' +}; + +const FormField = ({ label, type = "text", placeholder, value, onChange, options = null, autoComplete, name, required = false }) => ( +
+ + {options ? ( + + ) : ( + onChange(e.target.value)} + autoComplete={autoComplete} + name={name} + /> + )} +
+); + +const SimulateFoundry = ({ + formData, + isSimulating, + simulationResult, + handleInputChange, + showToast, + updateSimulationStatus +}) => { + const generateFoundryCommand = () => { + // Validate required fields for Foundry + if (!formData.from || !formData.to || !formData.calldata || !formData.rpcUrl) { + showToast('Please fill in all required fields (From, To, Calldata, RPC URL)', 'error'); + return; + } + + // Build the cast call command + let command = 'cast call'; + + // Add the target contract address + command += ` ${formData.to}`; + + // Add the calldata + command += ` "${formData.calldata}"`; + + // Add the --from flag + command += ` --from ${formData.from}`; + + // Add the --rpc-url flag + command += ` --rpc-url ${formData.rpcUrl}`; + + // Add msg.value if specified + if (formData.msgValue && formData.msgValue !== '0' && formData.msgValue !== '') { + const valueInWei = (parseFloat(formData.msgValue) * 1e18).toString(); + command += ` --value ${valueInWei}`; + } + + // Add block height if specified + if (formData.blockHeight && formData.blockHeight !== 'latest' && formData.blockHeight !== '') { + command += ` --block ${formData.blockHeight}`; + } + + // Add --trace flag for detailed execution trace + command += ` --trace`; + + // Store the generated command in simulationResult for display + updateSimulationStatus('simulationResult', { foundryCommand: command }); + showToast('Foundry command generated successfully!', 'success'); + }; + + return ( +
+ {/* Transaction Details */} +
+ handleInputChange('from', value)} + required={true} + /> + + handleInputChange('to', value)} + required={true} + /> +
+ + {/* RPC Configuration */} +
+ handleInputChange('rpcUrlPreset', value)} + options={rpcUrlOptions} + /> + + handleInputChange('rpcUrl', value)} + required={true} + /> +
+ + {/* Transaction Parameters */} +
+ handleInputChange('msgValue', value)} + /> + + handleInputChange('blockHeight', value)} + /> +
+ + {/* Calldata */} + handleInputChange('calldata', value)} + required={true} + /> + +
+ + Generate Command + +
+ + {simulationResult && simulationResult.foundryCommand && ( + showToast('Command copied to clipboard!', 'success')} + /> + )} +
+ ); +}; + +export default SimulateFoundry; diff --git a/src/components/simulate/SimulateTX.js b/src/components/simulate/SimulateTX.js new file mode 100644 index 0000000..982f00b --- /dev/null +++ b/src/components/simulate/SimulateTX.js @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react'; +import SimulateFoundry from './SimulateFoundry'; +import SimulateTenderly from './SimulateTenderly'; +import { chainOptions, rpcUrlOptions } from '../../utils/networkConfig'; + +const SimulateTX = ({ + simulationState, + updateSimulationFormData, + updateSimulationStatus, + showToast +}) => { + const { formData, isSimulating, simulationResult } = simulationState; + + // Initialize activeTab from localStorage or default to 'tenderly' + const [activeTab, setActiveTab] = useState(() => { + return localStorage.getItem('simulation_active_tab') || 'tenderly'; + }); + + useEffect(() => { + const savedProjectSlug = localStorage.getItem('tenderly_project_slug'); + + if (savedProjectSlug && !formData.projectSlug) { + updateSimulationFormData('projectSlug', savedProjectSlug); + } + }, [formData.projectSlug, updateSimulationFormData]); + + // Save active tab to localStorage whenever it changes + const handleTabChange = (tab) => { + setActiveTab(tab); + localStorage.setItem('simulation_active_tab', tab); + }; + + const handleInputChange = (field, value) => { + updateSimulationFormData(field, value); + + if (field === 'chainId') { + if (value === 'other') { + updateSimulationFormData('customChainId', ''); + } else { + updateSimulationFormData('customChainId', value); + } + } + + if (field === 'customChainId') { + const matchingNetwork = chainOptions.find(option => option.id === value && option.id !== 'other'); + + if (matchingNetwork) { + updateSimulationFormData('chainId', matchingNetwork.id); + } else { + updateSimulationFormData('chainId', 'other'); + } + } + + if (field === 'rpcUrlPreset') { + if (value === 'custom') { + updateSimulationFormData('rpcUrl', ''); + } else { + const selectedRpc = rpcUrlOptions.find(option => option.id === value); + if (selectedRpc) { + updateSimulationFormData('rpcUrl', selectedRpc.url); + } + } + } + + if (field === 'rpcUrl') { + const matchingRpc = rpcUrlOptions.find(option => option.url === value && option.id !== 'custom'); + + if (matchingRpc) { + updateSimulationFormData('rpcUrlPreset', matchingRpc.id); + } else { + updateSimulationFormData('rpcUrlPreset', 'custom'); + } + } + + if (field === 'projectSlug') { + localStorage.setItem('tenderly_project_slug', value); + } + }; + + const commonProps = { + formData, + isSimulating, + simulationResult, + handleInputChange, + showToast, + updateSimulationStatus + }; + + return ( +
+ {/* Tab Navigation */} +
+ + +
+ + {/* Tab Content */} +
+ {activeTab === 'foundry' && } + {activeTab === 'tenderly' && } +
+
+ ); +}; + +export default SimulateTX; diff --git a/src/components/simulate/SimulateTenderly.js b/src/components/simulate/SimulateTenderly.js new file mode 100644 index 0000000..666ebba --- /dev/null +++ b/src/components/simulate/SimulateTenderly.js @@ -0,0 +1,278 @@ +import React from 'react'; +import axios from 'axios'; +import LoadingButton from '../ui/LoadingButton'; +import { chainOptions } from '../../utils/networkConfig'; + +const SELECT_STYLES = { + appearance: 'none', + backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', + backgroundRepeat: 'no-repeat', + backgroundPosition: 'right 12px center', + backgroundSize: '16px', + paddingRight: '40px', + cursor: 'pointer' +}; + +const FormField = ({ label, type = "text", placeholder, value, onChange, options = null, autoComplete, name, required = false }) => ( +
+ + {options ? ( + + ) : ( + onChange(e.target.value)} + autoComplete={autoComplete} + name={name} + /> + )} +
+); + +const SimulationSuccessCard = ({ accountSlug, projectSlug, simulationResult }) => ( +
+ + +
+
+
🔍
+
+

View in Tenderly Dashboard

+

Analyze your transaction simulation with detailed traces and logs

+
+
+ + + 🚀 Open Tenderly Simulation → + +
+
+); + +const SimulateTenderly = ({ + formData, + isSimulating, + simulationResult, + handleInputChange, + showToast, + updateSimulationStatus +}) => { + const simulateTransaction = async () => { + // Validate required fields + if (!formData.accountSlug || !formData.projectSlug || !formData.tenderlyApiKey) { + showToast('Please fill in Account Slug, Project Slug, and Tenderly API Key', 'error'); + return; + } + + if (!formData.from || !formData.to || !formData.calldata || !formData.customChainId) { + showToast('Please fill in all transaction fields (From, To, Calldata, Chain ID)', 'error'); + return; + } + + updateSimulationStatus('isSimulating', true); + updateSimulationStatus('simulationResult', null); + + try { + const simulationPayload = { + 'network_id': formData.customChainId, + 'from': formData.from, + 'to': formData.to, + 'input': formData.calldata, + 'gas': 100000000, + 'value': formData.msgValue ? (parseFloat(formData.msgValue) * 1e18).toString() : '0', + 'save': true, + 'save_if_fails': true, + 'simulation_type': 'full' + }; + + if (formData.blockHeight && formData.blockHeight !== 'latest' && formData.blockHeight !== '') { + simulationPayload.block_number = parseInt(formData.blockHeight); + } + + const response = await axios.post( + `https://api.tenderly.co/api/v1/account/${formData.accountSlug}/project/${formData.projectSlug}/simulate`, + simulationPayload, + { + headers: { + 'X-Access-Key': formData.tenderlyApiKey, + 'content-type': 'application/json' + } + } + ); + + updateSimulationStatus('simulationResult', response.data); + showToast('Transaction simulation completed successfully! Click the link below to view in Tenderly.', 'success'); + + } catch (error) { + console.error('Simulation error:', error); + const errorMessage = error.response?.data?.error?.message || + error.response?.data?.message || + error.message || + 'Unknown error occurred'; + showToast(`Simulation failed: ${errorMessage}`, 'error'); + } finally { + updateSimulationStatus('isSimulating', false); + } + }; + + return ( +
+ {/* Transaction Details */} +
+ handleInputChange('from', value)} + required={true} + /> + + handleInputChange('to', value)} + required={true} + /> +
+ + {/* Network Configuration */} +
+ handleInputChange('chainId', value)} + options={chainOptions} + /> + + handleInputChange('customChainId', value)} + /> +
+ + {/* Transaction Parameters */} +
+ handleInputChange('msgValue', value)} + /> + + handleInputChange('blockHeight', value)} + /> +
+ + {/* Tenderly Configuration */} +
e.preventDefault()}> +
+ handleInputChange('accountSlug', value)} + autoComplete="username" + name="tenderly-account-slug" + required={true} + /> + + handleInputChange('projectSlug', value)} + required={true} + /> +
+ +
+
+ handleInputChange('tenderlyApiKey', value)} + autoComplete="current-password" + name="tenderly-api-key" + required={true} + /> +
+ â„šī¸ + You will need a Tenderly API key to use this service. + + Get API Key → + +
+
+
+ + handleInputChange('calldata', value)} + required={true} + /> + +
+ + Simulate Transaction + +
+ + + {simulationResult && !simulationResult.foundryCommand && ( + + )} +
+ ); +}; + +export default SimulateTenderly; diff --git a/src/components/ui/ResultDisplay.js b/src/components/ui/ResultDisplay.js index e37a6ba..e8972ce 100644 --- a/src/components/ui/ResultDisplay.js +++ b/src/components/ui/ResultDisplay.js @@ -7,6 +7,7 @@ const ResultDisplay = ({ title = "Result", onCopy, onEdit, + onSimulate, className = "result-container", contentClassName = "result-content" }) => { @@ -28,6 +29,15 @@ const ResultDisplay = ({ Edit )} + {onSimulate && ( + + )} { + const chain = chainOptions.find(option => option.id === chainId); + return chain ? chain.name : 'Unknown Network'; +}; + +// Helper function to get RPC URL by chain ID +export const getRpcUrlByChainId = (chainId) => { + const rpc = rpcUrlOptions.find(option => option.chainId === chainId); + return rpc ? rpc.url : ''; +}; + +// Helper function to get chain ID by RPC URL +export const getChainIdByRpcUrl = (url) => { + const rpc = rpcUrlOptions.find(option => option.url === url); + return rpc ? rpc.chainId : null; +}; +