mirror of
https://github.com/stronk-dev/RandomChad.git
synced 2025-07-05 02:35:08 +02:00
Avatar setting
This commit is contained in:
parent
c2a851cb76
commit
0b46af4057
@ -20,6 +20,10 @@
|
||||
{
|
||||
"source": "/testnetapi/**",
|
||||
"function": "testnetMetadata"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/avatar",
|
||||
"function": "setAvatarOfValidtor"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const testnetAPI = require( './modules/testnet' )
|
||||
const mainnetAPI = require( './modules/mainnet' )
|
||||
const setAvatarOfValidtor = require( './integrations/avatar' )
|
||||
|
||||
// Runtime config
|
||||
const runtime = {
|
||||
@ -13,3 +14,8 @@ exports.testnetMetadata = functions.runWith( runtime ).https.onRequest( testnetA
|
||||
|
||||
// Mainnet endpoint
|
||||
exports.mainnetMetadata = functions.runWith( runtime ).https.onRequest( mainnetAPI )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Integrations
|
||||
// /////////////////////////////*/
|
||||
exports.setAvatarOfValidtor = functions.https.onRequest( setAvatarOfValidtor )
|
87
functions/integrations/avatar.js
Normal file
87
functions/integrations/avatar.js
Normal file
@ -0,0 +1,87 @@
|
||||
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||
const Web3 = require( 'web3' )
|
||||
const web3 = new Web3()
|
||||
const { getStorage } = require( 'firebase-admin/storage' )
|
||||
|
||||
|
||||
module.exports = async function( req, res ) {
|
||||
|
||||
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
|
||||
|
||||
try {
|
||||
|
||||
// Get request data
|
||||
const { message, signature, signatory } = req.body
|
||||
if( !message || !signatory || !signature ) throw new Error( `Malformed request` )
|
||||
|
||||
// Decode message
|
||||
const confirmedSignatory = web3.eth.accounts.recover( message, signature )
|
||||
if( signatory.toLowerCase() !== confirmedSignatory.toLowerCase() ) throw new Error( `Bad signature` )
|
||||
|
||||
// Validate message
|
||||
const messageObject = JSON.parse( message )
|
||||
const { signer, tokenId, validator, chainId } = messageObject
|
||||
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !tokenId || !validator || chainId !== chain ) throw new Error( `Invalid message` )
|
||||
|
||||
// Check if validator was already assigned
|
||||
const validatorProfile = await db.collection( `${ chain === '0x1' ? 'mainnet' : 'rinkeby' }Validators` ).doc( validator ).get().then( dataFromSnap )
|
||||
if( validatorProfile && validatorProfile.owner !== signatory ) throw new Error( `Validator already claimed by another wallet. If this is in error, contact mentor.eth on Discord.\n\nThe reason someone else can claim your validator is that we don't want to you to have to expose your validator private key to the world for security reasons <3` )
|
||||
|
||||
// Write new data to db
|
||||
await db.collection( `${ chain === '0x1' ? 'mainnet' : 'rinkeby' }Validators` ).doc( validator ).set( {
|
||||
tokenId,
|
||||
owner: signatory,
|
||||
src: `https://storage.googleapis.com/rocketeer-nft.appspot.com/${ chain === '0x1' ? 'mainnet' : 'rinkeby' }Rocketeers/${ tokenId }.jpg`,
|
||||
updated: Date.now()
|
||||
} )
|
||||
|
||||
// Update the static overview JSON
|
||||
const storage = getStorage()
|
||||
const bucket = storage.bucket()
|
||||
const cacheFile = bucket.file( `integrations/${ chain === '0x1' ? 'mainnet' : 'rinkeby' }Avatars.json` )
|
||||
|
||||
// Load existing json
|
||||
let jsonstring = '{}'
|
||||
const [ fileExists ] = await cacheFile.exists()
|
||||
if( fileExists ) {
|
||||
// Read old json
|
||||
const [ oldJson ] = await cacheFile.download()
|
||||
jsonstring = oldJson
|
||||
}
|
||||
const cachedJson = JSON.parse( jsonstring )
|
||||
|
||||
// Get items that have not been updated
|
||||
const tenSecondsAgo = Date.now() - ( 10 * 1000 )
|
||||
const shouldBeUpdated = await db.collection( `${ chain === '0x1' ? 'mainnet' : 'rinkeby' }Validators` ).where( 'updated', '>', cachedJson.updated || tenSecondsAgo ).get().then( dataFromSnap )
|
||||
|
||||
// Update items that should be updated ( including current update )
|
||||
shouldBeUpdated.map( doc => {
|
||||
if( !cachedJson.images ) cachedJson.images = {}
|
||||
if( !cachedJson.ids ) cachedJson.ids = {}
|
||||
cachedJson.images[ doc.uid ] = doc.src
|
||||
cachedJson.ids[ doc.uid ] = doc.tokenId
|
||||
} )
|
||||
|
||||
// Save new data to file
|
||||
cachedJson.updated = Date.now()
|
||||
cachedJson.trail = shouldBeUpdated.length
|
||||
await cacheFile.save( JSON.stringify( cachedJson ) )
|
||||
await cacheFile.makePublic()
|
||||
|
||||
console.log( 'New data: ', cachedJson )
|
||||
|
||||
return res.send( {
|
||||
success: true,
|
||||
url: cacheFile.publicUrl()
|
||||
} )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( 'avatar integration error: ', e )
|
||||
return res.send( {
|
||||
error: e.message
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -31,7 +31,7 @@ const ABI = [
|
||||
async function getTotalSupply( network='mainnet' ) {
|
||||
|
||||
// Initialise contract connection
|
||||
const web3 = new Web3( `wss://${ network }.infura.io/ws/v3/${ infura.projectid }` )
|
||||
const web3 = new Web3( `wss://${ network }.infura.io/ws/v3/${ infura.projectid }` )
|
||||
const contract = new web3.eth.Contract( ABI, contractAddress[ network ] )
|
||||
|
||||
// Return the call promise which returns the total supply
|
||||
|
@ -6,9 +6,21 @@ const { getFirestore, FieldValue, FieldPath } = require( 'firebase-admin/firesto
|
||||
const app = initializeApp()
|
||||
const db = getFirestore()
|
||||
|
||||
const dataFromSnap = ( snapOfDocOrDocs, withDocId=true ) => {
|
||||
|
||||
// If these are multiple docs
|
||||
if( snapOfDocOrDocs.docs ) return snapOfDocOrDocs.docs.map( doc => ( { uid: doc.id, ...doc.data( ) } ) )
|
||||
|
||||
// If this is a single document
|
||||
return { ...snapOfDocOrDocs.data(), ...( withDocId && { uid: snapOfDocOrDocs.id } ) }
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
app: app,
|
||||
db: db,
|
||||
FieldValue: FieldValue,
|
||||
FieldPath: FieldPath
|
||||
FieldPath: FieldPath,
|
||||
dataFromSnap: dataFromSnap
|
||||
}
|
||||
|
1636
functions/package-lock.json
generated
1636
functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,8 @@
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"serve": "firebase emulators:start --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"shell": "NODE_ENV=development firebase functions:shell",
|
||||
"start": "NODE_ENV=development npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
|
24
minter/package-lock.json
generated
24
minter/package-lock.json
generated
@ -8086,6 +8086,14 @@
|
||||
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
||||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
||||
},
|
||||
"history": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz",
|
||||
"integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
@ -13498,6 +13506,22 @@
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
||||
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.0.tgz",
|
||||
"integrity": "sha512-FcTRCihYZvERMNbG54D9+Wkv2cj/OtoxNlA/87D7vxKYlmSmbF9J9XChI9Is44j/behEiOhbovgVZBhKQn+wgA==",
|
||||
"requires": {
|
||||
"history": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0.tgz",
|
||||
"integrity": "sha512-bPXyYipf0zu6K7mHSEmNO5YqLKq2q9N+Dsahw9Xh3oq1IirsI3vbnIYcVWin6A0zWyHmKhMGoV7Gr0j0kcuVFg==",
|
||||
"requires": {
|
||||
"react-router": "6.0.0"
|
||||
}
|
||||
},
|
||||
"react-scripts": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz",
|
||||
|
@ -9,6 +9,7 @@
|
||||
"ethers": "^5.4.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.0.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"use-interval": "^1.4.0",
|
||||
"web-vitals": "^1.1.2",
|
||||
|
@ -9,10 +9,11 @@ main {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body * {
|
||||
body, body * {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
@ -33,6 +34,15 @@ div.container {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
div.container.wide {
|
||||
width: 1024px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*Login button*/
|
||||
a.button {
|
||||
@ -114,3 +124,26 @@ h1, p, label {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ///////////////////////////////
|
||||
/// Avatar page
|
||||
// /////////////////////////////*/
|
||||
#avatar .rocketeer {
|
||||
margin: 1rem 0;
|
||||
border-radius: 50%;
|
||||
padding: 1rem;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#avatar input {
|
||||
padding: 1rem;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
#avatar p {
|
||||
margin: 3rem 0;
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import Minter from './components/minter'
|
||||
import Metamask from './components/metamask'
|
||||
import Verifier from './components/verifier'
|
||||
import Fox from './assets/metamask-fox.svg'
|
||||
import Avatar from './components/avatar'
|
||||
import { Container } from './components/generic'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { log } from './modules/helpers'
|
||||
import { useAddress, getAddress } from './modules/web3'
|
||||
import { HashRouter, Routes, Route } from 'react-router-dom'
|
||||
|
||||
|
||||
function App() {
|
||||
@ -12,88 +14,38 @@ function App() {
|
||||
// ///////////////////////////////
|
||||
// States
|
||||
// ///////////////////////////////
|
||||
const [ action, setAction ] = useState( undefined )
|
||||
const [ loading, setLoading ] = useState( 'Detecting metamask...' )
|
||||
const [ error, setError ] = useState( undefined )
|
||||
const address = useAddress()
|
||||
|
||||
|
||||
// ///////////////////////////////
|
||||
// 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 ) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
|
||||
setLoading( 'Connecting to Metamask' )
|
||||
const address = await getAddress()
|
||||
log( 'Received: ', address )
|
||||
|
||||
} catch( e ) {
|
||||
setError( `Metamask error: ${ e.message || JSON.stringify( e ) }. Please reload the page.` )
|
||||
} finally {
|
||||
setLoading( false )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
|
||||
// Check for metamask on load
|
||||
// Check for web3 on load
|
||||
useEffect( f => window.ethereum ? setLoading( false ) : setError( 'No web3 provider detected, please install metamask' ), [] )
|
||||
|
||||
|
||||
// Check for action on load
|
||||
useEffect( f => {
|
||||
checkAction()
|
||||
return window.addEventListener( 'popstate', checkAction )
|
||||
}, [ address, loading ] )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rendering
|
||||
// ///////////////////////////////
|
||||
log( 'Rendering with ', action, error, loading, address )
|
||||
// Initialisation interface
|
||||
if( error || loading || !address ) return <Container>
|
||||
{ error && <p>{ error }</p> }
|
||||
{ !error && loading && <div className="loading">
|
||||
|
||||
<div className="lds-dual-ring"></div>
|
||||
<p>{ loading }</p>
|
||||
|
||||
</div> }
|
||||
{ !address && ( !error && !loading ) && <>
|
||||
|
||||
<h1>Rocketeer { action === 'mint' ? 'Minter' : 'Verifier' }</h1>
|
||||
{ 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 verify that you are the owner of a Rocketeer</p> }
|
||||
<a className="button" href="/#" onClick={ metamasklogin }>
|
||||
<img alt="metamask fox" src={ Fox } />
|
||||
Connect wallet
|
||||
</a>
|
||||
|
||||
</> }
|
||||
if( error || loading ) return <Container>
|
||||
<p>{ error || loading }</p>
|
||||
</Container>
|
||||
return <HashRouter>
|
||||
|
||||
<Routes>
|
||||
|
||||
<Route exact path='/' element={ <Metamask /> } />
|
||||
<Route exact path='/mint' element={ <Minter /> } />
|
||||
<Route path='/verify/' element={ <Verifier /> }>
|
||||
<Route path='/verify/:verificationCode' element={ <Verifier /> } />
|
||||
</Route>
|
||||
<Route exact path='/avatar' element={ <Avatar /> } />
|
||||
|
||||
</Routes>
|
||||
|
||||
</HashRouter>
|
||||
|
||||
if( action === 'mint' ) return <Minter />
|
||||
if( action === 'verify' ) return <Verifier />
|
||||
else return <></>
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
94
minter/src/components/avatar.js
Normal file
94
minter/src/components/avatar.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { Container, Loading } from './generic'
|
||||
import '../App.css'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { log } from '../modules/helpers'
|
||||
import { useRocketeerImages, callApi } from '../modules/api'
|
||||
import { useAddress, useChainId, useBalanceOf, useTokenIds, sign } from '../modules/web3'
|
||||
|
||||
|
||||
export default function Verifier() {
|
||||
|
||||
// ///////////////////////////////
|
||||
// State management
|
||||
// ///////////////////////////////
|
||||
const balance = useBalanceOf()
|
||||
const chainId = useChainId()
|
||||
const address = useAddress()
|
||||
const [ loading, setLoading ] = useState( )
|
||||
const metamaskAddress = useAddress()
|
||||
const [ validatorAddress, setValidatorAddress ] = useState( )
|
||||
const rocketeers = useRocketeerImages()
|
||||
|
||||
// ///////////////////////////////
|
||||
// Functions
|
||||
// ///////////////////////////////
|
||||
async function attribute( id ) {
|
||||
|
||||
try {
|
||||
|
||||
const confirmed = window.confirm( `This will assign Rocketeer ${ id } to ${ validatorAddress }.\n\nMetamask will ask you to sign a message, this is NOT A TRANSACTION.` )
|
||||
if( !confirmed ) throw new Error( `Operation cancelled` )
|
||||
|
||||
setLoading( 'Signing verification message' )
|
||||
|
||||
const signature = await sign( JSON.stringify( {
|
||||
signer: address.toLowerCase(),
|
||||
tokenId: id,
|
||||
validator: validatorAddress.toLowerCase(),
|
||||
chainId
|
||||
} ), address )
|
||||
|
||||
log( 'Making request with ', signature )
|
||||
|
||||
setLoading( 'Updating profile' )
|
||||
|
||||
const { error, success } = await callApi( `/integrations/avatar`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( signature )
|
||||
} )
|
||||
|
||||
if( error ) throw new Error( error )
|
||||
|
||||
|
||||
} catch( e ) {
|
||||
alert( e.message )
|
||||
log( e )
|
||||
} finally {
|
||||
setLoading( false )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
useEffect( f => {
|
||||
if( !validatorAddress && metamaskAddress ) setValidatorAddress( metamaskAddress )
|
||||
}, [ metamaskAddress, validatorAddress ] )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rendering
|
||||
// ///////////////////////////////
|
||||
if( loading ) return <Loading message={ loading } />
|
||||
return <Container id="avatar" className={ rocketeers.length > 1 ? 'wide' : '' }>
|
||||
|
||||
<h1>Rocketeer avatar attribution</h1>
|
||||
|
||||
<p>Input the address you want to assign the avatar to.</p>
|
||||
<input type='text' value={ validatorAddress } />
|
||||
|
||||
<p>Click the Rocketeer you want to assign to this address.</p>
|
||||
<div className="row">
|
||||
|
||||
{ rocketeers.map( ( { id, src } ) => {
|
||||
|
||||
return <img key={ id } onClick={ f => attribute( id ) } className='rocketeer' src={ src } alt={ `Rocketeer number ${ id }` } />
|
||||
|
||||
} ) }
|
||||
|
||||
</div>
|
||||
|
||||
</Container>
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import LaunchBackground from '../assets/undraw_launch_day_4e04.svg'
|
||||
|
||||
export const Container = ( { children } ) => <main>
|
||||
export const Container = ( { className, children, ...props } ) => <main { ...props }>
|
||||
|
||||
<div className="container">
|
||||
<div className={ `container ${ className }` }>
|
||||
|
||||
{ children }
|
||||
|
||||
@ -10,4 +10,13 @@ export const Container = ( { children } ) => <main>
|
||||
|
||||
<img className="stretchBackground" src={ LaunchBackground } alt="Launching rocket" />
|
||||
|
||||
</main>
|
||||
</main>
|
||||
|
||||
export const Loading = ( { message } ) => <Container>
|
||||
<div className="loading">
|
||||
|
||||
<div className="lds-dual-ring"></div>
|
||||
<p>{ message }</p>
|
||||
|
||||
</div>
|
||||
</Container>
|
91
minter/src/components/metamask.js
Normal file
91
minter/src/components/metamask.js
Normal file
@ -0,0 +1,91 @@
|
||||
import Fox from '../assets/metamask-fox.svg'
|
||||
import { Container, Loading } from './generic'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { log } from '../modules/helpers'
|
||||
import { useAddress, getAddress } from '../modules/web3'
|
||||
import { useNavigate, Navigate, Link } from 'react-router-dom'
|
||||
|
||||
|
||||
// ///////////////////////////////
|
||||
// Render component
|
||||
// ///////////////////////////////
|
||||
export default function ComponentName( ) {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
// ///////////////////////////////
|
||||
// States
|
||||
// ///////////////////////////////
|
||||
const [ loading, setLoading ] = useState( 'Detecting metamask...' )
|
||||
const [ error, setError ] = useState( undefined )
|
||||
const address = useAddress()
|
||||
|
||||
|
||||
// ///////////////////////////////
|
||||
// Functions
|
||||
// ///////////////////////////////
|
||||
|
||||
// Handle user login interaction
|
||||
async function metamasklogin( e ) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
|
||||
setLoading( 'Connecting to Metamask' )
|
||||
const address = await getAddress()
|
||||
log( 'Received: ', address )
|
||||
|
||||
} catch( e ) {
|
||||
setError( `Metamask error: ${ e.message || JSON.stringify( e ) }. Please reload the page.` )
|
||||
} finally {
|
||||
setLoading( false )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
|
||||
// Check for metamask on load
|
||||
useEffect( f => window.ethereum ? setLoading( false ) : setError( 'No web3 provider detected, please install metamask' ), [] )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Render component
|
||||
// ///////////////////////////////
|
||||
|
||||
// Loading component
|
||||
if( loading ) return <Loading message={ loading } />
|
||||
|
||||
// Error interface
|
||||
if( error ) return <Container>
|
||||
<p>{ error }</p>
|
||||
</Container>
|
||||
|
||||
// Actions menu
|
||||
if( address ) return <Container>
|
||||
|
||||
<h1>Rocketeer Interface</h1>
|
||||
|
||||
<div>
|
||||
<Link className='button' to='/mint'>Mint Rocketeer</Link>
|
||||
<Link className='button' to='/verify'>Discord verify</Link>
|
||||
<Link className='button' to='/avatar'>Set address avatar</Link>
|
||||
</div>
|
||||
|
||||
|
||||
</Container>
|
||||
|
||||
// Login interface
|
||||
return <Container>
|
||||
|
||||
<h1>Rocketeer Interface</h1>
|
||||
<a className="button" href="/#" onClick={ metamasklogin }>
|
||||
<img alt="metamask fox" src={ Fox } />
|
||||
Connect wallet
|
||||
</a>
|
||||
|
||||
</Container>
|
||||
|
||||
}
|
@ -4,6 +4,7 @@ import '../App.css'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { log } from '../modules/helpers'
|
||||
import { useAddress, useChainId, useBalanceOf } from '../modules/web3'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export default function Verifier() {
|
||||
|
||||
@ -17,6 +18,7 @@ export default function Verifier() {
|
||||
const [ verifyUrl, setVerifyUrl ] = useState()
|
||||
const [ message, setMessage ] = useState()
|
||||
const [ error, setError ] = useState( )
|
||||
const { verificationCode } = useParams()
|
||||
|
||||
// ///////////////////////////////
|
||||
// Functions
|
||||
@ -28,27 +30,23 @@ export default function Verifier() {
|
||||
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 baseUrl = `https://mint.rocketeer.fans/#/verify/`
|
||||
const message = btoa( `{ "username": "${ username }", "address": "${ address }", "balance": "${ balance }" }` )
|
||||
|
||||
setVerifyUrl( baseUrl + `&message=${ message }` )
|
||||
setVerifyUrl( baseUrl + message )
|
||||
|
||||
}
|
||||
|
||||
function verifyIfNeeded() {
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
useEffect( f => {
|
||||
|
||||
log( 'Check the need to verify' )
|
||||
|
||||
if( !window.location.href.includes( 'message' ) ) return
|
||||
if( !verificationCode ) return
|
||||
|
||||
try {
|
||||
|
||||
log( 'Verification started' )
|
||||
|
||||
const { search } = window.location
|
||||
const query = new URLSearchParams( search )
|
||||
const message = query.get( 'message' )
|
||||
const verification = atob( message )
|
||||
const verification = atob( verificationCode )
|
||||
log( 'Received message: ', verification )
|
||||
const json = JSON.parse( verification )
|
||||
|
||||
@ -63,17 +61,8 @@ export default function Verifier() {
|
||||
return alert( 'Verification error, contact the team on Discord' )
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
useEffect( f => {
|
||||
log( 'Triggering verification check and popstate listener' )
|
||||
setError( false )
|
||||
verifyIfNeeded()
|
||||
return window.addEventListener( 'popstate', verifyIfNeeded )
|
||||
}, [] )
|
||||
|
||||
}, [ verificationCode ] )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rendering
|
||||
|
75
minter/src/modules/api.js
Normal file
75
minter/src/modules/api.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { log } from './helpers'
|
||||
import { useTokenIds } from './web3'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export async function callApi( path, options={} ) {
|
||||
|
||||
const api = {
|
||||
mainnet: 'https://rocketeer.fans/api',
|
||||
testnet: 'https://rocketeer.fans/testnetapi'
|
||||
}
|
||||
|
||||
const querySaysTestnet = window.location.href.includes( 'testnet' )
|
||||
const isLocal = window.location.hostname === 'localhost'
|
||||
const chain = ( isLocal || querySaysTestnet ) ? 'testnet' : 'mainnet'
|
||||
const callPath = api[ chain ] + path
|
||||
|
||||
log( 'Calling ', callPath )
|
||||
return fetch( `${ api[ chain ] }${ path }`, options ).then( res => res.json() )
|
||||
|
||||
}
|
||||
|
||||
export function getImage( id, ext='jpg' ) {
|
||||
|
||||
const api = {
|
||||
mainnet: 'https://storage.googleapis.com/rocketeer-nft.appspot.com/mainnetRocketeers/',
|
||||
testnet: 'https://storage.googleapis.com/rocketeer-nft.appspot.com/rinkebyRocketeers'
|
||||
}
|
||||
|
||||
const querySaysTestnet = window.location.href.includes( 'testnet' )
|
||||
const isLocal = window.location.hostname === 'localhost'
|
||||
const chain = ( isLocal || querySaysTestnet ) ? 'testnet' : 'mainnet'
|
||||
|
||||
return api[ chain ] + `/${ id }.${ext}`
|
||||
|
||||
}
|
||||
|
||||
export function useRocketeers() {
|
||||
|
||||
const ids = useTokenIds()
|
||||
const [ rocketeers, setRocketeers ] = useState( [] )
|
||||
|
||||
useEffect( f => {
|
||||
|
||||
( async function() {
|
||||
|
||||
const rocketeerMetas = await Promise.all( ids.map( id => callApi( `/rocketeer/${ id }` ) ) )
|
||||
log( 'Received rocketeers: ', rocketeerMetas )
|
||||
setRocketeers( rocketeerMetas )
|
||||
|
||||
} )( )
|
||||
|
||||
}, [ ids ] )
|
||||
|
||||
return rocketeers
|
||||
|
||||
}
|
||||
|
||||
export function useRocketeerImages() {
|
||||
|
||||
const ids = useTokenIds()
|
||||
const [ images, setImages ] = useState( [] )
|
||||
|
||||
useEffect( f => {
|
||||
|
||||
setImages( ids.map( id => ( {
|
||||
id,
|
||||
src: getImage( id )
|
||||
} ) ) )
|
||||
|
||||
}, [ ids ] )
|
||||
|
||||
return images
|
||||
|
||||
}
|
||||
|
@ -6,10 +6,27 @@ import { log, setListenerAndReturnUnlistener } from './helpers'
|
||||
import { ethers } from "ethers"
|
||||
|
||||
// Convenience objects
|
||||
const { providers: { Web3Provider }, Contract } = ethers
|
||||
const { providers: { Web3Provider }, Contract, utils: { verifyMessage } } = ethers
|
||||
export const provider = window.ethereum && new Web3Provider( window.ethereum )
|
||||
export const signer = provider && provider.getSigner()
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Wallet interactors
|
||||
// /////////////////////////////*/
|
||||
export async function sign( message, signatory ) {
|
||||
|
||||
const signature = await signer.signMessage( message )
|
||||
log( `Signed ${ message } to ${ signature }` )
|
||||
const verifiedSignatory = verifyMessage( message, signature ).toLowerCase()
|
||||
log( `Message was signed by ${ verifiedSignatory }. Signature validity: `, signatory === verifiedSignatory )
|
||||
return {
|
||||
message,
|
||||
signature,
|
||||
signatory
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Chain interactors
|
||||
// ///////////////////////////////
|
||||
@ -38,7 +55,10 @@ export function useAddress() {
|
||||
|
||||
setTimesChecked( timesChecked+1 )
|
||||
log( 'Checking for address' )
|
||||
if( window.ethereum && window.ethereum.selectedAddress ) return setAddress( window.ethereum.selectedAddress )
|
||||
if( window.ethereum && window.ethereum.selectedAddress ) {
|
||||
setAddress( window.ethereum.selectedAddress )
|
||||
return setInterval( null )
|
||||
}
|
||||
|
||||
// if checked five times and interval still running, slow it down
|
||||
if( timesChecked > 5 && !!interval ) setInterval( 5000 )
|
||||
@ -73,6 +93,7 @@ export function useAddress() {
|
||||
|
||||
}
|
||||
|
||||
// Totl supply hook
|
||||
export function useTotalSupply() {
|
||||
|
||||
const [ supply, setSupply ] = useState( 'loading' )
|
||||
@ -124,10 +145,11 @@ export function useTotalSupply() {
|
||||
|
||||
}
|
||||
|
||||
// Balance hook
|
||||
export function useBalanceOf() {
|
||||
|
||||
const [ balance, setBalance ] = useState( 'loading' )
|
||||
const contract = useContract( )
|
||||
const contract = useContract()
|
||||
const address = useAddress()
|
||||
|
||||
// Create listener to minting
|
||||
@ -147,7 +169,7 @@ export function useBalanceOf() {
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
log( 'Error getting initial supply: ', e )
|
||||
log( 'Error getting balance: ', e )
|
||||
|
||||
}
|
||||
|
||||
@ -190,6 +212,50 @@ export function useChainId() {
|
||||
|
||||
}
|
||||
|
||||
// Token ids of owner hook
|
||||
export function useTokenIds() {
|
||||
|
||||
// Deps
|
||||
const address = useAddress()
|
||||
const contract = useContract()
|
||||
const balance = useBalanceOf()
|
||||
|
||||
// State
|
||||
const [ tokens, setTokens ] = useState( [] )
|
||||
|
||||
// Grab tokens from contract
|
||||
useEffect( f => {
|
||||
|
||||
// Do nothing if there is no data yet
|
||||
if( !contract || !balance || !address ) return
|
||||
|
||||
// Load initial supply value
|
||||
( async ( ) => {
|
||||
|
||||
try {
|
||||
|
||||
const ids = await Promise.all( Array.from( { length: balance } ).map( async ( val, index ) => {
|
||||
const id = await contract.tokenOfOwnerByIndex( address, index )
|
||||
return id.toString()
|
||||
} ) )
|
||||
log( 'Tokens detected: ', ids )
|
||||
setTokens( ids )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
log( 'Error getting tokens of address: ', e )
|
||||
|
||||
}
|
||||
|
||||
} )( )
|
||||
|
||||
|
||||
}, [ contract, address, balance ] )
|
||||
|
||||
return tokens
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Contract interactors
|
||||
// ///////////////////////////////
|
||||
@ -272,6 +338,31 @@ const ABI = [
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "index",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "tokenOfOwnerByIndex",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
"constant": true
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -4,10 +4,11 @@
|
||||
// Globals
|
||||
// ////////////////////////////// */
|
||||
|
||||
body * {
|
||||
body, body * {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -85,7 +85,9 @@ function App() {
|
||||
|
||||
<a href={ rocketeer.image } className="button">Download Jpeg</a>
|
||||
|
||||
<a href='/#' onClick={ rocketeer?.image?.replace( 'jpg', 'svg' ) } className="button">Download Svg</a>
|
||||
<a href={ rocketeer?.image?.replace( 'jpg', 'svg' ) } className="button">Download Svg</a>
|
||||
|
||||
<a rel="noreferrer" target="_blank" href={ `https://opensea.io/assets/0xb3767b2033cf24334095dc82029dbf0e9528039d/${ rocketeerId }` } className="button">View on Opensea</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user