This commit is contained in:
Mentor Palokaj 2021-10-20 12:50:57 +02:00
parent 6f2a438ff7
commit 82d2b6da08
6 changed files with 303 additions and 91 deletions

View File

@ -1,44 +1,36 @@
import Minter from './components/minter'
import Verifier from './components/verifier'
import Fox from './assets/metamask-fox.svg' import Fox from './assets/metamask-fox.svg'
// import LaunchBackground from './assets/undraw_To_the_stars_qhyy.svg' import { Container } from './components/generic'
// import LaunchBackground from './assets/undraw_relaunch_day_902d-fixed.svg'
import LaunchBackground from './assets/undraw_launch_day_4e04.svg'
import './App.css'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { log } from './modules/helpers'
import { useAddress, getAddress } from './modules/web3'
import { getAddress, useAddress, useTotalSupply, useContract, useChainId, rocketeerUriOnOpensea } from './modules/web3'
import { log, setListenerAndReturnUnlistener } from './modules/helpers'
const Container = ( { children } ) => <main>
<div className="container">
{ children }
</div>
<img className="stretchBackground" src={ LaunchBackground } alt="Launching rocket" />
</main>
function App() { function App() {
// /////////////////////////////// // ///////////////////////////////
// States // States
// /////////////////////////////// // ///////////////////////////////
const [ action, setAction ] = useState( undefined )
const [ loading, setLoading ] = useState( 'Detecting metamask...' ) const [ loading, setLoading ] = useState( 'Detecting metamask...' )
const [ error, setError ] = useState( undefined ) const [ error, setError ] = useState( undefined )
const [ mintedTokenId, setMintedTokenId ] = useState( undefined )
const totalSupply = useTotalSupply( )
const address = useAddress() const address = useAddress()
const chainId = useChainId()
// Handle contract interactions
const contract = useContract()
// /////////////////////////////// // ///////////////////////////////
// Functions // Functions
// /////////////////////////////// // ///////////////////////////////
function checkAction() {
const verify = window.location.href.includes( 'mode=verify' )
log( `Location is ${window.location.href}, ${ !verify ? 'not opening' : 'opening' } verifier` )
if( verify ) return setAction( 'verify' )
return setAction( 'mint' )
}
// Handle user login interaction // Handle user login interaction
async function metamasklogin( e ) { async function metamasklogin( e ) {
@ -59,52 +51,24 @@ function App() {
} }
async function mintRocketeer( e ) {
e.preventDefault()
try {
if( !address ) setError( 'No destination address selected' )
setLoading( 'Confirm transaction in metamask' )
const response = await contract.spawnRocketeer( address )
log( 'Successful mint with: ', response )
setLoading( 'Waiting for confirmations...' )
} catch( e ) {
log( 'Minting error: ', e )
alert( `Minting error: ${ e.message }` )
setLoading( false )
}
}
// /////////////////////////////// // ///////////////////////////////
// Lifecycle // Lifecycle
// /////////////////////////////// // ///////////////////////////////
useEffect( f => setListenerAndReturnUnlistener( contract, 'Transfer', async ( from, to, amount, event ) => {
try {
log( `useEffect: Transfer ${ from } sent to ${ to } `, amount, event )
const [ transFrom, transTo, tokenId ] = event.args
setMintedTokenId( tokenId.toString() )
setLoading( false )
} catch( e ) {
log( 'Error getting Transfer event from contract: ', e )
}
} ), [ contract, loading ] )
// Check for metamask on load // Check for metamask on load
useEffect( f => window.ethereum ? setLoading( false ) : setError( 'No web3 provider detected, please install metamask' ), [] ) useEffect( f => window.ethereum ? setLoading( false ) : setError( 'No web3 provider detected, please install metamask' ), [] )
// Check for action on load
useEffect( f => {
checkAction()
window.addEventListener( 'popstate', checkAction )
}, [] )
// /////////////////////////////// // ///////////////////////////////
// Rendering // Rendering
// /////////////////////////////// // ///////////////////////////////
log( action, error, loading, address )
// Initialisation interface // Initialisation interface
if( error || loading || !address ) return <Container> if( error || loading || !address ) return <Container>
{ error && <p>{ error }</p> } { error && <p>{ error }</p> }
@ -116,9 +80,9 @@ function App() {
</div> } </div> }
{ !address && ( !error && !loading ) && <> { !address && ( !error && !loading ) && <>
<h1>Rocketeer Minter</h1> <h1>Rocketeer { action == 'mint' ? 'Minter' : 'Verifier' }</h1>
<p>This interface is used to mint new Rocketeer NFTs. Minting is free, except for the gas fees. After minting you can view your new Rocketeer and its attributes on Opensea.</p> { action == 'mint' && <p>This interface is used to mint new Rocketeer NFTs. Minting is free, except for the gas fees. After minting you can view your new Rocketeer and its attributes on Opensea.</p> }
{ action == 'verify' && <p>This interface is used to veriy that you are the owner of a Rocketeer</p> }
<a className="button" href="/#" onClick={ metamasklogin }> <a className="button" href="/#" onClick={ metamasklogin }>
<img alt="metamask fox" src={ Fox } /> <img alt="metamask fox" src={ Fox } />
Connect wallet Connect wallet
@ -127,32 +91,9 @@ function App() {
</> } </> }
</Container> </Container>
if( mintedTokenId ) return <Container> if( action === 'mint' ) return <Minter />
if( action === 'verify' ) return <Verifier />
<h1>Minting Successful!</h1> else return <></>
<a className="button" rel="noreferrer" target="_blank" alt="Link to opensea details of Rocketeer" href={ rocketeerUriOnOpensea( chainId, mintedTokenId ) }>View on Opensea</a>
</Container>
// Render main interface
return (
<Container>
<h1>Rocketeer Minter</h1>
<p>We are ready to mint! There are currently { totalSupply } minted Rocketeers.</p>
<label htmlFor='address'>Minting to:</label>
<input id='address' value={ address } disabled />
{ contract && <a className="button" href="/#" onClick={ mintRocketeer }>
<img alt="metamask fox" src={ Fox } />
Mint new Rocketeer
</a> }
<img className="stretchBackground" src={ LaunchBackground } alt="Launching rocket" />
</Container>
)
} }
export default App; export default App;

View File

@ -0,0 +1,13 @@
import LaunchBackground from '../assets/undraw_launch_day_4e04.svg'
export const Container = ( { children } ) => <main>
<div className="container">
{ children }
</div>
<img className="stretchBackground" src={ LaunchBackground } alt="Launching rocket" />
</main>

View File

@ -0,0 +1,107 @@
import Fox from '../assets/metamask-fox.svg'
import { Container } from './generic'
import '../App.css'
import { useState, useEffect } from 'react'
import { useAddress, useTotalSupply, useContract, useChainId, rocketeerUriOnOpensea } from '../modules/web3'
import { log, setListenerAndReturnUnlistener } from '../modules/helpers'
export default function Minter() {
// ///////////////////////////////
// States
// ///////////////////////////////
const [ loading, setLoading ] = useState( false )
const [ error, setError ] = useState( undefined )
const [ mintedTokenId, setMintedTokenId ] = useState( undefined )
const totalSupply = useTotalSupply( )
const address = useAddress()
const chainId = useChainId()
// Handle contract interactions
const contract = useContract()
// ///////////////////////////////
// Functions
// ///////////////////////////////
async function mintRocketeer( e ) {
e.preventDefault()
try {
if( !address ) setError( 'No destination address selected' )
setLoading( 'Confirm transaction in metamask' )
const response = await contract.spawnRocketeer( address )
log( 'Successful mint with: ', response )
setLoading( 'Waiting for confirmations...' )
} catch( e ) {
log( 'Minting error: ', e )
alert( `Minting error: ${ e.message }` )
setLoading( false )
}
}
// ///////////////////////////////
// Lifecycle
// ///////////////////////////////
useEffect( f => setListenerAndReturnUnlistener( contract, 'Transfer', async ( from, to, amount, event ) => {
try {
log( `useEffect: Transfer ${ from } sent to ${ to } `, amount, event )
const [ transFrom, transTo, tokenId ] = event.args
setMintedTokenId( tokenId.toString() )
setLoading( false )
} catch( e ) {
log( 'Error getting Transfer event from contract: ', e )
}
} ), [ contract, loading ] )
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( error || loading ) return <Container>
{ error && <p>{ error }</p> }
{ !error && loading && <div className="loading">
<div className="lds-dual-ring"></div>
<p>{ loading }</p>
</div> }
</Container>
if( mintedTokenId ) return <Container>
<h1>Minting Successful!</h1>
<a className="button" rel="noreferrer" target="_blank" alt="Link to opensea details of Rocketeer" href={ rocketeerUriOnOpensea( chainId, mintedTokenId ) }>View on Opensea</a>
</Container>
// Render main interface
return (
<Container>
<h1>Rocketeer Minter</h1>
<p>We are ready to mint! There are currently { totalSupply } minted Rocketeers.</p>
<label htmlFor='address'>Minting to:</label>
<input id='address' value={ address } disabled />
{ contract && <a className="button" href="/#" onClick={ mintRocketeer }>
<img alt="metamask fox" src={ Fox } />
Mint new Rocketeer
</a> }
</Container>
)
}

View File

@ -0,0 +1,93 @@
import { Container } from './generic'
import '../App.css'
import { useState, useEffect, useReducer } from 'react'
import { log, setListenerAndReturnUnlistener } from '../modules/helpers'
import { useAddress, useChainId, useBalanceOf } from '../modules/web3'
export default function Verifier() {
// ///////////////////////////////
// State management
// ///////////////////////////////
const balance = useBalanceOf()
const chainId = useChainId()
const address = useAddress()
const [ username, setUsername ] = useState( )
const [ verifyUrl, setVerifyUrl ] = useState()
const [ message, setMessage ] = useState()
// ///////////////////////////////
// Functions
// ///////////////////////////////
function showVerificationUrl( e ) {
e.preventDefault()
if( !username ) return alert( 'Please fill in your Discord username to get verified' )
if( balance < 1 ) return alert( `The address ${ address } does not own Rocketeers, did you select the right address?` )
const baseUrl = `https://mint.rocketeer.fans/?mode=verify`
const message = btoa( `{ "username": "${ username }", "address": "${ address }", "balance": "${ balance }" }` )
setVerifyUrl( baseUrl + `&message=${ message }` )
}
function verifyIfNeeded() {
if( !window.location.href.includes( 'message' ) ) return
try {
const { search } = window.location
const query = new URLSearchParams( search )
const message = query.get( 'message' )
const verification = atob( message )
log( 'Received message: ', verification )
const json = JSON.parse( verification )
return setMessage( json )
} catch( e ) {
log( e )
return alert( 'Verification error, contact the team on Discord' )
}
}
// ///////////////////////////////
// Lifecycle
// ///////////////////////////////
useEffect( f => {
verifyIfNeeded()
window.addEventListener( 'popstate', verifyIfNeeded )
}, [] )
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( message ) return <Container>
{ message.balance > 0 && <p> { message.username } has { message.balance } Rocketeers</p> }
{ message.balance < 1 && <p>🛑 Computer says no</p> }
</Container>
if( verifyUrl ) return <Container>
<h1>Verification URL</h1>
<p>Post this in the Discord channel #get-verified:</p>
<p>{ verifyUrl }</p>
</Container>
return <Container>
<h1>Verify your hodlr status</h1>
<p>Verify your Rocketeer status by signing a message with your wallet. This does NOT trigger a transaction. Therefore it is free.</p>
<input onChange={ e => setUsername( e.target.value ) } type="text" placeholder="Your Discord username" />
<a onClick={ showVerificationUrl } href="/#" className="button">Verify</a>
</Container>
}

View File

@ -13,6 +13,8 @@ body {
body * { body * {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%;
overflow-wrap: break-word;
} }
code { code {

View File

@ -99,6 +99,42 @@ export function useTotalSupply() {
} }
export function useBalanceOf() {
const [ balance, setBalance ] = useState( 'loading' )
const contract = useContract( )
const address = useAddress()
// Create listener to minting
useEffect( f => {
// Do nothing if there is not contract object
if( !contract || !address ) return
// Load initial supply value
( async ( ) => {
try {
const balance = await contract.balanceOf( address )
log( 'Balance detected: ', balance, `(${ balance.toString() })` )
setBalance( balance.toString() )
} catch( e ) {
log( 'Error getting initial supply: ', e )
}
} )( )
}, [ contract, address ] )
return balance
}
// Chain ID hook // Chain ID hook
export function useChainId() { export function useChainId() {
@ -138,7 +174,7 @@ const contractAddressByChainId = {
'0x4': '0x89D9f02D2877A35E8323DC1b578FD1f6014B04d0' '0x4': '0x89D9f02D2877A35E8323DC1b578FD1f6014B04d0'
} }
// Contract ABI with only totalSupply and Transfer // Contract ABI with only totalSupply, balanceOf and Transfer
const ABI = [ const ABI = [
{ {
"inputs": [], "inputs": [],
@ -154,6 +190,26 @@ const ABI = [
"type": "function", "type": "function",
"constant": true "constant": true
}, },
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [