+ )}
+
+
+ );
+};
+
+export default TransactionStatus;
diff --git a/src/hooks/useTransactionTracker.js b/src/hooks/useTransactionTracker.js
new file mode 100644
index 0000000..242b2b8
--- /dev/null
+++ b/src/hooks/useTransactionTracker.js
@@ -0,0 +1,165 @@
+import { useState, useEffect, useCallback } from 'react';
+
+/**
+ * Custom hook to track transaction progress
+ * @param {Object} provider - Ethers.js provider
+ * @returns {Object} - Transaction tracking utilities
+ */
+export const useTransactionTracker = (provider) => {
+ const [transactions, setTransactions] = useState({});
+
+ // Add a new transaction to track
+ const trackTransaction = useCallback((txHash, description) => {
+ setTransactions(prev => ({
+ ...prev,
+ [txHash]: {
+ hash: txHash,
+ description,
+ status: 'pending',
+ confirmations: 0,
+ receipt: null,
+ error: null,
+ startTime: Date.now(),
+ }
+ }));
+
+ return txHash;
+ }, []);
+
+ // Update transaction status
+ const updateTransaction = useCallback((txHash, updates) => {
+ setTransactions(prev => {
+ if (!prev[txHash]) return prev;
+
+ return {
+ ...prev,
+ [txHash]: {
+ ...prev[txHash],
+ ...updates,
+ }
+ };
+ });
+ }, []);
+
+ // Listen for transaction confirmations
+ useEffect(() => {
+ if (!provider) return;
+
+ const pendingTxHashes = Object.keys(transactions).filter(
+ hash => transactions[hash].status === 'pending'
+ );
+
+ const listeners = {};
+
+ // Set up listeners for each pending transaction
+ pendingTxHashes.forEach(txHash => {
+ if (!listeners[txHash]) {
+ const onReceipt = (receipt) => {
+ updateTransaction(txHash, {
+ status: receipt.status === 1 ? 'success' : 'failed',
+ receipt,
+ confirmations: 1,
+ });
+ };
+
+ const onError = (error) => {
+ updateTransaction(txHash, {
+ status: 'error',
+ error: error.message,
+ });
+ };
+
+ // Listen for transaction receipt
+ provider.once(txHash, onReceipt);
+
+ // Store listeners to clean up later
+ listeners[txHash] = { onReceipt, onError };
+
+ // Check transaction status immediately
+ provider.getTransaction(txHash)
+ .then(tx => {
+ if (tx) {
+ // Transaction found but not confirmed yet
+ updateTransaction(txHash, {
+ status: 'pending',
+ gasPrice: tx.gasPrice?.toString(),
+ });
+
+ // If transaction has a wait method, use it to get receipt
+ if (tx.wait) {
+ tx.wait()
+ .then(onReceipt)
+ .catch(onError);
+ }
+ } else {
+ // Transaction not found - might be dropped or not broadcast
+ updateTransaction(txHash, {
+ status: 'not_found',
+ });
+ }
+ })
+ .catch(onError);
+ }
+ });
+
+ // Clean up listeners
+ return () => {
+ pendingTxHashes.forEach(txHash => {
+ if (listeners[txHash]) {
+ provider.removeListener(txHash, listeners[txHash].onReceipt);
+ }
+ });
+ };
+ }, [transactions, provider, updateTransaction]);
+
+ // Get estimated confirmation time based on gas price
+ const getEstimatedTime = useCallback(async (txHash) => {
+ if (!provider || !transactions[txHash]) return null;
+
+ try {
+ const tx = transactions[txHash];
+
+ // If we don't have gas price info yet, try to get it
+ if (!tx.gasPrice) {
+ const txData = await provider.getTransaction(txHash);
+ if (txData && txData.gasPrice) {
+ updateTransaction(txHash, { gasPrice: txData.gasPrice.toString() });
+ } else {
+ return null;
+ }
+ }
+
+ // Get current gas prices from the network
+ const feeData = await provider.getFeeData();
+ if (!feeData || !feeData.gasPrice) return null;
+
+ const txGasPrice = BigInt(tx.gasPrice || '0');
+ const currentGasPrice = feeData.gasPrice.toBigInt();
+
+ // Calculate estimated time based on gas price difference
+ // This is a very rough estimation
+ if (txGasPrice >= currentGasPrice) {
+ return 'less than 1 minute';
+ } else if (txGasPrice >= currentGasPrice * BigInt(80) / BigInt(100)) {
+ return '1-2 minutes';
+ } else if (txGasPrice >= currentGasPrice * BigInt(50) / BigInt(100)) {
+ return '3-5 minutes';
+ } else {
+ return 'more than 5 minutes';
+ }
+ } catch (error) {
+ console.error('Error estimating confirmation time:', error);
+ return null;
+ }
+ }, [provider, transactions, updateTransaction]);
+
+ return {
+ transactions,
+ trackTransaction,
+ updateTransaction,
+ getEstimatedTime,
+ getTransaction: useCallback((txHash) => transactions[txHash], [transactions]),
+ };
+};
+
+export default useTransactionTracker;
diff --git a/src/store/methodReducer.js b/src/store/methodReducer.js
new file mode 100644
index 0000000..283a1cf
--- /dev/null
+++ b/src/store/methodReducer.js
@@ -0,0 +1,125 @@
+/**
+ * Reducer for managing method call state in AppMethod component
+ */
+
+// Action types
+export const METHOD_ACTIONS = {
+ SET_METHOD_DATA: 'SET_METHOD_DATA',
+ SET_METHOD_VALUES: 'SET_METHOD_VALUES',
+ UPDATE_METHOD_VALUE: 'UPDATE_METHOD_VALUE',
+ SET_PAYABLE_VALUE: 'SET_PAYABLE_VALUE',
+ CALL_METHOD_START: 'CALL_METHOD_START',
+ CALL_METHOD_SUCCESS: 'CALL_METHOD_SUCCESS',
+ CALL_METHOD_ERROR: 'CALL_METHOD_ERROR',
+ RESET_RESULT: 'RESET_RESULT',
+};
+
+// Initial state
+export const initialMethodState = {
+ methodName: '',
+ methodInputs: [],
+ methodValues: [],
+ methodStateMutability: '',
+ payableValue: '0',
+ isLoading: false,
+ callResult: '',
+ displayResult: '',
+ resultType: null, // 'success', 'error', or null
+ showResult: false,
+ transactionHash: null,
+ blockNumber: null,
+};
+
+/**
+ * Method reducer function
+ * @param {Object} state - Current state
+ * @param {Object} action - Dispatched action
+ * @returns {Object} - New state
+ */
+export const methodReducer = (state, action) => {
+ switch (action.type) {
+ case METHOD_ACTIONS.SET_METHOD_DATA:
+ return {
+ ...state,
+ methodName: action.payload.name,
+ methodInputs: action.payload.inputs,
+ methodValues: action.payload.inputs.map(() => null),
+ methodStateMutability: action.payload.stateMutability,
+ // Reset result when method changes
+ callResult: '',
+ displayResult: '',
+ resultType: null,
+ showResult: false,
+ transactionHash: null,
+ blockNumber: null,
+ };
+
+ case METHOD_ACTIONS.SET_METHOD_VALUES:
+ return {
+ ...state,
+ methodValues: action.payload,
+ };
+
+ case METHOD_ACTIONS.UPDATE_METHOD_VALUE:
+ const newValues = [...state.methodValues];
+ newValues[action.payload.index] = action.payload.value;
+ return {
+ ...state,
+ methodValues: newValues,
+ };
+
+ case METHOD_ACTIONS.SET_PAYABLE_VALUE:
+ return {
+ ...state,
+ payableValue: action.payload,
+ };
+
+ case METHOD_ACTIONS.CALL_METHOD_START:
+ return {
+ ...state,
+ isLoading: true,
+ callResult: '',
+ displayResult: '',
+ showResult: false,
+ resultType: null,
+ transactionHash: null,
+ blockNumber: null,
+ };
+
+ case METHOD_ACTIONS.CALL_METHOD_SUCCESS:
+ return {
+ ...state,
+ isLoading: false,
+ callResult: action.payload.result,
+ displayResult: action.payload.result,
+ resultType: 'success',
+ showResult: true,
+ transactionHash: action.payload.transactionHash,
+ blockNumber: action.payload.blockNumber,
+ };
+
+ case METHOD_ACTIONS.CALL_METHOD_ERROR:
+ return {
+ ...state,
+ isLoading: false,
+ callResult: action.payload,
+ displayResult: action.payload,
+ resultType: 'error',
+ showResult: true,
+ };
+
+ case METHOD_ACTIONS.RESET_RESULT:
+ return {
+ ...state,
+ callResult: '',
+ displayResult: '',
+ resultType: null,
+ showResult: false,
+ transactionHash: null,
+ blockNumber: null,
+ };
+
+ default:
+ return state;
+ }
+};
diff --git a/src/utils/contractUtils.js b/src/utils/contractUtils.js
new file mode 100644
index 0000000..6c8272a
--- /dev/null
+++ b/src/utils/contractUtils.js
@@ -0,0 +1,186 @@
+import { ethers } from 'ethers';
+
+/**
+ * Format contract parameter value based on its type
+ * @param {string} type - The Solidity parameter type
+ * @param {string} value - The user input value
+ * @returns {any} - The formatted value ready for contract call
+ */
+export const formatTypeValue = (type, value) => {
+ if (!value && value !== 0 && value !== false) {
+ throw new Error(`Value is required for type ${type}`);
+ }
+
+ // Handle array types
+ if (type.includes('[')) {
+ try {
+ // Parse the array from string input
+ let arrayValue = value;
+ if (typeof value === 'string') {
+ // Handle both comma-separated values and JSON arrays
+ if (value.trim().startsWith('[')) {
+ arrayValue = JSON.parse(value);
+ } else {
+ arrayValue = value.split(',').map(item => item.trim());
+ }
+ }
+
+ // Get the base type (without array brackets)
+ const baseType = type.substring(0, type.indexOf('['));
+
+ // Format each element in the array
+ return arrayValue.map(item => formatTypeValue(baseType, item));
+ } catch (error) {
+ throw new Error(`Invalid array format for ${type}: ${error.message}`);
+ }
+ }
+
+ // Handle tuple types
+ if (type.startsWith('tuple')) {
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ throw new Error(`Invalid tuple format: ${error.message}`);
+ }
+ }
+
+ // Handle address type
+ if (type === 'address') {
+ if (!ethers.utils.isAddress(value)) {
+ throw new Error('Invalid Ethereum address');
+ }
+ return value;
+ }
+
+ // Handle boolean type
+ if (type === 'bool') {
+ if (typeof value === 'boolean') return value;
+ if (value.toLowerCase() === 'true') return true;
+ if (value.toLowerCase() === 'false') return false;
+ throw new Error('Boolean value must be true or false');
+ }
+
+ // Handle integer types (uint/int)
+ if (type.startsWith('uint') || type.startsWith('int')) {
+ try {
+ // Check if the value is a valid number
+ if (isNaN(value)) {
+ throw new Error(`Not a valid number for ${type}`);
+ }
+ return ethers.BigNumber.from(value);
+ } catch (error) {
+ throw new Error(`Invalid number format for ${type}: ${error.message}`);
+ }
+ }
+
+ // Handle bytes types
+ if (type.startsWith('bytes')) {
+ if (type === 'bytes') {
+ return ethers.utils.arrayify(value);
+ }
+ // Handle fixed-size bytes
+ return value;
+ }
+
+ // Handle string type
+ if (type === 'string') {
+ return value;
+ }
+
+ // Default case
+ return value;
+};
+
+/**
+ * Format contract result for display
+ * @param {any} result - The raw result from contract call
+ * @param {Object} outputDef - The output definition from ABI
+ * @returns {string} - Formatted result for display
+ */
+export const formatContractResult = (result, outputDef) => {
+ if (result === undefined) {
+ return 'No return value';
+ }
+
+ if (result === null) {
+ return 'null';
+ }
+
+ if (result instanceof ethers.BigNumber) {
+ // Check if it might be representing ETH
+ if (outputDef &&
+ (outputDef.type.includes('uint') ||
+ (outputDef.name && outputDef.name.toLowerCase().includes('balance')))) {
+ return result.toString();
+ }
+ return result.toString();
+ }
+
+ if (Array.isArray(result)) {
+ return JSON.stringify(result, null, 2);
+ }
+
+ if (typeof result === 'boolean') {
+ return result.toString();
+ }
+
+ if (typeof result === 'object') {
+ try {
+ return JSON.stringify(result, null, 2);
+ } catch (e) {
+ return 'Complex object: ' + Object.prototype.toString.call(result);
+ }
+ }
+
+ if (typeof result === 'string') {
+ return result;
+ }
+
+ return String(result);
+};
+
+/**
+ * Parse and format blockchain errors
+ * @param {Error} error - The error object from ethers.js
+ * @returns {string} - User-friendly error message
+ */
+export const parseBlockchainError = (error) => {
+ const errorMessage = error.message || 'Unknown error';
+
+ // Check for common blockchain error patterns
+ if (errorMessage.includes('insufficient funds')) {
+ return 'Insufficient funds to complete this transaction';
+ }
+
+ if (errorMessage.includes('gas required exceeds allowance')) {
+ return 'Transaction requires more gas than allowed';
+ }
+
+ if (errorMessage.includes('nonce too low')) {
+ return 'Transaction nonce is too low. Try refreshing the page';
+ }
+
+ if (errorMessage.includes('replacement transaction underpriced')) {
+ return 'Gas price too low for replacement transaction';
+ }
+
+ if (errorMessage.includes('execution reverted')) {
+ // Extract custom error message if available
+ const revertMatch = errorMessage.match(/execution reverted: (.*?)(?:,|$)/);
+ if (revertMatch && revertMatch[1]) {
+ return `Smart contract reverted: ${revertMatch[1]}`;
+ }
+ return 'Transaction reverted by the smart contract';
+ }
+
+ if (errorMessage.includes('user rejected')) {
+ return 'Transaction was rejected in your wallet';
+ }
+
+ if (errorMessage.includes('network changed')) {
+ return 'Network changed during transaction. Please refresh the page';
+ }
+
+ // Return original error if no specific pattern is matched
+ return errorMessage;
+};
From 81c4748cd29834e65d7aa8d5523a97ee40649df4 Mon Sep 17 00:00:00 2001
From: Frozen <1884084+xrdavies@users.noreply.github.com>
Date: Thu, 22 May 2025 16:07:46 +0800
Subject: [PATCH 7/8] fix: rename filed name
---
src/components/AppMethod.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/AppMethod.jsx b/src/components/AppMethod.jsx
index 41bc93c..b766243 100644
--- a/src/components/AppMethod.jsx
+++ b/src/components/AppMethod.jsx
@@ -492,7 +492,7 @@ export default function AppMethod({ itemData, contract }) {
- Function Result
+ Result
{isLoading && }
@@ -511,7 +511,7 @@ export default function AppMethod({ itemData, contract }) {
- Function Output:
+ Output:
{/* Single result display */}
From bf74d33a04dfaca6d78a30ba15faa390c0e12957 Mon Sep 17 00:00:00 2001
From: Frozen <1884084+xrdavies@users.noreply.github.com>
Date: Thu, 22 May 2025 16:11:21 +0800
Subject: [PATCH 8/8] docs: update readme
---
README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 18e41a0..266213a 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,55 @@ Quick DApp is a tool for smart contract developers to interact with any smart co
With Quick DApp, you can just create a simple dapp in minutes and share it with your friends anywhere.
-# Plans
+## Features
+
+- **Instant Interface Generation**: Automatically creates a UI for any smart contract from its ABI
+- **Multi-network Support**: Works with Ethereum, Polygon, BSC, and other EVM-compatible networks
+- **Comprehensive Method Support**: Handles view/pure functions, state-changing functions, and payable functions
+- **Enhanced Transaction Tracking**: Real-time status updates with detailed feedback
+- **Advanced Type Handling**: Supports complex parameter types (arrays, structs, etc.)
+- **Clean Result Display**: Clearly formatted function return values
+- **User-friendly Error Messages**: Readable blockchain error information
+
+## Usage
+
+1. Connect your MetaMask wallet
+2. Enter your contract's ABI (JSON format) and address
+3. Select the network where your contract is deployed
+4. Click on any method to interact with it
+5. For read-only methods, results will display immediately
+6. For state-changing methods, you'll see transaction status and confirmations
+
+## Installation
+
+### Prerequisites
+
+- Node.js (v14 or later)
+- Yarn or npm
+
+### Setup
+
+1. Clone the repository
+ ```bash
+ git clone https://github.com/Web3Camp-Labs/quick-dapp.git
+ cd quick-dapp
+ ```
+
+2. Install dependencies
+ ```bash
+ yarn install
+ # or
+ npm install
+ ```
+
+3. Start the development server
+ ```bash
+ yarn start
+ # or
+ npm start
+ ```
+
+## Plans
- [ ] Add support for other wallets
- [ ] CoinBase
- [ ] WalletConnect
@@ -15,11 +63,37 @@ With Quick DApp, you can just create a simple dapp in minutes and share it with
- [ ] Save the created dapp locally
- [ ] Provide share link for dapp
- [ ] New UI
+- [x] Enhanced transaction tracking
+- [x] Improved error handling
+- [x] Better result formatting
+
+## Project Structure
+
+- `src/components/` - React components
+ - `AppMethod.jsx` - Core component for interacting with contract methods
+ - `TransactionStatus.jsx` - Transaction tracking UI
+- `src/utils/` - Utility functions
+ - `contractUtils.js` - Smart contract interaction helpers
+- `src/store/` - State management
+ - `methodReducer.js` - Reducer for method call state
+- `src/hooks/` - Custom React hooks
+ - `useTransactionTracker.js` - Hook for tracking transaction status
-# History
+## History
In the past years, **Patrick Gallagher** created a tool named OneClick DApp for the community. However, this tool has not worked for a long time since mid-2023. So we created a new tool to replace it under the name of Web3Camp.
-# Special Thanks
+## Technologies
+
+- React 17
+- ethers.js 5.5
+- Ant Design 4.18
+- Styled Components
+
+## Special Thanks
https://oneclickdapp.com
+
+## License
+
+This project is licensed under the MIT License.