| |
| from fastapi import FastAPI |
| from fastapi.responses import HTMLResponse |
| import uvicorn |
|
|
| app = FastAPI() |
|
|
| HTML = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <title>LLM Remix IDE — Hardened Alpha</title> |
| <!-- Pinned CDNs --> |
| <script src="https://cdn.jsdelivr.net/npm/ethers@6.13.2/dist/ethers.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/solc@0.8.24/soljson.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.1"></script> |
| <!-- Editor --> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.css"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/theme/dracula.min.css"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/mode/clike/clike.min.js"></script> |
| <style> |
| :root { --bg:#0a0a0a; --panel:#111; --accent:#ff6700; --ok:#19c37d; --err:#ff4d4f; } |
| body { background:var(--bg); color:#f8f8f8; font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin:0; } |
| header { padding:0.8rem 1rem; background:var(--panel); color:var(--accent); font-weight:600; display:flex; gap:.75rem; align-items:center; justify-content:space-between; } |
| .row { display:flex; gap:12px; flex-wrap:wrap; padding:12px; } |
| button, select, input { background:#141414; color:var(--accent); padding:8px 12px; border:none; border-radius:8px; cursor:pointer; } |
| button:hover { background:var(--accent); color:#000; } |
| select, input[type="text"] { color:#f8f8f8; } |
| .pane { background:var(--panel); border-radius:10px; padding:12px; margin:12px; } |
| #status { font-weight:700; padding:8px 12px; } |
| #output { white-space:pre-wrap; background:#0e0e0e; padding:12px; border-radius:8px; max-height:220px; overflow:auto; } |
| #editor { height:360px; border:1px solid #333; border-radius:8px; } |
| .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; } |
| .fn { background:#0e0e0e; padding:10px; border-radius:8px; } |
| .tag { font-size:.85rem; color:#bbb; margin-left:.5rem; } |
| .warn { color:var(--err); } |
| .ok { color:var(--ok); } |
| .small { font-size:.85rem; color:#bbb; } |
| .field { display:flex; gap:8px; align-items:center; margin:6px 0; } |
| .field input { width:100%; } |
| </style> |
| </head> |
| <body> |
| <header> |
| <div>🤖 LLM Remix IDE · safer alpha</div> |
| <div class="row"> |
| <button id="connectBtn">Connect Wallet</button> |
| <button id="switchSepoliaBtn" title="Switch to Sepolia 11155111">Switch → Sepolia</button> |
| <button id="generateBtn">Generate</button> |
| <button id="compileBtn">Compile</button> |
| <button id="deployBtn" title="Deploy to current network">Deploy</button> |
| </div> |
| </header> |
| |
| <div id="status" class="pane">Status: idle</div> |
| |
| <div class="row"> |
| <div class="pane" style="flex:1 1 420px;"> |
| <h3 style="margin-top:0">Prompt</h3> |
| <div class="field"> |
| <input id="prompt" type="text" placeholder="Describe the contract" |
| value="ERC20 token with daily rebase and transfer fee" /> |
| <select id="modelPreset"> |
| <option value="code">code preset</option> |
| <option value="vanilla">vanilla</option> |
| </select> |
| </div> |
| <div class="small">LLM runs in browser via Xenova. Generation quality is limited. Review code before deploy.</div> |
| </div> |
| |
| <div class="pane" style="flex:2 1 640px;"> |
| <h3 style="margin-top:0">Solidity Source</h3> |
| <div id="editor"></div> |
| <div class="small">Edits auto-save to localStorage.</div> |
| </div> |
| </div> |
| |
| <div class="row"> |
| <div class="pane" style="flex:1 1 420px;"> |
| <h3 style="margin-top:0">Compile Log</h3> |
| <div id="output">Output will appear here…</div> |
| </div> |
| <div class="pane" style="flex:1 1 420px;"> |
| <h3 style="margin-top:0">Deployment</h3> |
| <div class="field"> |
| <label class="small">Payable value (wei):</label> |
| <input id="deployValue" type="text" placeholder="0" value="0" /> |
| </div> |
| <div id="ctorFields"></div> |
| <div class="small">Mainnet is blocked by default. You can override with an explicit confirm.</div> |
| <div>Address: <span id="contractAddr">-</span></div> |
| </div> |
| </div> |
| |
| <div class="pane"> |
| <h3 style="margin-top:0;">Contract Functions <span class="tag small" id="fnSummary"></span></h3> |
| <div id="contractUI" class="grid"></div> |
| </div> |
| |
| <script> |
| let provider, signer, currentChainId, contract, abi, bytecode, generator, editor; |
| |
| // --------- Utils --------- |
| const byId = id => document.getElementById(id); |
| const setStatus = (t, cls="") => { const el = byId("status"); el.textContent = t; el.className = "pane " + cls; }; |
| const log = (msg) => { const out = byId("output"); out.textContent = String(msg); }; |
| const save = (k,v)=>localStorage.setItem(k,v); |
| const load = (k,d="")=>localStorage.getItem(k) ?? d; |
| |
| function parseArg(type, val) { |
| if (val === null || val === undefined) return val; |
| if (type.startsWith("uint") || type.startsWith("int")) return BigInt(val); |
| if (type === "bool") return val === true || val === "true" || val === "1"; |
| if (type === "address") { |
| if (!/^0x[0-9a-fA-F]{40}$/.test(val)) throw new Error("Invalid address: " + val); |
| return val; |
| } |
| if (type.endsWith("[]")) { |
| const inner = type.slice(0, -2); |
| const parts = String(val).split(",").map(s => s.trim()).filter(Boolean); |
| return parts.map(p => parseArg(inner, p)); |
| } |
| return val; |
| } |
| |
| function pickFirstArtifact(contracts) { |
| const names = Object.keys(contracts || {}); |
| for (const n of names) { |
| const art = contracts[n]; |
| if (art?.evm?.bytecode?.object) return [n, art]; |
| } |
| return [null, null]; |
| } |
| |
| // --------- Editor --------- |
| (function setupEditor(){ |
| editor = CodeMirror(byId("editor"), { |
| value: load("src", `// Example\npragma solidity ^0.8.24;\ncontract Storage { uint public data; function set(uint x) public { data = x; } }`), |
| mode: "text/x-solidity", |
| theme: "dracula", |
| lineNumbers: true |
| }); |
| editor.on("change", ()=> save("src", editor.getValue())); |
| byId("prompt").value = load("prompt", byId("prompt").value); |
| byId("prompt").addEventListener("change", e=> save("prompt", e.target.value)); |
| })(); |
| |
| // --------- Wallet --------- |
| byId("connectBtn").onclick = async () => { |
| if (!window.ethereum) return alert("No wallet provider found"); |
| provider = new ethers.BrowserProvider(window.ethereum); |
| signer = await provider.getSigner(); |
| currentChainId = (await provider.getNetwork()).chainId; // BigInt |
| setStatus(`Wallet: ${await signer.getAddress()} · chainId ${currentChainId}`); |
| }; |
| |
| byId("switchSepoliaBtn").onclick = async () => { |
| if (!window.ethereum) return; |
| try { |
| await window.ethereum.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0xaa36a7" }] }); // 11155111 |
| provider = new ethers.BrowserProvider(window.ethereum); |
| signer = await provider.getSigner(); |
| currentChainId = (await provider.getNetwork()).chainId; |
| setStatus(`Switched. chainId ${currentChainId}`, "ok"); |
| } catch (e) { |
| setStatus("Switch failed: " + (e?.message || e), "warn"); |
| } |
| }; |
| |
| // --------- LLM --------- |
| (async () => { |
| setStatus("Loading LLM…"); |
| generator = await window.transformers.pipeline("text-generation", "Xenova/distilgpt2"); |
| setStatus("LLM ready"); |
| })(); |
| |
| function systemPrompt(preset, user) { |
| const base = [ |
| "Write ONLY valid Solidity source. No commentary.", |
| "Target pragma ^0.8.24.", |
| "Avoid unsafe patterns. No tx.origin. No inline assembly unless needed.", |
| "Prefer OpenZeppelin interfaces if applicable.", |
| ].join("\n"); |
| if (preset === "code") { |
| return `${base}\nTask: ${user}\nReturn one file with a single primary contract.`; |
| } |
| return `Write a Solidity contract: ${user}`; |
| } |
| |
| byId("generateBtn").onclick = async () => { |
| const user = byId("prompt").value; |
| const preset = byId("modelPreset").value; |
| setStatus("Generating…"); |
| let attempt = 0, code = ""; |
| while (attempt < 3) { |
| attempt++; |
| const out = await generator(systemPrompt(preset, user), { max_new_tokens: 400 }); |
| const txt = out?.[0]?.generated_text || ""; |
| // try to strip junk |
| const m = txt.match(/pragma[\s\S]*$/); |
| code = m ? m[0] : txt; |
| if (code.includes("pragma solidity") && code.includes("contract")) break; |
| } |
| editor.setValue(code); |
| setStatus(`Generated attempt ${attempt}`); |
| }; |
| |
| // --------- Compile --------- |
| byId("compileBtn").onclick = async () => { |
| const source = editor.getValue(); |
| setStatus("Compiling…"); |
| const input = { |
| language: "Solidity", |
| sources: { "Contract.sol": { content: source } }, |
| settings: { outputSelection: { "*": { "*": [ "abi", "evm.bytecode", "evm.deployedBytecode" ] } } } |
| }; |
| try { |
| const compiled = JSON.parse(solc.compile(JSON.stringify(input))); |
| if (compiled.errors?.length) { |
| const lines = compiled.errors.map(e => `${e.severity.toUpperCase()}: ${e.formattedMessage.trim()}`).join("\n"); |
| log(lines); |
| const fatal = compiled.errors.some(e => e.severity === "error"); |
| if (fatal) { setStatus("Compile errors", "warn"); return; } |
| } |
| const file = compiled.contracts["Contract.sol"]; |
| const [name, art] = pickFirstArtifact(file); |
| if (!name) throw new Error("No contract bytecode found"); |
| abi = art.abi; |
| bytecode = art.evm.bytecode.object; |
| contract = { name, abi, bytecode }; |
| log(`✅ Compiled ${name}\nFunctions: ${abi.filter(x=>x.type==="function").length}`); |
| buildCtorUI(abi); |
| buildUI(null, abi); // Prebuild shells |
| setStatus("Compiled", "ok"); |
| } catch (e) { |
| log("❌ Compile failed:\n" + (e?.message || e)); |
| setStatus("Compile failed", "warn"); |
| } |
| }; |
| |
| // --------- Deploy --------- |
| byId("deployBtn").onclick = async () => { |
| try { |
| if (!signer) return alert("Connect wallet first"); |
| if (!abi || !bytecode) return alert("Compile first"); |
| const net = await provider.getNetwork(); |
| const isMainnet = net.chainId === 1n; |
| if (isMainnet) { |
| const ok = confirm("Mainnet detected. This is dangerous. Continue?"); |
| if (!ok) return; |
| } |
| const ctor = abi.find(x => x.type === "constructor") || { inputs: [] }; |
| const args = []; |
| for (const inp of ctor.inputs) { |
| const el = byId(`ctor_${inp.name || "arg"}`); |
| const parsed = parseArg(inp.type, el?.value ?? ""); |
| args.push(parsed); |
| } |
| const valueWei = byId("deployValue").value?.trim() || "0"; |
| const overrides = valueWei && valueWei !== "0" ? { value: BigInt(valueWei) } : {}; |
| const factory = new ethers.ContractFactory(abi, bytecode, signer); |
| const est = await provider.estimateGas(factory.getDeployTransaction(...args, overrides)); |
| log(`Gas estimate: ${est.toString()}`); |
| const instance = await factory.deploy(...args, overrides); |
| setStatus("Deploying…"); |
| await instance.waitForDeployment(); |
| const addr = await instance.getAddress(); |
| byId("contractAddr").textContent = addr; |
| setStatus("Deployed at " + addr, "ok"); |
| buildUI(instance, abi); |
| } catch (e) { |
| setStatus("Deploy failed: " + (e?.message || e), "warn"); |
| } |
| }; |
| |
| // --------- Dynamic UI --------- |
| function buildCtorUI(abi) { |
| const box = byId("ctorFields"); |
| box.innerHTML = ""; |
| const ctor = abi.find(x => x.type === "constructor"); |
| if (!ctor || !ctor.inputs?.length) { box.innerHTML = "<div class='small'>No constructor args</div>"; return; } |
| for (const inp of ctor.inputs) { |
| const row = document.createElement("div"); row.className = "field"; |
| const label = document.createElement("label"); label.className = "small"; |
| label.textContent = `${inp.name || "arg"} (${inp.type})`; |
| const input = document.createElement("input"); input.type = "text"; input.id = `ctor_${inp.name || "arg"}`; |
| row.appendChild(label); row.appendChild(input); box.appendChild(row); |
| } |
| } |
| |
| function buildUI(instance, abi) { |
| const div = byId("contractUI"); div.innerHTML = ""; |
| const fns = abi.filter(x => x.type === "function"); |
| byId("fnSummary").textContent = `${fns.length} functions`; |
| for (const fn of fns) { |
| const card = document.createElement("div"); card.className = "fn"; |
| const title = document.createElement("div"); |
| const readonly = fn.stateMutability === "view" || fn.stateMutability === "pure"; |
| title.innerHTML = `<b>${fn.name}</b> <span class="tag">${readonly ? "read" : "write"} • ${fn.stateMutability}</span>`; |
| card.appendChild(title); |
| // inputs |
| const argsInputs = []; |
| for (const inp of fn.inputs) { |
| const row = document.createElement("div"); row.className = "field"; |
| const label = document.createElement("label"); label.className = "small"; |
| label.textContent = `${inp.name || "arg"} (${inp.type})`; |
| const input = document.createElement("input"); input.type = "text"; |
| row.appendChild(label); row.appendChild(input); card.appendChild(row); |
| argsInputs.push({ def: inp, el: input }); |
| } |
| // payable override |
| let valueEl = null; |
| if (!readonly && fn.stateMutability === "payable") { |
| const row = document.createElement("div"); row.className = "field"; |
| const label = document.createElement("label"); label.className = "small"; label.textContent = "value (wei)"; |
| const input = document.createElement("input"); input.type = "text"; input.placeholder = "0"; |
| row.appendChild(label); row.appendChild(input); card.appendChild(row); |
| valueEl = input; |
| } |
| const btn = document.createElement("button"); btn.textContent = "Run"; |
| btn.onclick = async () => { |
| try { |
| if (!instance) throw new Error("Deploy or attach first"); |
| const args = argsInputs.map(a => parseArg(a.def.type, a.el.value)); |
| if (readonly) { |
| const res = await instance[fn.name](...args); |
| log(`Result: ${res?.toString?.() ?? JSON.stringify(res)}`); |
| } else { |
| const ov = {}; |
| if (valueEl && valueEl.value) ov.value = BigInt(valueEl.value); |
| const tx = await instance[fn.name](...args, ov); |
| log(`Sent: ${tx.hash}`); |
| const rc = await tx.wait(); |
| log(`Mined in block ${rc.blockNumber}`); |
| } |
| } catch (e) { |
| log("Error: " + (e?.message || e)); |
| } |
| }; |
| card.appendChild(btn); |
| div.appendChild(card); |
| } |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def root(): |
| return HTML |
|
|
| if __name__ == "__main__": |
| uvicorn.run("app:app", host="0.0.0.0", port=7860) |