Vivara-Forge / app.py
fugthchat's picture
Update app.py
03d1966 verified
import streamlit as st
from solcx import compile_source, install_solc
import json
import streamlit.components.v1 as components
# --- PAGE CONFIG ---
st.set_page_config(
layout="wide",
page_title="Vivara Forge",
page_icon="🦁",
initial_sidebar_state="collapsed",
)
# --- HIDE ALL STREAMLIT UI ---
st.markdown(
"""
<style>
#MainMenu, header, footer, [data-testid="stToolbar"], [data-testid="stDecoration"],
[data-testid="stStatusWidget"], .stDeployButton, div[data-testid="stSidebarNav"],
section[data-testid="stSidebar"], .viewerBadge_container__1QSob,
[data-testid="stHeader"], [data-testid="stFooter"] {
display: none !important;
visibility: hidden !important;
}
.stApp { background: #1e1e1e !important; }
.block-container { padding: 0 !important; max-width: 100% !important; }
[data-testid="stAppViewContainer"] { padding: 0 !important; }
/* Hide the form completely */
.stForm { position: absolute; left: -9999px; }
</style>
""",
unsafe_allow_html=True,
)
# --- SESSION STATE ---
if "code" not in st.session_state:
st.session_state.code = """// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MyToken {
string public name = "Vivara Coin";
string public symbol = "VIVR";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Allowance exceeded");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}"""
if "abi" not in st.session_state:
st.session_state.abi = None
if "bytecode" not in st.session_state:
st.session_state.bytecode = None
if "contract_name" not in st.session_state:
st.session_state.contract_name = None
if "logs" not in st.session_state:
st.session_state.logs = []
if "compile_trigger" not in st.session_state:
st.session_state.compile_trigger = False
# --- COMPILE FUNCTION ---
def do_compile():
try:
st.session_state.logs.append("[INFO] Installing solc 0.8.20...")
install_solc("0.8.20")
st.session_state.logs.append("[INFO] Compiling Solidity code...")
compiled = compile_source(
st.session_state.code, output_values=["abi", "bin"], solc_version="0.8.20"
)
contract_id, interface = list(compiled.items())[-1]
st.session_state.abi = interface["abi"]
st.session_state.bytecode = interface["bin"]
st.session_state.contract_name = contract_id.split(":")[-1]
st.session_state.logs.append(
f"[SUCCESS] Compiled: {st.session_state.contract_name}"
)
return True
except Exception as e:
st.session_state.logs.append(f"[ERROR] {str(e)}")
return False
# Check for compile trigger
if st.session_state.compile_trigger:
do_compile()
st.session_state.compile_trigger = False
# --- FULL REMIX-LIKE HTML APP ---
abi_json = json.dumps(st.session_state.abi) if st.session_state.abi else "null"
bytecode = st.session_state.bytecode or ""
contract_name = st.session_state.contract_name or "No Contract"
logs_html = (
"\\n".join(st.session_state.logs[-15:])
if st.session_state.logs
else "[INFO] Vivara Forge Ready - Connect MetaMask to deploy"
)
# Escape the code for JS
code_escaped = (
st.session_state.code.replace("\\", "\\\\")
.replace("`", "\\`")
.replace("${", "\\${")
)
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdn.ethers.io/lib/ethers-5.7.umd.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: 'Inter', sans-serif;
background: #1e1e1e;
color: #d4d4d4;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}}
/* HEADER */
.header {{
height: 44px;
background: linear-gradient(180deg, #2d2d2d 0%, #252526 100%);
border-bottom: 1px solid #3c3c3c;
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
}}
.logo {{
font-family: 'JetBrains Mono';
font-weight: 600;
font-size: 14px;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
}}
.logo .dot {{ width: 10px; height: 10px; background: #3fc04f; border-radius: 50%; box-shadow: 0 0 8px #3fc04f; }}
.header-btn {{
background: #0e639c;
color: white;
border: none;
padding: 7px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.15s;
}}
.header-btn:hover {{ background: #1177bb; transform: translateY(-1px); }}
.header-btn.orange {{ background: linear-gradient(180deg, #e8850f 0%, #d4730f 100%); }}
.header-btn.orange:hover {{ background: linear-gradient(180deg, #f59622 0%, #e8850f 100%); }}
.header-btn.green {{ background: linear-gradient(180deg, #3ab55f 0%, #2ea44f 100%); }}
.header-btn.green:hover {{ background: linear-gradient(180deg, #4cc76f 0%, #3ab55f 100%); }}
.wallet-status {{
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
}}
.wallet-badge {{
background: #333;
padding: 5px 12px;
border-radius: 20px;
font-family: 'JetBrains Mono';
font-size: 11px;
border: 1px solid #444;
}}
.wallet-badge.connected {{ border-color: #3fc04f; color: #3fc04f; }}
/* MAIN LAYOUT */
.main {{ flex: 1; display: flex; min-height: 0; }}
/* LEFT SIDEBAR */
.sidebar {{
width: 240px;
background: #252526;
border-right: 1px solid #3c3c3c;
display: flex;
flex-direction: column;
overflow-y: auto;
}}
.sidebar-section {{ padding: 14px; border-bottom: 1px solid #3c3c3c; }}
.sidebar-title {{
font-size: 11px;
text-transform: uppercase;
color: #858585;
margin-bottom: 10px;
letter-spacing: 1px;
font-weight: 600;
}}
.file-item {{
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
cursor: pointer;
border-radius: 4px;
font-size: 13px;
margin-bottom: 2px;
}}
.file-item:hover {{ background: #2a2d2e; }}
.file-item.active {{ background: #37373d; border-left: 2px solid #0e639c; }}
.env-select {{
width: 100%;
background: #3c3c3c;
border: 1px solid #555;
color: #d4d4d4;
padding: 10px;
border-radius: 6px;
font-size: 12px;
margin-bottom: 12px;
cursor: pointer;
}}
.env-select:focus {{ border-color: #0e639c; outline: none; }}
.input-group {{ margin-bottom: 12px; }}
.input-group label {{
display: block;
font-size: 11px;
color: #858585;
margin-bottom: 6px;
text-transform: uppercase;
}}
.input-group input {{
width: 100%;
background: #3c3c3c;
border: 1px solid #555;
color: #d4d4d4;
padding: 8px 10px;
border-radius: 6px;
font-size: 12px;
font-family: 'JetBrains Mono';
}}
.input-group input:focus {{ border-color: #0e639c; outline: none; }}
/* EDITOR AREA */
.editor-container {{ flex: 1; display: flex; flex-direction: column; min-width: 0; }}
.tab-bar {{
height: 38px;
background: #252526;
display: flex;
align-items: flex-end;
border-bottom: 1px solid #3c3c3c;
padding-left: 8px;
}}
.tab {{
padding: 8px 20px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
margin-bottom: -1px;
border-radius: 6px 6px 0 0;
background: #2d2d2d;
}}
.tab.active {{
background: #1e1e1e;
border-color: #3c3c3c;
border-top: 2px solid #0e639c;
}}
.tab .close {{
opacity: 0.5;
font-size: 16px;
margin-left: 4px;
}}
.tab .close:hover {{ opacity: 1; }}
.editor {{
flex: 1;
display: flex;
background: #1e1e1e;
overflow: hidden;
}}
.line-numbers {{
width: 55px;
background: #1e1e1e;
border-right: 1px solid #333;
padding: 14px 10px;
text-align: right;
font-family: 'JetBrains Mono';
font-size: 13px;
color: #6e7681;
line-height: 1.6;
user-select: none;
}}
.code-area {{
flex: 1;
padding: 14px 16px;
font-family: 'JetBrains Mono';
font-size: 13px;
line-height: 1.6;
overflow: auto;
white-space: pre;
color: #d4d4d4;
}}
.keyword {{ color: #569cd6; }}
.string {{ color: #ce9178; }}
.comment {{ color: #6a9955; font-style: italic; }}
.number {{ color: #b5cea8; }}
.type {{ color: #4ec9b0; }}
.function-name {{ color: #dcdcaa; }}
/* RIGHT PANEL */
.right-panel {{
width: 320px;
background: #252526;
border-left: 1px solid #3c3c3c;
display: flex;
flex-direction: column;
}}
.panel-header {{
padding: 14px;
border-bottom: 1px solid #3c3c3c;
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}}
.panel-content {{ flex: 1; padding: 14px; overflow-y: auto; }}
.deploy-btn {{
width: 100%;
background: linear-gradient(180deg, #e8850f 0%, #d4730f 100%);
color: white;
border: none;
padding: 14px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
margin-bottom: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}}
.deploy-btn:hover {{ transform: translateY(-1px); box-shadow: 0 4px 12px rgba(212, 115, 15, 0.3); }}
.deploy-btn:disabled {{ background: #555; cursor: not-allowed; transform: none; box-shadow: none; }}
.info-card {{
background: #1e1e1e;
padding: 12px;
border-radius: 6px;
margin-bottom: 14px;
border: 1px solid #333;
}}
.info-card .row {{
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 12px;
}}
.info-card .row:last-child {{ margin-bottom: 0; }}
.info-card .label {{ color: #858585; }}
.info-card .value {{ font-family: 'JetBrains Mono'; color: #4ec9b0; }}
.contract-card {{
background: #1e1e1e;
padding: 10px;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #333;
font-size: 11px;
}}
.contract-card .name {{ color: #4ec9b0; font-weight: 600; margin-bottom: 4px; }}
.contract-card .address {{ color: #858585; font-family: 'JetBrains Mono'; word-break: break-all; }}
/* TERMINAL */
.terminal-container {{
height: 160px;
background: #1e1e1e;
border-top: 1px solid #3c3c3c;
display: flex;
flex-direction: column;
}}
.terminal-header {{
padding: 8px 14px;
background: #252526;
border-bottom: 1px solid #3c3c3c;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
}}
.terminal {{
flex: 1;
font-family: 'JetBrains Mono';
font-size: 12px;
padding: 10px 14px;
overflow-y: auto;
color: #d4d4d4;
line-height: 1.5;
}}
.terminal .error {{ color: #f14c4c; }}
.terminal .success {{ color: #3fc04f; }}
.terminal .info {{ color: #858585; }}
/* STATUS BAR */
.status-bar {{
height: 26px;
background: #007acc;
display: flex;
align-items: center;
padding: 0 14px;
font-size: 12px;
gap: 20px;
}}
.status-item {{ display: flex; align-items: center; gap: 6px; }}
</style>
</head>
<body>
<div class="header">
<div class="logo"><div class="dot"></div> VIVARA FORGE</div>
<button class="header-btn green" onclick="connectWallet()">🦊 Connect Wallet</button>
<button class="header-btn orange" onclick="compileContract()">⚑ Compile</button>
<div class="wallet-status">
<span id="networkBadge" class="wallet-badge">Not Connected</span>
<span id="addressBadge" class="wallet-badge">0x...</span>
</div>
</div>
<div class="main">
<div class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">πŸ“ File Explorer</div>
<div class="file-item active">πŸ“„ Contract.sol</div>
<div class="file-item">πŸ“„ Token.sol</div>
<div class="file-item">πŸ“ artifacts</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">βš™οΈ Deploy Environment</div>
<select class="env-select" id="envSelect">
<option value="metamask">🦊 Injected - MetaMask</option>
<option value="sepolia">πŸ”— Sepolia Testnet</option>
<option value="polygon">🟣 Polygon Mumbai</option>
</select>
<div class="input-group">
<label>Account</label>
<input type="text" id="accountInput" value="Connect wallet..." readonly>
</div>
<div class="input-group">
<label>Gas Limit</label>
<input type="text" id="gasLimit" value="3000000">
</div>
<div class="input-group">
<label>Value (ETH)</label>
<input type="text" id="valueEth" value="0">
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">πŸ”§ Compiler</div>
<select class="env-select" id="compilerVersion">
<option value="0.8.20">Solidity v0.8.20</option>
<option value="0.8.19">Solidity v0.8.19</option>
<option value="0.8.18">Solidity v0.8.18</option>
</select>
</div>
</div>
<div class="editor-container">
<div class="tab-bar">
<div class="tab active">πŸ“„ Contract.sol <span class="close">Γ—</span></div>
</div>
<div class="editor">
<div class="line-numbers" id="lineNumbers"></div>
<div class="code-area" id="codeDisplay"></div>
</div>
<div class="terminal-container">
<div class="terminal-header">πŸ“Ÿ Terminal</div>
<div class="terminal" id="terminal"></div>
</div>
</div>
<div class="right-panel">
<div class="panel-header">πŸš€ Deploy & Run</div>
<div class="panel-content">
<div class="info-card">
<div class="row">
<span class="label">Contract</span>
<span class="value" id="contractName">{contract_name}</span>
</div>
<div class="row">
<span class="label">Status</span>
<span class="value" id="compileStatus">{"βœ… Compiled" if st.session_state.bytecode else "⏳ Not Compiled"}</span>
</div>
<div class="row">
<span class="label">Bytecode</span>
<span class="value">{len(bytecode) if bytecode else 0} bytes</span>
</div>
</div>
<div class="input-group">
<label>Constructor Arguments</label>
<input type="text" id="constructorArgs" placeholder="e.g., 1000000">
</div>
<button class="deploy-btn" id="deployBtn" onclick="deployContract()" {"" if st.session_state.bytecode else "disabled"}>
πŸš€ Deploy Contract
</button>
<div class="sidebar-title" style="margin-top: 20px;">Deployed Contracts</div>
<div id="contractsList"></div>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-item">🦁 Vivara Forge v2.0.1</div>
<div class="status-item" id="statusNetwork">Network: --</div>
<div class="status-item" id="statusBalance">Balance: --</div>
</div>
<script>
const contractABI = {abi_json};
const contractBytecode = "{bytecode}";
let provider = null;
let signer = null;
let userAddress = null;
const code = `{code_escaped}`;
function highlightCode(code) {{
return code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/(\/\/.*$)/gm, '<span class="comment">$1</span>')
.replace(/(".*?"|'.*?')/g, '<span class="string">$1</span>')
.replace(/\\b(pragma|solidity|contract|function|public|private|external|internal|view|pure|payable|returns|return|if|else|for|while|mapping|event|emit|require|constructor|import|is|memory|storage|calldata|indexed|modifier|override|virtual)\\b/g, '<span class="keyword">$1</span>')
.replace(/\\b(address|uint256|uint8|uint128|int256|string|bool|bytes|bytes32|bytes4)\\b/g, '<span class="type">$1</span>')
.replace(/\\b(\\d+)\\b/g, '<span class="number">$1</span>');
}}
function updateEditor() {{
const lines = code.split('\\n');
document.getElementById('lineNumbers').innerHTML = lines.map((_, i) => i + 1).join('<br>');
document.getElementById('codeDisplay').innerHTML = highlightCode(code);
}}
function log(msg, type = '') {{
const terminal = document.getElementById('terminal');
const time = new Date().toLocaleTimeString();
terminal.innerHTML += `<div class="${{type}}">[${time}] ${msg}</div>`;
terminal.scrollTop = terminal.scrollHeight;
}}
async function connectWallet() {{
if (!window.ethereum) {{
log('❌ MetaMask not detected! Please install MetaMask extension.', 'error');
alert('MetaMask not found! Please install MetaMask browser extension.');
return;
}}
try {{
log('Connecting to MetaMask...', 'info');
provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
signer = provider.getSigner();
userAddress = await signer.getAddress();
const network = await provider.getNetwork();
const balance = await provider.getBalance(userAddress);
const balanceEth = parseFloat(ethers.utils.formatEther(balance)).toFixed(4);
document.getElementById('addressBadge').textContent = userAddress.slice(0,6) + '...' + userAddress.slice(-4);
document.getElementById('addressBadge').classList.add('connected');
document.getElementById('networkBadge').textContent = network.name || 'Chain ' + network.chainId;
document.getElementById('networkBadge').classList.add('connected');
document.getElementById('accountInput').value = userAddress.slice(0,12) + '...';
document.getElementById('statusNetwork').textContent = 'Network: ' + (network.name || network.chainId);
document.getElementById('statusBalance').textContent = 'Balance: ' + balanceEth + ' ETH';
log('βœ… Wallet connected: ' + userAddress, 'success');
log('Network: ' + (network.name || 'Chain ' + network.chainId), 'info');
log('Balance: ' + balanceEth + ' ETH', 'info');
}} catch (e) {{
log('❌ Connection failed: ' + e.message, 'error');
}}
}}
function compileContract() {{
log('⚑ Sending compile request to server...', 'info');
log('This will compile with Solidity 0.8.20', 'info');
// Trigger Streamlit rerun with compile flag
const url = new URL(window.parent.location.href);
url.searchParams.set('compile', 'true');
window.parent.location.href = url.toString();
}}
async function deployContract() {{
if (!signer) {{
log('❌ Please connect your wallet first!', 'error');
alert('Please connect MetaMask first!');
return;
}}
if (!contractBytecode || contractBytecode === '') {{
log('❌ Contract not compiled! Click Compile first.', 'error');
alert('Please compile the contract first!');
return;
}}
try {{
const btn = document.getElementById('deployBtn');
btn.disabled = true;
btn.innerHTML = '⏳ Deploying...';
log('πŸš€ Starting deployment...', 'info');
const factory = new ethers.ContractFactory(contractABI, contractBytecode, signer);
const argsInput = document.getElementById('constructorArgs').value.trim();
let args = [];
if (argsInput) {{
args = argsInput.split(',').map(a => {{
const trimmed = a.trim();
if (/^\\d+$/.test(trimmed)) return trimmed;
return trimmed;
}});
}}
log('Constructor args: [' + (args.length ? args.join(', ') : 'none') + ']', 'info');
const contract = await factory.deploy(...args);
log('πŸ“€ Transaction sent: ' + contract.deployTransaction.hash, 'info');
log('⏳ Waiting for confirmation...', 'info');
await contract.deployed();
log('βœ… CONTRACT DEPLOYED!', 'success');
log('πŸ“ Address: ' + contract.address, 'success');
const list = document.getElementById('contractsList');
list.innerHTML += `
<div class="contract-card">
<div class="name">${{document.getElementById('contractName').textContent}}</div>
<div class="address">${{contract.address}}</div>
</div>
`;
btn.disabled = false;
btn.innerHTML = 'πŸš€ Deploy Contract';
}} catch (e) {{
log('❌ Deployment failed: ' + e.message, 'error');
document.getElementById('deployBtn').disabled = false;
document.getElementById('deployBtn').innerHTML = 'πŸš€ Deploy Contract';
}}
}}
// Initialize
updateEditor();
log('Vivara Forge v2.0.1 Ready', 'info');
log('Connect MetaMask and compile to deploy contracts', 'info');
// Auto-connect if MetaMask already authorized
if (window.ethereum && window.ethereum.selectedAddress) {{
setTimeout(connectWallet, 500);
}}
</script>
</body>
</html>
"""
# Check URL params for compile trigger
query_params = st.query_params
if query_params.get("compile") == "true":
do_compile()
# Clear the param
st.query_params.clear()
# Render full-screen HTML IDE
components.html(html, height=850, scrolling=False)