draft update by address changing room

This commit is contained in:
Mentor Palokaj 2021-12-08 10:17:58 +01:00
parent 30057f43ed
commit 03c217b55b
9 changed files with 244 additions and 130 deletions

View File

@ -1,67 +1,21 @@
const app = require( './express' )() const app = require( './express' )()
const { getTotalSupply } = require( '../modules/contract' ) const { getTotalSupply } = require( '../modules/contract' )
const { safelyReturnRocketeer, web2domain, safelyReturnMultipleRocketeers } = require( '../nft-media/rocketeer' ) const { web2domain } = require( '../nft-media/rocketeer' )
const { setAvatar, resetAvatar } = require( '../integrations/avatar' ) const { setAvatar, resetAvatar } = require( '../integrations/avatar' )
const { generateNewOutfit, setPrimaryOutfit } = require( '../integrations/changingroom' ) const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
// /////////////////////////////// // ///////////////////////////////
// Specific Rocketeer instances // Specific Rocketeer instances
// /////////////////////////////// // ///////////////////////////////
app.get( '/api/rocketeer/:id', async ( req, res ) => { app.get( '/api/rocketeer/:id', async ( req, res ) => rocketeerFromRequest( req, res, 'mainnet' ) )
app.get( '/api/rocketeers/', async ( req, res ) => multipleRocketeersFromRequest( req, res, 'mainnet' ) )
// 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 old rocketeer if it exists
const rocketeer = await safelyReturnRocketeer( id, 'mainnet' )
// Return the new rocketeer
return res.json( rocketeer )
} catch( e ) {
// Log error for debugging
console.error( `Mainnet api error for ${ id }: `, e )
// Return error to frontend
return res.json( { error: e.mesage || e.toString() } )
}
} )
app.get( '/api/rocketeers/', async ( req, res ) => {
try {
// Parse the request
let { ids } = req.query
ids = ids.split( ',' )
if( ids.length > 100 ) throw new Error( 'Please do not ask for so much data at once :)' )
const rocketeers = await safelyReturnMultipleRocketeers( ids, 'testnet' )
return res.json( rocketeers )
} catch( e ) {
return res.json( { error: e.message || e.toString() } )
}
} )
/* /////////////////////////////// /* ///////////////////////////////
// VGR's dashboard integration // VGR's dashboard integration
// /////////////////////////////*/ // /////////////////////////////*/
app.post( '/api/integrations/avatar/', setAvatar ) app.post( '/api/integrations/avatar/', setAvatar )
app.post( '/api/rocketeers/:address', generateMultipleNewOutfits )
app.delete( '/api/integrations/avatar/', resetAvatar ) app.delete( '/api/integrations/avatar/', resetAvatar )
/* /////////////////////////////// /* ///////////////////////////////

View File

@ -1,66 +1,20 @@
const app = require( './express' )() const app = require( './express' )()
const { getTotalSupply } = require( '../modules/contract' ) const { getTotalSupply } = require( '../modules/contract' )
const { safelyReturnRocketeer, web2domain, safelyReturnMultipleRocketeers } = require( '../nft-media/rocketeer' ) const { web2domain } = require( '../nft-media/rocketeer' )
const { generateNewOutfit, setPrimaryOutfit } = require( '../integrations/changingroom' ) const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
//////////////////////////////// ////////////////////////////////
// Specific Rocketeer instances // Specific Rocketeer instances
//////////////////////////////// ////////////////////////////////
app.get( '/testnetapi/rocketeer/:id', async ( req, res ) => { app.get( '/testnetapi/rocketeer/:id', ( req, res ) => rocketeerFromRequest( req, res, 'rinkeby' ) )
app.get( '/testnetapi/rocketeers/', ( req, res ) => multipleRocketeersFromRequest( req, res, 'rinkeby' ) )
// 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 old rocketeer if it exists
const rocketeer = await safelyReturnRocketeer( id, 'rinkeby' )
// Return the new rocketeer
return res.json( rocketeer )
} catch( e ) {
// Log error for debugging
console.error( `Testnet api error for ${ id }: `, e )
// Return error to frontend
return res.json( { error: e.mesage || e.toString() } )
}
} )
app.get( '/testnetapi/rocketeers/', async ( req, res ) => {
try {
// Parse the request
let { ids } = req.query
ids = ids.split( ',' )
if( ids.length > 100 ) throw new Error( 'Please do not ask for so much data at once :)' )
const rocketeers = await safelyReturnMultipleRocketeers( ids, 'testnet' )
return res.json( rocketeers )
} catch( e ) {
return res.json( { error: e.message || e.toString() } )
}
} )
/* /////////////////////////////// /* ///////////////////////////////
// Changing room endpoints // Changing room endpoints
// /////////////////////////////*/ // /////////////////////////////*/
app.post( '/testnetapi/rocketeer/:id/outfits', generateNewOutfit ) app.post( '/testnetapi/rocketeer/:id/outfits', generateNewOutfit )
app.post( '/testnetapi/rocketeers/:address', generateMultipleNewOutfits )
app.put( '/testnetapi/rocketeer/:id/outfits', setPrimaryOutfit ) app.put( '/testnetapi/rocketeer/:id/outfits', setPrimaryOutfit )
// Collection data // Collection data

