/* =====================================================================
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.doneProgress tersimpan: "+p.done+"/"+p.total+" batch sudah terkirim ("+p.wallets+" wallet). Klik Send 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);
*/