From c335b0efda86ae65115cea1bb2ec635abfbfb527 Mon Sep 17 00:00:00 2001 From: Max Tyrrell Date: Sun, 25 Apr 2021 20:46:24 +0200 Subject: [PATCH] feat: write initial version of frontend --- smart-contracts/.gitignore => .gitignore | 0 .../.tool-versions => .tool-versions | 0 .../contracts => contracts}/Box.sol | 0 .../hardhat.config.js => hardhat.config.js | 6 + .../package-lock.json => package-lock.json | 4 +- package.json | 39 +++++ .../scripts => scripts}/deploy.js | 0 scripts/set-values.js | 20 +++ smart-contracts/package.json | 20 --- snowpack.config.js | 21 +++ src/index.html | 55 +++++++ src/script.js | 142 +++++++++++++++++ src/style.scss | 143 ++++++++++++++++++ {smart-contracts/test => test}/Box.js | 0 14 files changed, 428 insertions(+), 22 deletions(-) rename smart-contracts/.gitignore => .gitignore (100%) rename smart-contracts/.tool-versions => .tool-versions (100%) rename {smart-contracts/contracts => contracts}/Box.sol (100%) rename smart-contracts/hardhat.config.js => hardhat.config.js (62%) rename smart-contracts/package-lock.json => package-lock.json (99%) create mode 100644 package.json rename {smart-contracts/scripts => scripts}/deploy.js (100%) create mode 100644 scripts/set-values.js delete mode 100644 smart-contracts/package.json create mode 100644 snowpack.config.js create mode 100644 src/index.html create mode 100644 src/script.js create mode 100644 src/style.scss rename {smart-contracts/test => test}/Box.js (100%) diff --git a/smart-contracts/.gitignore b/.gitignore similarity index 100% rename from smart-contracts/.gitignore rename to .gitignore diff --git a/smart-contracts/.tool-versions b/.tool-versions similarity index 100% rename from smart-contracts/.tool-versions rename to .tool-versions diff --git a/smart-contracts/contracts/Box.sol b/contracts/Box.sol similarity index 100% rename from smart-contracts/contracts/Box.sol rename to contracts/Box.sol diff --git a/smart-contracts/hardhat.config.js b/hardhat.config.js similarity index 62% rename from smart-contracts/hardhat.config.js rename to hardhat.config.js index f6d12b0..1a38aae 100644 --- a/smart-contracts/hardhat.config.js +++ b/hardhat.config.js @@ -6,4 +6,10 @@ require('@nomiclabs/hardhat-waffle') */ module.exports = { solidity: "0.7.3", + networks: { + hardhat: { + // https://hardhat.org/metamask-issue.html + chainId: 1337 + }, + } }; diff --git a/smart-contracts/package-lock.json b/package-lock.json similarity index 99% rename from smart-contracts/package-lock.json rename to package-lock.json index b2966be..a6e8493 100644 --- a/smart-contracts/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "shai-coin", + "name": "ethereum-box-smart-contracts", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "shai-coin", + "name": "ethereum-box-smart-contracts", "version": "1.0.0", "license": "MIT", "devDependencies": { diff --git a/package.json b/package.json new file mode 100644 index 0000000..c13a205 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "ethereum-box", + "version": "1.0.0", + "description": "A simple but test-complete example of an end-to-end Etherum dApp implementation.", + "scripts": { + "test": "hardhat test", + "build": "snowpack build", + "start": "snowpack dev" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xtyrrell/ethereum-box.git" + }, + "keywords": [ + "solidity", + "ethereum", + "smart-contract", + "dapp" + ], + "author": "xtyrrell", + "license": "MIT", + "bugs": { + "url": "https://github.com/xtyrrell/ethereum-box/issues" + }, + "homepage": "https://github.com/xtyrrell/ethereum-box#readme", + "devDependencies": { + "@snowpack/plugin-sass": "^1.4.0", + "snowpack": "^3.3.5", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "@openzeppelin/contracts": "^3.0.1", + "chai": "^4.3.4", + "ethereum-waffle": "^3.3.0", + "hardhat": "^2.2.1" + }, + "dependencies": { + "ethers": "^5.1.3" + } +} diff --git a/smart-contracts/scripts/deploy.js b/scripts/deploy.js similarity index 100% rename from smart-contracts/scripts/deploy.js rename to scripts/deploy.js diff --git a/scripts/set-values.js b/scripts/set-values.js new file mode 100644 index 0000000..bbd0b7b --- /dev/null +++ b/scripts/set-values.js @@ -0,0 +1,20 @@ +// This is an example of interacting with the Box contract through ethers +async function main() { + // You'll need to change this to the deployed contract address, which is output + // when running scripts/deploy.js + const CONTRACT_ADDRESS = "0x5fbdb2315678afecb367f032d93f642f64180aa3" + + const Box = await ethers.getContractFactory("Box") + + const box = await Box.attach(CONTRACT_ADDRESS) + + await box.storePublic(10) + await box.storeRestricted(99) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) diff --git a/smart-contracts/package.json b/smart-contracts/package.json deleted file mode 100644 index 098eb66..0000000 --- a/smart-contracts/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ethereum-box-smart-contracts", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "hardhat test" - }, - "author": "", - "license": "MIT", - "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.0.2", - "@nomiclabs/hardhat-waffle": "^2.0.1", - "@openzeppelin/contracts": "^3.0.1", - "chai": "^4.3.4", - "ethereum-waffle": "^3.3.0", - "ethers": "^5.1.3", - "hardhat": "^2.2.1" - } -} diff --git a/snowpack.config.js b/snowpack.config.js new file mode 100644 index 0000000..30931ad --- /dev/null +++ b/snowpack.config.js @@ -0,0 +1,21 @@ +// Snowpack Configuration File +// See all supported options: https://www.snowpack.dev/reference/configuration + +/** @type {import("snowpack").SnowpackUserConfig } */ +module.exports = { + mount: { + "src": "/", + }, + plugins: [ + "@snowpack/plugin-sass", + ], + packageOptions: { + /* ... */ + }, + devOptions: { + /* ... */ + }, + buildOptions: { + /* ... */ + }, +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..55f7fb5 --- /dev/null +++ b/src/index.html @@ -0,0 +1,55 @@ + + + + + + + + The Ethereum Box dApp + + +

The Ethereum Box dApp 📦

+

We're interacting live with a contract on the Ethereum blockchain. ⚡🛡️🔗

+

The contract we're connected to has address on network .

+ +
+

Reading values

+

In the Ethereum Box Box.sol smart contract, there are two uint256 state variables:

+ +

The value of publicValue is .

+

The value of restrictedValue is .

+
+ +
+

Writing values

+

Now, let's try set these values.

+ +
+

The storePublic(uint256 newValue) public function is public and has no access control, so can use any account to set this value.

+

Set value of publicValue: + + +

+
+ +
+

The storeRestricted(uint256 newValue) public onlyOwner function is access-restricted, so you'll need to be the contract owner to set this value.

+

Set value of restrictedValue: + + +

+
+ +

When you've confirmed the transaction, click Reload Values above to see your changes take effect.

+
+ + + + + + \ No newline at end of file diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..9e8dbd5 --- /dev/null +++ b/src/script.js @@ -0,0 +1,142 @@ +import { ethers } from "ethers"; + +// TODO: Pull this from artifacts. don't hardcode it +const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; +const NETWORK = "http://127.0.0.1:8545"; + +// --- +// utilities +// --- + +const debugDiv = document.getElementById("debug") +const log = (...args) => { + debugDiv.innerText += args.map(x => `> ${typeof x === "string" ? x : JSON.stringify(x)}\n`).join("") + console.log(...args) +} + +log("using ethers:", ethers); +log("using window.ethereum:", window.ethereum) + +// --- +// the main action +// --- + +async function setContractInfoInDom() { + document.querySelectorAll(".value.network").forEach(node => node.textContent = NETWORK) + document.querySelectorAll(".value.contract-address").forEach(node => node.textContent = CONTRACT_ADDRESS) +} + +async function readValuesIntoDom(readOnlyContract) { + // first, clear the values if they already exist + document.querySelectorAll(".readPublicValue").forEach(node => node.textContent = "") + document.querySelectorAll(".readRestrictedValue").forEach(node => node.textContent = "") + + const retrievedPublicValue = (await readOnlyContract.retrievePublic()).toString() + log("retrieved public retrievedPublicValue", retrievedPublicValue) + + const retrievedRestrictedValue = (await readOnlyContract.retrieveRestricted()).toString() + log("retrieved public retrievedRestrictedValue", retrievedRestrictedValue) + + document.querySelectorAll(".readPublicValue").forEach(node => node.textContent = retrievedPublicValue) + document.querySelectorAll(".readRestrictedValue").forEach(node => node.textContent = retrievedRestrictedValue) +} + +async function setPublicValue(contract) { + const newPublicValue = document.querySelector(".writePublicValue .value-input").value + + log("new public value in form", newPublicValue) + log("write public value response", await contract.storePublic(newPublicValue)) + + document.querySelector(".writePublicValue .value-input").value = "" +} + +async function setRestrictedValue(contract) { + const newRestrictedValue = document.querySelector(".writeRestrictedValue .value-input").value + + log("new restricted value in form", newRestrictedValue) + + try { + log("write restricted value response", await contract.storeRestricted(newRestrictedValue)) + } catch (e) { + const notAuthorised = e?.data?.message?.includes("Ownable: caller is not the owner") + + if (notAuthorised) { + alert("Your attempt to change this value was rolled back because you didn't pass the access control check. Only the account that deployed this contract can change `restrictedValue`.") + } else { + alert("Oops! There was an issue running this transaction: " + e.data.message) + } + } + + document.querySelector(".writeRestrictedValue .value-input").value = "" +} + +async function requestSignerAccounts() { + // await window.ethereum.enable() + await ethereum.send('eth_requestAccounts') + + // A Web3Provider wraps a standard Web3 provider, which is + // what Metamask injects as window.ethereum into each page + const provider = new ethers.providers.Web3Provider(window.ethereum) + + // The Metamask plugin also allows signing transactions to + // send ether and pay to change state within the blockchain. + // For this, you need the account signer... + const signer = provider.getSigner() + + return signer +} + +// get a provider/signer +// connect to contract by giving it an ABI description, address and provider/signer +// then we can run the contract methods +async function main() { + const directProvider = new ethers.providers.JsonRpcProvider(NETWORK) + + // TODO: Symlink an artifacts to `smart-contracts/artifacts` and use the generated ABI rather than this + // hardcoded interface and contract address + + const contractInterface = [ + 'function storePublic(uint256 newValue) public', + 'function storeRestricted(uint256 newValue) public', + 'function retrievePublic() public view returns (uint256)', + 'function retrieveRestricted() public view returns (uint256)' + ] + + await setContractInfoInDom() + + const readOnlyContract = new ethers.Contract(CONTRACT_ADDRESS, contractInterface, directProvider) + readValuesIntoDom(readOnlyContract) + + document.querySelectorAll('.reload-values').forEach(node => node.addEventListener('click', () => { + readValuesIntoDom(readOnlyContract) + })) + + + // --- + + const signer = await requestSignerAccounts() + const contract = new ethers.Contract(CONTRACT_ADDRESS, contractInterface, signer) + + document.querySelector("form.writePublicValue").addEventListener('submit', event => { + event.preventDefault() + setPublicValue(contract) + }) + + document.querySelector("form.writeRestrictedValue").addEventListener('submit', event => { + event.preventDefault() + setRestrictedValue(contract) + }) + + // try { + // await contract.storePublic(9999) + // log('Successfully ran `contract.storePublic(value)`') + // } catch (e) { + // log('Error trying to run `contract.storePublic(value)`') + // } + + + // await contract.storeRestricted(1010101) +} + + +main() diff --git a/src/style.scss b/src/style.scss new file mode 100644 index 0000000..2d1d199 --- /dev/null +++ b/src/style.scss @@ -0,0 +1,143 @@ +// --- +// base styles +// --- + +$bg: rgb(210, 246, 222); + +* { + box-sizing: border-box; +} + +html { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-size: 1.25rem; + background-color: $bg; + color: #162614; +} + +body { + max-width: 900px; + margin: auto; + padding: 1rem; +} + +h1, h2, h3, h4, h5, h6 { + color: rgb(0, 104, 36); +} + +p { + line-height: 1.85; +} + +code { + color: rgb(39, 144, 74); + font-weight: bold; + + &::before, &::after { + content: "`"; + } +} + +.notice { + background-color: rgb(101, 207, 136); + padding: 0.75rem; + border-radius: 5px; + + &:before { + content: "💡 " + } +} + +pre { + padding: 0.25rem; + color: #d9dff3; + background-color: #3a4638; + overflow-x: scroll; + min-height: 10rem; + max-height: 30rem; +} + +footer { + margin-top: 6rem; +} + +// --- +// mixins +// --- + +// creds to https://codepen.io/JCLee/pen/dyPejGV +// by https://codepen.io/JCLee +@mixin loading { + overflow: hidden; + position: relative; + z-index: -9999999; + + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-clip: content-box; + background-image: linear-gradient( + 90deg, + rgba($bg, 0) 0, + rgba($bg, 0.2) 20%, + rgba($bg, 0.5) 60%, + rgba($bg, 0) + ); + animation: shimmer 2s infinite; + content: ''; + z-index: -9999998; + } + + @keyframes shimmer { + 100% { + transform: translateX(100%); + } + } +} + +.loading { + @include loading(); +} + +// --- +// specific styles +// --- + +.value { + color: rgb(46, 51, 59); + background-color: #BFCEC0; + font-family: 'Courier New', Courier, monospace; + padding: 0.25rem; + margin: 0.25rem; + font-weight: bold; + + &:empty { + @include loading(); + } + + // give the loading values some width so + // the shimmer is easier to see + &:empty::before { + content: "..."; + opacity: 0; + } + + &.network:empty::before { + content: "...................."; + } + + &.contract-address:empty::before { + content: "............................................"; + } +} + +.notice { + visibility: hidden; +} +.writing-values form:focus-within ~ .notice { + visibility: visible; +} diff --git a/smart-contracts/test/Box.js b/test/Box.js similarity index 100% rename from smart-contracts/test/Box.js rename to test/Box.js