View File

@ -1,4 +1,4 @@
const { generateNewOutfitFromId } = require( '../nft-media/changing-room' ) const { generateNewOutfitFromId, generateNewOutfitsByAddress } = require( '../nft-media/changing-room' )
const { db, dataFromSnap } = require( '../modules/firebase' ) const { db, dataFromSnap } = require( '../modules/firebase' )
// Web3 APIs // Web3 APIs
@ -67,6 +67,57 @@ exports.generateNewOutfit = async function( req, res ) {
} }
/* ///////////////////////////////
// POST handler for new avatars
// /////////////////////////////*/
exports.generateMultipleNewOutfits = async function( req, res ) {
// Parse the request
let { address } = req.params
if( !address ) return res.json( { error: `No address specified in URL` } )
// Protect against malformed input
if( !address.match( /0x.{40}/ ) ) return res.json( { error: `Malformed request` } )
// ⚠️ WIP
const network = 'rinkeby'
if( !process.env.NODE_ENV == 'development' ) return res.json( { error: `This endpoint is not live yet. While I appreciate your enthusiasm please don't touch this one yet :)` } )
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, action, chainId } = messageObject
// const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
// if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || action != 'generateMultipleNewOutfits' || !network ) throw new Error( `Invalid setPrimaryOutfit message with ${ signer }, ${confirmedSignatory}, ${action}, ${chainId}, ${network}` )
// Check that the signer is the owner of the token
const outfits = await generateNewOutfitsByAddress( address, network )
return res.json( outfits )
} catch( e ) {
// Log error for debugging
console.error( `POST generateMultipleNewOutfits Changing room api error for ${ address }: `, e )
// Return error to frontend
return res.json( { error: e.mesage || e.toString() } )
}
}
/* /////////////////////////////// /* ///////////////////////////////
// PUT handler for changing the // PUT handler for changing the
// current outfit // current outfit

View File

@ -0,0 +1,53 @@
const { safelyReturnRocketeer, safelyReturnMultipleRocketeers } = require( '../nft-media/rocketeer' )
exports.rocketeerFromRequest = async function( req, res, network='mainnet' ) {
// 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 old rocketeer if it exists
const rocketeer = await safelyReturnRocketeer( id, network )
// Return the new rocketeer
return res.json( rocketeer )
} catch( e ) {
// Log error for debugging
console.error( `${ network } api error for ${ id }: `, e )
// Return error to frontend
return res.json( { error: e.mesage || e.toString() } )
}
}
exports.multipleRocketeersFromRequest = async function( req, res, network='mainnet' ) {
try {
// Parse the request
let { ids } = req.query
ids = ids.split( ',' )
if( ids.length > 100 ) throw new Error( 'Please do not ask for so much data at once :)' )
const rocketeers = await safelyReturnMultipleRocketeers( ids, network )
return res.json( rocketeers )
} catch( e ) {
return res.json( { error: e.message || e.toString() } )
}
}

View File

@ -44,6 +44,51 @@ const ABI = [
"stateMutability": "view", "stateMutability": "view",
"type": "function", "type": "function",
"constant": true "constant": true
},
{
"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
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
} }
] ]
@ -71,8 +116,28 @@ async function getOwingAddressOfTokenId( id, network='mainnet' ) {
} }
async function getTokenIdsOfAddress( address, network='mainnet' ) {
// Initialise contract connection
const web3 = new Web3( `wss://${ network }.infura.io/ws/v3/${ infura.projectid }` )
const contract = new web3.eth.Contract( ABI, contractAddress[ network ] )
// Get balance of address
const balance = await contract.methods.balanceOf( address ).call()
// Get tokens of address
const ids = await Promise.all( Array.from( { length: balance } ).map( async ( val, index ) => {
const id = await contract.methods.tokenOfOwnerByIndex( address, index ).call()
return id.toString()
} ) )
return ids
}
module.exports = { module.exports = {
getTotalSupply, getTotalSupply,
contractAddress, contractAddress,
getOwingAddressOfTokenId getOwingAddressOfTokenId,
getTokenIdsOfAddress
} }

View File

@ -1,11 +1,13 @@
const { db, dataFromSnap } = require( '../modules/firebase' ) const { db, dataFromSnap } = require( '../modules/firebase' )
const { getRgbArrayFromColorName, randomNumberBetween } = require( '../modules/helpers' ) const { getRgbArrayFromColorName, randomNumberBetween } = require( '../modules/helpers' )
const { getTokenIdsOfAddress } = require( '../modules/contract' )
const svgFromAttributes = require( './svg-generator' ) const svgFromAttributes = require( './svg-generator' )
const Throttle = require( 'promise-parallel-throttle' )
// /////////////////////////////// // ///////////////////////////////
// Rocketeer generator // Rocketeer generator
// /////////////////////////////// // ///////////////////////////////
exports.generateNewOutfitFromId = async function( id, network='mainnet' ) { async function generateNewOutfitFromId( id, network='mainnet' ) {
/* /////////////////////////////// /* ///////////////////////////////
@ -27,7 +29,7 @@ exports.generateNewOutfitFromId = async function( id, network='mainnet' ) {
// Apply entropy levels based on edition status and outfits available // Apply entropy levels based on edition status and outfits available
const { value: edition } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "edition" ) const { value: edition } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "edition" )
if( edition != 'regular' ) colorEntropy *= specialEditionMultiplier if( edition != 'regular' ) colorEntropy *= specialEditionMultiplier
if( available_outfits ) colorEntropy *= ( entropyMultiplier * available_outfits ) if( available_outfits ) colorEntropy *= ( entropyMultiplier ** available_outfits )
// Check whether this Rocketeer is allowed to change // Check whether this Rocketeer is allowed to change
const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change ) const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change )
@ -88,3 +90,31 @@ exports.generateNewOutfitFromId = async function( id, network='mainnet' ) {
return newOutfitSvg return newOutfitSvg
} }
async function generateNewOutfitsByAddress( address, network='mainnet' ) {
const ids = await getTokenIdsOfAddress( address, network )
const queue = ids.map( id => function() {
return generateNewOutfitFromId( id, network ).then( outfit => ( { id: id, src: outfit } ) ).catch( e => {
console.log( e )
return ( { id: id, error: e.message } )
} )
} )
const outfits = await Throttle.all( queue, {
maxInProgress: 2,
progressCallback: ( { amountDone } ) => process.env.NODE_ENV == 'development' ? console.log( `Completed ${amountDone}/${queue.length}` ) : false
} )
return {
success: outfits.filter( ( { src } ) => src ),
error: outfits.filter( ( { error } ) => error ),
}
}
module.exports = {
generateNewOutfitFromId,
generateNewOutfitsByAddress
}

View File

@ -151,13 +151,14 @@ async function safelyReturnRocketeer( id, network ) {
async function safelyReturnMultipleRocketeers( ids=[], network='mainnet' ) { async function safelyReturnMultipleRocketeers( ids=[], network='mainnet' ) {
// Chech if this is an illegal ID // Chech if this is an illegal ID
const invalidIds = await Promise.all( ids.map( id => isInvalidRocketeerId( id, network ) ) ) const invalidIds = await Promise.all( ids.map( id => isInvalidRocketeerId( id, network ) ) )
if( invalidIds.includes( true ) ) throw invalidIds if( invalidIds.includes( true ) ) throw invalidIds
// Get old rocketeers and append their ids // Get old rocketeers and append their ids
const rocketeers = await Promise.all( ids.map( async id => ( { const rocketeers = await Promise.all( ids.map( async id => ( {
...await getExistingRocketeer( id ), ...await getExistingRocketeer( id, network ),
id: id id: id
} ) ) ) } ) ) )

View File

@ -4837,6 +4837,11 @@
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
}, },
"promise-parallel-throttle": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/promise-parallel-throttle/-/promise-parallel-throttle-3.3.0.tgz",
"integrity": "sha512-tThe11SfFXlGMhuO2D+Nba6L8FJFM17w2zwlMV1kqaLfuT2E8NMtMF1WhJBZaSpWz6V76pP/bGAj8BXTAMOncw=="
},
"proto3-json-serializer": { "proto3-json-serializer": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.5.tgz",

View File

@ -22,6 +22,7 @@
"firebase-admin": "^10.0.0", "firebase-admin": "^10.0.0",
"firebase-functions": "^3.11.0", "firebase-functions": "^3.11.0",
"jsdom": "^18.0.0", "jsdom": "^18.0.0",
"promise-parallel-throttle": "^3.3.0",
"puppeteer": "^12.0.0", "puppeteer": "^12.0.0",
"puppeteer-extra": "^3.2.3", "puppeteer-extra": "^3.2.3",
"puppeteer-extra-plugin-stealth": "^2.9.0", "puppeteer-extra-plugin-stealth": "^2.9.0",