Files
chainboard/html/index.html
T
2021-02-25 18:45:41 -08:00

251 lines
9.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BSC Testnet Imageboard</title>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="shortcut icon" href="https://chainboard.neocities.org/favicon.png" />
<style>
html {
min-height: 100%;
}
body {
background: rgb(24,24,24);
background: linear-gradient(180deg, rgba(24,24,24,1) 60%, rgba(0,0,0,1) 100%);
color: white;
}
a:link {
color: #a859d9;
}
a:visited {
color: #a045d9;
}
a:hover {
color: #ab40ed;
}
a:active {
color: #752fa1;
}
h1 {
color: #4287f5;
}
div.info {
display: flex;
font-size: .8em;
}
div.address {
margin-right: .8em;
}
div.post {
background-color: #202020;
border: 1px solid black;
margin-bottom: 0.5em;
margin-top: 0.5em;
max-width: max-content;
padding: 0.5em;
}
div.tx {
}
div.timestamp {
color: #cfcfcf;
font-size: 0.9em;
}
div#guide {
background-color: #222222;
border: 1px solid black;
margin: 1em;
max-width: max-content;
padding-right: 1em;
}
div#footer {
margin-top: 1em;
}
img.postImg {
height: auto;
max-height: 256px;
max-width: 100%;
}
img.postImgFull {
height: auto;
max-width: 100%;
}
</style>
</head>
<body>
<h1>BSC Testnet Imageboard</h1>
<div id="menu"><button onclick="guide()">Guide</button><button onclick="getPosts();">Get Posts</button></div>
<script src="web3.min.js"></script>
<script src="validator.min.js"></script>
<form id="postForm" onsubmit="submitPost();return false;">
<div><textarea id="submitPostTextArea" rows="5" cols="60"></textarea></div>
<input id="postUpload" type="file">
<button>Submit</button>
</form>
<div id="posts">
</div>
<div id="footer">
🌐 <a href="https://git.kiwifarms.net/CrunkLord420/chainboard">Powered by ChainBoard under AGPL-3.0-only+NIGGER</a>
</div>
<script>
const configChainID = '0x61';
const configContract = '0x4682e630031B6d9219d15E84D921152C2E65f9F4';
//const configChainID = '0x539';
//const configRPC = 'http://localhost:7545/';
const configRPC = 'https://data-seed-prebsc-1-s2.binance.org:8545/';
function guide() {
let guideDiv = document.getElementById('guide');
if (guideDiv) {
guideDiv.remove();
} else {
guideDiv = document.createElement('div');
guideDiv.id = 'guide';
guideDiv.innerHTML = '<ul><li>Install <a href="https://metamask.io/">MetaMask</a></li><li><a href="https://metamask.zendesk.com/hc/en-us/articles/360043227612-How-to-add-custom-Network-RPC-and-or-Block-Explorer">Add BSC Testnet to MetaMask</a></li><ul><li>RPC: ' + configRPC + '</li><li>ChainID: 97</li><li>Symbol: BNB</li><li>Block Explorer: https://testnet.bscscan.com/</li><li><a href="https://docs.binance.org/smart-chain/developer/rpc.html">Additional RPC URLs</a></li></ul><li>Get free testnet BNB from the <a href="https://testnet.binance.org/faucet-smart">faucet</a></li><li>Extra Information</li><ul><li>Contract: <a href="https://testnet.bscscan.com/address/' + configContract + '">' + configContract + '</a></li><li><a href="abi.json">Contract ABI</a></li></ul>';
document.body.insertBefore(guideDiv, document.getElementById('postForm'));
}
}
if (typeof window.ethereum === 'undefined') {
guide();
}
const textArea = document.getElementById("submitPostTextArea");
const postDiv = document.getElementById("posts");
const postUpload = document.getElementById("postUpload");
const web3 = new Web3(configRPC);
let abi, contract;
async function addMeme(memeText, mimeType, data) {
const accounts = (await ethereum.request({ method: 'eth_requestAccounts' }))[0];
const tx = contract.methods.createPost(memeText, mimeType, data).encodeABI();
if (tx.length >= 262144) {
alert('Error: transaction too large\nCurrent: ' + tx.length + '\nMaximum: 262144');
return;
}
const estimate = await contract.methods.createPost(memeText, mimeType, data).estimateGas({from: ethereum.selectedAddress, gas: 10000000, value: 0}).catch((err) => {
console.error(err);
});
if (estimate === undefined) {
alert('Error: couldn\'t estimate gas price');
return;
}
const transactionParameters = {
nonce: '0x00', // ignored by MetaMask
gasPrice: '0x2540be400', // customizable by user during MetaMask confirmation.
gas: estimate.toString(16), // customizable by user during MetaMask confirmation.
to: contract._address, // Required except during contract publications.
from: ethereum.selectedAddress, // must match user's active address.
value: '0x00', // Only required to send ether to the recipient from the initiating external account.
data: tx, // Optional, but used for defining smart contract creation and interaction.
chainId: configChainID, // Used to prevent transaction reuse across blockchains. Auto-filled by MetaMask.
};
// txHash is a hex string
// As with any RPC call, it may throw an error
const txHash = await ethereum.request({
method: 'eth_sendTransaction',
params: [transactionParameters],
});
textArea.value = '';
postUpload.value = '';
}
async function submitPost() {
if (typeof window.ethereum === 'undefined') {
alert('Error: no "ethereum provider" found, install MetaMask (or alternative)');
return;
}
const chainId = await ethereum.request({ method: 'eth_chainId' });
if (chainId !== configChainID) {
alert('Wrong Chain ID\nCurrent: ' + chainId + '\nRequired: ' + configChainID);
return;
}
let mimeType = '';
let data = '';
if (postUpload.files.length > 0) {
const file = postUpload.files.item(0);
mimeType = file.type;
//data = btoa(String.fromCharCode.apply(null, new Uint8Array(await file.arrayBuffer())));
data = btoa(new Uint8Array(await file.arrayBuffer()).reduce((data, byte) => data + String.fromCharCode(byte), ''));
}
addMeme(textArea.value, mimeType, data);
}
function resizeImg(el) {
if (el.className === 'postImg') {
el.className = 'postImgFull';
} else {
el.className = 'postImg';
}
}
async function getPosts() {
const posts = await contract.getPastEvents("Post", {fromBlock: 0}).catch((err) => {
console.error(err);
return;
});
postDiv.innerHTML = '';
for (let i=posts.length-1; i>=0; i--) {
const div = document.createElement('div');
div.className = 'post';
const timestampDiv = document.createElement('div');
timestampDiv.className = 'timestamp';
timestampDiv.innerText = (new Date(parseInt(posts[i].returnValues.timestamp)*1000)).toUTCString();
div.appendChild(timestampDiv);
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
const addressDiv = document.createElement('div');
addressDiv.className = 'address';
const txDiv = document.createElement('div');
txDiv.className = 'tx';
txDiv.innerHTML = '<a href="https://testnet.bscscan.com/tx/' + posts[i].transactionHash + '">Tx</a>';
const textDiv = document.createElement('div');
textDiv.className = 'postText';
addressDiv.innerHTML = '<a href="https://testnet.bscscan.com/address/' + posts[i].returnValues.poster + '">' + posts[i].returnValues.poster + '</a>';
textDiv.innerText = posts[i].returnValues.text;
infoDiv.appendChild(addressDiv);
infoDiv.appendChild(txDiv);
div.appendChild(infoDiv);
if (posts[i].returnValues.mime) {
if (validator.isBase64(posts[i].returnValues.data)) {
if (validator.isMimeType(posts[i].returnValues.mime)) {
if (posts[i].returnValues.mime === 'image/png' || posts[i].returnValues.mime === 'image/jpeg' || posts[i].returnValues.mime === 'image/gif') {
const imgDiv = document.createElement('div');
imgDiv.className = 'postImg';
imgDiv.innerHTML = '<a href="javascript:resizeImg(postImg' + i +')"><img id="postImg' + i + '" class="postImg" src="data:' + posts[i].returnValues.mime + ';base64,' + posts[i].returnValues.data + '"></a>';
div.appendChild(imgDiv);
} else if (posts[i].returnValues.mime === 'video/mp4' || posts[i].returnValues.mime === 'video/webm') {
const imgDiv = document.createElement('div');
imgDiv.className = 'postImg';
imgDiv.innerHTML = '<video id="postImg' + i + '" class="postImg" controls><source src="data:' + posts[i].returnValues.mime + ';base64,' + posts[i].returnValues.data + '" type="' + posts[i].returnValues.mime + '"></video>';
div.appendChild(imgDiv);
} else if (posts[i].returnValues.mime === 'audio/mpeg' || posts[i].returnValues.mime === 'audio/ogg' || posts[i].returnValues.mime === 'audio/opus') {
const imgDiv = document.createElement('div');
imgDiv.className = 'postImg';
imgDiv.innerHTML = '<audio id="postImg' + i + '" class="postImg" controls><source src="data:' + posts[i].returnValues.mime + ';base64,' + posts[i].returnValues.data + '" type="' + posts[i].returnValues.mime + '"></audio>';
div.appendChild(imgDiv);
} else {
const downloadDiv = document.createElement('div');
downloadDiv.className = 'postDownload';
downloadDiv.innerHTML = '<a href="data:' + posts[i].returnValues.mime + ';base64,' + posts[i].returnValues.data + '">Download (' + posts[i].returnValues.mime + ')</a>';
div.appendChild(downloadDiv);
}
} else {
const postErrorDiv = document.createElement('div');
postErrorDiv.className = 'postError';
postErrorDiv.innerHTML = 'ERROR: invalid mime type';
div.appendChild(postErrorDiv);
}
} else {
const postErrorDiv = document.createElement('div');
postErrorDiv.className = 'postError';
postErrorDiv.innerHTML = 'ERROR: data is not valid base64';
div.appendChild(postErrorDiv);
}
}
div.appendChild(textDiv);
postDiv.appendChild(div);
}
}
async function initContract() {
abi = await (await fetch('/abi.json')).json();
contract = new web3.eth.Contract(abi, configContract);
getPosts();
}
initContract();
</script>
</body>
</html>