Skip to content

Instantly share code, notes, and snippets.

@qkum
Created July 31, 2022 22:44
Show Gist options
  • Save qkum/b9eec0c2419a85908e740a6a0cd72583 to your computer and use it in GitHub Desktop.
Save qkum/b9eec0c2419a85908e740a6a0cd72583 to your computer and use it in GitHub Desktop.
V2 Staking App
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.1.3",
"ethers": "^5.6.9",
"react": "^18.2.0",
"react-bootstrap-icons": "^1.8.4",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
import React from 'react'
const NavBar = props => {
return (
<>
<div className="navBar">
<div className="navButton">Markets</div>
<div className="navButton">Assets</div>
{props.isConnected() ? (
<div className="connectButton">
Connected
</div>
) : (
<div
onClick={() => props.connect()}
className="connectButton">
Connect Wallet
</div>
)}
</div>
</>
)
}
export default NavBar
import React, { useState } from 'react';
const StakeModal = props => {
const {
onClose,
stakingLength,
stakingPercent,
setAmount,
stakeEther,
} = props
return (
<>
<div className="modal-class" onClick={props.onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-body">
<h2 className="titleHeader">Stake Ether</h2>
<div className="row">
<div className="col-md-9 fieldContainer">
<input
className="inputField"
placeholder="0.0"
onChange={e => props.setAmount(e.target.value)}
/>
</div>
<div className="col-md-3 inputFieldUnitsContainer">
<span>ETH</span>
</div>
</div>
<div className="row">
<h6 className="titleHeader stakingTerms">{stakingLength} days @ {stakingPercent} APY</h6>
</div>
<div className="row">
<div
onClick={() => stakeEther()}
className="orangeButton"
>
Stake
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default StakeModal
.App {
background-color: #fff;
text-align: center;
min-height: 100vh; /* required for full page background */
}
#root {
min-height: 100vh; /* required for full page background */
}
.appBody {
height: 100%;
margin-top: 80px;
}
/*////////////////////////////////////////////////////////////////////////////*/
/* Nav Bar */
.navButton {
float: left;
display: inline-block;
margin-left: 24px;
padding: 5px;
color: #1168f1;
font-size: 24px;
}
.connectButton {
float: right;
margin-right: 24px;
background-color: #1168f1;
border-radius: 4px;
padding: 5px 15px;
color: #f1f1f3;
font-size: 24px;
cursor: pointer;
}
.connectButton:hover {
background-color: #f90;
}
.navBar {
padding-top: 18px;
overflow: hidden;
padding-bottom: 10px;
border-bottom: 1px solid rgba(17, 104, 241, 0.08);
}
/*////////////////////////////////////////////////////////////////////////////*/
/* Market */
.marketContainer {
background-color: #1168f1;
width: 620px;
height: 170px;
margin: 0 auto;
top: 50%;
border-radius: 24px;
padding: 8px;
}
.subContainer {
margin-bottom: 20px;
}
.marketOption {
display: flex;
margin-left: 22px;
}
.optionData {
margin-left: 10px;
color: #fff;
font-weight: 700;
}
.optionPercent {
display: block;
font-size: 24px;
font-weight: 1000;
color: #f90;
}
.logoImg {
background-color: #fff;
border-radius: 100px;
margin-bottom: 14px;
margin-right: 14px;
width: 28px;
height: 28px;
}
.marketHeader {
color: #fff;
font-size: 32px;
font-weight: 800;
}
.hoverButton:hover {
background: #f90;
cursor: pointer;
}
.hoverButton:active {
/* box-shadow: 0 0 0 white; */
box-shadow: inset;
}
/*////////////////////////////////////////////////////////////////////////////*/
/* Assets */
.assetContainer {
background-color: #1168f1;
width: 620px;
min-height: 225px;
margin: 0 auto;
top: 50%;
border-radius: 24px;
padding: 6px;
padding: 8px;
margin-top: 50px; /* space between sections */
color: #fff;
font-weight: 500;
}
.columnHeaders {
font-weight: 800;
color: #f90;
}
.stakedLogoImg {
background-color: #fff;
border-radius: 100px;
margin-bottom: 14px;
margin-right: 14px;
width: 18px;
height: 18px;
}
/*////////////////////////////////////////////////////////////////////////////*/
/* Glyphs */
.glyphContainer {
background-color: #fff;
display: inline-block;
width: 60px;
height: 60px;
border-radius: 12px;
}
.glyph {
font-size: 34px;
}
/******************************************************************************/
/* Stake Modal */
.modal-class {
background-color: rgba(0,0,0,0.5);
position: fixed;
left: 0;
top: -400px;
right: 0px;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgb(86, 90, 105);
}
.modal-content {
background-color: #fff;
width: 300px !important;
padding: 0px 15px;
background-color: rgb(237, 238, 242) !important;
border: 1px solid rgb(206, 208, 217) !important;
border-radius: 12px !important;
}
.fieldContainer {
padding-left: 0px !important;
}
.inputField {
padding-left: 10px;
border-radius: 36px;
border: 1px solid #65cdee;
height: 2rem;
margin: 10px 0;
width: 100%;
}
.inputFieldUnitsContainer {
padding-left: 0px !important;
padding-top: 14px;
}
.orangeButton {
width: 100%;
height: 45px;
line-height: 45px;
font-size: 24px;
color: #fff;
border-radius: 20px;
background-color: #dd2f81;
cursor: pointer
}
.orangeMiniButton {
width: 100%;
height: 25px;
line-height: 25px;
font-size: 12px;
color: #000;
border-radius: 20px;
background-color: #fff;
cursor: pointer
}
.orangeMiniButton:hover {
color: #fff;
background-color: #f90;;
}
.stakingTerms {
color: blue;
}
import './App.css';
import react, { useEffect, useState } from 'react';
import { ethers } from 'ethers';
import artifact from './artifacts/contracts/Staking.sol/Staking.json'
import NavBar from './components/NavBar'
import StakeModal from './components/StakeModal'
import { Bank, PiggyBank, Coin } from 'react-bootstrap-icons'
const CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
function App() {
// general
const [provider, setProvider] = useState(undefined)
const [signer, setSigner] = useState(undefined)
const [contract, setContract] = useState(undefined)
const [signerAddress, setSignerAddress] = useState(undefined)
// assets
const [assetIds, setAssetIds] = useState([])
const [assets, setAssets] = useState([])
// staking
const [showStakeModal, setShowStakeModal] = useState(false)
const [stakingLength, setStakingLength] = useState(undefined)
const [stakingPercent, setStakingPercent] = useState(undefined)
const [amount, setAmount] = useState(0)
// helpers
const toWei = ether => ethers.utils.parseEther(ether)
const toEther = wei => ethers.utils.formatEther(wei)
useEffect(() => {
const onLoad = async () => {
const provider = await new ethers.providers.Web3Provider(window.ethereum)
setProvider(provider)
const contract = await new ethers.Contract(
CONTRACT_ADDRESS,
artifact.abi
)
setContract(contract)
}
onLoad()
}, [])
const isConnected = () => signer !== undefined
const getSigner = async () => {
provider.send("eth_requestAccounts", [])
const signer = provider.getSigner()
return signer
}
const getAssetIds = async (address, signer) => {
const assetIds = await contract.connect(signer).getPositionIdsForAddress(address)
return assetIds
}
const calcDaysRemaining = (unlockDate) => {
const timeNow = Date.now() / 1000
const secondsRemaining = unlockDate - timeNow
return Math.max( (secondsRemaining / 60 / 60 / 24).toFixed(0), 0)
}
const getAssets = async (ids, signer) => {
const queriedAssets = await Promise.all(
ids.map(id => contract.connect(signer).getPositionById(id))
)
queriedAssets.map(async asset => {
const parsedAsset = {
positionId: asset.positionId,
percentInterest: Number(asset.percentInterest) / 100,
daysRemaining: calcDaysRemaining( Number(asset.unlockDate) ),
etherInterest: toEther(asset.weiInterest),
etherStaked: toEther(asset.weiStaked),
open: asset.open,
}
setAssets(prev => [...prev, parsedAsset])
})
}
const connectAndLoad = async () => {
const signer = await getSigner(provider)
setSigner(signer)
const signerAddress = await signer.getAddress()
setSignerAddress(signerAddress)
const assetIds = await getAssetIds(signerAddress, signer)
setAssetIds(assetIds)
getAssets(assetIds, signer)
}
const openStakingModal = (stakingLength, stakingPercent) => {
setShowStakeModal(true)
setStakingLength(stakingLength)
setStakingPercent(stakingPercent)
}
const stakeEther = () => {
const wei = toWei(amount)
const data = { value: wei }
contract.connect(signer).stakeEther(stakingLength, data)
}
const withdraw = positionId => {
contract.connect(signer).closePosition(positionId)
}
return (
<div className="App">
<div>
<NavBar
isConnected={isConnected}
connect={connectAndLoad}
/>
</div>
<div className="appBody">
<div className="marketContainer">
<div className="subContainer">
<span>
<img className="logoImg" src="eth-logo.webp"/>
</span>
<span className="marketHeader">Ethereum Market</span>
</div>
<div className="row">
<div className="col-md-4">
<div onClick={() => openStakingModal(30, '7%')} className="marketOption">
<div className="glyphContainer hoverButton">
<span className="glyph">
<Coin />
</span>
</div>
<div className="optionData">
<span>1 Month</span>
<span className="optionPercent">7%</span>
</div>
</div>
</div>
<div className="col-md-4">
<div onClick={() => openStakingModal(90, '10%')} className="marketOption">
<div className="glyphContainer hoverButton">
<span className="glyph">
<Coin />
</span>
</div>
<div className="optionData">
<span>3 Months</span>
<span className="optionPercent">10%</span>
</div>
</div>
</div>
<div className="col-md-4">
<div onClick={() => openStakingModal(180, '12%')} className="marketOption">
<div className="glyphContainer hoverButton">
<span className="glyph">
<Coin />
</span>
</div>
<div className="optionData">
<span>6 Months</span>
<span className="optionPercent">12%</span>
</div>
</div>
</div>
</div>
</div>
<div className="assetContainer">
<div className="subContainer">
<span className="marketHeader">Staked Assets</span>
</div>
<div>
<div className="row columnHeaders">
<div className="col-md-2">Assets</div>
<div className="col-md-2">Percent Interest</div>
<div className="col-md-2">Staked</div>
<div className="col-md-2">Interest</div>
<div className="col-md-2">Days Remaining</div>
<div className="col-md-2"></div>
</div>
</div>
<br />
{assets.length > 0 && assets.map((a, idx) => (
<div className="row">
<div className="col-md-2">
<span>
<img className="stakedLogoImg" src="eth-logo.webp" />
</span>
</div>
<div className="col-md-2">
{a.percentInterest} %
</div>
<div className="col-md-2">
{a.etherStaked}
</div>
<div className="col-md-2">
{a.etherInterest}
</div>
<div className="col-md-2">
{a.daysRemaining}
</div>
<div className="col-md-2">
{a.open ? (
<div onClick={() => withdraw(a.positionId)} className="orangeMiniButton">Withdraw</div>
) : (
<span>closed</span>
)}
</div>
</div>
))}
</div>
</div>
{showStakeModal && (
<StakeModal
onClose={() => setShowStakeModal(false)}
stakingLength={stakingLength}
stakingPercent={stakingPercent}
amount={amount}
setAmount={setAmount}
stakeEther={stakeEther}
/>
)}
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import 'bootstrap/dist/css/bootstrap.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment