359 lines
12 KiB
HTML
359 lines
12 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>ETH/USD Price Feed - ChainID 138</title>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
|
min-height: 100vh;
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
align-items: center;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.container {
|
||
|
|
background: white;
|
||
|
|
border-radius: 20px;
|
||
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
|
|
padding: 40px;
|
||
|
|
max-width: 600px;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
color: #333;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
font-size: 28px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: #666;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.price-display {
|
||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
|
color: white;
|
||
|
|
border-radius: 15px;
|
||
|
|
padding: 30px;
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.price-value {
|
||
|
|
font-size: 48px;
|
||
|
|
font-weight: bold;
|
||
|
|
margin: 10px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.price-label {
|
||
|
|
font-size: 14px;
|
||
|
|
opacity: 0.9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.price-info {
|
||
|
|
background: #f8f9fa;
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 20px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 10px 0;
|
||
|
|
border-bottom: 1px solid #e9ecef;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-label {
|
||
|
|
color: #666;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-value {
|
||
|
|
color: #333;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
button {
|
||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 15px 30px;
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
width: 100%;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
button:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||
|
|
}
|
||
|
|
|
||
|
|
button:active {
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
button:disabled {
|
||
|
|
opacity: 0.6;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status {
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 10px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status.show {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status.info {
|
||
|
|
background: #e7f3ff;
|
||
|
|
color: #0066cc;
|
||
|
|
border: 1px solid #b3d9ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status.success {
|
||
|
|
background: #d4edda;
|
||
|
|
color: #155724;
|
||
|
|
border: 1px solid #c3e6cb;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status.error {
|
||
|
|
background: #f8d7da;
|
||
|
|
color: #721c24;
|
||
|
|
border: 1px solid #f5c6cb;
|
||
|
|
}
|
||
|
|
|
||
|
|
.oracle-address {
|
||
|
|
font-family: monospace;
|
||
|
|
font-size: 12px;
|
||
|
|
color: #666;
|
||
|
|
word-break: break-all;
|
||
|
|
margin-top: 10px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<h1>ETH/USD Price Feed</h1>
|
||
|
|
<p class="subtitle">ChainID 138 - Oracle Contract</p>
|
||
|
|
|
||
|
|
<div class="price-display">
|
||
|
|
<div class="price-label">Current Price</div>
|
||
|
|
<div class="price-value" id="priceValue">$0.00</div>
|
||
|
|
<div class="price-label" id="priceStatus">Not loaded</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="price-info" id="priceInfo" style="display: none;">
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Round ID</span>
|
||
|
|
<span class="info-value" id="roundId">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Last Updated</span>
|
||
|
|
<span class="info-value" id="updatedAt">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Started At</span>
|
||
|
|
<span class="info-value" id="startedAt">-</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="status" id="status"></div>
|
||
|
|
|
||
|
|
<button id="connectBtn">Connect MetaMask</button>
|
||
|
|
<button id="fetchPriceBtn" disabled>Fetch Price</button>
|
||
|
|
<button id="autoRefreshBtn" disabled>Auto Refresh (60s)</button>
|
||
|
|
|
||
|
|
<div class="oracle-address">
|
||
|
|
Oracle: 0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="https://cdn.ethers.io/lib/ethers-5.7.2.umd.min.js"></script>
|
||
|
|
<script>
|
||
|
|
const ORACLE_ADDRESS = '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6';
|
||
|
|
const RPC_URL = 'https://rpc-core.d-bis.org';
|
||
|
|
const CHAIN_ID = 138;
|
||
|
|
|
||
|
|
const ORACLE_ABI = [
|
||
|
|
"function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
|
||
|
|
"function decimals() external view returns (uint8)",
|
||
|
|
"function description() external view returns (string memory)"
|
||
|
|
];
|
||
|
|
|
||
|
|
let provider = null;
|
||
|
|
let signer = null;
|
||
|
|
let oracle = null;
|
||
|
|
let autoRefreshInterval = null;
|
||
|
|
|
||
|
|
const connectBtn = document.getElementById('connectBtn');
|
||
|
|
const fetchPriceBtn = document.getElementById('fetchPriceBtn');
|
||
|
|
const autoRefreshBtn = document.getElementById('autoRefreshBtn');
|
||
|
|
const priceValue = document.getElementById('priceValue');
|
||
|
|
const priceStatus = document.getElementById('priceStatus');
|
||
|
|
const priceInfo = document.getElementById('priceInfo');
|
||
|
|
const status = document.getElementById('status');
|
||
|
|
|
||
|
|
function showStatus(type, message) {
|
||
|
|
status.className = `status ${type} show`;
|
||
|
|
status.textContent = message;
|
||
|
|
setTimeout(() => {
|
||
|
|
status.classList.remove('show');
|
||
|
|
}, 5000);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function connectWallet() {
|
||
|
|
if (typeof window.ethereum === 'undefined') {
|
||
|
|
showStatus('error', 'MetaMask is not installed. Please install MetaMask to continue.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
showStatus('info', 'Connecting to MetaMask...');
|
||
|
|
|
||
|
|
// Request account access
|
||
|
|
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
|
|
|
||
|
|
// Check if on correct network
|
||
|
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||
|
|
const chainIdDecimal = parseInt(chainId, 16);
|
||
|
|
|
||
|
|
if (chainIdDecimal !== CHAIN_ID) {
|
||
|
|
showStatus('info', 'Switching to ChainID 138...');
|
||
|
|
try {
|
||
|
|
await window.ethereum.request({
|
||
|
|
method: 'wallet_switchEthereumChain',
|
||
|
|
params: [{ chainId: '0x8a' }] // 138 in hex
|
||
|
|
});
|
||
|
|
} catch (switchError) {
|
||
|
|
if (switchError.code === 4902) {
|
||
|
|
// Network doesn't exist, add it
|
||
|
|
await window.ethereum.request({
|
||
|
|
method: 'wallet_addEthereumChain',
|
||
|
|
params: [{
|
||
|
|
chainId: '0x8a',
|
||
|
|
chainName: 'SMOM-DBIS-138',
|
||
|
|
nativeCurrency: {
|
||
|
|
name: 'Ether',
|
||
|
|
symbol: 'ETH',
|
||
|
|
decimals: 18
|
||
|
|
},
|
||
|
|
rpcUrls: [RPC_URL],
|
||
|
|
blockExplorerUrls: ['https://explorer.d-bis.org']
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
throw switchError;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create provider and signer
|
||
|
|
provider = new ethers.providers.Web3Provider(window.ethereum);
|
||
|
|
signer = provider.getSigner();
|
||
|
|
oracle = new ethers.Contract(ORACLE_ADDRESS, ORACLE_ABI, provider);
|
||
|
|
|
||
|
|
connectBtn.disabled = true;
|
||
|
|
fetchPriceBtn.disabled = false;
|
||
|
|
autoRefreshBtn.disabled = false;
|
||
|
|
|
||
|
|
showStatus('success', 'Connected to MetaMask!');
|
||
|
|
await fetchPrice();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error connecting:', error);
|
||
|
|
showStatus('error', `Connection failed: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchPrice() {
|
||
|
|
if (!oracle) {
|
||
|
|
showStatus('error', 'Please connect MetaMask first');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
showStatus('info', 'Fetching price...');
|
||
|
|
priceStatus.textContent = 'Loading...';
|
||
|
|
|
||
|
|
const result = await oracle.latestRoundData();
|
||
|
|
const price = result.answer.toNumber() / 1e8; // Convert from 8 decimals
|
||
|
|
const roundId = result.roundId.toString();
|
||
|
|
const startedAt = new Date(result.startedAt.toNumber() * 1000).toLocaleString();
|
||
|
|
const updatedAt = new Date(result.updatedAt.toNumber() * 1000).toLocaleString();
|
||
|
|
|
||
|
|
priceValue.textContent = `$${price.toFixed(2)}`;
|
||
|
|
priceStatus.textContent = `Updated ${updatedAt}`;
|
||
|
|
|
||
|
|
document.getElementById('roundId').textContent = roundId;
|
||
|
|
document.getElementById('updatedAt').textContent = updatedAt;
|
||
|
|
document.getElementById('startedAt').textContent = startedAt;
|
||
|
|
priceInfo.style.display = 'block';
|
||
|
|
|
||
|
|
showStatus('success', 'Price fetched successfully!');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching price:', error);
|
||
|
|
showStatus('error', `Failed to fetch price: ${error.message}`);
|
||
|
|
priceStatus.textContent = 'Error loading price';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleAutoRefresh() {
|
||
|
|
if (autoRefreshInterval) {
|
||
|
|
clearInterval(autoRefreshInterval);
|
||
|
|
autoRefreshInterval = null;
|
||
|
|
autoRefreshBtn.textContent = 'Auto Refresh (60s)';
|
||
|
|
showStatus('info', 'Auto refresh stopped');
|
||
|
|
} else {
|
||
|
|
autoRefreshInterval = setInterval(fetchPrice, 60000); // 60 seconds
|
||
|
|
autoRefreshBtn.textContent = 'Stop Auto Refresh';
|
||
|
|
showStatus('success', 'Auto refresh enabled (60s interval)');
|
||
|
|
fetchPrice();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event listeners
|
||
|
|
connectBtn.addEventListener('click', connectWallet);
|
||
|
|
fetchPriceBtn.addEventListener('click', fetchPrice);
|
||
|
|
autoRefreshBtn.addEventListener('click', toggleAutoRefresh);
|
||
|
|
|
||
|
|
// Listen for network changes
|
||
|
|
if (window.ethereum) {
|
||
|
|
window.ethereum.on('chainChanged', (chainId) => {
|
||
|
|
window.location.reload();
|
||
|
|
});
|
||
|
|
|
||
|
|
window.ethereum.on('accountsChanged', (accounts) => {
|
||
|
|
if (accounts.length === 0) {
|
||
|
|
window.location.reload();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
|