262 lines
8.0 KiB
JavaScript
262 lines
8.0 KiB
JavaScript
import './App.css';
|
|
import { read, utils } from "xlsx";
|
|
import { ethers, formatEther, parseEther } from "ethers";
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { tokenAbi } from "./tokenAbi";
|
|
|
|
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 [from, setFrom] = useState("");
|
|
const [toArray, setToArray] = useState([]);
|
|
const [amountArray, setAmountArray] = useState([]);
|
|
const isToken = true;
|
|
const [provider, setProvider] = useState();
|
|
const [text, setText] = useState("파일 업로드")
|
|
const tokenRef = useRef(null);
|
|
|
|
const handleFile = (file) => {
|
|
if (!file) return;
|
|
setText(file.name);
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (evt) => {
|
|
try {
|
|
const data = new Uint8Array(evt.target.result);
|
|
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 filteredData = jsonData.filter(row => {
|
|
return (
|
|
typeof row.address === "string" &&
|
|
!row.address.toUpperCase().includes("EX") &&
|
|
!String(row.amount).toUpperCase().includes("EX")
|
|
);
|
|
});
|
|
|
|
const addresses = filteredData.map((row) => row.address);
|
|
const amount = filteredData.map((row) => {
|
|
// 소수점 2자리까지 반올림 처리
|
|
const rounded = Math.round(Number(row.amount) * 100) / 100;
|
|
return parseEther(rounded.toFixed(2));
|
|
});
|
|
setToArray(addresses);
|
|
setAmountArray(amount);
|
|
} catch (error) {
|
|
alert(error)
|
|
}
|
|
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
};
|
|
|
|
const handleDrop = (e) => {
|
|
e.preventDefault();
|
|
const file = e.dataTransfer.files[0];
|
|
handleFile(file);
|
|
};
|
|
|
|
const handleFileChange = (e) => {
|
|
const file = e.target.files[0];
|
|
handleFile(file);
|
|
};
|
|
|
|
// const handleCheckboxChange = (e) => {
|
|
// setIsToken(e.target.checked);
|
|
// };
|
|
|
|
const handleDragOver = (e) => {
|
|
e.preventDefault();
|
|
};
|
|
|
|
useEffect(()=>{
|
|
if (!window.ethereum) return alert("MetaMask not installed");
|
|
setProvider(new ethers.BrowserProvider(window.ethereum))
|
|
},[])
|
|
|
|
|
|
const walletProvider = useCallback(async ()=>{
|
|
try {
|
|
await window.ethereum.request({
|
|
method: 'wallet_switchEthereumChain',
|
|
params: [{ chainId: '0x38' }] //0x38
|
|
});
|
|
} 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
|
|
},
|
|
blockExplorerUrls: ['https://bscscan.com/']
|
|
}]
|
|
});
|
|
}
|
|
|
|
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 senderAddress = await signer;
|
|
setFrom(senderAddress.address)
|
|
},[provider])
|
|
|
|
useEffect(()=>{
|
|
if(provider){
|
|
walletProvider()
|
|
}
|
|
|
|
return () => {
|
|
if (window.ethereum?.removeListener) {
|
|
window.ethereum.removeListener("accountsChanged", () => {});
|
|
window.ethereum.removeListener("chainChanged", () => {});
|
|
}
|
|
};
|
|
|
|
},[provider,walletProvider])
|
|
|
|
const sendToken = async () => {
|
|
const tokenAddress = tokenRef.current?.value;
|
|
|
|
let totalAmount = 0;
|
|
amountArray.map((row)=> totalAmount += parseFloat(formatEther(row)));
|
|
|
|
const signer = await provider.getSigner();
|
|
const contract = new ethers.Contract(contractAddress, abi, signer);
|
|
|
|
const batchSize = 200;
|
|
const loopCount = Math.ceil(toArray.length / batchSize);
|
|
|
|
let status;
|
|
|
|
if(toArray.length !== 0 && amountArray.length !== 0 && toArray.length === amountArray.length)
|
|
{
|
|
try {
|
|
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)
|
|
{
|
|
//token approve
|
|
const tx = await tokenContract.approve(
|
|
contractAddress,
|
|
parseEther(totalAmount.toString())
|
|
)
|
|
await provider.waitForTransaction(tx.hash);
|
|
}
|
|
} else {
|
|
throw new Error("잔액부족")
|
|
}
|
|
} catch (error) {
|
|
alert(error)
|
|
}
|
|
} else {
|
|
alert("양식이 맞지 않습니다.")
|
|
}
|
|
|
|
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)
|
|
|
|
//multisender
|
|
try {
|
|
const tx = await contract.batchTransferFrom(
|
|
tokenAddress,
|
|
chunkAddresses,
|
|
chunkAmounts,
|
|
isToken,
|
|
{value : isToken ? 0 : parseEther(chunkTotal.toString())}
|
|
);
|
|
|
|
status = await provider.waitForTransaction(tx.hash);
|
|
} catch (error) {
|
|
alert(error)
|
|
}
|
|
|
|
}
|
|
|
|
if(status){
|
|
setText("파일 업로드")
|
|
setToArray([]);
|
|
setAmountArray([]);
|
|
}
|
|
|
|
};
|
|
|
|
return (
|
|
<div className="App">
|
|
<h1>Admin MultiSender</h1>
|
|
<div
|
|
style={{
|
|
width : "600px",
|
|
height : "100px",
|
|
margin : "0 Auto",
|
|
display : "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
{/* <div>
|
|
<span style={{fontSize: "18px"}}>토큰 전송 </span>
|
|
<input type="checkbox" onChange={handleCheckboxChange} />
|
|
</div> */}
|
|
<div>
|
|
<span style={{fontSize: "18px"}}>토큰 컨트랙트 : </span>
|
|
<input type="text" ref={tokenRef} placeholder='' style={{fontSize: "18px", marginTop : "10px", width : "450px"}} defaultValue={"0x36b8dE7c6B06B3f170003452114f0B8E6BcFEE18"}/>
|
|
</div>
|
|
<div>
|
|
<span style={{fontSize: "18px"}}>보내는 계좌 : </span>
|
|
<input type='text' placeholder={from} style={{fontSize: "18px", marginTop : "10px", width: "450px", border: "none"}} disabled />
|
|
</div>
|
|
</div>
|
|
|
|
<input type="file" id="fileInput" style={{ display: "none" }} onChange={handleFileChange} />
|
|
<label htmlFor="fileInput">
|
|
<div
|
|
id="upload"
|
|
onDrop={handleDrop}
|
|
onDragOver={handleDragOver}
|
|
style={{
|
|
// display : toArray.length == 0 ? "block" : "none",
|
|
border: '2px dashed gray',
|
|
padding: '50px',
|
|
textAlign: 'center',
|
|
marginTop: '50px',
|
|
marginBottom: '20px',
|
|
margin: "0 Auto",
|
|
width: "200px"
|
|
}}
|
|
>
|
|
{text}
|
|
</div>
|
|
</label>
|
|
|
|
<button type='button' onClick={sendToken} style={{marginTop : "20px", padding: "8px", width: "100px", fontSize: "20px"}}>전송</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|