Created
July 31, 2022 22:44
-
-
Save qkum/b9eec0c2419a85908e740a6a0cd72583 to your computer and use it in GitHub Desktop.
V2 Staking App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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