diff --git a/src/App.css b/src/App.css index 74b5e05..e38f8d0 100644 --- a/src/App.css +++ b/src/App.css @@ -36,3 +36,155 @@ transform: rotate(360deg); } } + +/* Status display styles */ +.status-container { + margin-top: 30px; + width: 80%; + margin: 30px auto 0; +} + +.status-header { + margin-bottom: 10px; + font-weight: bold; +} + +.status-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #ccc; + padding: 10px; + border-radius: 4px; +} + +.status-item { + display: flex; + justify-content: space-between; + padding: 8px; + margin: 5px 0; + border-radius: 4px; + transition: all 0.3s ease; +} + +.status-item:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.status-address { + font-size: 14px; + font-family: monospace; + word-break: break-all; + max-width: 60%; +} + +.status-details { + text-align: right; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.status-amount { + font-size: 12px; + margin-bottom: 2px; +} + +.status-text { + font-size: 12px; + font-weight: bold; +} + +.status-error { + font-size: 10px; + margin-top: 2px; + color: #721c24; + max-width: 200px; + word-wrap: break-word; +} + +/* Status colors */ +.status-pending { + background-color: #f8f9fa; + border: 1px solid #ddd; +} + +.status-pending .status-amount, +.status-pending .status-text { + color: #6c757d; +} + +.status-success { + background-color: #d4edda; + border: 1px solid #c3e6cb; +} + +.status-success .status-amount, +.status-success .status-text { + color: #155724; +} + +.status-failed { + background-color: #f8d7da; + border: 1px solid #f5c6cb; +} + +.status-failed .status-amount, +.status-failed .status-text { + color: #721c24; +} + +/* Error message styles */ +.error-container { + margin-top: 20px; + padding: 10px; + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + border-radius: 4px; + width: 80%; + margin: 20px auto 0; +} + +.error-title { + font-weight: bold; + margin-right: 5px; +} + +/* Upload area improvements */ +.upload-area { + border: 2px dashed gray; + padding: 50px; + text-align: center; + margin: 50px auto 20px; + width: 200px; + transition: all 0.3s ease; +} + +.upload-area:hover { + border-color: #007bff; + background-color: #f8f9ff; +} + +/* Button improvements */ +.action-button { + margin-top: 20px; + padding: 8px; + width: 100px; + font-size: 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s ease; +} + +.action-button:hover { + background-color: #0069d9; +} + +.action-button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} diff --git a/src/App.jsx b/src/App.jsx index ce0f70b..f5b6cb6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,20 +1,35 @@ import './App.css'; -import { read, utils } from "xlsx"; -import { ethers, formatEther, parseEther } from "ethers"; +import { read, utils } from 'xlsx'; +import { ethers, formatEther, parseEther } from 'ethers'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { tokenAbi } from "./tokenAbi"; +import { tokenAbi } from './tokenAbi'; -const contractAddress = "0x1ebA64fDe3BF54545c86B9e3bB40c72f50f8D012"; +const contractAddress = '0x1ebA64fDe3BF54545c86B9e3bB40c72f50f8D012'; function App() { - const abi = [{"inputs": [{"internalType": "address","name": "token","type": "address"},{"internalType": "address[]","name": "addresses","type": "address[]"},{"internalType": "uint256[]","name": "amounts","type": "uint256[]"},{"internalType": "bool","name": "isToken","type": "bool"}],"name": "batchTransferFrom","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "payable","type": "function"}] + const abi = [ + { + inputs: [ + { internalType: 'address', name: 'token', type: 'address' }, + { internalType: 'address[]', name: 'addresses', type: 'address[]' }, + { internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' }, + { internalType: 'bool', name: 'isToken', type: 'bool' }, + ], + name: 'batchTransferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, + ]; - const [from, setFrom] = useState(""); + const [from, setFrom] = useState(''); const [toArray, setToArray] = useState([]); const [amountArray, setAmountArray] = useState([]); + const [transferStatuses, setTransferStatuses] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); const isToken = true; const [provider, setProvider] = useState(); - const [text, setText] = useState("파일 업로드") + const [text, setText] = useState('파일 업로드'); const tokenRef = useRef(null); const handleFile = (file) => { @@ -28,28 +43,36 @@ function App() { const workbook = read(data, { type: 'array' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; - const jsonData = utils.sheet_to_json(worksheet, {raw:true}); + const jsonData = utils.sheet_to_json(worksheet, { raw: true }); - const filteredData = jsonData.filter(row => { + const filteredData = jsonData.filter((row) => { return ( - typeof row.address === "string" && - !row.address.toUpperCase().includes("EX") && - !String(row.amount).toUpperCase().includes("EX") + typeof row.address === 'string' && + !row.address.toUpperCase().includes('EX') && + !String(row.amount).toUpperCase().includes('EX') ); }); - const addresses = filteredData.map((row) => row.address); + const addresses = filteredData.map((row) => row.address.trim()); const amount = filteredData.map((row) => { // 소수점 2자리까지 반올림 처리 const rounded = Math.round(Number(row.amount) * 100) / 100; return parseEther(rounded.toFixed(2)); }); + + // Initialize transfer statuses for each address + const statuses = addresses.map(() => ({ + status: 'pending', + message: '', + })); + setToArray(addresses); setAmountArray(amount); + setTransferStatuses(statuses); + setErrorMessage(''); // Clear any previous errors } catch (error) { - alert(error) + setErrorMessage(`파일 처리 오류: ${error.message}`); } - }; reader.readAsArrayBuffer(file); }; @@ -73,114 +96,134 @@ function App() { e.preventDefault(); }; - useEffect(()=>{ - if (!window.ethereum) return alert("MetaMask not installed"); - setProvider(new ethers.BrowserProvider(window.ethereum)) - },[]) + useEffect(() => { + if (!window.ethereum) + return setErrorMessage('MetaMask가 설치되지 않았습니다.'); + setProvider(new ethers.BrowserProvider(window.ethereum)); + }, []); - - const walletProvider = useCallback(async ()=>{ + const walletProvider = useCallback(async () => { try { await window.ethereum.request({ method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x38' }] //0x38 + params: [{ chainId: '0x38' }], //0x38 56 }); } catch (err) { await window.ethereum.request({ method: 'wallet_addEthereumChain', - params: [{ - chainId: '0x38', //0x38 - chainName: 'BNB Smart Chain Mainnet', - rpcUrls: ['https://binance.llamarpc.com'], - nativeCurrency: { - name: 'BNB', - symbol: 'BNB', - decimals: 18 + params: [ + { + chainId: '0x38', //0x38 + chainName: 'BNB Smart Chain Mainnet', + rpcUrls: ['https://binance.llamarpc.com'], + nativeCurrency: { + name: 'BNB', + symbol: 'BNB', + decimals: 18, + }, + blockExplorerUrls: ['https://bscscan.com/'], }, - blockExplorerUrls: ['https://bscscan.com/'] - }] + ], }); } - - await provider.send("eth_requestAccounts", []); - - window.ethereum.on("accountsChanged", (accounts) => { + + await provider.send('eth_requestAccounts', []); + + window.ethereum.on('accountsChanged', (accounts) => { if (accounts.length > 0) { setFrom(accounts[0]); } else { setFrom(null); } }); - const signer = provider.getSigner(); + const signer = provider.getSigner(); const senderAddress = await signer; - setFrom(senderAddress.address) - },[provider]) - - useEffect(()=>{ - if(provider){ - walletProvider() + setFrom(senderAddress.address); + }, [provider]); + + useEffect(() => { + if (provider) { + walletProvider(); } return () => { if (window.ethereum?.removeListener) { - window.ethereum.removeListener("accountsChanged", () => {}); - window.ethereum.removeListener("chainChanged", () => {}); + window.ethereum.removeListener('accountsChanged', () => {}); + window.ethereum.removeListener('chainChanged', () => {}); } }; - - },[provider,walletProvider]) + }, [provider, walletProvider]); const sendToken = async () => { const tokenAddress = tokenRef.current?.value; let totalAmount = 0; - amountArray.map((row)=> totalAmount += parseFloat(formatEther(row))); + amountArray.map((row) => (totalAmount += parseFloat(formatEther(row)))); const signer = await provider.getSigner(); const contract = new ethers.Contract(contractAddress, abi, signer); - const batchSize = 200; + const batchSize = 100; const loopCount = Math.ceil(toArray.length / batchSize); let status; - if(toArray.length !== 0 && amountArray.length !== 0 && toArray.length === amountArray.length) - { + if ( + toArray.length !== 0 && + amountArray.length !== 0 && + toArray.length === amountArray.length + ) { try { - const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer); + const tokenContract = new ethers.Contract( + tokenAddress, + tokenAbi, + signer + ); - let balance = isToken ? await tokenContract.balanceOf(from) : await provider.getBalance(from); - console.log(balance, parseEther(totalAmount.toString())) - if(balance >= parseEther(totalAmount.toString())){ - if(isToken) - { + let balance = isToken + ? await tokenContract.balanceOf(from) + : await provider.getBalance(from); + console.log(balance, parseEther(totalAmount.toString())); + if (balance >= parseEther(totalAmount.toString())) { + if (isToken) { //token approve const tx = await tokenContract.approve( contractAddress, parseEther(totalAmount.toString()) - ) + ); await provider.waitForTransaction(tx.hash); } - } else { - throw new Error("잔액부족") + } else { + throw new Error('잔액부족'); } } catch (error) { - alert(error) + console.error('failed to send'); + setErrorMessage(`전송 준비 중 오류: ${error.message}`); + return; } } else { - alert("양식이 맞지 않습니다.") + setErrorMessage('파일 양식이 맞지 않습니다. 주소와 금액을 확인해주세요.'); + return; } - - for (let i = 0; i < loopCount; i++) - { + + for (let i = 0; i < loopCount; i++) { const start = i * batchSize; const end = start + batchSize; const chunkAddresses = toArray.slice(start, end); const chunkAmounts = amountArray.slice(start, end); - const chunkTotal = chunkAmounts.reduce((acc, amt) => acc + parseFloat(formatEther(amt)), 0); - console.log(tokenAddress, chunkAddresses, chunkAmounts, isToken,chunkTotal) - + const chunkTotal = chunkAmounts.reduce( + (acc, amt) => acc + parseFloat(formatEther(amt)), + 0 + ); + console.log( + tokenAddress, + chunkAddresses, + chunkAmounts, + isToken, + chunkTotal + ); + //multisender try { const tx = await contract.batchTransferFrom( @@ -188,72 +231,173 @@ function App() { chunkAddresses, chunkAmounts, isToken, - {value : isToken ? 0 : parseEther(chunkTotal.toString())} + { value: isToken ? 0 : parseEther(chunkTotal.toString()) } ); - status = await provider.waitForTransaction(tx.hash); + const result = await provider.waitForTransaction(tx.hash); + + // Update statuses for this batch + if (result && result.status === 1) { + // Success - update all addresses in this batch to success + setTransferStatuses((prev) => { + const newStatuses = [...prev]; + for (let j = start; j < end && j < newStatuses.length; j++) { + newStatuses[j] = { status: 'success', message: '전송 성공' }; + } + return newStatuses; + }); + } else { + // Failed - update all addresses in this batch to failed + setTransferStatuses((prev) => { + const newStatuses = [...prev]; + for (let j = start; j < end && j < newStatuses.length; j++) { + newStatuses[j] = { status: 'failed', message: '트랜잭션 실패' }; + } + return newStatuses; + }); + } + + status = result; } catch (error) { - alert(error) + // Update all addresses in this batch to failed with error message + setTransferStatuses((prev) => { + const newStatuses = [...prev]; + for (let j = start; j < end && j < newStatuses.length; j++) { + newStatuses[j] = { + status: 'failed', + message: `전송 실패: ${error.message || '알 수 없는 오류'}`, + }; + } + return newStatuses; + }); + + setErrorMessage( + `배치 전송 실패: ${error.message || '알 수 없는 오류'}` + ); } - } - if(status){ - setText("파일 업로드") + if (status) { + setText('파일 업로드'); setToArray([]); - setAmountArray([]); + setAmountArray([]); } - }; return ( -