Copy <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Token Dashboard - OPN Chain</title>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.address {
font-family: monospace;
font-size: 12px;
background: #f3f4f6;
padding: 4px 8px;
border-radius: 4px;
}
.tabs {
display: flex;
border-bottom: 2px solid #e5e7eb;
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab.active {
color: #2563eb;
border-bottom-color: #2563eb;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Token Dashboard</h1>
<p>Manage and monitor your ERC-20 token on OPN Chain</p>
<p class="address" id="walletAddress">Not connected</p>
</header>
<div class="stats-grid">
<div class="stat-card">
<h3>Total Supply</h3>
<div class="value" id="totalSupply">-</div>
<div class="sub">Maximum: <span id="maxSupply">-</span></div>
</div>
<div class="stat-card">
<h3>Your Balance</h3>
<div class="value" id="userBalance">-</div>
<div class="sub" id="userBalanceUSD">$0.00 USD</div>
</div>
<div class="stat-card">
<h3>Token Price</h3>
<div class="value" id="tokenPrice">-</div>
<div class="sub">1 OPN = <span id="tokenRate">-</span> tokens</div>
</div>
<div class="stat-card">
<h3>Sale Progress</h3>
<div class="value" id="saleProgress">-</div>
<div class="sub">Raised: <span id="totalRaised">-</span> OPN</div>
</div>
</div>
<div class="actions">
<div class="tabs">
<div class="tab active" onclick="switchTab('wallet')">Wallet</div>
<div class="tab" onclick="switchTab('sale')">Token Sale</div>
<div class="tab" onclick="switchTab('admin')">Admin</div>
</div>
<div id="wallet-tab" class="tab-content active">
<div class="action-group">
<h3>Transfer Tokens</h3>
<input type="text" id="transferTo" placeholder="Recipient Address (0x...)">
<input type="number" id="transferAmount" placeholder="Amount">
<button onclick="transferTokens()">Transfer</button>
</div>
<div class="action-group">
<h3>Burn Tokens</h3>
<input type="number" id="burnAmount" placeholder="Amount to burn">
<button onclick="burnTokens()" class="danger">Burn</button>
</div>
<div class="action-group">
<h3>Approve Spender</h3>
<input type="text" id="spenderAddress" placeholder="Spender Address">
<input type="number" id="approveAmount" placeholder="Amount">
<button onclick="approveSpender()">Approve</button>
</div>
</div>
<div id="sale-tab" class="tab-content">
<div class="action-group">
<h3>Buy Tokens</h3>
<p>Sale Status: <span id="saleStatus">-</span></p>
<p>Time Remaining: <span id="timeRemaining">-</span></p>
<input type="number" id="buyAmount" placeholder="OPN Amount" step="0.1" min="0.1" max="10">
<p>You will receive: <span id="receiveAmount">0</span> tokens</p>
<button onclick="buyTokens()">Buy Tokens</button>
</div>
<div class="action-group">
<h3>Your Purchase History</h3>
<p>Total Purchased: <span id="userPurchased">0</span> OPN</p>
<p>Tokens Received: <span id="tokensReceived">0</span></p>
</div>
</div>
<div id="admin-tab" class="tab-content">
<div class="action-group">
<h3>Mint Tokens (Owner Only)</h3>
<input type="text" id="mintTo" placeholder="Recipient Address">
<input type="number" id="mintAmount" placeholder="Amount">
<button onclick="mintTokens()" class="success">Mint</button>
</div>
<div class="action-group">
<h3>Pause/Unpause (Owner Only)</h3>
<p>Current Status: <span id="pauseStatus">-</span></p>
<button onclick="togglePause()" class="warning">Toggle Pause</button>
</div>
<div class="action-group">
<h3>Sale Management (Owner Only)</h3>
<button onclick="startSale()" class="success">Start Sale</button>
<button onclick="endSale()" class="warning">End Sale</button>
<button onclick="withdrawFunds()">Withdraw Funds</button>
</div>
</div>
</div>
<div id="status"></div>
<div class="chart-container">
<h3>Token Distribution</h3>
<canvas id="distributionChart"></canvas>
</div>
<div class="chart-container">
<h3>Transaction History</h3>
<div id="transactionList"></div>
</div>
</div>
<script>
// Configuration - Update these with your deployed addresses
const TOKEN_ADDRESS = 'YOUR_TOKEN_ADDRESS';
const SALE_ADDRESS = 'YOUR_SALE_ADDRESS';
// ABIs (simplified - add full ABIs from artifacts)
const TOKEN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address, uint256) returns (bool)",
"function approve(address, uint256) returns (bool)",
"function mint(address, uint256)",
"function burn(uint256)",
"function pause()",
"function unpause()",
"function paused() view returns (bool)",
"function owner() view returns (address)",
"function MAX_SUPPLY() view returns (uint256)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
const SALE_ABI = [
"function buyTokens() payable",
"function rate() view returns (uint256)",
"function tokensSold() view returns (uint256)",
"function totalRaised() view returns (uint256)",
"function purchases(address) view returns (uint256)",
"function saleStart() view returns (uint256)",
"function saleEnd() view returns (uint256)",
"function status() view returns (uint8)",
"function startSale()",
"function endSale()",
"function withdrawFunds()",
"function getSaleInfo() view returns (uint256, uint256, uint256, uint256, uint256, uint8, uint256)"
];
let web3;
let tokenContract;
let saleContract;
let userAccount;
let isOwner = false;
// Initialize
async function init() {
if (typeof window.ethereum !== 'undefined') {
web3 = new Web3(window.ethereum);
try {
// Request account access
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
userAccount = accounts[0];
// Check network
const chainId = await web3.eth.getChainId();
if (chainId !== 984) {
showStatus('Please switch to OPN Testnet!', 'error');
return;
}
// Initialize contracts
tokenContract = new web3.eth.Contract(TOKEN_ABI, TOKEN_ADDRESS);
saleContract = new web3.eth.Contract(SALE_ABI, SALE_ADDRESS);
// Check if user is owner
const owner = await tokenContract.methods.owner().call();
isOwner = owner.toLowerCase() === userAccount.toLowerCase();
// Update UI
document.getElementById('walletAddress').textContent =
`Connected: ${userAccount.substring(0, 6)}...${userAccount.substring(38)}`;
// Load data
await loadTokenData();
await loadSaleData();
// Set up event listeners
tokenContract.events.Transfer()
.on('data', handleTransferEvent)
.on('error', console.error);
// Update data every 10 seconds
setInterval(async () => {
await loadTokenData();
await loadSaleData();
}, 10000);
} catch (error) {
console.error('Error:', error);
showStatus('Error connecting to MetaMask', 'error');
}
} else {
showStatus('Please install MetaMask!', 'error');
}
}
// Load token data
async function loadTokenData() {
try {
const [name, symbol, decimals, totalSupply, maxSupply, paused, userBalance] = await Promise.all([
tokenContract.methods.name().call(),
tokenContract.methods.symbol().call(),
tokenContract.methods.decimals().call(),
tokenContract.methods.totalSupply().call(),
tokenContract.methods.MAX_SUPPLY().call(),
tokenContract.methods.paused().call(),
tokenContract.methods.balanceOf(userAccount).call()
]);
// Update UI
document.getElementById('totalSupply').textContent =
formatTokenAmount(totalSupply, decimals) + ' ' + symbol;
document.getElementById('maxSupply').textContent =
formatTokenAmount(maxSupply, decimals) + ' ' + symbol;
document.getElementById('userBalance').textContent =
formatTokenAmount(userBalance, decimals) + ' ' + symbol;
document.getElementById('pauseStatus').textContent =
paused ? 'Paused' : 'Active';
// Update chart
updateDistributionChart(totalSupply, userBalance);
} catch (error) {
console.error('Error loading token data:', error);
}
}
// Load sale data
async function loadSaleData() {
try {
const saleInfo = await saleContract.methods.getSaleInfo().call();
const userPurchased = await saleContract.methods.purchases(userAccount).call();
const rate = saleInfo[0];
const tokensSold = saleInfo[1];
const totalRaised = saleInfo[2];
const saleStart = parseInt(saleInfo[3]);
const saleEnd = parseInt(saleInfo[4]);
const status = parseInt(saleInfo[5]);
// Update UI
document.getElementById('tokenRate').textContent = rate;
document.getElementById('tokenPrice').textContent =
(1 / rate).toFixed(6) + ' OPN';
document.getElementById('totalRaised').textContent =
web3.utils.fromWei(totalRaised, 'ether');
document.getElementById('userPurchased').textContent =
web3.utils.fromWei(userPurchased, 'ether');
document.getElementById('tokensReceived').textContent =
formatTokenAmount(
web3.utils.toBN(userPurchased).mul(web3.utils.toBN(rate)),
18
);
// Sale status
const statusText = ['Pending', 'Active', 'Completed', 'Cancelled'];
document.getElementById('saleStatus').textContent = statusText[status];
// Time remaining
updateTimeRemaining(saleEnd);
// Sale progress
const maxSale = await tokenContract.methods.balanceOf(SALE_ADDRESS).call();
const progress = (parseFloat(tokensSold) / parseFloat(maxSale) * 100).toFixed(2);
document.getElementById('saleProgress').textContent = progress + '%';
} catch (error) {
console.error('Error loading sale data:', error);
}
}
// Transfer tokens
async function transferTokens() {
const to = document.getElementById('transferTo').value;
const amount = document.getElementById('transferAmount').value;
if (!web3.utils.isAddress(to)) {
showStatus('Invalid recipient address', 'error');
return;
}
if (!amount || amount <= 0) {
showStatus('Invalid amount', 'error');
return;
}
try {
showStatus('Sending transaction...', 'info');
const decimals = await tokenContract.methods.decimals().call();
const value = web3.utils.toBN(amount).mul(web3.utils.toBN(10).pow(web3.utils.toBN(decimals)));
const tx = await tokenContract.methods.transfer(to, value).send({
from: userAccount,
gas: 100000
});
showStatus(`Transfer successful! Hash: ${tx.transactionHash}`, 'success');
// Clear form
document.getElementById('transferTo').value = '';
document.getElementById('transferAmount').value = '';
// Reload data
await loadTokenData();
} catch (error) {
console.error('Transfer error:', error);
showStatus('Transfer failed: ' + error.message, 'error');
}
}
// Buy tokens
async function buyTokens() {
const amount = document.getElementById('buyAmount').value;
if (!amount || amount <= 0) {
showStatus('Invalid amount', 'error');
return;
}
try {
showStatus('Processing purchase...', 'info');
const value = web3.utils.toWei(amount, 'ether');
const tx = await saleContract.methods.buyTokens().send({
from: userAccount,
value: value,
gas: 200000
});
showStatus(`Purchase successful! Hash: ${tx.transactionHash}`, 'success');
// Clear form
document.getElementById('buyAmount').value = '';
// Reload data
await loadTokenData();
await loadSaleData();
} catch (error) {
console.error('Purchase error:', error);
showStatus('Purchase failed: ' + error.message, 'error');
}
}
// Burn tokens
async function burnTokens() {
const amount = document.getElementById('burnAmount').value;
if (!amount || amount <= 0) {
showStatus('Invalid amount', 'error');
return;
}
if (!confirm(`Are you sure you want to burn ${amount} tokens? This cannot be undone!`)) {
return;
}
try {
showStatus('Burning tokens...', 'info');
const decimals = await tokenContract.methods.decimals().call();
const value = web3.utils.toBN(amount).mul(web3.utils.toBN(10).pow(web3.utils.toBN(decimals)));
const tx = await tokenContract.methods.burn(value).send({
from: userAccount,
gas: 100000
});
showStatus(`Burn successful! Hash: ${tx.transactionHash}`, 'success');
// Clear form
document.getElementById('burnAmount').value = '';
// Reload data
await loadTokenData();
} catch (error) {
console.error('Burn error:', error);
showStatus('Burn failed: ' + error.message, 'error');
}
}
// Mint tokens (owner only)
async function mintTokens() {
if (!isOwner) {
showStatus('Only owner can mint tokens', 'error');
return;
}
const to = document.getElementById('mintTo').value;
const amount = document.getElementById('mintAmount').value;
if (!web3.utils.isAddress(to)) {
showStatus('Invalid recipient address', 'error');
return;
}
if (!amount || amount <= 0) {
showStatus('Invalid amount', 'error');
return;
}
try {
showStatus('Minting tokens...', 'info');
const decimals = await tokenContract.methods.decimals().call();
const value = web3.utils.toBN(amount).mul(web3.utils.toBN(10).pow(web3.utils.toBN(decimals)));
const tx = await tokenContract.methods.mint(to, value).send({
from: userAccount,
gas: 150000
});
showStatus(`Mint successful! Hash: ${tx.transactionHash}`, 'success');
// Clear form
document.getElementById('mintTo').value = '';
document.getElementById('mintAmount').value = '';
// Reload data
await loadTokenData();
} catch (error) {
console.error('Mint error:', error);
showStatus('Mint failed: ' + error.message, 'error');
}
}
// Helper functions
function formatTokenAmount(amount, decimals) {
const divisor = web3.utils.toBN(10).pow(web3.utils.toBN(decimals));
const wholePart = web3.utils.toBN(amount).div(divisor).toString();
const fractionalPart = web3.utils.toBN(amount).mod(divisor).toString().padStart(decimals, '0');
// Format with commas
const formatted = wholePart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// Trim trailing zeros from fractional part
const trimmed = fractionalPart.replace(/0+$/, '');
return trimmed.length > 0 ? `${formatted}.${trimmed}` : formatted;
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `show ${type}`;
setTimeout(() => {
status.classList.remove('show');
}, 5000);
}
function switchTab(tab) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
});
// Show selected tab
document.getElementById(`${tab}-tab`).classList.add('active');
event.target.classList.add('active');
}
function updateTimeRemaining(saleEnd) {
const now = Math.floor(Date.now() / 1000);
const remaining = saleEnd - now;
if (remaining <= 0) {
document.getElementById('timeRemaining').textContent = 'Sale ended';
return;
}
const days = Math.floor(remaining / 86400);
const hours = Math.floor((remaining % 86400) / 3600);
const minutes = Math.floor((remaining % 3600) / 60);
document.getElementById('timeRemaining').textContent =
`${days}d ${hours}h ${minutes}m`;
}
function handleTransferEvent(event) {
console.log('Transfer event:', event);
// You could add notifications or update UI here
}
let distributionChart;
function updateDistributionChart(totalSupply, userBalance) {
const ctx = document.getElementById('distributionChart').getContext('2d');
if (distributionChart) {
distributionChart.destroy();
}
distributionChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Your Balance', 'Others'],
datasets: [{
data: [
parseFloat(web3.utils.fromWei(userBalance, 'ether')),
parseFloat(web3.utils.fromWei(
web3.utils.toBN(totalSupply).sub(web3.utils.toBN(userBalance)),
'ether'
))
],
backgroundColor: ['#3b82f6', '#e5e7eb']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Calculate token amount when entering OPN amount
document.getElementById('buyAmount').addEventListener('input', async (e) => {
const amount = e.target.value;
if (amount && amount > 0) {
const rate = await saleContract.methods.rate().call();
const tokens = amount * rate;
document.getElementById('receiveAmount').textContent = tokens.toLocaleString();
} else {
document.getElementById('receiveAmount').textContent = '0';
}
});
// Initialize on load
window.addEventListener('load', init);
// Handle account changes
if (window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
window.location.reload();
});
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload();
});
}
</script>
</body>
</html>