/* ===================================================================== TOKENFORGE PRO — PATCH: airdrop resume per batch Ganti 3 bagian di file HTML kamu: [1] HTML — baris "batch size" di tab Airdrop [2] JS — object `store` (tambah remove) [3] JS — seluruh function runAirdrop() + helper baru ===================================================================== */ /* ---------- [1] HTML: ganti
yang berisi tombol "Validate list" + input batch size dengan blok ini ---------- */ /*
*/ /* ---------- [2] JS: ganti object `store` dengan ini (tambah remove) ---------- */ const _mem={}; const store={ get:k=>{try{return localStorage.getItem(k)??_mem[k]??null}catch(e){return _mem[k]??null}}, set:(k,v)=>{_mem[k]=v;try{localStorage.setItem(k,v)}catch(e){}}, remove:k=>{delete _mem[k];try{localStorage.removeItem(k)}catch(e){}} }; /* ---------- [3] JS: helper progress + runAirdrop() baru ---------- */ /* Kunci progress = hash dari (chain, MultiSender, token, mode, amount, batch size, isi list). Kalau salah satu berubah → dianggap airdrop baru, mulai dari batch 1. */ function airdropKey(msAddr,tokenAddr,mode,equal,batch){ const list=document.getElementById("adList").value.trim(); const raw=[chainId,msAddr.toLowerCase(),tokenAddr.toLowerCase(),mode,equal,batch,list].join("|"); return "tf_ad_"+ethers.id(raw).slice(2,22); } function loadProgress(key){try{return JSON.parse(store.get(key)||"null")}catch(e){return null}} function saveProgress(key,p){store.set(key,JSON.stringify(p));} let _adKey=null; /* Banner "ada progress tersimpan" — dipanggil saat validate / sebelum kirim */ function showResumeUI(){ const msAddr=(document.getElementById("msAddr")?.value||"").trim(); const tokenAddr=(document.getElementById("adToken")?.value||"").trim(); const box=document.getElementById("adResume"), rb=document.getElementById("adResetBtn"); if(!box)return; if(!chainId||!ethers.isAddress(msAddr)||!ethers.isAddress(tokenAddr)){box.style.display="none";if(rb)rb.style.display="none";return;} const mode=document.getElementById("adMode").value; const equal=mode==="equal"?String(+document.getElementById("adEqual").value):""; const batch=Math.max(1,parseInt(document.getElementById("adBatch").value)||200); _adKey=airdropKey(msAddr,tokenAddr,mode,equal,batch); const p=loadProgress(_adKey); if(p&&p.done>0&&p.doneSend Airdrop untuk lanjut dari batch "+(p.done+1)+"."+(p.pending?" Batch "+(p.done+1)+" masih ada tx pending, akan dicek dulu.":""); box.style.display="";if(rb)rb.style.display=""; document.getElementById("adStart").value=String(p.done+1); }else{box.style.display="none";if(rb)rb.style.display="none";} } function resetAirdropProgress(){ if(_adKey)store.remove(_adKey); document.getElementById("adStart").value="1"; showResumeUI(); log("Progress airdrop di-reset — akan mulai dari batch 1.","warn"); } async function runAirdrop(){ if(!signer)return; const msAddr=document.getElementById("msAddr").value.trim(); const tokenAddr=document.getElementById("adToken").value.trim(); if(!ethers.isAddress(msAddr)){log("Deploy or paste a MultiSender address.","err");return;} if(!ethers.isAddress(tokenAddr)){log("Enter the token address to distribute.","err");return;} const {valid,mode}=validateList(); if(!valid.length){log("No valid recipients.","err");return;} const batch=Math.max(1,parseInt(document.getElementById("adBatch").value)||200); const btn=document.getElementById("adBtn");btn.disabled=true; const prog=document.getElementById("adProg");prog.style.display="";prog.value=0; const chunks=[];for(let i=0;i0&&p.done1&&manual<=chunks.length&&manual-1!==start){start=manual-1;pendingHash=null;log("Mulai manual dari batch "+manual+".","warn");} if(start>0)log("↻ Resume: "+start+"/"+chunks.length+" batch sudah terkirim sebelumnya — lanjut dari batch "+(start+1)+".","step"); try{ const token=new ethers.Contract(tokenAddr,ERC20_ABI,signer); const dec=await token.decimals(); const ms=new ethers.Contract(msAddr,MS_ABI,signer); const equalAmt=mode==="equal"?ethers.parseUnits(equalStr,dec):null; /* ---- kalau batch terakhir sebelum gagal masih pending, cek dulu nasibnya ---- */ if(pendingHash){ log("Batch "+(start+1)+" punya tx pending "+txLink(pendingHash)+" — cek status…","step"); let rc=await provider.getTransactionReceipt(pendingHash); if(!rc){log("Belum mined, menunggu…","info");try{rc=await provider.waitForTransaction(pendingHash,1,180000);}catch(_){rc=null;}} if(rc&&rc.status===1){ log("✔ Batch "+(start+1)+" ternyata sudah sukses — skip.","ok"); start++;saveProgress(key,{done:start,total:chunks.length,wallets:chunks.slice(0,start).reduce((a,c)=>a+c.length,0),pending:null}); }else{ log(rc?"Batch "+(start+1)+" revert — dikirim ulang.":"Tx pending tidak ketemu (mungkin di-drop) — dikirim ulang.","warn"); saveProgress(key,{done:start,total:chunks.length,wallets:chunks.slice(0,start).reduce((a,c)=>a+c.length,0),pending:null}); } } if(start>=chunks.length){log("Semua batch sudah terkirim sebelumnya. Tidak ada yang perlu dikirim.","ok");store.remove(key);showResumeUI();btn.disabled=false;prog.value=100;return;} /* ---- total & approval HANYA untuk batch yang tersisa ---- */ const remaining=chunks.slice(start); let total=0n; for(const ch of remaining){ if(mode==="equal")total+=equalAmt*BigInt(ch.length); else ch.forEach(v=>total+=ethers.parseUnits(v.amt,dec)); } const bal=await token.balanceOf(account); if(bal0n){ const usd=await ethUsd(); const usdTxt=usd?" (~$"+(parseFloat(ethers.formatEther(feeTotal))*usd).toFixed(2)+")":""; log("Service fee "+ethers.formatEther(fee)+" "+sym+" per batch × "+remaining.length+" = "+ethers.formatEther(feeTotal)+" "+sym+usdTxt,"info"); const nat=await provider.getBalance(account); if(nata+ch.length,0), feePaid=0n; prog.value=Math.round(done/valid.length*100); log("Sending "+(valid.length-done)+" recipients in "+remaining.length+" batch(es) of ≤"+batch+(start?" (batch "+(start+1)+"–"+chunks.length+")":"")+"…","step"); for(let i=start;iv.addr); const f=fee>0n?await msFeeWei(msAddr):0n; const opts=f>0n?{value:f}:{}; saveProgress(key,{done:i,total:chunks.length,wallets:done,pending:null}); let tx; if(mode==="equal"){tx=await ms.disperseTokenEqual(tokenAddr,addrs,equalAmt,opts);} else{const amts=ch.map(v=>ethers.parseUnits(v.amt,dec));tx=await ms.disperseToken(tokenAddr,addrs,amts,opts);} saveProgress(key,{done:i,total:chunks.length,wallets:done,pending:tx.hash}); // tx sudah dikirim, belum confirm log("Batch "+(i+1)+"/"+chunks.length+" ("+ch.length+") tx "+txLink(tx.hash)+" — waiting…","info"); const rc=await tx.wait(); if(!rc||rc.status!==1)throw new Error("batch "+(i+1)+" reverted on-chain"); done+=ch.length;feePaid+=f;prog.value=Math.round(done/valid.length*100); saveProgress(key,{done:i+1,total:chunks.length,wallets:done,pending:null}); // confirm → simpan log("✔ Batch "+(i+1)+" confirmed ("+done+"/"+valid.length+")","ok"); } store.remove(key); // selesai semua → hapus progress document.getElementById("adStart").value="1"; showResumeUI(); log("✔ Airdrop complete — "+done+" wallets funded","ok"); const r=document.getElementById("adRes");r.className="result show"; r.innerHTML=kv("Recipients",done+"")+kv("Batches",chunks.length+"")+kv("Token",addrLink(tokenAddr))+ (feePaid>0n?kv("Service fee paid",ethers.formatEther(feePaid)+" "+sym):""); }catch(e){ log("Airdrop failed: "+(e.shortMessage||e.reason||e.message),"err"); const q=loadProgress(key); if(q&&q.done>0){log("Progress tersimpan: "+q.done+"/"+q.total+" batch sukses. Klik Send Airdrop lagi untuk lanjut dari batch "+(q.done+1)+" (list & setting jangan diubah).","warn");} showResumeUI(); } btn.disabled=false; } /* ---------- [4] JS kecil: tambahkan pemanggilan showResumeUI ---------- - di akhir function validateList(), sebelum `return {valid,mode};` → showResumeUI(); - di akhir refreshChain() → showResumeUI(); - di DOMContentLoaded, setelah msa.addEventListener(...) → document.getElementById("adToken").addEventListener("blur",showResumeUI); document.getElementById("adList").addEventListener("blur",showResumeUI); */