mirror of
https://github.com/stronk-dev/RandomChad.git
synced 2026-04-20 16:45:10 +02:00
Backend v1
This commit is contained in:
parent
a4b960c2d2
commit
66b2598cb9
@ -40,6 +40,9 @@ app.get( '/api/rocketeer/:id', async ( req, res ) => {
|
|||||||
|
|
||||||
} )
|
} )
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// VGR's dashboard integration
|
||||||
|
// /////////////////////////////*/
|
||||||
app.post( '/api/integrations/avatar/', setAvatar )
|
app.post( '/api/integrations/avatar/', setAvatar )
|
||||||
app.delete( '/api/integrations/avatar/', resetAvatar )
|
app.delete( '/api/integrations/avatar/', resetAvatar )
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const app = require( './express' )()
|
const app = require( './express' )()
|
||||||
const { getTotalSupply } = require( '../modules/contract' )
|
const { getTotalSupply } = require( '../modules/contract' )
|
||||||
const { safelyReturnRocketeer, web2domain } = require( '../nft-media/rocketeer' )
|
const { safelyReturnRocketeer, web2domain } = require( '../nft-media/rocketeer' )
|
||||||
|
const { generateNewOutfit, setPrimaryOutfit } = require( '../integrations/changingroom' )
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Specific Rocketeer instances
|
// Specific Rocketeer instances
|
||||||
@ -38,6 +39,11 @@ app.get( '/testnetapi/rocketeer/:id', async ( req, res ) => {
|
|||||||
|
|
||||||
} )
|
} )
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// Changing room endpoints
|
||||||
|
// /////////////////////////////*/
|
||||||
|
app.post( '/testnetapi/rocketeer/:id/outfits', generateNewOutfit )
|
||||||
|
app.put( '/testnetapi/rocketeer/:id/outfits', setPrimaryOutfit )
|
||||||
|
|
||||||
// Collection data
|
// Collection data
|
||||||
app.get( '/testnetapi/collection', async ( req, res ) => res.json( {
|
app.get( '/testnetapi/collection', async ( req, res ) => res.json( {
|
||||||
|
|||||||
2249
functions/firebase-debug.log
Normal file
2249
functions/firebase-debug.log
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
const functions = require( 'firebase-functions' )
|
const functions = require( 'firebase-functions' )
|
||||||
const { integration }= functions.config()
|
const { integration } = functions.config()
|
||||||
const { db, dataFromSnap } = require( '../modules/firebase' )
|
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||||
const Web3 = require( 'web3' )
|
const Web3 = require( 'web3' )
|
||||||
const web3 = new Web3()
|
const web3 = new Web3()
|
||||||
|
|||||||
131
functions/integrations/changingroom.js
Normal file
131
functions/integrations/changingroom.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
const { generateNewOutfitFromId } = require( '../nft-media/changing-room' )
|
||||||
|
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||||
|
const Web3 = require( 'web3' )
|
||||||
|
const web3 = new Web3()
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// POST handler for new avatars
|
||||||
|
// /////////////////////////////*/
|
||||||
|
exports.generateNewOutfit = async function( req, res ) {
|
||||||
|
|
||||||
|
// Parse the request
|
||||||
|
let { id } = req.params
|
||||||
|
if( !id ) return res.json( { error: `No ID specified in URL` } )
|
||||||
|
|
||||||
|
// Protect against malformed input
|
||||||
|
id = Math.floor( Math.abs( id ) )
|
||||||
|
if( typeof id !== 'number' ) return res.json( { error: `Malformed request` } )
|
||||||
|
|
||||||
|
// Set ID to string so firestore can handle it
|
||||||
|
id = `${ id }`
|
||||||
|
|
||||||
|
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, rocketeerId, chainId } = messageObject
|
||||||
|
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !rocketeerId || chainId !== chain || !network ) throw new Error( `Invalid message` )
|
||||||
|
if( rocketeerId != id ) throw new Error( `Invalid Rocketeer in message` )
|
||||||
|
|
||||||
|
// Set chain based on envronnment
|
||||||
|
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
|
||||||
|
const network = chain == '0x1' ? 'mainnet' : 'rinkeby'
|
||||||
|
|
||||||
|
// Generate new rocketeer svg
|
||||||
|
const mediaLink = await generateNewOutfitFromId( id, network )
|
||||||
|
|
||||||
|
return res.json( {
|
||||||
|
outfit: mediaLink
|
||||||
|
} )
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
|
||||||
|
// Log error for debugging
|
||||||
|
console.error( `POST Changing room api error for ${ id }: `, e )
|
||||||
|
|
||||||
|
// Return error to frontend
|
||||||
|
return res.json( { error: e.mesage || e.toString() } )
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// PUT handler for changing the
|
||||||
|
// current outfit
|
||||||
|
// /////////////////////////////*/
|
||||||
|
exports.setPrimaryOutfit = async function( req, res ) {
|
||||||
|
|
||||||
|
// Parse the request
|
||||||
|
let { id } = req.params
|
||||||
|
if( !id ) return res.json( { error: `No ID specified in URL` } )
|
||||||
|
|
||||||
|
// Protect against malformed input
|
||||||
|
id = Math.floor( Math.abs( id ) )
|
||||||
|
if( typeof id !== 'number' ) return res.json( { error: `Malformed request` } )
|
||||||
|
|
||||||
|
// Set ID to string so firestore can handle it
|
||||||
|
id = `${ id }`
|
||||||
|
|
||||||
|
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 )
|
||||||
|
let { signer, outfitId, chainId } = messageObject
|
||||||
|
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !outfitId || chainId !== chain || !network ) throw new Error( `Invalid message` )
|
||||||
|
|
||||||
|
// Validate id format
|
||||||
|
outfitId = Math.floor( Math.abs( outfitId ) )
|
||||||
|
if( typeof outfitId !== 'number' ) return res.json( { error: `Malformed request` } )
|
||||||
|
|
||||||
|
// Set ID to string so firestore can handle it
|
||||||
|
outfitId = `${ outfitId }`
|
||||||
|
|
||||||
|
// Set chain based on envronnment
|
||||||
|
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
|
||||||
|
const network = chain == '0x1' ? 'mainnet' : 'rinkeby'
|
||||||
|
|
||||||
|
// Retreive old Rocketeer data
|
||||||
|
const rocketeer = await db.collection( `${ network }Rocketeers` ).doc( id ).get().then( dataFromSnap )
|
||||||
|
|
||||||
|
// Grab attributes that will not change
|
||||||
|
const { value: available_outfits } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "available outfits" ) || { value: 0 }
|
||||||
|
|
||||||
|
// Only allow to set existing outfits
|
||||||
|
if( available_outfits < outfitId ) throw new Error( `Your Rocketeer has ${ available_outfits }, you can't select outfit ${ outfitId }` )
|
||||||
|
|
||||||
|
// Change the primary media file
|
||||||
|
const imagePath = `${ outfitId == 0 ? id : `${ id }-${ outfitId }` }.jpg`
|
||||||
|
await db.collection( `${ network }Rocketeers` ).doc( id ).set( {
|
||||||
|
image: `https://storage.googleapis.com/rocketeer-nft.appspot.com/${ network }Rocketeers/${ imagePath }`
|
||||||
|
}, { merge: true } )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
|
||||||
|
// Log error for debugging
|
||||||
|
console.error( `PUT Changing room api error for ${ id }: `, e )
|
||||||
|
|
||||||
|
// Return error to frontend
|
||||||
|
return res.json( { error: e.mesage || e.toString() } )
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -43,6 +43,7 @@ exports.pickRandomAttributes = ( attributes ) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nameColor = require('color-namer')
|
const nameColor = require('color-namer')
|
||||||
|
const Color = require('color')
|
||||||
exports.getColorName = ( rgb ) => {
|
exports.getColorName = ( rgb ) => {
|
||||||
try {
|
try {
|
||||||
return nameColor( rgb ).basic[0].name
|
return nameColor( rgb ).basic[0].name
|
||||||
@ -50,6 +51,13 @@ exports.getColorName = ( rgb ) => {
|
|||||||
return rgb
|
return rgb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
exports.getRgbArrayFromColorName = name => {
|
||||||
|
|
||||||
|
const { hex } = nameColor( name ).basic[0]
|
||||||
|
const color = Color( hex )
|
||||||
|
return color.rgb().array()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// ///////////////////////////////
|
// ///////////////////////////////
|
||||||
// Attribute sources
|
// Attribute sources
|
||||||
|
|||||||
88
functions/nft-media/changing-room.js
Normal file
88
functions/nft-media/changing-room.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||||
|
const { getRgbArrayFromColorName, randomNumberBetween } = require( '../modules/helpers' )
|
||||||
|
const svgFromAttributes = require( './svg-generator' )
|
||||||
|
|
||||||
|
// ///////////////////////////////
|
||||||
|
// Rocketeer generator
|
||||||
|
// ///////////////////////////////
|
||||||
|
exports.generateNewOutfitFromId = async function( id, network='mainnet' ) {
|
||||||
|
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// Changing room variables
|
||||||
|
// /////////////////////////////*/
|
||||||
|
// Set the entropy level. 255 would mean 0 can become 255 and -255
|
||||||
|
let colorEntropy = 20
|
||||||
|
const newOutfitAllowedInterval = 1000 * 60 * 60 * 24 * 30
|
||||||
|
const specialEditionMultiplier = 1.1
|
||||||
|
|
||||||
|
// Retreive old Rocketeer data
|
||||||
|
const rocketeer = await db.collection( `${ network }Rocketeers` ).doc( id ).get().then( dataFromSnap )
|
||||||
|
|
||||||
|
// Apply special properties
|
||||||
|
const { value: edition } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "edition" )
|
||||||
|
if( edition != 'regular' ) colorEntropy *= specialEditionMultiplier
|
||||||
|
|
||||||
|
// Validate this request
|
||||||
|
const { value: available_outfits } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "available outfits" ) || { value: 0 }
|
||||||
|
const { value: last_outfit_change } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "last outfit change" ) || { value: 0 }
|
||||||
|
|
||||||
|
// Check whether this Rocketeer is allowed to change
|
||||||
|
const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change )
|
||||||
|
if( timeUntilAllowedToChange > 0 ) throw new Error( `You changed your outfit too recently, a change is avalable in ${ Math.floor( timeUntilAllowedToChange / ( 1000 * 60 * 60 ) ) } hours` )
|
||||||
|
|
||||||
|
// Grab attributes that will not change
|
||||||
|
const staticAttributes = rocketeer.attributes.filter( ( { trait_type } ) => ![ 'last outfit change', 'available outfits' ].includes( trait_type ) )
|
||||||
|
|
||||||
|
// Mark this Rocketeer as outfit changed so other requests can't clash with this one
|
||||||
|
await db.collection( `${ network }Rocketeers` ).doc( id ).set( {
|
||||||
|
attributes: [
|
||||||
|
...staticAttributes,
|
||||||
|
{ trait_type: 'available outfits', value: available_outfits + 1, },
|
||||||
|
{ trait_type: 'last outfit change', value: Date.now(), display_type: "date" }
|
||||||
|
]
|
||||||
|
} ,{ merge: true } )
|
||||||
|
|
||||||
|
// Generate colors with entropy based on color names
|
||||||
|
rocketeer.attributes = rocketeer.attributes.map( attribute => {
|
||||||
|
|
||||||
|
if( !attribute.trait_type.includes( 'color' ) ) return attribute
|
||||||
|
|
||||||
|
// Generate rgb with entropy
|
||||||
|
const rgbArray = getRgbArrayFromColorName( attribute.value )
|
||||||
|
const rgb = rgbArray.map( baseValue => {
|
||||||
|
|
||||||
|
// Choose whether to increment or decrement
|
||||||
|
const increment = !!( Math.random() > .5 )
|
||||||
|
|
||||||
|
// Determine by how much to change the color
|
||||||
|
const entropy = increment ? colorEntropy : ( -1 * colorEntropy )
|
||||||
|
|
||||||
|
// Generate a new value
|
||||||
|
let newValue = randomNumberBetween( baseValue, baseValue + entropy )
|
||||||
|
|
||||||
|
// If the color if out of bounds, cycle it into the 255 range
|
||||||
|
if( newValue > 255 ) newValue -= 255
|
||||||
|
if( newValue < 0 ) newValue = Math.abs( newValue )
|
||||||
|
|
||||||
|
// Return the new rgb value
|
||||||
|
return newValue
|
||||||
|
|
||||||
|
} )
|
||||||
|
|
||||||
|
const [ r, g, b ] = rgb
|
||||||
|
|
||||||
|
return {
|
||||||
|
...attribute,
|
||||||
|
value: `rgb( ${ r }, ${ g }, ${ b } )`
|
||||||
|
}
|
||||||
|
|
||||||
|
} )
|
||||||
|
|
||||||
|
// Generate, compile and upload image
|
||||||
|
// Path format of new rocketeers is id-outfitnumber.{svg,jpg}
|
||||||
|
const newOutfitSvg = await svgFromAttributes( rocketeer.attributes, `${ network }Rocketeers/${ id }-${ available_outfits + 1 }` )
|
||||||
|
|
||||||
|
return newOutfitSvg
|
||||||
|
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ const { getStorage } = require( 'firebase-admin/storage' )
|
|||||||
const { convert } = require("convert-svg-to-jpeg")
|
const { convert } = require("convert-svg-to-jpeg")
|
||||||
|
|
||||||
// Existing file checker
|
// Existing file checker
|
||||||
const checkIfFilesExist = async ( svg, jpeg, path ) => {
|
const failIfFilesExist = async ( svg, jpeg, path ) => {
|
||||||
|
|
||||||
const [ [ svgExists ], [ jpegExists ] ] = await Promise.all( [ svg.exists(), jpeg.exists() ] )
|
const [ [ svgExists ], [ jpegExists ] ] = await Promise.all( [ svg.exists(), jpeg.exists() ] )
|
||||||
if( svgExists || jpegExists ) throw new Error( `${ svgExists ? 'SVG' : '' } ${ jpegExists ? ' and JPEG' : '' } already present at ${ path }. This should never happen!` )
|
if( svgExists || jpegExists ) throw new Error( `${ svgExists ? 'SVG' : '' } ${ jpegExists ? ' and JPEG' : '' } already present at ${ path }. This should never happen!` )
|
||||||
@ -27,7 +27,7 @@ module.exports = async function svgFromAttributes( attributes=[], path='' ) {
|
|||||||
const bucket = storage.bucket()
|
const bucket = storage.bucket()
|
||||||
const svgFile = bucket.file( `${path}.svg` )
|
const svgFile = bucket.file( `${path}.svg` )
|
||||||
const rasterFile = bucket.file( `${path}.jpg` )
|
const rasterFile = bucket.file( `${path}.jpg` )
|
||||||
await checkIfFilesExist( svgFile, rasterFile, path )
|
await failIfFilesExist( svgFile, rasterFile, path )
|
||||||
|
|
||||||
// Get properties
|
// Get properties
|
||||||
const { value: primary_color } = attributes.find( ( { trait_type } ) => trait_type == "outfit color" )
|
const { value: primary_color } = attributes.find( ( { trait_type } ) => trait_type == "outfit color" )
|
||||||
@ -141,7 +141,7 @@ module.exports = async function svgFromAttributes( attributes=[], path='' ) {
|
|||||||
const bakedRaster = await convert( bakedSvg, { } )
|
const bakedRaster = await convert( bakedSvg, { } )
|
||||||
|
|
||||||
// Double check that files do not yet exist (in case of weird race condition)
|
// Double check that files do not yet exist (in case of weird race condition)
|
||||||
await checkIfFilesExist( svgFile, rasterFile, path )
|
await failIfFilesExist( svgFile, rasterFile, path )
|
||||||
|
|
||||||
// Save files
|
// Save files
|
||||||
await svgFile.save( bakedSvg )
|
await svgFile.save( bakedSvg )
|
||||||
|
|||||||
1667
functions/package-lock.json
generated
1667
functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"color": "^4.0.2",
|
||||||
"color-namer": "^1.4.0",
|
"color-namer": "^1.4.0",
|
||||||
"convert-svg-to-jpeg": "^0.5.0",
|
"convert-svg-to-jpeg": "^0.5.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user