<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Web3 Contract Example</title>
<!-- Pin web3 to a specific version -->
<script src="https://cdn.jsdelivr.net/npm/web3@1.8.1/dist/web3.min.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 1rem; max-width: 720px; margin: auto; }
button { margin-top: 0.5rem; }
.row { margin: 0.5rem 0; }
.warn { color: #a00; font-weight: bold; }
</style>
</head>
<body>
<h2>Web3 Contract Demo</h2>
<div id="status" class="row">
Status: <span id="statusText">idle</span>
</div>
<div class="row">
<button id="connectBtn">Connect Wallet</button>
<span id="account" style="margin-left:1rem"></span>
</div>
<div class="row">
<strong>Network:</strong>
<span id="network">—</span>
<span id="networkWarn" class="warn" style="margin-left:1rem; display:none">Wrong network</span>
</div>
<div class="row">
<strong>Current value:</strong>
<span id="currentValue">—</span>
<button id="refreshBtn">Refresh</button>
</div>
<div class="row">
<input id="newValue" type="number" placeholder="New value" />
<button id="setBtn">Set Value</button>
</div>
<div class="row">
<small>Tip: Place a `contractABI.json` file in the same folder (optional). Otherwise replace the `ABI` and `contractAddress` constants in the source.</small>
</div>
<script>
// === Configuration ===
// Replace with your deployed address if you know it.
const contractAddress = "0xYourContractAddressHere";
// Optionally set the expected chainId (Hex string like '0x1' for mainnet or '0x5' for Goerli).
// Set to null to skip network mismatch warnings.
const expectedChainId = null; // e.g. '0x5'
// Default ABI placeholder; we will try to load ./contractABI.json if present.
let ABI = [ /* ... ABI array ... */ ];
let web3;
let contract;
let currentAccount;
let currentChainId;
function setStatus(text) { document.getElementById('statusText').textContent = text; }
// Try to load an ABI file from the same directory (contractABI.json)
async function tryLoadLocalAbi() {
try {
const resp = await fetch('./contractABI.json', { cache: 'no-store' });
if (!resp.ok) return;
const parsed = await resp.json();
if (Array.isArray(parsed)) {
ABI = parsed;
console.info('Loaded ABI from contractABI.json');
} else if (parsed.abi && Array.isArray(parsed.abi)) {
ABI = parsed.abi;
console.info('Loaded ABI from contractABI.json (wrapped format)');
}
} catch (err) {
// ignore - local ABI optional
}
}
// Decode revert reason from error.data (if present)
function decodeRevertReason(data) {
try {
if (!data) return null;
// standard Error(string) selector: 0x08c379a0
if (data.startsWith('0x08c379a0')) {
// drop selector and length prefix: solidity ABI encode => 4 + offset(32) + strLen(32) + strBytes
const hex = data.slice(10); // remove 0x08c379a0
// Derive the string hex from the remaining payload
// Skip first 64 chars (offset + strLen) -> start at char 128 (0-based)
const strHex = hex.slice(128);
const strBytes = strHex.slice(0, strHex.length - (strHex.length % 2));
const str = decodeURIComponent(strBytes.match(/.{1,2}/g).map(byte => '%' + byte).join(''));
return str;
}
return null;
} catch (e) {
return null;
}
}
async function connectWallet() {
if (!window.ethereum) {
setStatus('No Ethereum provider (install MetaMask).');
return;
}
try {
setStatus('Initializing...');
await tryLoadLocalAbi();
web3 = new Web3(window.ethereum);
// Request accounts (user will be prompted if not previously allowed)
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
currentAccount = accounts && accounts[0];
document.getElementById('account').textContent = currentAccount || '';
// Read chainId
currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
document.getElementById('network').textContent = currentChainId || 'unknown';
if (expectedChainId && currentChainId !== expectedChainId) {
document.getElementById('networkWarn').style.display = 'inline';
} else {
document.getElementById('networkWarn').style.display = 'none';
}
contract = new web3.eth.Contract(ABI, contractAddress);
setStatus('Connected');
await readValue();
} catch (err) {
console.error(err);
setStatus('Connection failed');
}
}
async function readValue() {
if (!contract) { setStatus('Not connected'); return; }
try {
setStatus('Reading value...');
const value = await contract.methods.getValue().call();
document.getElementById('currentValue').textContent = value;
setStatus('Read complete');
} catch (err) {
console.error(err);
const reason = decodeRevertReason(err?.data?.original || err?.data || err?.error?.data);
setStatus('Read failed' + (reason ? `: ${reason}` : ''));
}
}
function validateInput(v) {
if (v === '') return { ok: false, msg: 'Value required' };
// numeric check
const n = Number(v);
if (!Number.isFinite(n)) return { ok: false, msg: 'Not a valid number' };
// check safe integer range if integer expected
if (!Number.isSafeInteger(n)) {
// allow decimals if the contract expects them, else warn
return { ok: true, value: n, warn: 'Value is outside safe integer range; consider using a string/BN' };
}
return { ok: true, value: n };
}
async function setValue() {
if (!contract || !currentAccount) { setStatus('Not connected'); return; }
const input = document.getElementById('newValue');
const v = input.value;
const check = validateInput(v);
if (!check.ok) {
setStatus(check.msg);
return;
}
try {
setStatus('Estimating gas...');
// Use BN/string if necessary - here we pass the value as-is; adapt if the contract expects specific types
const method = contract.methods.setValue(check.value);
// estimate gas
const gasEstimate = await method.estimateGas({ from: currentAccount });
setStatus('Sending transaction...');
const receipt = await method.send({ from: currentAccount, gas: Math.floor(gasEstimate * 1.2) });
console.log('Tx receipt', receipt);
setStatus('Transaction confirmed');
await readValue();
} catch (err) {
console.error(err);
// Try to show revert reason
let reason = null;
// common places providers put data
reason = decodeRevertReason(err?.data?.original || err?.data || err?.error?.data || err?.receipt?.revertReason);
if (!reason && err?.message) reason = err.message;
setStatus('Tx failed or rejected' + (reason ? `: ${reason}` : ''));
}
}
// React to account / chain changes
if (window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
currentAccount = accounts[0] || null;
document.getElementById('account').textContent = currentAccount || '';
if (!currentAccount) setStatus('Disconnected');
});
window.ethereum.on('chainChanged', (chainId) => {
// update UI and optionally warn about mismatch
currentChainId = chainId;
document.getElementById('network').textContent = currentChainId || 'unknown';
if (expectedChainId && currentChainId !== expectedChainId) {
document.getElementById('networkWarn').style.display = 'inline';
} else {
document.getElementById('networkWarn').style.display = 'none';
}
// Recommend reload to reinitialize provider state (keeps things simple).
// Some apps handle chain re-init without reload.
window.location.reload();
});
}
document.getElementById('connectBtn').addEventListener('click', connectWallet);
document.getElementById('refreshBtn').addEventListener('click', readValue);
document.getElementById('setBtn').addEventListener('click', setValue);
// Optionally auto-connect if user already approved - prefer eth_accounts
window.addEventListener('DOMContentLoaded', async () => {
if (!window.ethereum) return;
try {
await tryLoadLocalAbi();
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts && accounts.length > 0) {
await connectWallet();
}
} catch (e) {
// ignore
}
});
</script>
</body>
</html>
Web3 Contract Demo
Status: idle
Network:
—
Current value:
—
Tip: Place a `contractABI.json` file in the same folder (optional). Otherwise replace the `ABI` and `contractAddress` constants in the source.