diff --git a/minter/src/App.js b/minter/src/App.js index 0e0da1e..f99dd6e 100644 --- a/minter/src/App.js +++ b/minter/src/App.js @@ -1,44 +1,36 @@ +import Minter from './components/minter' +import Verifier from './components/verifier' import Fox from './assets/metamask-fox.svg' -// import LaunchBackground from './assets/undraw_To_the_stars_qhyy.svg' -// import LaunchBackground from './assets/undraw_relaunch_day_902d-fixed.svg' -import LaunchBackground from './assets/undraw_launch_day_4e04.svg' -import './App.css' - +import { Container } from './components/generic' 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 } ) =>
- -
- - { children } - -
- - Launching rocket - -
function App() { // /////////////////////////////// // States // /////////////////////////////// + const [ action, setAction ] = useState( undefined ) const [ loading, setLoading ] = useState( 'Detecting metamask...' ) 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 // /////////////////////////////// + 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 async function metamasklogin( e ) { @@ -58,53 +50,25 @@ 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 // /////////////////////////////// - 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 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 // /////////////////////////////// - + log( action, error, loading, address ) // Initialisation interface if( error || loading || !address ) return { error &&

{ error }

} @@ -116,9 +80,9 @@ function App() { } { !address && ( !error && !loading ) && <> -

Rocketeer Minter

-

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.

- +

Rocketeer { action == 'mint' ? 'Minter' : 'Verifier' }

+ { action == 'mint' &&

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.

} + { action == 'verify' &&

This interface is used to veriy that you are the owner of a Rocketeer

} metamask fox Connect wallet @@ -127,32 +91,9 @@ function App() { }
- if( mintedTokenId ) return - -

Minting Successful!

-
View on Opensea - - - - // Render main interface - return ( - - -

Rocketeer Minter

-

We are ready to mint! There are currently { totalSupply } minted Rocketeers.

- - - - { contract && - metamask fox - Mint new Rocketeer - } - - - Launching rocket - -
- ) + if( action === 'mint' ) return + if( action === 'verify' ) return + else return <> } export default App; diff --git a/minter/src/components/generic.js b/minter/src/components/generic.js new file mode 100644 index 0000000..8e37597 --- /dev/null +++ b/minter/src/components/generic.js @@ -0,0 +1,13 @@ +import LaunchBackground from '../assets/undraw_launch_day_4e04.svg' + +export const Container = ( { children } ) =>
+ +
+ + { children } + +
+ + Launching rocket + +
\ No newline at end of file diff --git a/minter/src/components/minter.js b/minter/src/components/minter.js new file mode 100644 index 0000000..5c93306 --- /dev/null +++ b/minter/src/components/minter.js @@ -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 + { error &&

{ error }

} + { !error && loading &&
+ +
+

{ loading }

+ +
} +
+ + if( mintedTokenId ) return + +

Minting Successful!

+ View on Opensea + +
+ + // Render main interface + return ( + + +

Rocketeer Minter

+

We are ready to mint! There are currently { totalSupply } minted Rocketeers.

+ + + + { contract && + metamask fox + Mint new Rocketeer + } + + +
+ ) +} + diff --git a/minter/src/components/verifier.js b/minter/src/components/verifier.js new file mode 100644 index 0000000..edbb50a --- /dev/null +++ b/minter/src/components/verifier.js @@ -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 + { message.balance > 0 &&

✅ { message.username } has { message.balance } Rocketeers

} + { message.balance < 1 &&

🛑 Computer says no

} +
+ + if( verifyUrl ) return + +

Verification URL

+

Post this in the Discord channel #get-verified:

+

{ verifyUrl }

+ +
+ + return + +

Verify your hodlr status

+

Verify your Rocketeer status by signing a message with your wallet. This does NOT trigger a transaction. Therefore it is free.

+ setUsername( e.target.value ) } type="text" placeholder="Your Discord username" /> + Verify + +
+} \ No newline at end of file diff --git a/minter/src/index.css b/minter/src/index.css index bf087cf..c5785ad 100644 --- a/minter/src/index.css +++ b/minter/src/index.css @@ -13,6 +13,8 @@ body { body * { box-sizing: border-box; + max-width: 100%; + overflow-wrap: break-word; } code { diff --git a/minter/src/modules/web3.js b/minter/src/modules/web3.js index 5bc644a..07afe2b 100644 --- a/minter/src/modules/web3.js +++ b/minter/src/modules/web3.js @@ -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 export function useChainId() { @@ -138,7 +174,7 @@ const contractAddressByChainId = { '0x4': '0x89D9f02D2877A35E8323DC1b578FD1f6014B04d0' } -// Contract ABI with only totalSupply and Transfer +// Contract ABI with only totalSupply, balanceOf and Transfer const ABI = [ { "inputs": [], @@ -154,6 +190,26 @@ const ABI = [ "type": "function", "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, "inputs": [