ipfs document storage for etica - works on etica network/currencys
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

338 lines
12 KiB

// app.js — frontend logic for IPFS Web Uploader (EGAZ / ETI)
const $ = (id) => document.getElementById(id);
const log = (msg) => {
const el = $("log");
const now = new Date().toISOString().replace("T"," ").replace("Z","");
el.textContent += `[${now}] ${msg}\n`;
el.scrollTop = el.scrollHeight;
};
let CFG = null;
let RATES = null;
let CURRENT_QUOTE = null;
let CURRENT_TX = null;
let POLL_TIMER = null;
let SELECTED_ACCOUNT = null;
let UPLOADED_CID = null;
async function api(path, opts) {
const res = await fetch(path, opts);
if (!res.ok) throw new Error(`${path} HTTP ${res.status}`);
return res.json();
}
// ===== helpers =====
function middleEllipsis(addr, head=14, tail=12){
if (!addr) return "–";
if (addr.length <= head+tail) return addr;
return addr.slice(0, head) + "…" + addr.slice(-tail);
}
function fmtDec(decStr, digits=6){
const [i,f=""] = String(decStr).split(".");
const cut = f.slice(0, digits).replace(/0+$/,"");
return cut ? `${i}.${cut}` : i;
}
function bytesHuman(n){
const b=Number(n); if(b<1024) return `${b} B`;
const u=['KB','MB','GB','TB']; let i=-1,x=b;
do{x/=1024;i++;}while(x>=1024&&i<u.length-1);
return `${x.toFixed(2)} ${u[i]}`;
}
function parseUnits(decStr, decimals=18){
if (typeof decStr !== "string") decStr = String(decStr);
const neg=decStr.startsWith("-"); if(neg) decStr=decStr.slice(1);
if(!/^\d+(\.\d+)?$/.test(decStr)) throw new Error("bad decimal");
const [ip,frRaw=""]=decStr.split(".");
const fr=(frRaw+"0".repeat(decimals)).slice(0,decimals);
const bi=BigInt(ip+fr.replace(/^0+$/,""));
return neg?-bi:bi;
}
function to0xHex(big){ return "0x" + big.toString(16); }
function encodeErc20Transfer(to, amountWei){
const method="a9059cbb";
const addr=to.toLowerCase().replace(/^0x/,"").padStart(64,"0");
const amt=amountWei.toString(16).padStart(64,"0");
return "0x"+method+addr+amt;
}
// ===== theme toggle =====
$("btnTheme").onclick = () => {
const cur = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
const next = cur === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = next;
try { localStorage.setItem("theme", next); } catch {}
};
// ===== config/health/rates =====
async function loadConfigAndHealth(){
CFG = await api("/api/config/public");
$("cfgChain").textContent = CFG.chain;
$("cfgRpc").textContent = CFG.rpc;
$("cfgPayTo").textContent = CFG.payTo;
$("cfgTier").textContent = `${CFG.tierMB} MB`;
$("cfgPpt").textContent = `${CFG.pricePerTierEgaz} ${CFG.currencyBase || "EGAZ"}`;
$("cfgCurrencies").textContent = (CFG.supportedCurrencies||["EGAZ"]).join(", ");
$("cfgConf").textContent = CFG.requiredConfirmations ?? "3";
$("confReq").textContent = CFG.requiredConfirmations ?? "3";
const h = await api("/api/health");
$("healthBlock").textContent = h.latestBlock;
$("healthReq").textContent = h.requiredConfirmations;
$("healthNow").textContent = h.now;
log(`RPC ok, latest=${h.latestBlock}`);
try{
RATES = await api("/api/rates");
$("rateSrc").textContent = RATES.source || "fallback";
$("rateEgazUsd").textContent = RATES.egazUsd ? `$${RATES.egazUsd}` : "–";
$("rateEtiUsd").textContent = RATES.etiUsd ? `$${RATES.etiUsd}` : "–";
$("rateEtiPerEgaz").textContent = RATES.etiPerEgaz ? `${RATES.etiPerEgaz.toFixed(6)} ETI` : "–";
log(`Rates source=${RATES.source} egazUsd=${RATES.egazUsd||0} etiUsd=${RATES.etiUsd||0} etiPerEgaz=${RATES.etiPerEgaz||1}`);
}catch(e){
$("rateSrc").textContent = "unavailable";
$("rateEgazUsd").textContent = "–";
$("rateEtiUsd").textContent = "–";
$("rateEtiPerEgaz").textContent = "–";
log(`rates error: ${e.message}`);
}
}
// ===== wallet =====
async function ensureWallet(){
if (!window.ethereum) {
$("tbAcct").textContent = "wallet: no metamask";
$("btnConnectTop").disabled = true;
$("btnConnectTop").textContent = "No MetaMask";
return;
}
const accts = await ethereum.request({ method:"eth_accounts" });
if (accts && accts.length){ setAccount(accts[0]); }
ethereum.on?.("accountsChanged", (acc) => setAccount(acc[0] || null));
}
function setAccount(a){
SELECTED_ACCOUNT = a || null;
$("tbAcct").textContent = a ? `wallet: ${middleEllipsis(a)}` : "wallet: disconnected";
if (a){
$("btnConnectTop").textContent = "Connected";
$("btnConnectTop").disabled = true;
loadFiles();
} else {
$("btnConnectTop").textContent = "Connect MetaMask";
$("btnConnectTop").disabled = false;
renderFiles([]);
}
}
$("btnConnectTop").onclick = async ()=>{
if (!window.ethereum){ alert("MetaMask not found"); return; }
const acc = await ethereum.request({ method:"eth_requestAccounts" });
setAccount(acc[0] || null);
};
// ===== files panel =====
async function loadFiles(){
try{
$("filesStatus").textContent = "loading…";
const addr = SELECTED_ACCOUNT || "0x0000000000000000000000000000000000000000";
const res = await api(`/api/files?address=${addr}`);
renderFiles(res.files || []);
$("filesStatus").textContent = `showing ${(res.files||[]).length} file(s) for ${middleEllipsis(addr)}`;
}catch(e){
$("filesStatus").textContent = `error: ${e.message}`;
renderFiles([]);
}
}
function renderFiles(files){
const wrap = $("filesTableWrap");
const empty = $("filesEmpty");
const tbody = $("filesTbody");
tbody.innerHTML = "";
if (!files || files.length === 0){
empty.classList.remove("hidden");
wrap.classList.add("hidden");
return;
}
empty.classList.add("hidden");
wrap.classList.remove("hidden");
for (const f of files){
const tr = document.createElement("tr");
const size = f.size_bytes ?? f.sizeBytes ?? 0;
const cid = f.cid || "";
const path = f.path || "";
const label = f.label || "";
const when = f.created_at || f.createdAt || "";
const status = f.status || "stored";
tr.innerHTML = `
<td>${f.filename || "—"}</td>
<td>${bytesHuman(size)}</td>
<td class="mono">${cid || "—"}</td>
<td class="mono">${path || "—"}</td>
<td>${label || "—"}</td>
<td><span class="tag">${status}</span></td>
<td class="small">${when ? new Date(when).toLocaleString() : "—"}</td>
`;
tbody.appendChild(tr);
}
}
$("btnRefreshFiles").onclick = loadFiles;
// ===== quote & pay & upload =====
function resetQuoteUI(){
$("quoteBox").classList.add("hidden");
$("confirmBox").classList.add("hidden");
$("quoteStatus").textContent = "";
$("payStatus").textContent = "";
$("btnUploadNow").classList.add("hidden");
CURRENT_QUOTE = null; CURRENT_TX = null; UPLOADED_CID = null;
if (POLL_TIMER) { clearInterval(POLL_TIMER); POLL_TIMER = null; }
}
$("btnQuote").onclick = async ()=>{
try{
resetQuoteUI();
const f = $("file").files[0];
if (!f){ alert("Choose a file"); return; }
$("quoteStatus").textContent = "requesting quote…";
const body = {
filename: f.name,
sizeBytes: f.size,
address: SELECTED_ACCOUNT || "0x0000000000000000000000000000000000000000",
path: $("path").value || null
};
const q = await api("/api/quote", {
method: "POST",
headers: { "content-type":"application/json" },
body: JSON.stringify(body)
});
CURRENT_QUOTE = q;
$("qId").textContent = q.quoteId;
$("qFile").textContent = q.filename;
$("qSize").textContent = bytesHuman(q.sizeBytes);
$("qTiers").textContent = q.tiers;
$("qTierMb").textContent = `${q.tierMB} MB`;
$("qEgaz").textContent = `${fmtDec(q.priceEgaz)} EGAZ`;
$("qEti").textContent = `${fmtDec(q.priceEti)} ETI`;
$("qPayTo").textContent = q.payTo;
$("qExp").textContent = new Date(q.expiresAt).toLocaleString();
$("quoteBox").classList.remove("hidden");
$("quoteStatus").textContent = "quote ready";
log(`quote ${q.quoteId} priceEgaz=${q.priceEgaz} priceEti=${q.priceEti}`);
}catch(e){
$("quoteStatus").textContent = `error: ${e.message}`;
log(`quote error: ${e.stack||e}`);
}
};
$("btnPay").onclick = async ()=>{
try{
if (!CURRENT_QUOTE) { alert("Get a quote first"); return; }
if (!window.ethereum) { alert("MetaMask not found"); return; }
if (!SELECTED_ACCOUNT){
const acc = await ethereum.request({ method:"eth_requestAccounts" });
setAccount(acc[0] || null);
if (!SELECTED_ACCOUNT) throw new Error("no account");
}
const sel = $("currencySel").value; // "EGAZ" or "ETI"
const payTo = CURRENT_QUOTE.payTo;
let txParams = { from: SELECTED_ACCOUNT };
if (sel === "EGAZ"){
const wei = parseUnits(CURRENT_QUOTE.priceEgaz, 18);
txParams.to = payTo;
txParams.value = to0xHex(wei);
txParams.data = "0x";
log(`sending EGAZ value=${CURRENT_QUOTE.priceEgaz} -> ${payTo}`);
} else {
const token = CFG.etiContract || "0x34c61EA91bAcdA647269d4e310A86b875c09946f";
const wei = parseUnits(CURRENT_QUOTE.priceEti, 18);
txParams.to = token; txParams.value = "0x0"; txParams.data = encodeErc20Transfer(payTo, wei);
log(`sending ETI token=${token} amount=${CURRENT_QUOTE.priceEti} -> ${payTo}`);
}
$("payStatus").textContent = "opening MetaMask…";
const txHash = await ethereum.request({ method: "eth_sendTransaction", params: [txParams] });
CURRENT_TX = txHash;
$("payStatus").textContent = `tx sent: ${middleEllipsis(txHash)}`;
log(`tx sent ${txHash}`);
const rec = await api("/api/payments/record", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ quoteId: String(CURRENT_QUOTE.quoteId), txHash })
});
log(`payments/record ok currency=${rec.currency||"?"}`);
$("confirmBox").classList.remove("hidden");
$("confTx").textContent = txHash;
$("confReq").textContent = CFG.requiredConfirmations ?? 3;
pollStatusStart(Number(CURRENT_QUOTE.quoteId));
}catch(e){
$("payStatus").textContent = `error: ${e.message}`;
log(`pay error: ${e.stack||e}`);
}
};
async function uploadNow(){
try{
const f = $("file").files[0];
if (!f){ alert("Choose a file"); return; }
if (!CURRENT_QUOTE){ alert("Get quote & pay first"); return; }
const form = new FormData();
form.append("file", f);
form.append("quoteId", String(CURRENT_QUOTE.quoteId));
form.append("address", SELECTED_ACCOUNT || "");
if ($("path").value) form.append("path", $("path").value);
if ($("label").value) form.append("label", $("label").value);
$("payStatus").textContent = "uploading to IPFS…";
const res = await fetch("/api/upload", { method:"POST", body: form });
if (!res.ok) throw new Error(`/api/upload HTTP ${res.status}`);
const j = await res.json();
if (!j.ok) throw new Error(j.error || "upload failed");
UPLOADED_CID = j.cid;
$("payStatus").textContent = `uploaded: ${j.cid}`;
log(`upload ok cid=${j.cid}`);
await loadFiles();
}catch(e){
$("payStatus").textContent = `upload error: ${e.message}`;
log(`upload error: ${e.stack||e}`);
}
}
$("btnUploadNow").onclick = uploadNow;
async function pollOnce(quoteId){
try{
const st = await api(`/api/payments/status?quoteId=${quoteId}`);
$("confStatus").textContent = st.status;
$("confCount").textContent = st.confs;
if (st.txHash) $("confTx").textContent = st.txHash;
if (st.status === "paid"){
log(`quote ${quoteId} paid (confs ${st.confs}/${st.required})`);
clearInterval(POLL_TIMER); POLL_TIMER = null;
$("btnUploadNow").classList.remove("hidden");
// Auto-upload if a file is selected and not uploaded yet
if ($("file").files[0] && !UPLOADED_CID){
uploadNow();
}
}
}catch(e){
log(`status poll error: ${e.message}`);
}
}
function pollStatusStart(quoteId){
if (POLL_TIMER) clearInterval(POLL_TIMER);
pollOnce(quoteId);
POLL_TIMER = setInterval(()=>pollOnce(quoteId), 5000);
}
// ===== init =====
(async function init(){
try{
await loadConfigAndHealth();
await ensureWallet();
await loadFiles();
}catch(e){
log(`init error: ${e.message}`);
}
})();