Remove a bunch of stuff, we only want to serve the image generation part
20
.firebaserc
@ -1,20 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "rocketeer-nft"
|
||||
},
|
||||
"targets": {
|
||||
"rocketeer-nft": {
|
||||
"hosting": {
|
||||
"website": [
|
||||
"rocketeer"
|
||||
],
|
||||
"minter": [
|
||||
"rocketeer-nft"
|
||||
],
|
||||
"viewer": [
|
||||
"rocketeer-viewer"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
.github/workflows/deploy-functions.yml
vendored
@ -1,59 +0,0 @@
|
||||
name: Deploy function updates ( main )
|
||||
|
||||
on:
|
||||
# Trigger: on new tag push
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/deploy-functions.yml'
|
||||
- 'functions/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Publish changes
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Get the repo files from current commit
|
||||
- name: Cloning repository
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# Install frontend dependencies based on nvmrc
|
||||
- name: Read .nvmrc
|
||||
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
|
||||
id: nvmfunctions
|
||||
working-directory: functions
|
||||
|
||||
- name: Set Node.js (.nvmrc)
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "${{ steps.nvmfunctions.outputs.NVMRC }}"
|
||||
|
||||
- name: Install functions dependencies
|
||||
run: npm i
|
||||
working-directory: functions
|
||||
|
||||
# Backup firestore before deploying to database
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v0'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GCP_SERVICE_ACCOUT_JSON_PRODUCTION }}'
|
||||
- name: 'Set up Cloud SDK'
|
||||
uses: 'google-github-actions/setup-gcloud@v0'
|
||||
|
||||
# Note to self: needs IAM permissions
|
||||
# see https://firebase.google.com/docs/firestore/manage-data/export-import
|
||||
- name: Backup firestore
|
||||
run: |
|
||||
gcloud config set project ${{ secrets.FIREBASE_PROJECT_ID_PRODUCTION }}
|
||||
echo "Project is now set to ${{ secrets.FIREBASE_PROJECT_ID_PRODUCTION }}"
|
||||
gcloud firestore export ${{ secrets.GCP_BUCKET_LINK_PRODUCTION }}/firestore-backups/$(date +'%Y-%m-%d-%s')/
|
||||
|
||||
- name: Deploy to Firebase
|
||||
uses: docker://w9jds/firebase-action:master
|
||||
with:
|
||||
args: deploy --only=functions
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.FIREBASE_DEPLOYMENT_GCP_KEY }}
|
64
.github/workflows/deploy-minter.yml
vendored
@ -1,64 +0,0 @@
|
||||
name: Deploy Minter on push/merge
|
||||
|
||||
# Only trigger on PR/push and only for frontend files
|
||||
on:
|
||||
# Trigger on PR close
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
paths:
|
||||
- 'minter/*.json'
|
||||
- 'minter/*.js'
|
||||
- 'minter/**'
|
||||
- '.github/workflows/deploy-minter.yml'
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Trigger on push to master (edge case, local merge)
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'minter/*.json'
|
||||
- 'minter/*.js'
|
||||
- 'minter/**'
|
||||
- '.github/workflows/deploy-minter.yml'
|
||||
|
||||
jobs:
|
||||
# Build the frontend giles
|
||||
build:
|
||||
name: Compile frontend
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Get the repo files from current commit
|
||||
- name: Cloning repository
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Read .nvmrc
|
||||
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
|
||||
id: nvm
|
||||
working-directory: minter
|
||||
|
||||
- name: Set Node.js (.nvmrc)
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "${{ steps.nvm.outputs.NVMRC }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
working-directory: minter
|
||||
|
||||
- name: Build website files
|
||||
env:
|
||||
NODE_ENV: production
|
||||
CI: false
|
||||
run: npm run build
|
||||
working-directory: minter
|
||||
|
||||
- name: Deploy to Firebase
|
||||
uses: w9jds/firebase-action@master
|
||||
with:
|
||||
args: deploy --only hosting:minter
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.FIREBASE_DEPLOYMENT_GCP_KEY }}
|
64
.github/workflows/deploy-viewer.yml
vendored
@ -1,64 +0,0 @@
|
||||
name: Deploy viewer on push/merge
|
||||
|
||||
# Only trigger on PR/push and only for frontend files
|
||||
on:
|
||||
# Trigger on PR close
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
paths:
|
||||
- 'viewer/*.json'
|
||||
- 'viewer/*.js'
|
||||
- 'viewer/**'
|
||||
- '.github/workflows/deploy-viewer.yml'
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Trigger on push to master (edge case, local merge)
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'viewer/*.json'
|
||||
- 'viewer/*.js'
|
||||
- 'viewer/**'
|
||||
- '.github/workflows/deploy-viewer.yml'
|
||||
|
||||
jobs:
|
||||
# Build the frontend giles
|
||||
build:
|
||||
name: Compile frontend
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Get the repo files from current commit
|
||||
- name: Cloning repository
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Read .nvmrc
|
||||
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
|
||||
id: nvm
|
||||
working-directory: viewer
|
||||
|
||||
- name: Set Node.js (.nvmrc)
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "${{ steps.nvm.outputs.NVMRC }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
working-directory: viewer
|
||||
|
||||
- name: Build website files
|
||||
env:
|
||||
NODE_ENV: production
|
||||
CI: false
|
||||
run: npm run build
|
||||
working-directory: viewer
|
||||
|
||||
- name: Deploy to Firebase
|
||||
uses: w9jds/firebase-action@master
|
||||
with:
|
||||
args: deploy --only hosting:viewer
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.FIREBASE_DEPLOYMENT_GCP_KEY }}
|
63
.github/workflows/deploy-website.yml
vendored
@ -1,63 +0,0 @@
|
||||
name: Deploy Website on push/merge
|
||||
|
||||
# Only trigger on PR/push and only for frontend files
|
||||
on:
|
||||
# Trigger on PR close
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
paths:
|
||||
- 'website/*.json'
|
||||
- 'website/*.js'
|
||||
- 'website/**'
|
||||
- '.github/workflows/deploy-website.yml'
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Trigger on push to master (edge case, local merge)
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'website/*.json'
|
||||
- 'website/*.js'
|
||||
- 'website/**'
|
||||
- '.github/workflows/deploy-website.yml'
|
||||
|
||||
jobs:
|
||||
# Build the frontend giles
|
||||
build:
|
||||
name: Compile frontend
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Get the repo files from current commit
|
||||
- name: Cloning repository
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Read .nvmrc
|
||||
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
|
||||
id: nvm
|
||||
working-directory: website
|
||||
|
||||
- name: Set Node.js (.nvmrc)
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "${{ steps.nvm.outputs.NVMRC }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
working-directory: website
|
||||
|
||||
- name: Build website files
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: npm run build
|
||||
working-directory: website
|
||||
|
||||
- name: Deploy to Firebase
|
||||
uses: w9jds/firebase-action@master
|
||||
with:
|
||||
args: deploy --only hosting:website
|
||||
env:
|
||||
GCP_SA_KEY: ${{ secrets.FIREBASE_DEPLOYMENT_GCP_KEY }}
|
3
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
.*
|
||||
node_modules
|
||||
build/
|
||||
docs/
|
||||
*.log
|
||||
.backups
|
||||
notes.txt
|
||||
|
@ -1,5 +0,0 @@
|
||||
# Contribution guidelines
|
||||
|
||||
Contributions are welcome in all forms.
|
||||
|
||||
Keep in mind that this is a community project run in the spare time of the creators, so responses might take a while.
|
29
README.md
@ -1,29 +0,0 @@
|
||||
# Rocketeer NFT
|
||||
|
||||
This is the official repository of the [Rocketeers]( https://rocketeer.fans ) NFT collection.
|
||||
|
||||
- [Mint Rocketeers here]( https://mint.rocketeer.fans/#/mint )
|
||||
- [View your Rocketeer portfolio here]( https://mint.rocketeer.fans/#/portfolio )
|
||||
- [Set your Rocketpool node avatar here]( https://mint.rocketeer.fans/#/avatar )
|
||||
|
||||
Do you want to contribute to this project? Read `CONTRIBUTING.md`.
|
||||
|
||||
## Rocketeer components
|
||||
|
||||
The Rocketeer project consists out of a Solidity `ERC721` contract and a number of `web2` interfaces.
|
||||
|
||||
### Contract code
|
||||
|
||||
You can find the contract source in `contracts`. The `migrations/*` files set the parameters used for deployment.
|
||||
|
||||
### Minter code
|
||||
|
||||
The minter interface hosted at [mint.rocketeer.fans]( https://mint.rocketeer.fans/ ) is a React app that connects to Metamask. The code is inside the `minter` folder.
|
||||
|
||||
### Viewer code
|
||||
|
||||
The Rocketeer viewer hosted at [viewer.rocketeer.fans]( https://viewer.rocketeer.fans/ ) is the official place to view your Rocketeers. It's code is inside the `viewer` folder.
|
||||
|
||||
### Oracle code
|
||||
|
||||
The metadata and image oracle generates the Rocketeer data when one is minted. The code is inside the `functions` folder.
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 14 MiB |
Before Width: | Height: | Size: 270 KiB |
Before Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 148 KiB |
@ -1,154 +0,0 @@
|
||||
const wait = ( durationinMs=1000 ) => new Promise( resolve => setTimeout( resolve, durationinMs ) )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Twitter
|
||||
// scraping for signer.is
|
||||
// /////////////////////////////*/
|
||||
|
||||
function get_address_from_base64( text ) {
|
||||
|
||||
const [ batch, base64 ] = text.match( /(?:https:\/\/signer.is\/#\/verify\/)(.*?)(?:(<\/)|(">)|($))/ ) || []
|
||||
|
||||
try {
|
||||
|
||||
const text = atob( base64 )
|
||||
const json = JSON.parse( decodeURIComponent( text ) )
|
||||
|
||||
return json.claimed_signatory
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.log( `Decoding error for ${ base64 } `, e )
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function get_addresses_from_twitter_links( links ) {
|
||||
|
||||
const resolved_twitter_redirects = await Promise.all( links.map( url => fetch( url ).then( res => res.text() ) ) )
|
||||
const addresses = resolved_twitter_redirects.map( get_address_from_base64 )
|
||||
return addresses
|
||||
|
||||
}
|
||||
|
||||
async function get_address_from_twitter_link( link ) {
|
||||
|
||||
const resolved_twitter_redirect = await fetch( link ).then( res => res.text() )
|
||||
const address = get_address_from_base64( resolved_twitter_redirect )
|
||||
return address
|
||||
|
||||
}
|
||||
|
||||
async function scrape_signer_links_in_replies( ) {
|
||||
|
||||
const hrefs = document.querySelectorAll( 'a' )
|
||||
const has_signer_is = [ ...hrefs ].filter( ( { innerText, ...rest } ) => {
|
||||
return innerText.includes( 'signer.is/#/verify' )
|
||||
} )
|
||||
const signer_is_hrefs = has_signer_is.map( ( { href } ) => href )
|
||||
const addresses = await get_addresses_from_twitter_links( signer_is_hrefs )
|
||||
|
||||
return addresses
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function scrape_signer_links_in_dm( ) {
|
||||
|
||||
const scroll_interval = 50
|
||||
console.log( `This function runs for an indeterminate length, keep an eye on it and run get_addresses_from_twitter_links when results stagnate` )
|
||||
|
||||
function get_handle_from_element( element ) {
|
||||
const [ match, handle ] = element.innerHTML.match( /(@.+?)(?:<\/)/ ) || []
|
||||
if( handle ) return handle
|
||||
else return false
|
||||
}
|
||||
|
||||
const hits = []
|
||||
const done = []
|
||||
|
||||
while( true ) {
|
||||
|
||||
const messages = document.querySelectorAll( '[aria-selected=false]' )
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
|
||||
// Get the handle of the message we are trying
|
||||
const handle = get_handle_from_element( messages[i] )
|
||||
if( done.includes( handle ) ) continue
|
||||
if( !messages[i].isConnected ) continue
|
||||
|
||||
// open the message panel and grab the link
|
||||
messages[i].click()
|
||||
await wait()
|
||||
const links = document.querySelectorAll( 'a' )
|
||||
const { href, ...rest } = [ ...links ].find( ( { innerText } ) => innerText.includes( 'signer.is/#/verify' ) ) || []
|
||||
|
||||
// Save the link and mark the handle as done of need be
|
||||
if( href ) {
|
||||
|
||||
const address = await get_address_from_twitter_link( href ).catch( e => false )
|
||||
hits.push( address )
|
||||
|
||||
}
|
||||
done.push( handle )
|
||||
|
||||
document.querySelector( '[aria-label="Back"]' ).click()
|
||||
await wait()
|
||||
window.scrollBy(0, -scroll_interval)
|
||||
|
||||
}
|
||||
|
||||
console.log( `Checked ${ done.length } handles. Found: `, hits.join( '\n' ) )
|
||||
await wait()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function discord_channel_scraping() {
|
||||
|
||||
const hrefs = document.querySelectorAll( 'a' )
|
||||
const has_signer_is = [ ...hrefs ].filter( ( { innerText, ...rest } ) => {
|
||||
return innerText.includes( 'signer.is/#/verify' )
|
||||
} )
|
||||
console.log( has_signer_is[0].href )
|
||||
const signer_is_hrefs = has_signer_is.map( ( { title } ) => title )
|
||||
const addresses = signer_is_hrefs.map( get_address_from_base64 )
|
||||
console.log( addresses.join( '\n' ) )
|
||||
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Function handlers
|
||||
// /////////////////////////////*/
|
||||
async function get_all_addressed_from_replies() {
|
||||
|
||||
let all = []
|
||||
const scroll_interval = 300
|
||||
console.log( 'This function will run in perpetuity because twitter does not let us access all tweets unless they are in view. Manually handle that.' )
|
||||
while( true ) {
|
||||
|
||||
const addresses = await scrape_signer_links_in_replies()
|
||||
let new_all = [ ...all, ...addresses ]
|
||||
new_all = [ ...new Set( new_all ) ]
|
||||
|
||||
if( all.length != new_all.length ) console.log( new_all.join( '\n' ) )
|
||||
|
||||
all = new_all
|
||||
|
||||
window.scrollBy(0,scroll_interval)
|
||||
await wait( 1000 )
|
||||
|
||||
}
|
||||
|
||||
console.log( `${ all.length } addresses: \n`, all.join( '\n' ) )
|
||||
|
||||
}
|
||||
|
||||
// get_all_addressed_from_replies( )
|
||||
|
||||
scrape_signer_links_in_dm( )
|
Before Width: | Height: | Size: 28 KiB |
@ -1 +0,0 @@
|
||||
14
|
@ -1,100 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
|
||||
import "@openzeppelin/contracts/utils/Strings.sol";
|
||||
|
||||
import "./common/meta-transactions/ContentMixin.sol";
|
||||
import "./common/meta-transactions/NativeMetaTransaction.sol";
|
||||
|
||||
contract OwnableDelegateProxy {}
|
||||
|
||||
contract ProxyRegistry {
|
||||
mapping(address => OwnableDelegateProxy) public proxies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ERC721Tradable
|
||||
* ERC721Tradable - ERC721 contract that whitelists a trading address, and has minting functionality.
|
||||
* @dev I added the _getCurrentTokenId manually, the rest of the contract is verbatim from https://github.com/ProjectOpenSea/opensea-creatures/blob/master/contracts/ERC721Tradable.sol
|
||||
*/
|
||||
abstract contract ERC721Tradable is ContextMixin, ERC721Enumerable, NativeMetaTransaction, Ownable {
|
||||
using SafeMath for uint256;
|
||||
|
||||
address proxyRegistryAddress;
|
||||
uint256 private _currentTokenId = 0;
|
||||
|
||||
constructor(
|
||||
string memory _name,
|
||||
string memory _symbol,
|
||||
address _proxyRegistryAddress
|
||||
) ERC721(_name, _symbol) {
|
||||
proxyRegistryAddress = _proxyRegistryAddress;
|
||||
_initializeEIP712(_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Mints a token to an address with a tokenURI.
|
||||
* @param _to address of the future owner of the token
|
||||
*/
|
||||
function mintTo(address _to) internal {
|
||||
uint256 newTokenId = _getNextTokenId();
|
||||
_mint(_to, newTokenId);
|
||||
_incrementTokenId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev calculates the next token ID based on value of _currentTokenId
|
||||
* @return uint256 for the next token ID
|
||||
*/
|
||||
function _getNextTokenId() internal view returns (uint256) {
|
||||
return _currentTokenId.add(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev increments the value of _currentTokenId
|
||||
*/
|
||||
function _incrementTokenId() private {
|
||||
_currentTokenId++;
|
||||
}
|
||||
|
||||
function baseTokenURI() virtual public pure returns (string memory);
|
||||
|
||||
function tokenURI(uint256 _tokenId) override public pure returns (string memory) {
|
||||
return string(abi.encodePacked(baseTokenURI(), Strings.toString(_tokenId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
|
||||
*/
|
||||
function isApprovedForAll(address owner, address operator)
|
||||
override
|
||||
public
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
// Whitelist OpenSea proxy contract for easy trading.
|
||||
ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
|
||||
if (address(proxyRegistry.proxies(owner)) == operator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.isApprovedForAll(owner, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used instead of msg.sender as transactions won't be sent by the original token owner, but by OpenSea.
|
||||
*/
|
||||
function _msgSender()
|
||||
internal
|
||||
override
|
||||
view
|
||||
returns (address sender)
|
||||
{
|
||||
return ContextMixin.msgSender();
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.4.22 <0.9.0;
|
||||
|
||||
contract Migrations {
|
||||
address public owner = msg.sender;
|
||||
uint public last_completed_migration;
|
||||
|
||||
modifier restricted() {
|
||||
require(
|
||||
msg.sender == owner,
|
||||
"This function is restricted to the contract's owner"
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
function setCompleted(uint completed) public restricted {
|
||||
last_completed_migration = completed;
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./ERC721Tradable.sol";
|
||||
|
||||
/**
|
||||
* @title Rocketeer
|
||||
* Rocketeer - a contract for my non-fungible rocketeers
|
||||
*/
|
||||
contract Rocketeer is ERC721Tradable {
|
||||
|
||||
// ///////////////////////////////
|
||||
// Globals
|
||||
// ///////////////////////////////
|
||||
|
||||
// Max supply is the diameter of the moon in KM
|
||||
uint256 private ROCKETEER_MAX_SUPPLY = 3475;
|
||||
|
||||
// Construct as Opensea tradable item
|
||||
constructor(address _proxyRegistryAddress)
|
||||
ERC721Tradable("Rocketeer", "RCT", _proxyRegistryAddress)
|
||||
{
|
||||
|
||||
// Birth the genesis Rocketeers
|
||||
spawnRocketeer( owner() );
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Oracles
|
||||
// ///////////////////////////////
|
||||
|
||||
// TODO: add Api data
|
||||
// https://docs.opensea.io/docs/metadata-standards
|
||||
function baseTokenURI() override public pure returns (string memory) {
|
||||
// return "https://rocketeer.fans/testnetapi/rocketeer/";
|
||||
return "https://rocketeer.fans/api/rocketeer/";
|
||||
}
|
||||
|
||||
// TODO: add API link
|
||||
// https://docs.opensea.io/docs/contract-level-metadata
|
||||
function contractURI() public pure returns (string memory) {
|
||||
// return "https://rocketeer.fans/testnetapi/collection/";
|
||||
return "https://rocketeer.fans/api/rocketeer/";
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Minting
|
||||
// ///////////////////////////////
|
||||
|
||||
function spawnRocketeer( address _to ) public {
|
||||
|
||||
uint256 nextTokenId = _getNextTokenId();
|
||||
|
||||
// Every 42nd unit becomes a special edition, gas fees paid for but not owned by the minter
|
||||
if( nextTokenId % 42 == 0 ) {
|
||||
mintTo( owner() );
|
||||
}
|
||||
|
||||
// No more than max supply
|
||||
require( nextTokenId <= ROCKETEER_MAX_SUPPLY, "Maximum Rocketeer supply reached" );
|
||||
|
||||
mintTo( _to );
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
abstract contract ContextMixin {
|
||||
function msgSender()
|
||||
internal
|
||||
view
|
||||
returns (address payable sender)
|
||||
{
|
||||
if (msg.sender == address(this)) {
|
||||
bytes memory array = msg.data;
|
||||
uint256 index = msg.data.length;
|
||||
assembly {
|
||||
// Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
|
||||
sender := and(
|
||||
mload(add(array, index)),
|
||||
0xffffffffffffffffffffffffffffffffffffffff
|
||||
)
|
||||
}
|
||||
} else {
|
||||
sender = payable(msg.sender);
|
||||
}
|
||||
return sender;
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {Initializable} from "./Initializable.sol";
|
||||
|
||||
contract EIP712Base is Initializable {
|
||||
struct EIP712Domain {
|
||||
string name;
|
||||
string version;
|
||||
address verifyingContract;
|
||||
bytes32 salt;
|
||||
}
|
||||
|
||||
string constant public ERC712_VERSION = "1";
|
||||
|
||||
bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256(
|
||||
bytes(
|
||||
"EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)"
|
||||
)
|
||||
);
|
||||
bytes32 internal domainSeperator;
|
||||
|
||||
// supposed to be called once while initializing.
|
||||
// one of the contracts that inherits this contract follows proxy pattern
|
||||
// so it is not possible to do this in a constructor
|
||||
function _initializeEIP712(
|
||||
string memory name
|
||||
)
|
||||
internal
|
||||
initializer
|
||||
{
|
||||
_setDomainSeperator(name);
|
||||
}
|
||||
|
||||
function _setDomainSeperator(string memory name) internal {
|
||||
domainSeperator = keccak256(
|
||||
abi.encode(
|
||||
EIP712_DOMAIN_TYPEHASH,
|
||||
keccak256(bytes(name)),
|
||||
keccak256(bytes(ERC712_VERSION)),
|
||||
address(this),
|
||||
bytes32(getChainId())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getDomainSeperator() public view returns (bytes32) {
|
||||
return domainSeperator;
|
||||
}
|
||||
|
||||
function getChainId() public view returns (uint256) {
|
||||
uint256 id;
|
||||
assembly {
|
||||
id := chainid()
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept message hash and returns hash message in EIP712 compatible form
|
||||
* So that it can be used to recover signer from signature signed using EIP712 formatted data
|
||||
* https://eips.ethereum.org/EIPS/eip-712
|
||||
* "\\x19" makes the encoding deterministic
|
||||
* "\\x01" is the version byte to make it compatible to EIP-191
|
||||
*/
|
||||
function toTypedMessageHash(bytes32 messageHash)
|
||||
internal
|
||||
view
|
||||
returns (bytes32)
|
||||
{
|
||||
return
|
||||
keccak256(
|
||||
abi.encodePacked("\x19\x01", getDomainSeperator(), messageHash)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract Initializable {
|
||||
bool inited = false;
|
||||
|
||||
modifier initializer() {
|
||||
require(!inited, "already inited");
|
||||
_;
|
||||
inited = true;
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
|
||||
import {EIP712Base} from "./EIP712Base.sol";
|
||||
|
||||
contract NativeMetaTransaction is EIP712Base {
|
||||
using SafeMath for uint256;
|
||||
bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256(
|
||||
bytes(
|
||||
"MetaTransaction(uint256 nonce,address from,bytes functionSignature)"
|
||||
)
|
||||
);
|
||||
event MetaTransactionExecuted(
|
||||
address userAddress,
|
||||
address payable relayerAddress,
|
||||
bytes functionSignature
|
||||
);
|
||||
mapping(address => uint256) nonces;
|
||||
|
||||
/*
|
||||
* Meta transaction structure.
|
||||
* No point of including value field here as if user is doing value transfer then he has the funds to pay for gas
|
||||
* He should call the desired function directly in that case.
|
||||
*/
|
||||
struct MetaTransaction {
|
||||
uint256 nonce;
|
||||
address from;
|
||||
bytes functionSignature;
|
||||
}
|
||||
|
||||
function executeMetaTransaction(
|
||||
address userAddress,
|
||||
bytes memory functionSignature,
|
||||
bytes32 sigR,
|
||||
bytes32 sigS,
|
||||
uint8 sigV
|
||||
) public payable returns (bytes memory) {
|
||||
MetaTransaction memory metaTx = MetaTransaction({
|
||||
nonce: nonces[userAddress],
|
||||
from: userAddress,
|
||||
functionSignature: functionSignature
|
||||
});
|
||||
|
||||
require(
|
||||
verify(userAddress, metaTx, sigR, sigS, sigV),
|
||||
"Signer and signature do not match"
|
||||
);
|
||||
|
||||
// increase nonce for user (to avoid re-use)
|
||||
nonces[userAddress] = nonces[userAddress].add(1);
|
||||
|
||||
emit MetaTransactionExecuted(
|
||||
userAddress,
|
||||
payable(msg.sender),
|
||||
functionSignature
|
||||
);
|
||||
|
||||
// Append userAddress and relayer address at the end to extract it from calling context
|
||||
(bool success, bytes memory returnData) = address(this).call(
|
||||
abi.encodePacked(functionSignature, userAddress)
|
||||
);
|
||||
require(success, "Function call not successful");
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
function hashMetaTransaction(MetaTransaction memory metaTx)
|
||||
internal
|
||||
pure
|
||||
returns (bytes32)
|
||||
{
|
||||
return
|
||||
keccak256(
|
||||
abi.encode(
|
||||
META_TRANSACTION_TYPEHASH,
|
||||
metaTx.nonce,
|
||||
metaTx.from,
|
||||
keccak256(metaTx.functionSignature)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getNonce(address user) public view returns (uint256 nonce) {
|
||||
nonce = nonces[user];
|
||||
}
|
||||
|
||||
function verify(
|
||||
address signer,
|
||||
MetaTransaction memory metaTx,
|
||||
bytes32 sigR,
|
||||
bytes32 sigS,
|
||||
uint8 sigV
|
||||
) internal view returns (bool) {
|
||||
require(signer != address(0), "NativeMetaTransaction: INVALID_SIGNER");
|
||||
return
|
||||
signer ==
|
||||
ecrecover(
|
||||
toTypedMessageHash(hashMetaTransaction(metaTx)),
|
||||
sigV,
|
||||
sigR,
|
||||
sigS
|
||||
);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"hosting": [
|
||||
{
|
||||
"target": "website",
|
||||
"public": "website/docs",
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/api/**",
|
||||
"function": "mainnetMetadata"
|
||||
},
|
||||
{
|
||||
"source": "/testnetapi/**",
|
||||
"function": "testnetMetadata"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "minter",
|
||||
"public": "minter/build"
|
||||
},
|
||||
{
|
||||
"target": "viewer",
|
||||
"public": "viewer/build"
|
||||
}
|
||||
],
|
||||
"storage": {
|
||||
"rules": "storage.rules"
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"indexes": [],
|
||||
"fieldOverrides": []
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
rules_version = '2';
|
||||
service firebase.storage {
|
||||
|
||||
match /b/{bucket}/o {
|
||||
|
||||
|
||||
// Disallow all by default
|
||||
match /{allPaths=**} {
|
||||
allow read, write: if false
|
||||
}
|
||||
|
||||
// Allow creation if size is not too big and resource does not exist
|
||||
match /api/{fileName} {
|
||||
|
||||
allow read: if true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
module.exports = {
|
||||
|
||||
// Recommended features
|
||||
"extends": [ "eslint:recommended" ],
|
||||
|
||||
//Parser features
|
||||
parser: "@babel/eslint-parser",
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true
|
||||
}
|
||||
},
|
||||
|
||||
// Specific rules, 2: err, 1: warn, 0: off
|
||||
rules: {
|
||||
|
||||
"no-case-declarations": 0,
|
||||
"prefer-arrow-callback": 2,
|
||||
"no-mixed-spaces-and-tabs": 1,
|
||||
"no-unused-vars": [ 1, { vars: 'all', args: 'none' } ], // All variables, no function arguments
|
||||
|
||||
},
|
||||
|
||||
// What environment to run in
|
||||
env:{
|
||||
node: true,
|
||||
browser: true,
|
||||
mocha: true,
|
||||
jest: true,
|
||||
es6: true
|
||||
},
|
||||
|
||||
// What global variables should be assumed to exist
|
||||
globals: {
|
||||
context: false,
|
||||
// cy: true,
|
||||
// window: true,
|
||||
// location: true,
|
||||
// fetch: true
|
||||
}
|
||||
|
||||
}
|
2
functions/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.*
|
@ -1 +0,0 @@
|
||||
16
|
@ -1,7 +0,0 @@
|
||||
## Requirements
|
||||
|
||||
- [ ] `./functions`: set Infura project ID through `firebase functions:config:set infura.projectid=`
|
||||
- [ ] also set `integration.secret`
|
||||
- [ ] also set `discord.webhookurl`
|
||||
- [ ] also set `mailgun.api_key`, `mailgun.api_url`, `mailgun.from_domain`, `mailgun.from_email`
|
||||
- [ ] `./functions/package.json`: dependencies for backend, run `npm i` in `./functions`
|
Before Width: | Height: | Size: 2.1 MiB |
@ -1,19 +0,0 @@
|
||||
const express = require( 'express' )
|
||||
const cors = require( 'cors' )
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
// CORS enabled express generator
|
||||
module.exports = f => {
|
||||
|
||||
// Create express server
|
||||
const app = express()
|
||||
|
||||
// Enable CORS
|
||||
app.use( cors( { origin: true } ) )
|
||||
|
||||
// Enable body parser
|
||||
app.use( bodyParser.json() )
|
||||
|
||||
return app
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
const app = require( './express' )()
|
||||
const { getTotalSupply } = require( '../modules/contract' )
|
||||
const { web2domain } = require( '../nft-media/rocketeer' )
|
||||
const { setAvatar, resetAvatar } = require( '../integrations/avatar' )
|
||||
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
||||
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
||||
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
||||
const { order_merch } = require( '../integrations/merch' )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Specific Rocketeer instances
|
||||
// ///////////////////////////////
|
||||
app.get( '/api/rocketeer/:id', async ( req, res ) => rocketeerFromRequest( req, res, 'mainnet' ) )
|
||||
app.get( '/api/rocketeers/', async ( req, res ) => multipleRocketeersFromRequest( req, res, 'mainnet' ) )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// VGR's dashboard integration
|
||||
// /////////////////////////////*/
|
||||
app.post( '/api/integrations/avatar/', setAvatar )
|
||||
app.post( '/api/rocketeers/:address', generateMultipleNewOutfits )
|
||||
app.delete( '/api/integrations/avatar/', resetAvatar )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Changing room endpoints
|
||||
// /////////////////////////////*/
|
||||
app.post( '/api/rocketeer/:id/outfits', generateNewOutfit )
|
||||
app.put( '/api/rocketeer/:id/outfits', setPrimaryOutfit )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Notification API
|
||||
// /////////////////////////////*/
|
||||
app.post( '/api/notifications/:address', subscribe_address_to_notifications )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Merch API
|
||||
// /////////////////////////////*/
|
||||
app.post( '/api/merch/order', order_merch )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Static collection data
|
||||
// ///////////////////////////////
|
||||
app.get( '/api/rocketeer', async ( req, res ) => res.json( {
|
||||
totalSupply: await getTotalSupply( 'mainnet' ).catch( f => 'error' ),
|
||||
description: '"Moon boots touch the earth. Visored faces tilt upward. Their sole thought is wen." ~ Rocketeer Haiku\n\nThe Rocketeer NFT collection is inspired by the undying patience and excited optimism of the Rocket Pool and ETH2 staking communites.\n\nJoin us at https://rocketeer.fans/',
|
||||
external_url: web2domain,
|
||||
image: "https://rocketeer.fans/assets/draft-rocketeer.png",
|
||||
name: `Rocketeers`,
|
||||
seller_fee_basis_points: 500,
|
||||
fee_recipient: "0x7DBF6820D32cFBd5D656bf9BFf0deF229B37cF0E"
|
||||
} ) )
|
||||
|
||||
module.exports = app
|
@ -1,43 +0,0 @@
|
||||
const app = require( './express' )()
|
||||
const { getTotalSupply } = require( '../modules/contract' )
|
||||
const { web2domain } = require( '../nft-media/rocketeer' )
|
||||
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
||||
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
||||
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
||||
const { order_merch } = require( '../integrations/merch' )
|
||||
|
||||
////////////////////////////////
|
||||
// Specific Rocketeer instances
|
||||
////////////////////////////////
|
||||
app.get( '/testnetapi/rocketeer/:id', ( req, res ) => rocketeerFromRequest( req, res, 'rinkeby' ) )
|
||||
app.get( '/testnetapi/rocketeers/', ( req, res ) => multipleRocketeersFromRequest( req, res, 'rinkeby' ) )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Changing room endpoints
|
||||
// /////////////////////////////*/
|
||||
app.post( '/testnetapi/rocketeer/:id/outfits', generateNewOutfit )
|
||||
app.post( '/testnetapi/rocketeers/:address', generateMultipleNewOutfits )
|
||||
app.put( '/testnetapi/rocketeer/:id/outfits', setPrimaryOutfit )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Notification API
|
||||
// /////////////////////////////*/
|
||||
app.post( '/testnetapi/notifications/:address', subscribe_address_to_notifications )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Merch API
|
||||
// /////////////////////////////*/
|
||||
app.post( '/testnetapi/merch/order', order_merch )
|
||||
|
||||
// Collection data
|
||||
app.get( '/testnetapi/collection', async ( req, res ) => res.json( {
|
||||
totalSupply: await getTotalSupply( 'rinkeby' ).catch( f => 'error' ),
|
||||
description: "A testnet collection.\n\nTesting newlines.\n\nAnd emoji 😎.\n\nAlso: urls; https://rocketeer.fans/",
|
||||
external_url: web2domain,
|
||||
image: "https://rocketeer.fans/assets/draft-rocketeer.png",
|
||||
name: `Rocketeer collection`,
|
||||
seller_fee_basis_points: 0,
|
||||
fee_recipient: "0x0"
|
||||
} ) )
|
||||
|
||||
module.exports = app
|
@ -1,32 +0,0 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const testnetAPI = require( './endpoints/testnet' )
|
||||
const mainnetAPI = require( './endpoints/mainnet' )
|
||||
|
||||
// Runtime config
|
||||
const expensive_runtime = {
|
||||
timeoutSeconds: 540,
|
||||
memory: '4GB'
|
||||
}
|
||||
const cheap_runtime = {
|
||||
timeoutSeconds: 540,
|
||||
memory: '512MB'
|
||||
}
|
||||
|
||||
// Testnet endpoint
|
||||
exports.testnetMetadata = functions.runWith( cheap_runtime ).https.onRequest( testnetAPI )
|
||||
|
||||
// Mainnet endpoint
|
||||
exports.mainnetMetadata = functions.runWith( cheap_runtime ).https.onRequest( mainnetAPI )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Firestore listeners
|
||||
// /////////////////////////////*/
|
||||
const { handleQueuedRocketeerOutfit } = require( './nft-media/changing-room' )
|
||||
exports.mainnetGenerateOutfitsOnQueue = functions.runWith( expensive_runtime ).firestore.document( `mainnetQueueOutfitGeneration/{rocketeerId}` ).onWrite( handleQueuedRocketeerOutfit )
|
||||
exports.rinkebyGenerateOutfitsOnQueue = functions.runWith( expensive_runtime ).firestore.document( `rinkebyQueueOutfitGeneration/{rocketeerId}` ).onWrite( handleQueuedRocketeerOutfit )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Daemons
|
||||
// /////////////////////////////*/
|
||||
const { notify_holders_of_changing_room_updates } = require( './integrations/changingroom' )
|
||||
exports.notify_holders_of_changing_room_updates = functions.runWith( cheap_runtime ).pubsub.schedule( '30 1 * * *' ).onRun( notify_holders_of_changing_room_updates )
|
@ -1,143 +0,0 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const { integration } = functions.config()
|
||||
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||
const Web3 = require( 'web3' )
|
||||
const web3 = new Web3()
|
||||
const { getStorage } = require( 'firebase-admin/storage' )
|
||||
|
||||
|
||||
exports.setAvatar = async function( req, res ) {
|
||||
|
||||
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
|
||||
// const chain = '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, network } = messageObject
|
||||
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !tokenId || !validator || chainId !== chain || !network ) throw new Error( `Invalid message` )
|
||||
|
||||
// Check if validator was already assigned
|
||||
const validatorProfile = await db.collection( `${ network }Validators` ).doc( validator ).get().then( dataFromSnap )
|
||||
if( validatorProfile.owner && 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( `${ network }Validators` ).doc( validator ).set( {
|
||||
tokenId,
|
||||
owner: signatory,
|
||||
src: `https://storage.googleapis.com/rocketeer-nft.appspot.com/${ network }Rocketeers/${ tokenId }.jpg`,
|
||||
updated: Date.now()
|
||||
} )
|
||||
|
||||
// Update the static overview JSON
|
||||
const storage = getStorage()
|
||||
const bucket = storage.bucket()
|
||||
const cacheFile = bucket.file( `integrations/${ network }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( `${ network }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()
|
||||
|
||||
return res.json( {
|
||||
success: true,
|
||||
url: cacheFile.publicUrl()
|
||||
} )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( 'avatar integration error: ', e )
|
||||
return res.json( {
|
||||
error: e.message
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.resetAvatar = async function( req, res ) {
|
||||
|
||||
// const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
|
||||
const network = 'mainnet'
|
||||
// const chain = '0x1'
|
||||
|
||||
try {
|
||||
|
||||
// Get request data
|
||||
const { address, secret } = req.body
|
||||
if( !address || !secret || secret != integration.secret ) throw new Error( `Malformed request` )
|
||||
|
||||
// Check if validator was already assigned
|
||||
await db.collection( `${ network }Validators` ).doc( address ).delete()
|
||||
|
||||
// Update the static overview JSON
|
||||
const storage = getStorage()
|
||||
const bucket = storage.bucket()
|
||||
const cacheFile = bucket.file( `integrations/${ network }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 )
|
||||
|
||||
// Delete the address
|
||||
if( cachedJson.images[ address ] ) delete jsonstring.images[ address ]
|
||||
if( cachedJson.ids[ address ] ) delete jsonstring.ids[ address ]
|
||||
|
||||
// Save new data to file
|
||||
cachedJson.updated = Date.now()
|
||||
await cacheFile.save( JSON.stringify( cachedJson ) )
|
||||
await cacheFile.makePublic()
|
||||
|
||||
return res.json( {
|
||||
success: true,
|
||||
url: cacheFile.publicUrl()
|
||||
} )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( 'avatar deletion integration error: ', e )
|
||||
return res.json( {
|
||||
error: e.message
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,341 +0,0 @@
|
||||
const { generateNewOutfitFromId, queueRocketeersOfAddressForOutfitChange } = require( '../nft-media/changing-room' )
|
||||
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||
const { dev, log } = require( '../modules/helpers' )
|
||||
const { ask_signer_is_for_available_emails } = require( './signer_is' )
|
||||
const { send_outfit_available_email } = require( './ses' )
|
||||
const { throttle_and_retry } = require( '../modules/helpers' )
|
||||
const { notify_discord_of_outfit_notifications } = require( './discord' )
|
||||
|
||||
// Web3 APIs
|
||||
const { getOwingAddressOfTokenId } = require( '../modules/contract' )
|
||||
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
|
||||
const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
|
||||
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !rocketeerId || !network ) throw new Error( `Invalid generateNewOutfit message with ${signer}, ${confirmedSignatory}, ${rocketeerId}, ${network}` )
|
||||
if( rocketeerId != id ) throw new Error( `Invalid Rocketeer in message` )
|
||||
|
||||
// Check that the signer is the owner of the token
|
||||
const owner = await getOwingAddressOfTokenId( id, network )
|
||||
if( owner !== confirmedSignatory ) throw new Error( `You are not the owner of this Rocketeer. Did you sign with the right wallet?` )
|
||||
|
||||
// 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() } )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// 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` } )
|
||||
|
||||
// Lowercase the address
|
||||
address = address.toLowerCase()
|
||||
|
||||
// Internal beta
|
||||
// if( !address.includes( '0xe3ae14' ) && !address.includes( '0x7dbf68' ) ) return res.json( { error: `Sorry this endpoint is in private beta for now <3` } )
|
||||
|
||||
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 amountOfOutfits = await queueRocketeersOfAddressForOutfitChange( address, network )
|
||||
|
||||
await db.collection( 'meta' ).doc( address ).set( {
|
||||
last_changing_room: Date.now(),
|
||||
outfits_last_changing_room: amountOfOutfits,
|
||||
outfits_in_queue: amountOfOutfits,
|
||||
updated: Date.now()
|
||||
}, { merge: true } )
|
||||
|
||||
return res.json( { amountOfOutfits } )
|
||||
|
||||
|
||||
|
||||
} 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
|
||||
// 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
|
||||
const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
|
||||
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || outfitId == undefined || !network ) throw new Error( `Invalid setPrimaryOutfit message with ${ signer }, ${confirmedSignatory}, ${outfitId}, ${chainId}, ${network}` )
|
||||
|
||||
// Validate id format
|
||||
outfitId = Math.floor( Math.abs( outfitId ) )
|
||||
if( typeof outfitId !== 'number' ) return res.json( { error: `Malformed request` } )
|
||||
|
||||
// Check that the signer is the owner of the token
|
||||
const owner = await getOwingAddressOfTokenId( id, network )
|
||||
if( owner !== confirmedSignatory ) throw new Error( `You are not the owner of this Rocketeer. Did you sign with the right wallet?` )
|
||||
|
||||
// Set ID to string so firestore can handle it
|
||||
outfitId = `${ outfitId }`
|
||||
|
||||
// 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 } )
|
||||
|
||||
return res.json( { success: 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() } )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Notify of changing room updates
|
||||
// /////////////////////////////*/
|
||||
exports.notify_holders_of_changing_room_updates = async context => {
|
||||
|
||||
// One month in ms
|
||||
const newOutfitAllowedInterval = 1000 * 60 * 60 * 24 * 30
|
||||
|
||||
try {
|
||||
|
||||
// Get all Rocketeers with outfits available
|
||||
// const network = dev ? `rinkeby` : `mainnet`
|
||||
const network = 'mainnet'
|
||||
const limit = dev ? 5000 : 5000 // max supply 3475
|
||||
console.log( `Getting ${ limit } rocketeers on ${ network }` )
|
||||
let all_rocketeers = await db.collection( `${ network }Rocketeers` ).limit( limit ).get().then( dataFromSnap )
|
||||
console.log( `Got ${ all_rocketeers.length } Rocketeers` )
|
||||
|
||||
// FIlter out API abuse rocketeers
|
||||
all_rocketeers = all_rocketeers.filter( ( {uid} ) => uid > 0 && uid <= 3475 )
|
||||
console.log( `Proceeding with ${ all_rocketeers.length } valid Rocketeers` )
|
||||
|
||||
|
||||
// Check which rocketeers have outfits available
|
||||
const has_outfit_available = all_rocketeers.filter( rocketeer => {
|
||||
|
||||
const { value: last_outfit_change } = rocketeer.attributes.find( ( { trait_type } ) => trait_type === 'last outfit change' ) || { value: 0 }
|
||||
const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change )
|
||||
|
||||
// Keep those with changes allowed
|
||||
if( timeUntilAllowedToChange < 0 ) return true
|
||||
|
||||
// If outfit available in the future, discard
|
||||
return false
|
||||
|
||||
} )
|
||||
log( `${ has_outfit_available.length } Rocketeers have outfits available` )
|
||||
|
||||
// Get owner cache
|
||||
const one_day_in_ms = 1000 * 60 * 60 * 24
|
||||
const owner_cache = await db.collection( `rocketeer_owner_cache` ).where( 'updated', '>', Date.now() - one_day_in_ms ).get().then( dataFromSnap )
|
||||
|
||||
// Get the owning wallets of available outfits
|
||||
const get_rocketeer_owners_queue = has_outfit_available.map( ( { uid } ) => async () => {
|
||||
|
||||
// Try for cached owner
|
||||
const cached_owner = owner_cache.find( ( { tokenId } ) => uid == tokenId )
|
||||
if( cached_owner ) return cached_owner
|
||||
|
||||
// Ask infura for owner
|
||||
let owning_address = await getOwingAddressOfTokenId( uid )
|
||||
owning_address = owning_address.toLowerCase()
|
||||
|
||||
return { uid, owning_address }
|
||||
|
||||
} )
|
||||
const rocketeers_with_owners = await throttle_and_retry( get_rocketeer_owners_queue, 50, `get owners`, 2, 5 )
|
||||
console.log( `${ rocketeers_with_owners.length } Rocketeer owners found` )
|
||||
|
||||
// Set owner cache to spare infura
|
||||
const owner_cache_writing_queue = rocketeers_with_owners.map( ( { uid, owning_address } ) => () => {
|
||||
return db.collection( `rocketeer_owner_cache` ).doc( uid ).set( { tokenId: uid, owning_address, updated: Date.now(), updated_human: new Date().toString() }, { merge: true } )
|
||||
} )
|
||||
await throttle_and_retry( owner_cache_writing_queue, 50, `writing owner cache`, 2, 10 )
|
||||
|
||||
// Get the owners we have already emailed recently
|
||||
const owner_meta = await db.collection( `meta` ).get().then( dataFromSnap )
|
||||
console.log( `${ owner_meta.length } Owners in cache` )
|
||||
const owners_emailed_recently = owner_meta
|
||||
// Too recently means last_emailed is larger than the point in the past past which it's been too long
|
||||
.filter( ( { last_emailed_about_outfit } ) => last_emailed_about_outfit && ( last_emailed_about_outfit > ( Date.now() - newOutfitAllowedInterval ) ) )
|
||||
.map( ( { uid } ) => uid.toLowerCase() )
|
||||
|
||||
// Remove owners from list of they were emailed too recently
|
||||
console.log( `${ owners_emailed_recently.length } owners emailed too recently: `, owners_emailed_recently.slice( 0, 10 ) )
|
||||
|
||||
// Check which owners have signer.is emails
|
||||
let owners_of_rocketeers = rocketeers_with_owners.map( ( { owning_address } ) => owning_address ).map( address => address.toLowerCase() )
|
||||
owners_of_rocketeers = [ ...new Set( owners_of_rocketeers ) ]
|
||||
log( `${ owners_of_rocketeers.length } unique owners found` )
|
||||
const owners_with_signer_email = await ask_signer_is_for_available_emails( owners_of_rocketeers )
|
||||
console.log( owners_with_signer_email )
|
||||
console.log( `${ owners_with_signer_email.length } Owners have signer emails` )
|
||||
|
||||
// Filter out owners that were emailed too recdently
|
||||
const owners_to_email = owners_with_signer_email.map( address => address.toLowerCase() ).filter( address => !owners_emailed_recently.includes( address ) )
|
||||
|
||||
// // List the owning emails
|
||||
console.log( `${ owners_to_email.length } owners to email: `, owners_to_email.slice( 0, 10 ).join( ', ' ) )
|
||||
|
||||
// Take note of who we emailed so as to not spam them
|
||||
const meta_writing_queue = owners_to_email.map( ( address ) => () => {
|
||||
return db.collection( `meta` ).doc( address ).set( { last_emailed_about_outfit: Date.now(), updated: Date.now(), updated_human: new Date().toString() }, { merge: true } )
|
||||
} )
|
||||
await throttle_and_retry( meta_writing_queue, 50, `keep track of who we emailed`, 2, 10 )
|
||||
|
||||
// Format rocketeers by address
|
||||
const rocketeers_by_address = has_outfit_available.reduce( ( wallets, rocketeer ) => {
|
||||
|
||||
const new_wallet_list = { ...wallets }
|
||||
const { owning_address } = rocketeers_with_owners.find( ( { uid } ) => uid == rocketeer.uid )
|
||||
|
||||
// If this owner has no email, ignore it
|
||||
if( !owners_with_signer_email.includes( owning_address ) ) return new_wallet_list
|
||||
|
||||
// If the wallet object does now have this one yet, add an empty array
|
||||
if( !new_wallet_list[ owning_address ] ) new_wallet_list[owning_address] = []
|
||||
|
||||
new_wallet_list[owning_address] = [ ...new_wallet_list[owning_address], rocketeer ]
|
||||
return new_wallet_list
|
||||
|
||||
}, {} )
|
||||
|
||||
// Send emails to the relevant owners
|
||||
const email_sending_queue = owners_to_email.map( ( owning_address ) => async () => {
|
||||
|
||||
const rocketeers = rocketeers_by_address[ owning_address ]
|
||||
await send_outfit_available_email( rocketeers, `${ owning_address }@signer.is` )
|
||||
|
||||
} )
|
||||
await throttle_and_retry( email_sending_queue, 10, `send email`, 2, 10 )
|
||||
|
||||
// Log result
|
||||
console.log( `Sent ${ owners_to_email.length } emails for ${ network } outfits` )
|
||||
|
||||
// Notify Discord too
|
||||
if( !owners_to_email.length ) return console.log( `Zero people to email, not sending Discord webhook` )
|
||||
await notify_discord_of_outfit_notifications( owners_to_email.length, has_outfit_available.length )
|
||||
|
||||
|
||||
} catch( e ) {
|
||||
console.error( `notify_holders_of_changing_room_updates error: `, e )
|
||||
}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const { discord } = functions.config()
|
||||
const fetch = require( 'isomorphic-fetch' )
|
||||
const { dev } = require('../modules/helpers')
|
||||
|
||||
exports.notify_discord_of_new_outfit = async function( username, content, avatar_url, image_title, image_url ) {
|
||||
|
||||
try {
|
||||
|
||||
// Construct discord webhook message
|
||||
const message = {
|
||||
username,
|
||||
content,
|
||||
avatar_url,
|
||||
allowed_mentions: {
|
||||
parse: [ 'users' ]
|
||||
},
|
||||
embeds: [
|
||||
{ title: 'Current outfit', thumbnail: { url: avatar_url } },
|
||||
{ title: image_title, thumbnail: { url: image_url } } ]
|
||||
}
|
||||
|
||||
// Construct request options
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( message )
|
||||
}
|
||||
|
||||
// Make webhook request
|
||||
const data = await fetch( discord.webhookurl, options ).then( res => res.json() )
|
||||
if( data.code ) throw new Error ( `Discord webhook failed with ${ data.code }: ${ data.message }` )
|
||||
|
||||
} catch( e ) {
|
||||
console.error( 'Discord error ', e )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.notify_discord_of_outfit_notifications = async function( amount_notified=0, amount_with_available=0 ) {
|
||||
|
||||
if( !dev && amount_with_available == 0 ) return console.log( `Not sending Discord message for 0 updates` )
|
||||
|
||||
try {
|
||||
|
||||
// Construct discord webhook message
|
||||
const message = {
|
||||
username: "Gretal Marchall Alon of Jupiter",
|
||||
content: `${ amount_with_available } Rocketeers have outfits available. I emailed ${ amount_notified } holders to tell them they have new outfits available in the changing room at https://mint.rocketeer.fans/#/outfits. Want to get email notifications too? Create an email address for your wallet at: https://signer.is/#/email, you'll get a monthly email when your Rocketeers have outfits available.`,
|
||||
avatar_url: "https://storage.googleapis.com/rocketeer-nft.appspot.com/mainnetRocketeers/1.jpg"
|
||||
}
|
||||
|
||||
// Construct request options
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( message )
|
||||
}
|
||||
|
||||
// Make webhook request
|
||||
const data = await fetch( discord.chatterwebhookurl, options )
|
||||
|
||||
} catch( e ) {
|
||||
console.error( 'Discord error ', e )
|
||||
}
|
||||
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const { printapi } = functions.config()
|
||||
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||
const { log } = require( '../modules/helpers' )
|
||||
const fetch = require( 'isomorphic-fetch' )
|
||||
|
||||
/* ///////////////////////////////
|
||||
// API handlers
|
||||
// /////////////////////////////*/
|
||||
async function call_printapi( endpoint, data, method='POST', format='json', authenticated=true ) {
|
||||
|
||||
const logs = []
|
||||
|
||||
try {
|
||||
|
||||
logs.push( `Call requested: ${method}/${format} ${endpoint} with `, JSON.stringify( data ) )
|
||||
|
||||
// Format url, if it has https use the link as provided
|
||||
const url = endpoint.includes( 'https://' ) ? endpoint : `${ printapi.base_url }${ endpoint }`
|
||||
|
||||
const access_token = authenticated && await get_auth_token()
|
||||
if( authenticated ) log( `Found access token: `, access_token && access_token.slice( 0, 10 ) )
|
||||
if( authenticated && !access_token ) throw new Error( `No access_token found` )
|
||||
|
||||
// Generate headers based on input
|
||||
const headers = {
|
||||
...( format == 'json' && data && { 'Content-Type': 'application/json' } ),
|
||||
...( format == 'form' && data && { 'Content-Type': 'application/x-www-form-urlencoded' } ),
|
||||
...( authenticated && { Authorization: `Bearer ${ access_token }` } )
|
||||
}
|
||||
logs.push( `Headers `, JSON.stringify( headers ) )
|
||||
// Generate data body
|
||||
let body = {}
|
||||
|
||||
// fetch expects json to be stringified
|
||||
if( format == 'json' ) body = JSON.stringify( data )
|
||||
|
||||
// Formdata being formdata
|
||||
if( format == 'form' ) body = new URLSearchParams( data )
|
||||
|
||||
logs.push( `API request data `, body )
|
||||
|
||||
// Focmat fetch options
|
||||
const options = {
|
||||
method,
|
||||
headers,
|
||||
body
|
||||
}
|
||||
|
||||
// Call api
|
||||
logs.push( `Calling ${ url }`, )
|
||||
const response = await fetch( url, options ).then( async res => {
|
||||
|
||||
const json_res = res.clone()
|
||||
const text_res = res.clone()
|
||||
|
||||
try {
|
||||
|
||||
const json_response = await json_res.json()
|
||||
logs.push( `API json response: `, json_response )
|
||||
return json_response
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
const text_response = await text_res.text()
|
||||
logs.push( `API text response: `, text_response )
|
||||
throw new Error( `Non JSON output from API` )
|
||||
|
||||
}
|
||||
|
||||
} )
|
||||
logs.push( `Production call: `, JSON.stringify( { ...headers, ...body } ) )
|
||||
logs.push( `Received `, JSON.stringify( response ) )
|
||||
|
||||
return response
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( `Error calling printapi: `, e )
|
||||
return {
|
||||
error: e.message,
|
||||
tracelog: logs
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function get_auth_token( ) {
|
||||
|
||||
const token_grace_period = 1000 * 60
|
||||
const logs = []
|
||||
|
||||
try {
|
||||
|
||||
// Get cached token
|
||||
let { expires=0, access_token } = await db.collection( 'secrets' ).doc( 'printapi' ).get( ).then( dataFromSnap )
|
||||
logs.push( `Old access token: `, access_token && access_token.slice( 0, 10 ) )
|
||||
|
||||
// If token is still valid
|
||||
if( ( expires - token_grace_period ) > Date.now() ) {
|
||||
logs.push( `Old access token still valid` )
|
||||
return access_token
|
||||
}
|
||||
|
||||
// Grab new token and save it
|
||||
logs.push( `Requesting new token` )
|
||||
const credentials = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: printapi.client_credentials,
|
||||
client_secret: printapi.client_secret
|
||||
}
|
||||
const { access_token: new_access_token, expires_in, ...errors } = await call_printapi( `/v2/oauth`, credentials, 'POST', 'form', false )
|
||||
|
||||
logs.push( `New access token: `, new_access_token && new_access_token.slice( 0, 10 ) )
|
||||
if( errors ) logs.push( `Access token error: `, errors )
|
||||
if( !new_access_token ) throw new Error( `No access token available` )
|
||||
|
||||
// Write new access token to cache
|
||||
await db.collection( 'secrets' ).doc( 'printapi' ).set( {
|
||||
access_token: new_access_token,
|
||||
// expires_in is in seconds
|
||||
expires: Date.now() + ( expires_in * 1000 )
|
||||
}, { merge: true } )
|
||||
|
||||
|
||||
|
||||
return new_access_token
|
||||
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( `Error getting auth token `, e )
|
||||
console.log( 'Access token error: ', JSON.stringify( logs ) )
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Order flow functionality
|
||||
// /////////////////////////////*/
|
||||
async function make_printapi_order ( { image_url, product_id, quantity=1, address={}, email } ) {
|
||||
|
||||
// Demo data
|
||||
// email = 'info@rocketeer.fans'
|
||||
// image_url = 'https://storage.googleapis.com/rocketeer-nft.appspot.com/mainnetRocketeers/1.jpg'
|
||||
// product_id = 'kurk_20x20'
|
||||
// address = {
|
||||
// "address": {
|
||||
// "name": "John Doe",
|
||||
// "line1": "Osloweg 75",
|
||||
// "postCode": "9700 GE",
|
||||
// "city": "Groningen",
|
||||
// "country": "NL"
|
||||
// }
|
||||
// }
|
||||
|
||||
const logs = []
|
||||
|
||||
try {
|
||||
|
||||
// Validations
|
||||
if( !email || !image_url || !product_id ) throw new Error( `Missing order data` )
|
||||
if( Object.keys( address ).length != 5 ) throw new Error( `Malformed address` )
|
||||
|
||||
// Make the order on printapi backenc
|
||||
const order = {
|
||||
email,
|
||||
items: [
|
||||
{
|
||||
productId: product_id,
|
||||
quantity,
|
||||
files: { content: image_url }
|
||||
}
|
||||
],
|
||||
shipping: {
|
||||
address
|
||||
}
|
||||
}
|
||||
logs.push( `Creating order: `, order )
|
||||
const { checkout, error: order_error, ...order_details } = await call_printapi( `/v2/orders`, order )
|
||||
logs.push( `Order made with `, checkout, order_details )
|
||||
|
||||
if( order_error ) {
|
||||
logs.push( `Order errored with `, order_error )
|
||||
throw new Error( order_error )
|
||||
}
|
||||
|
||||
// Generate pament link
|
||||
const { error: checkout_error, paymentUrl, amount, ...checkout_details } = await call_printapi( checkout.setupUrl, {
|
||||
billing: {
|
||||
address
|
||||
},
|
||||
returnUrl: `https://tools.rocketeer.fans/#/merch/success/${ order_details.id }`
|
||||
} )
|
||||
|
||||
logs.push( `Checkout responded with`, paymentUrl, amount, checkout_details )
|
||||
|
||||
if( checkout_error ) {
|
||||
logs.push( `Checkout errored with `, checkout_error )
|
||||
throw new Error( checkout_error )
|
||||
}
|
||||
|
||||
return {
|
||||
paymentUrl,
|
||||
amount
|
||||
}
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
return {
|
||||
error: e.message,
|
||||
tracelog: logs
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.order_merch = async ( req, res ) => {
|
||||
|
||||
const logs = []
|
||||
|
||||
try {
|
||||
|
||||
logs.push( `Making API request based on body: `, req.body )
|
||||
const { error, ...order } = await make_printapi_order( req.body )
|
||||
logs.push( `Received: `, error, order )
|
||||
if( error ) throw new Error( error )
|
||||
|
||||
return res.json( { ...order, tracelog: logs } )
|
||||
|
||||
} catch( e ) {
|
||||
return res.json( {
|
||||
error: e.message,
|
||||
tracelog: logs
|
||||
} )
|
||||
}
|
||||
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
const { db } = require( '../modules/firebase' )
|
||||
|
||||
// Web3 APIs
|
||||
const Web3 = require( 'web3' )
|
||||
const web3 = new Web3()
|
||||
|
||||
/* ///////////////////////////////
|
||||
// POST handler for notifier signup
|
||||
// /////////////////////////////*/
|
||||
exports.subscribe_address_to_notifications = 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` } )
|
||||
|
||||
// Lowercase the address
|
||||
address = address.toLowerCase()
|
||||
|
||||
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, discord_handle, chainId } = messageObject
|
||||
const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
|
||||
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !discord_handle || !network ) {
|
||||
throw new Error( `Invalid subscribeToAddress message with ${ signer }, ${confirmedSignatory}, ${discord_handle}, ${chainId}, ${network}` )
|
||||
}
|
||||
|
||||
await db.collection( `${network}Notifications` ).doc( address ).set( {
|
||||
discord_handle
|
||||
} )
|
||||
|
||||
return res.json( {
|
||||
success: true
|
||||
} )
|
||||
|
||||
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
// Log error for debugging
|
||||
console.error( `POST subscribeToAddress ${ address }: `, e )
|
||||
|
||||
// Return error to frontend
|
||||
return res.json( { error: e.mesage || e.toString() } )
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
const { contractAddress } = require( '../modules/contract' )
|
||||
const puppeteer = require( 'puppeteer-extra' )
|
||||
const StealthPlugin = require('puppeteer-extra-plugin-stealth')
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Force opensea to update metadata
|
||||
// /////////////////////////////*/
|
||||
exports.forceOpenseaToUpdateMetadataForRocketeer = async function( tokenId, network='mainnet' ) {
|
||||
|
||||
try {
|
||||
|
||||
const contract = contractAddress[ network ]
|
||||
|
||||
puppeteer.use(StealthPlugin())
|
||||
|
||||
const browser = await puppeteer.launch( { headless: true } )
|
||||
const page = await browser.newPage()
|
||||
|
||||
await page.goto( `https://opensea.io/assets/${ contract }/${ tokenId }`, { waitUntil: 'networkidle2' } )
|
||||
await page.screenshot( { path: 'pre-debug.png' } )
|
||||
await page.click( `i[value=refresh]` )
|
||||
await page.waitForTimeout(5000)
|
||||
await browser.close()
|
||||
|
||||
return true
|
||||
|
||||
} catch( e ) {
|
||||
// Silently log but do not break
|
||||
console.error( e )
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
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 > 250 ) 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() } )
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
const AWS = require('aws-sdk')
|
||||
const functions = require( 'firebase-functions' )
|
||||
const { aws } = functions.config()
|
||||
const { log } = require( '../modules/helpers' )
|
||||
const SES_CONFIG = {
|
||||
accessKeyId: aws?.ses?.keyid,
|
||||
secretAccessKey: aws?.ses?.secretkey,
|
||||
region: aws?.ses?.region,
|
||||
}
|
||||
const AWS_SES = new AWS.SES( SES_CONFIG )
|
||||
|
||||
// Email templates
|
||||
const pug = require('pug')
|
||||
const { promises: fs } = require( 'fs' )
|
||||
const csso = require('csso')
|
||||
const juice = require('juice')
|
||||
|
||||
async function compile_pug_to_email( pugFile, data ) {
|
||||
|
||||
const [ emailPug, inlineNormalise, styleExtra, styleOutlook, rocketeerStyles ] = await Promise.all( [
|
||||
fs.readFile( pugFile ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/normalize.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/extra.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/outlook.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/rocketeers.css`, 'utf8' )
|
||||
] )
|
||||
|
||||
const { css } = csso.minify( [ styleExtra, styleOutlook, inlineNormalise, rocketeerStyles ].join( '\n' ) )
|
||||
const html = pug.render( emailPug, { data, headStyles: css } )
|
||||
const emailifiedHtml = juice.inlineContent( html, [ inlineNormalise, rocketeerStyles ].join( '\n' ), { removeStyleTags: false } )
|
||||
|
||||
return emailifiedHtml
|
||||
|
||||
}
|
||||
|
||||
async function send_email( recipient, subject, html, text ) {
|
||||
|
||||
const options = {
|
||||
Source: aws.ses.fromemail,
|
||||
Destination: {
|
||||
ToAddresses: [ recipient ]
|
||||
},
|
||||
ReplyToAddresses: [],
|
||||
Message: {
|
||||
Body: {
|
||||
Html: { Charset: 'UTF-8', Data: html },
|
||||
Text: { Charset: 'UTF-8', Data: text }
|
||||
},
|
||||
Subject: { Charset: 'UTF-8', Data: subject }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log( `Sending email "${ options.Message.Subject.Data }" from ${ options.Source } to ${ options.Destination.ToAddresses[0] }` )
|
||||
|
||||
return AWS_SES.sendEmail( options ).promise()
|
||||
|
||||
}
|
||||
|
||||
exports.send_outfit_available_email = async ( rocketeers, email ) => {
|
||||
|
||||
|
||||
const email_html = await compile_pug_to_email( `${ __dirname }/../templates/outfit_available.email.pug`, rocketeers )
|
||||
const email_text = ( await fs.readFile( `${ __dirname }/../templates/outfit_available.email.txt`, 'utf8' ) )
|
||||
// .replace( '%%address%%', email_data.address )
|
||||
|
||||
return send_email( email, `Your Rocketeer${rocketeers.length > 1 ? 's' : '' } ${rocketeers.length > 1 ? 'have outfits' : 'has an outfit' } available!`, email_html, email_text )
|
||||
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
const fetch = require( 'isomorphic-fetch' )
|
||||
|
||||
exports.ask_signer_is_for_available_emails = async function( addresses ) {
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Check available email addresses */
|
||||
const endpoint = `https://signer.is/check_availability/`
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
addresses
|
||||
} )
|
||||
}
|
||||
|
||||
const res = await fetch( endpoint, options )
|
||||
const available_addresses = await res.json()
|
||||
|
||||
return available_addresses?.emails_available || []
|
||||
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
// Dependencies
|
||||
const Web3 = require( 'web3' )
|
||||
const functions = require( 'firebase-functions' )
|
||||
const { infura } = functions.config()
|
||||
|
||||
// Contract data
|
||||
const contractAddress = {
|
||||
mainnet: '0xb3767b2033CF24334095DC82029dbF0E9528039d',
|
||||
rinkeby: '0x95d6b9549315212D3FDce9FdCa9d80978b8bB41D'
|
||||
}
|
||||
|
||||
// ABI with only the supply definitions
|
||||
const ABI = [
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
"constant": true
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
||||
// Total current supply, in accordance with ERC721 spec
|
||||
async function getTotalSupply( 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 ] )
|
||||
|
||||
// Return the call promise which returns the total supply
|
||||
return contract.methods.totalSupply().call()
|
||||
|
||||
}
|
||||
|
||||
// Total current supply, in accordance with ERC721 spec
|
||||
async function getOwingAddressOfTokenId( id, 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 ] )
|
||||
|
||||
// Return the call promise which returns the total supply
|
||||
return contract.methods.ownerOf( id ).call()
|
||||
|
||||
}
|
||||
|
||||
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 = {
|
||||
getTotalSupply,
|
||||
contractAddress,
|
||||
getOwingAddressOfTokenId,
|
||||
getTokenIdsOfAddress
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
const functions = require( 'firebase-functions' )
|
||||
const juice = require('juice')
|
||||
|
||||
// Email package
|
||||
const { mailgun } = functions.config()
|
||||
const formData = require( 'form-data' )
|
||||
const Mailgun = require( 'mailgun.js' )
|
||||
const instance = new Mailgun( formData )
|
||||
const mail = instance.client( {
|
||||
username: 'api',
|
||||
key: mailgun.api_key,
|
||||
url: mailgun.api_url
|
||||
})
|
||||
)
|
||||
// Email templates
|
||||
const pug = require('pug')
|
||||
const { promises: fs } = require( 'fs' )
|
||||
const csso = require('csso')
|
||||
|
||||
async function compilePugToEmail( pugFile, rocketeer ) {
|
||||
|
||||
const [ emailPug, inlineNormalise, styleExtra, styleOutlook, rocketeerStyles ] = await Promise.all( [
|
||||
fs.readFile( pugFile ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/normalize.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/extra.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/css-resets/outlook.css`, 'utf8' ),
|
||||
fs.readFile( `${ __dirname }/../templates/rocketeers.css`, 'utf8' )
|
||||
] )
|
||||
|
||||
const { css } = csso.minify( [ styleExtra, styleOutlook, inlineNormalise, rocketeerStyles ].join( '\n' ) )
|
||||
const html = pug.render( emailPug, { rocketeer, headStyles: css } )
|
||||
const emailifiedHtml = juice.inlineContent( html, [ inlineNormalise, rocketeerStyles ].join( '\n' ), { removeStyleTags: false } )
|
||||
|
||||
return emailifiedHtml
|
||||
|
||||
}
|
||||
|
||||
|
||||
exports.send_email_outfit_available = async ( email, rocketeer ) => {
|
||||
|
||||
try {
|
||||
|
||||
rocketeer = { ...rocketeer, first_name: rocketeer.name.split( ' ' )[0] }
|
||||
|
||||
// Build email
|
||||
const msg = {
|
||||
to: email,
|
||||
from: mailgun.from_email,
|
||||
subject: `Outfit available for Rocketeer ${ rocketeer.name }`,
|
||||
text: ( await fs.readFile( `${ __dirname }/../templates/outfit-available.txt`, 'utf8' ) ).replace( '%%name%%', rocketeer.name ),
|
||||
html: await compilePugToEmail( `${ __dirname }/../templates/outfit-available.email.pug`, rocketeer ),
|
||||
}
|
||||
|
||||
await mail.messages.create( mailgun.from_domain, msg )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( e )
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
// Dependencies
|
||||
const { initializeApp } = require( 'firebase-admin/app' )
|
||||
const { getFirestore, FieldValue, FieldPath } = require( 'firebase-admin/firestore' )
|
||||
|
||||
// Admin api
|
||||
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,
|
||||
dataFromSnap: dataFromSnap
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © Arthur Koch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,67 +0,0 @@
|
||||
# normalize.email.css
|
||||
|
||||
CSS resets for HTML emails
|
||||
|
||||
It's just a little css library for best default email compatibility. You can use it with your favourite email framework and self-coded templates.
|
||||
|
||||
## What does it do?
|
||||
|
||||
- Preserves useful defaults for most email clients
|
||||
- Makes native platform font styling
|
||||
- Corrects some popular bugs
|
||||
- Explains what code does using comments
|
||||
|
||||
Please let me know if comments not informative and must be detailed
|
||||
|
||||
## Contents
|
||||
|
||||
- normalize.css - must be inlined to your newsletter in production
|
||||
- extra.css - must be placed between `<style>` tags in `<head>` of your newsletter in production
|
||||
- outlook.css - must be placed between `<style>` tags with conditional comment in `<head>` of your newsletter in production. Check out example.html to learn correct code
|
||||
|
||||
## Example
|
||||
|
||||
``` html
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- normalize.css contents must be inlined to newsletter -->
|
||||
<link href="normalize.css" rel="stylesheet">
|
||||
<link href="extra.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Put extra.css contents here */
|
||||
</style>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<link href="outlook.css" rel="stylesheet">
|
||||
/* Put outlook.css contents here */
|
||||
<![endif]-->
|
||||
<!-- Left title element empty to prevent viewing this text in subject line on Android 4 email clients -->
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body class="body">
|
||||
<div class="webkit">
|
||||
<!-- An example of bulletproof container with limited row length -->
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<!-- Add here this element -->
|
||||
<!-- <th></th> -->
|
||||
<!-- to align container to center -->
|
||||
<th width="500" align="left">
|
||||
<!-- Content here -->
|
||||
<!-- You can use any HTML code which you prefer -->
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
```
|
@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- normalize.css contents must be inlined to newsletter -->
|
||||
<link href="normalize.css" rel="stylesheet">
|
||||
<link href="extra.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Put extra.css contents here */
|
||||
</style>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<link href="outlook.css" rel="stylesheet">
|
||||
/* Put outlook.css contents here */
|
||||
<![endif]-->
|
||||
<!-- Left title element empty to prevent viewing this text in subject line on Android 4 email clients -->
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body class="body">
|
||||
<div class="webkit">
|
||||
<!-- An example of bulletproof container with limited row length -->
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<!-- Add here this element -->
|
||||
<!-- <th></th> -->
|
||||
<!-- to align container to center -->
|
||||
<th width="500" align="left">
|
||||
<p>You are receiving this because a known security researcher submitted proof of finding credentials for your npm user account on the internet.</p>
|
||||
|
||||
<p>In order to prevent unauthorized access, we've changed the password to your account and invalidated all of your active npm tokens.</p>
|
||||
|
||||
<p>Please click on the following link, or paste this into your browser to reset your password:</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://www.npmjs.com/forgot">https://www.npmjs.com/forgot</a></li>
|
||||
</ul>
|
||||
|
||||
<p>When you reset your password please do not set it back to the old value.</p>
|
||||
|
||||
<p>We have no reason to believe that your account was compromised, but cannot be certain of this. This reset is preemptive, to prevent future compromise.</p>
|
||||
|
||||
<p>If you have questions:</p>
|
||||
<ol>
|
||||
<li>You can reply to this message or email <a href="support@npmjs.com">support@npmjs.com</a>.</li>
|
||||
<li>You can also read more about this undertaking in our <a href="http://blog.npmjs.org/post/161515829950/credentials-resets">blog post</a>.</li>
|
||||
</ol>
|
||||
|
||||
<p>Npm loves you.</p>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,29 +0,0 @@
|
||||
/* Extra.css */
|
||||
/* Contents of this file must be placed between <style> tags in <head> of your newsletter in production */
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
u + .body {
|
||||
/* iOS Gmail viewport fix */
|
||||
/* Make sure that your body element has .body class */
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
a[x-apple-data-detectors=true] {
|
||||
/* Set default text color inheritance for auto-detected iOS links like date, time, address, etc */
|
||||
color: inherit !important;
|
||||
text-decoration: inherit !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
body {
|
||||
/* Set native platform font styling */
|
||||
font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
}
|
||||
.webkit {
|
||||
/* Webkit and Microsoft font-size fix */
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/* Normalize.css */
|
||||
/* Contents of this file must be inlined to your newsletter in production */
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a,
|
||||
li a,
|
||||
p a {
|
||||
/* Set sexy underline styling for links except images */
|
||||
text-decoration: none;
|
||||
color: #2837b8 !important;
|
||||
border-bottom: #d3d6f0 1px solid;
|
||||
}
|
||||
h1 {
|
||||
/* Mail.ru <h1> styling fix */
|
||||
font-size: 2em;
|
||||
line-height: initial;
|
||||
margin: 0.67em 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
/* Null tables spaces */
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 0;
|
||||
}
|
||||
table th {
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
img {
|
||||
/* Flexible images fix + prevent any borders for images */
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
/* Set image's ALT text styling */
|
||||
color: #2837b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
/* We don't touch horizontal margins to prevent hiding bullets in Oultook */
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
ol li,
|
||||
ul li {
|
||||
line-height: 1.6em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
p {
|
||||
line-height: 1.6em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
span.code {
|
||||
/* Monospace emphasis for code examples */
|
||||
font-family: consolas, courier, monospace;
|
||||
color: grey;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/* Outlook.css */
|
||||
/* Contents of this file must be placed between <style> tags with conditional comment in <head> of your newsletter in production */
|
||||
|
||||
body {
|
||||
/* Reset font styling. Useful when we links custom fonts to our newsletter */
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
a {
|
||||
/* Reset default links styling */
|
||||
color: #2837b8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
/* Reset default headings margin */
|
||||
margin: .5em 0;
|
||||
}
|
||||
img {
|
||||
/* Scaled images fix */
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
table {
|
||||
/* Null tables spaces */
|
||||
border-collapse: collapse;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
/*:root {
|
||||
--color-primary: #8076fa;
|
||||
--color-text: rgb(77, 86, 128);
|
||||
--color-accent: rgb( 248, 117, 136 );
|
||||
--color-backdrop: rgba( 0, 0, 0, .05 );
|
||||
}*/
|
||||
|
||||
/* Note that web fonts DO NOT WORK in most email clients */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo&family=Comfortaa:wght@500&display=swap');
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Archivo', 'Helvetica Neue', sans-serif;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Flow text
|
||||
// /////////////////////////////*/
|
||||
html {
|
||||
font-size: calc( 18px + .1vw );
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Brand styles
|
||||
// /////////////////////////////*/
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
font-family: 'Comfortaa', sans-serif;
|
||||
text-align: left;
|
||||
/*color: var( --color-primary );*/
|
||||
color: #8076fa;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 1rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
/*color: var( --color-accent );*/
|
||||
color: rgb( 248, 117, 136 );
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
line-height: 1.5rem;
|
||||
/*color: var( --color-text );*/
|
||||
color: rgb(77, 86, 128);
|
||||
text-align: left
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
margin: .5rem;
|
||||
margin-left: 0;
|
||||
text-decoration: none;
|
||||
/*border: 2px solid var( --color-primary );*/
|
||||
border: 2px solid #8076fa;
|
||||
/*color: var( --color-primary );*/
|
||||
color: #8076fa;
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
/*background: var( --color-backdrop );*/
|
||||
background: rgba( 0, 0, 0, .05 );
|
||||
border: none;
|
||||
/*border-left: 2px solid var( --color-primary );*/
|
||||
border-left: 2px solid #8076fa;
|
||||
padding: 1rem;
|
||||
margin: 1rem 10% 1rem 0;
|
||||
width: 90%;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
doctype html
|
||||
html( lang='en' )
|
||||
head
|
||||
style= headStyles
|
||||
body
|
||||
p Hello,
|
||||
p Someone requested for emails to #{ data.address }@signer.is (and #{ data.ENS } ENS) to be delivered to this email address.
|
||||
p If this was not you, you can safely ignore this email.
|
||||
p To receive emails at this address, please verify your ownership of this email by clicking the link below.
|
||||
a( href=data.verification_link ) Click here to verify your email address.
|
||||
p Is the above link not working? Visit this page manually: #{ data.verification_link }
|
||||
p Have a great day,
|
||||
p ~ Signer.is
|
@ -1,13 +0,0 @@
|
||||
Hello,
|
||||
|
||||
Someone requested for emails to %%address%%@signer.is (and %%ENS%% ENS) to be delivered to this email address.
|
||||
|
||||
If this was not you, you can safely ignore this email.
|
||||
|
||||
To receive emails at this address, please verify your ownership of this email by clicking the link below.
|
||||
|
||||
Open this link to verify your email address: %%verification_link%%.
|
||||
|
||||
Have a great day,
|
||||
|
||||
~ Signer.is
|
@ -1,300 +0,0 @@
|
||||
const { db, dataFromSnap, FieldValue } = require( '../modules/firebase' )
|
||||
const { getRgbArrayFromColorName, randomNumberBetween } = require( '../modules/helpers' )
|
||||
const { getTokenIdsOfAddress } = require( '../modules/contract' )
|
||||
const svgFromAttributes = require( './svg-generator' )
|
||||
const { notify_discord_of_new_outfit } = require( '../integrations/discord' )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rocketeer outfit generator
|
||||
// ///////////////////////////////
|
||||
async function getRocketeerIfOutfitAvailable( id, network='mainnet', retry=false ) {
|
||||
|
||||
if( retry ) console.log( `Retry of getRocketeerIfOutfitAvailable for ${ id } on ${ network }` )
|
||||
|
||||
const newOutfitAllowedInterval = 1000 * 60 * 60 * 24 * 30
|
||||
|
||||
// Retreive old Rocketeer data
|
||||
const rocketeer = await db.collection( `${ network }Rocketeers` ).doc( id ).get().then( dataFromSnap )
|
||||
|
||||
// If this is a retry attempt, skip last change validation
|
||||
if( retry ) return rocketeer
|
||||
|
||||
// Validate this request
|
||||
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 (${ new Date( Date.now() + timeUntilAllowedToChange ).toString() })` )
|
||||
|
||||
return rocketeer
|
||||
|
||||
|
||||
}
|
||||
async function generateNewOutfitFromId( id, network='mainnet', retry ) {
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Changing room variables
|
||||
// /////////////////////////////*/
|
||||
// Set the entropy level. 255 would mean 0 can become 255 and -255
|
||||
let colorEntropy = 10
|
||||
const specialEditionMultiplier = 1.1
|
||||
const entropyMultiplier = 1.1
|
||||
|
||||
// Retreive old Rocketeer data if outfit is available
|
||||
const rocketeer = await getRocketeerIfOutfitAvailable( id, network, retry )
|
||||
|
||||
// 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 }
|
||||
|
||||
// Apply entropy levels based on edition status and outfits available
|
||||
const { value: edition } = rocketeer.attributes.find( ( { trait_type } ) => trait_type == "edition" )
|
||||
if( edition != 'regular' ) colorEntropy *= specialEditionMultiplier
|
||||
if( available_outfits ) colorEntropy *= ( entropyMultiplier ** available_outfits )
|
||||
|
||||
// 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}
|
||||
try {
|
||||
|
||||
// Generate new outfit
|
||||
const newOutfitSvg = await svgFromAttributes( rocketeer.attributes, `${ network }Rocketeers/${ id }-${ available_outfits + 1 }` )
|
||||
|
||||
// Notify discord
|
||||
const [ firstname ] = rocketeer.name.split( ' ' )
|
||||
await notify_discord_of_new_outfit(
|
||||
rocketeer.name,
|
||||
`${ firstname } obtained a new outfit on ${ network }! \n\nView this Rocketeer on Opensea: https://opensea.io/assets/0xb3767b2033cf24334095dc82029dbf0e9528039d/${ id }.\n\nView all outfits on the Rocketeer toolkit: https://tools.rocketeer.fans/#/outfits/${ id }.`,
|
||||
rocketeer.image,
|
||||
`Outfit #${ available_outfits + 1 }`,
|
||||
newOutfitSvg.replace( '.svg','.jpg' )
|
||||
)
|
||||
|
||||
return newOutfitSvg
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
// If the svg generation failed, reset the attributes to their previous value
|
||||
await db.collection( `${ network }Rocketeers` ).doc( id ).set( {
|
||||
attributes: [
|
||||
...staticAttributes,
|
||||
{ trait_type: 'available outfits', value: available_outfits, },
|
||||
{ trait_type: 'last outfit change', value: last_outfit_change, display_type: "date" }
|
||||
]
|
||||
} ,{ merge: true } )
|
||||
|
||||
// Propagate error
|
||||
throw e
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function queueRocketeersOfAddressForOutfitChange( address, network='mainnet' ) {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const ids = await getTokenIdsOfAddress( address, network )
|
||||
|
||||
const idsWithOutfitsAvailable = await Promise.all( ids.map( async id => {
|
||||
|
||||
try {
|
||||
|
||||
// If rocketeer has outfit, return id
|
||||
await getRocketeerIfOutfitAvailable( id )
|
||||
return id
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
// If no outfit available, return false
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
} ) )
|
||||
|
||||
// Filter out the 'false' entries
|
||||
const onlyIds = idsWithOutfitsAvailable.filter( id => id )
|
||||
|
||||
// Mark Rocketeers for processing
|
||||
await Promise.all( onlyIds.map( id => db.collection( `${ network }QueueOutfitGeneration` ).doc( id ).set( {
|
||||
updated: Date.now(),
|
||||
running: false,
|
||||
network,
|
||||
address
|
||||
}, { merge: true } ) ) )
|
||||
|
||||
// Return amount queued for meta tracking
|
||||
return onlyIds.length
|
||||
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
console.error( `Error in queueRocketeersOfAddressForOutfitChange: `, e )
|
||||
throw e
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function handleQueuedRocketeerOutfit( change, context ) {
|
||||
|
||||
// If this was not a newly added queue item, exit gracefully
|
||||
if( change.before.exists ) return
|
||||
|
||||
// If this was a deletion, exit gracefully
|
||||
if( !change.after.exists ) return
|
||||
|
||||
const { rocketeerId } = context.params
|
||||
const { network, running, address, retry } = change.after.data()
|
||||
|
||||
if( retry ) console.log( `Document change for ${network}QueueOutfitGeneration/${rocketeerId} is a retry attempt` )
|
||||
|
||||
try {
|
||||
|
||||
/////
|
||||
// Validations
|
||||
|
||||
// If process is already running, stop
|
||||
if( running ) throw new Error( `Rocketeer ${ rocketeerId } is already generating a new outfit for ${ network }` )
|
||||
|
||||
/////
|
||||
// Start the generation process
|
||||
|
||||
// Mark this entry as running
|
||||
await db.collection( `${network}QueueOutfitGeneration` ).doc( rocketeerId ).set( { running: true, updated: Date.now() }, { merge: true } )
|
||||
|
||||
// Generate the new outfit
|
||||
await generateNewOutfitFromId( rocketeerId, network, retry )
|
||||
|
||||
} catch( e ) {
|
||||
|
||||
// if this was just a "too recently" error, exit gracefully
|
||||
if( e.message.includes( 'You changed your outfit too recently' ) ) return
|
||||
|
||||
// Log error to console and store
|
||||
console.error( `handleQueuedRocketeerOutfit error: `, e )
|
||||
await db.collection( 'errors' ).add( {
|
||||
source: `handleQueuedRocketeerOutfit`,
|
||||
network,
|
||||
rocketeerId,
|
||||
updated: Date.now(),
|
||||
timestamp: new Date().toString(),
|
||||
error: e.message || e.toString()
|
||||
} )
|
||||
|
||||
} finally {
|
||||
|
||||
// Delete queue entry
|
||||
await db.collection( `${network}QueueOutfitGeneration` ).doc( rocketeerId ).delete( )
|
||||
|
||||
// Mark the outfits generating as decremented
|
||||
await db.collection( 'meta' ).doc( address ).set( {
|
||||
updated: Date.now(),
|
||||
outfits_in_queue: FieldValue.increment( -1 )
|
||||
}, { merge: true } )
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// async function generateNewOutfitsByAddress( address, network='mainnet' ) {
|
||||
|
||||
|
||||
// try {
|
||||
// const ids = await getTokenIdsOfAddress( address, network )
|
||||
|
||||
// // Build outfit generation queue
|
||||
// const queue = ids.map( id => function() {
|
||||
|
||||
// // Generate new outfit and return it
|
||||
// // Since "no outfit available until X" is an error, we'll catch the errors and propagate them
|
||||
// return generateNewOutfitFromId( id, network ).then( outfit => ( { id: id, src: outfit } ) ).catch( e => {
|
||||
|
||||
// // Log out unexpected errors
|
||||
// if( !e.message.includes( 'You changed your outfit too recently' ) ) console.error( 'Unexpected error in generateNewOutfitFromId: ', e )
|
||||
|
||||
// return { id: id, error: e.message || e.toString() }
|
||||
|
||||
// } )
|
||||
|
||||
// } )
|
||||
|
||||
// const outfits = await Throttle.all( queue, {
|
||||
// maxInProgress: 10,
|
||||
// failFast: false,
|
||||
// progressCallback: ( { amountDone, rejectedIndexes } ) => {
|
||||
// process.env.NODE_ENV == 'development' ? console.log( `Completed ${amountDone}/${queue.length}, rejected: `, rejectedIndexes ) : false
|
||||
// }
|
||||
// } )
|
||||
|
||||
// return {
|
||||
// success: outfits.filter( ( { src } ) => src ),
|
||||
// error: outfits.filter( ( { error } ) => error ),
|
||||
// }
|
||||
|
||||
// } catch( e ) {
|
||||
// console.error( `Error in generateNewOutfitsByAddress: `, e )
|
||||
// throw e
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
module.exports = {
|
||||
generateNewOutfitFromId,
|
||||
// generateNewOutfitsByAddress,
|
||||
queueRocketeersOfAddressForOutfitChange,
|
||||
handleQueuedRocketeerOutfit
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
const name = require( 'random-name' )
|
||||
const { db } = require( '../modules/firebase' )
|
||||
const { getTotalSupply } = require( '../modules/contract' )
|
||||
const { pickRandomArrayEntry, pickRandomAttributes, randomNumberBetween, globalAttributes, heavenlyBodies, web2domain, getColorName } = require( '../modules/helpers' )
|
||||
const svgFromAttributes = require( './svg-generator' )
|
||||
const { forceOpenseaToUpdateMetadataForRocketeer } = require( '../integrations/opensea' )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Caching
|
||||
// ///////////////////////////////
|
||||
async function isInvalidRocketeerId( id, network='mainnet' ) {
|
||||
|
||||
// Force type onto id
|
||||
id = Number( id )
|
||||
|
||||
// Chech if this is an illegal ID
|
||||
try {
|
||||
|
||||
// Get the last know total supply
|
||||
let { cachedTotalSupply } = await db.collection( 'meta' ).doc( network ).get().then( doc => doc.data() ) || {}
|
||||
|
||||
// Cast total supply into number
|
||||
cachedTotalSupply = Number( cachedTotalSupply )
|
||||
|
||||
// If the requested ID is larger than that, check if the new total supply is more
|
||||
if( !cachedTotalSupply || cachedTotalSupply < id ) {
|
||||
|
||||
// Get net total supply through infura, if infura fails, return the cached value just in case
|
||||
const totalSupply = Number( await getTotalSupply( network ) )
|
||||
|
||||
// Write new value to cache
|
||||
await db.collection( 'meta' ).doc( network ).set( { cachedTotalSupply: totalSupply }, { merge: true } )
|
||||
|
||||
// If the requested ID is larger than total supply, exit
|
||||
if( totalSupply < id ) return `Invalid ID ${ id }, total supply is ${ totalSupply }`
|
||||
|
||||
// If all good, return true
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
} catch( e ) {
|
||||
return e
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function getExistingRocketeer( id, network='mainnet' ) {
|
||||
|
||||
return db.collection( `${ network }Rocketeers` ).doc( id ).get().then( doc => doc.data() ).catch( f => false )
|
||||
|
||||
}
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rocketeer generator
|
||||
// ///////////////////////////////
|
||||
async function generateRocketeer( id, network='mainnet' ) {
|
||||
|
||||
// Put dibs on the Rocketeer ID to make race conditions more unlikely
|
||||
await db.collection( `${ network }Rocketeers` ).doc( id ).set( {}, { merge: true } )
|
||||
|
||||
// The base object of a new Rocketeer
|
||||
const rocketeer = {
|
||||
name: `${ name.first() } ${ name.middle() } ${ name.last() } of ${ id % 42 == 0 ? 'the Towel' : pickRandomArrayEntry( heavenlyBodies ) }`,
|
||||
description: '',
|
||||
image: ``,
|
||||
external_url: `https://viewer.rocketeer.fans/?rocketeer=${ id }` + ( network == 'mainnet' ? '' : '&testnet=true' ),
|
||||
attributes: []
|
||||
}
|
||||
|
||||
// Generate randomized attributes
|
||||
rocketeer.attributes = pickRandomAttributes( globalAttributes )
|
||||
|
||||
// Set birthday
|
||||
rocketeer.attributes.push( {
|
||||
"display_type": "date",
|
||||
"trait_type": "birthday",
|
||||
"value": Math.floor( Date.now() / 1000 )
|
||||
} )
|
||||
|
||||
// Special editions
|
||||
const edition = { "trait_type": "edition", value: "regular" }
|
||||
if( id <= 50 ) edition.value = 'genesis'
|
||||
if( id >= ( 3475 - 166 ) ) edition.value = 'straggler'
|
||||
if( id % 42 === 0 ) edition.value = 'hitchhiker'
|
||||
if( ( id - 1 ) % 42 == 0 ) edition.value = 'generous'
|
||||
rocketeer.attributes.push( edition )
|
||||
|
||||
// Create description
|
||||
rocketeer.description = `${ rocketeer.name } is a proud member of the ${ rocketeer.attributes.find( ( { trait_type } ) => trait_type == 'patch' ).value } guild.`
|
||||
|
||||
// Write the incomplete Rocketeer to the database, because opensea doesn't update metadata by itself
|
||||
await db.collection( `${ network }Rocketeers` ).doc( id ).set( rocketeer, { merge: true } )
|
||||
|
||||
// Generate color attributes
|
||||
rocketeer.attributes.push( {
|
||||
"trait_type": "outfit color",
|
||||
value: `rgb( ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) } )`
|
||||
} )
|
||||
rocketeer.attributes.push( {
|
||||
"trait_type": "outfit accent color",
|
||||
value: `rgb( ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) } )`
|
||||
} )
|
||||
rocketeer.attributes.push( {
|
||||
"trait_type": "backpack color",
|
||||
value: `rgb( ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) } )`
|
||||
} )
|
||||
rocketeer.attributes.push( {
|
||||
"trait_type": "visor color",
|
||||
value: `rgb( ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) }, ${ randomNumberBetween( 0, 255 ) } )`
|
||||
} )
|
||||
|
||||
// Generate, compile and upload image
|
||||
rocketeer.image = await svgFromAttributes( rocketeer.attributes, `${ network }Rocketeers/${id}` )
|
||||
|
||||
// Namify the attributes
|
||||
rocketeer.attributes = rocketeer.attributes.map( attribute => {
|
||||
|
||||
if( !attribute.trait_type.includes( 'color' ) ) return attribute
|
||||
return {
|
||||
...attribute,
|
||||
value: getColorName( attribute.value )
|
||||
}
|
||||
|
||||
} )
|
||||
|
||||
// Save new Rocketeer
|
||||
await db.collection( `${ network }Rocketeers` ).doc( id ).set( rocketeer, { merge: true } )
|
||||
|
||||
// Force opensea to update metadata
|
||||
await forceOpenseaToUpdateMetadataForRocketeer( id, network )
|
||||
|
||||
return rocketeer
|
||||
|
||||
}
|
||||
|
||||
async function safelyReturnRocketeer( id, network ) {
|
||||
|
||||
// Chech if this is an illegal ID
|
||||
const invalidId = await isInvalidRocketeerId( id, network )
|
||||
if( invalidId ) throw invalidId
|
||||
|
||||
// Get old rocketeer if it exists
|
||||
const oldRocketeer = await getExistingRocketeer( id, network )
|
||||
if( oldRocketeer ) return oldRocketeer
|
||||
|
||||
// If no old rocketeer exists, make a new one and save it
|
||||
return generateRocketeer( id, network )
|
||||
|
||||
}
|
||||
|
||||
async function safelyReturnMultipleRocketeers( ids=[], network='mainnet' ) {
|
||||
|
||||
|
||||
// Chech if this is an illegal ID
|
||||
const invalidIds = await Promise.all( ids.map( id => isInvalidRocketeerId( id, network ) ) )
|
||||
if( invalidIds.includes( true ) ) throw invalidIds
|
||||
|
||||
// Get old rocketeers and append their ids
|
||||
const rocketeers = await Promise.all( ids.map( async id => ( {
|
||||
...await getExistingRocketeer( id, network ),
|
||||
id: id
|
||||
} ) ) )
|
||||
|
||||
// Send back an array of rocketeers, but not any failed ones
|
||||
return rocketeers.filter( rocketeer => rocketeer )
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
web2domain,
|
||||
safelyReturnRocketeer,
|
||||
safelyReturnMultipleRocketeers
|
||||
}
|
16644
functions/package-lock.json
generated
@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "functions",
|
||||
"description": "Cloud Functions for Firebase",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"runtime": "firebase functions:config:get > .runtimeconfig.json",
|
||||
"serve": "firebase emulators:start --only functions",
|
||||
"shell": "development=true firebase functions:shell",
|
||||
"start": "development=true npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.1149.0",
|
||||
"body-parser": "^1.19.1",
|
||||
"color": "^4.0.2",
|
||||
"color-namer": "^1.4.0",
|
||||
"convert-svg-to-jpeg": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"csso": "^5.0.3",
|
||||
"express": "^4.17.1",
|
||||
"firebase-admin": "^10.0.0",
|
||||
"firebase-functions": "^3.11.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsdom": "^18.0.0",
|
||||
"juice": "^8.0.0",
|
||||
"mailgun.js": "^4.1.4",
|
||||
"promise-parallel-throttle": "^3.3.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"pug": "^3.0.2",
|
||||
"puppeteer": "^12.0.0",
|
||||
"puppeteer-extra": "^3.2.3",
|
||||
"puppeteer-extra-plugin-stealth": "^2.9.0",
|
||||
"random-name": "^0.1.2",
|
||||
"web3": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
"@babel/eslint-parser": "^7.15.8",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"firebase-functions-test": "^0.2.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © Arthur Koch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,67 +0,0 @@
|
||||
# normalize.email.css
|
||||
|
||||
CSS resets for HTML emails
|
||||
|
||||
It's just a little css library for best default email compatibility. You can use it with your favourite email framework and self-coded templates.
|
||||
|
||||
## What does it do?
|
||||
|
||||
- Preserves useful defaults for most email clients
|
||||
- Makes native platform font styling
|
||||
- Corrects some popular bugs
|
||||
- Explains what code does using comments
|
||||
|
||||
Please let me know if comments not informative and must be detailed
|
||||
|
||||
## Contents
|
||||
|
||||
- normalize.css - must be inlined to your newsletter in production
|
||||
- extra.css - must be placed between `<style>` tags in `<head>` of your newsletter in production
|
||||
- outlook.css - must be placed between `<style>` tags with conditional comment in `<head>` of your newsletter in production. Check out example.html to learn correct code
|
||||
|
||||
## Example
|
||||
|
||||
``` html
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- normalize.css contents must be inlined to newsletter -->
|
||||
<link href="normalize.css" rel="stylesheet">
|
||||
<link href="extra.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Put extra.css contents here */
|
||||
</style>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<link href="outlook.css" rel="stylesheet">
|
||||
/* Put outlook.css contents here */
|
||||
<![endif]-->
|
||||
<!-- Left title element empty to prevent viewing this text in subject line on Android 4 email clients -->
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body class="body">
|
||||
<div class="webkit">
|
||||
<!-- An example of bulletproof container with limited row length -->
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<!-- Add here this element -->
|
||||
<!-- <th></th> -->
|
||||
<!-- to align container to center -->
|
||||
<th width="500" align="left">
|
||||
<!-- Content here -->
|
||||
<!-- You can use any HTML code which you prefer -->
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
```
|
@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- normalize.css contents must be inlined to newsletter -->
|
||||
<link href="normalize.css" rel="stylesheet">
|
||||
<link href="extra.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Put extra.css contents here */
|
||||
</style>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<link href="outlook.css" rel="stylesheet">
|
||||
/* Put outlook.css contents here */
|
||||
<![endif]-->
|
||||
<!-- Left title element empty to prevent viewing this text in subject line on Android 4 email clients -->
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body class="body">
|
||||
<div class="webkit">
|
||||
<!-- An example of bulletproof container with limited row length -->
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<!-- Add here this element -->
|
||||
<!-- <th></th> -->
|
||||
<!-- to align container to center -->
|
||||
<th width="500" align="left">
|
||||
<p>You are receiving this because a known security researcher submitted proof of finding credentials for your npm user account on the internet.</p>
|
||||
|
||||
<p>In order to prevent unauthorized access, we've changed the password to your account and invalidated all of your active npm tokens.</p>
|
||||
|
||||
<p>Please click on the following link, or paste this into your browser to reset your password:</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://www.npmjs.com/forgot">https://www.npmjs.com/forgot</a></li>
|
||||
</ul>
|
||||
|
||||
<p>When you reset your password please do not set it back to the old value.</p>
|
||||
|
||||
<p>We have no reason to believe that your account was compromised, but cannot be certain of this. This reset is preemptive, to prevent future compromise.</p>
|
||||
|
||||
<p>If you have questions:</p>
|
||||
<ol>
|
||||
<li>You can reply to this message or email <a href="support@npmjs.com">support@npmjs.com</a>.</li>
|
||||
<li>You can also read more about this undertaking in our <a href="http://blog.npmjs.org/post/161515829950/credentials-resets">blog post</a>.</li>
|
||||
</ol>
|
||||
|
||||
<p>Npm loves you.</p>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,29 +0,0 @@
|
||||
/* Extra.css */
|
||||
/* Contents of this file must be placed between <style> tags in <head> of your newsletter in production */
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
u + .body {
|
||||
/* iOS Gmail viewport fix */
|
||||
/* Make sure that your body element has .body class */
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
a[x-apple-data-detectors=true] {
|
||||
/* Set default text color inheritance for auto-detected iOS links like date, time, address, etc */
|
||||
color: inherit !important;
|
||||
text-decoration: inherit !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
body {
|
||||
/* Set native platform font styling */
|
||||
font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
}
|
||||
.webkit {
|
||||
/* Webkit and Microsoft font-size fix */
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
64
functions/templates/css-resets/normalize.css
vendored
@ -1,64 +0,0 @@
|
||||
/* Normalize.css */
|
||||
/* Contents of this file must be inlined to your newsletter in production */
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a,
|
||||
li a,
|
||||
p a {
|
||||
/* Set sexy underline styling for links except images */
|
||||
text-decoration: none;
|
||||
color: #2837b8 !important;
|
||||
border-bottom: #d3d6f0 1px solid;
|
||||
}
|
||||
h1 {
|
||||
/* Mail.ru <h1> styling fix */
|
||||
font-size: 2em;
|
||||
line-height: initial;
|
||||
margin: 0.67em 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
/* Null tables spaces */
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 0;
|
||||
}
|
||||
table th {
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
img {
|
||||
/* Flexible images fix + prevent any borders for images */
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
/* Set image's ALT text styling */
|
||||
color: #2837b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
/* We don't touch horizontal margins to prevent hiding bullets in Oultook */
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
ol li,
|
||||
ul li {
|
||||
line-height: 1.6em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
p {
|
||||
line-height: 1.6em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
span.code {
|
||||
/* Monospace emphasis for code examples */
|
||||
font-family: consolas, courier, monospace;
|
||||
color: grey;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/* Outlook.css */
|
||||
/* Contents of this file must be placed between <style> tags with conditional comment in <head> of your newsletter in production */
|
||||
|
||||
body {
|
||||
/* Reset font styling. Useful when we links custom fonts to our newsletter */
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
a {
|
||||
/* Reset default links styling */
|
||||
color: #2837b8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
/* Reset default headings margin */
|
||||
margin: .5em 0;
|
||||
}
|
||||
img {
|
||||
/* Scaled images fix */
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
table {
|
||||
/* Null tables spaces */
|
||||
border-collapse: collapse;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
doctype html
|
||||
html( lang='en' )
|
||||
head
|
||||
style= headStyles
|
||||
body
|
||||
p Hello!
|
||||
p Your Rocketeers have new outfits available.
|
||||
p To generate new outfits, visit the changing room here: https://mint.rocketeer.fans/#/outfits
|
||||
p Have a great day,
|
||||
p ~ Rocketeer HQ
|
@ -1,9 +0,0 @@
|
||||
Hello!
|
||||
|
||||
Your Rocketeers have new outfits available.
|
||||
|
||||
To generate new outfits, visit the changing room here: https://mint.rocketeer.fans/#/outfits
|
||||
|
||||
Have a great day,
|
||||
|
||||
~ Rocketeer HQ
|
@ -1,75 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Flow text
|
||||
// /////////////////////////////*/
|
||||
html {
|
||||
font-size: calc( 18px + .1vw );
|
||||
}
|
||||
|
||||
/* ///////////////////////////////
|
||||
// Brand styles
|
||||
// /////////////////////////////*/
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-style: italic;
|
||||
margin: 0 0 1rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
line-height: 1.5rem;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
margin: .5rem;
|
||||
margin-left: 0;
|
||||
text-decoration: none;
|
||||
/*border: 2px solid var( --color-primary );*/
|
||||
border: 2px solid black;
|
||||
/*color: var( --color-primary );*/
|
||||
color: black;
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
/*background: var( --color-backdrop );*/
|
||||
background: rgba( 0, 0, 0, .05 );
|
||||
border: none;
|
||||
/*border-left: 2px solid var( --color-primary );*/
|
||||
border-left: 2px solid black;
|
||||
padding: 1rem;
|
||||
margin: 1rem 10% 1rem 0;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
img.avatar {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
padding: 1rem 0;
|
||||
}
|
29
index.js
Normal file
@ -0,0 +1,29 @@
|
||||
require = require("esm")(module);
|
||||
import express from 'express';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Web application framework
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
// Error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
res.locals.message = err.message;
|
||||
// Also log it to the console
|
||||
console.log(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
|
||||
// Render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
// Start listening on the defined port
|
||||
app.listen(4243, "0.0.0.0", function () {
|
||||
console.log(`Listening on port 4243`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})();
|
@ -1,5 +0,0 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function (deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
const Rocketeer = artifacts.require("./Rocketeer.sol");
|
||||
|
||||
module.exports = async (deployer, network, addresses) => {
|
||||
|
||||
// OpenSea proxy registry addresses for rinkeby and mainnet.
|
||||
// Source: https://github.com/ProjectOpenSea/opensea-creatures
|
||||
let proxyRegistryAddress = ""
|
||||
if (network === 'rinkeby') {
|
||||
proxyRegistryAddress = "0xf57b2c51ded3a29e6891aba85459d600256cf317"
|
||||
} else {
|
||||
proxyRegistryAddress = "0xa5409ec958c83c3f309868babaca7c86dcb077c1"
|
||||
}
|
||||
|
||||
// Deploy rocketeer contract
|
||||
await deployer.deploy(Rocketeer, proxyRegistryAddress, {gas: 5000000})
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
40613
minter/package-lock.json
generated
@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "minter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"country-code-lookup": "^0.0.20",
|
||||
"ethers": "^5.4.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.0.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"styled-components": "^5.3.3",
|
||||
"use-interval": "^1.4.0",
|
||||
"web-vitals": "^1.1.2",
|
||||
"web3": "^1.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Rocketeer minting interface"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Mint Rocketeers</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,162 +0,0 @@
|
||||
/*main {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
body * {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
div.container {
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
div.container.wide {
|
||||
width: 1024px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
p.row {
|
||||
max-width: 100%!important;
|
||||
}
|
||||
*/
|
||||
/*Login button*/
|
||||
/*.button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba( 0, 0, 0, .3 );
|
||||
color: rgba( 0, 0, 0, .8 );
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 1.1rem .5rem 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
a img {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
input#address {
|
||||
padding: .2rem .5rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stretchBackground {
|
||||
position: absolute;
|
||||
width: 120%;
|
||||
min-width: 1920px;
|
||||
opacity: .5;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
background: rgba( 255, 255, 255, 1 );
|
||||
box-shadow: 0px 0 5px 5px rgb(0 0 0 / 10%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1, p, label {
|
||||
padding: .2rem .5rem;
|
||||
}*/
|
||||
|
||||
/*Loading spinner*/
|
||||
/*.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading * {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid black;
|
||||
border-color: black transparent black transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
/* ///////////////////////////////
|
||||
/// Avatar page
|
||||
// /////////////////////////////*/
|
||||
/*#avatar .rocketeer {
|
||||
margin: 1rem 0;
|
||||
border-radius: 50%;
|
||||
padding: 1rem;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#avatar input:not([type=radio]) {
|
||||
padding: 1rem;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
#avatar p {
|
||||
margin: 3rem 0;
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.radios {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radios .row {
|
||||
width: 200px;
|
||||
}*/
|
@ -1,38 +0,0 @@
|
||||
import Container from './components/atoms/Container'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { HashRouter} from 'react-router-dom'
|
||||
import Router from './components/router'
|
||||
import Theme from './components/atoms/Theme'
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
// ///////////////////////////////
|
||||
// States
|
||||
// ///////////////////////////////
|
||||
const [ loading, setLoading ] = useState( 'Detecting metamask...' )
|
||||
const [ error, setError ] = useState( undefined )
|
||||
|
||||
// ///////////////////////////////
|
||||
// Lifecycle
|
||||
// ///////////////////////////////
|
||||
|
||||
// Check for web3 on load
|
||||
useEffect( f => window.ethereum ? setLoading( false ) : setError( 'No web3 provider detected, please install metamask' ), [] )
|
||||
|
||||
|
||||
// ///////////////////////////////
|
||||
// Rendering
|
||||
// ///////////////////////////////
|
||||
|
||||
return <Theme>
|
||||
<HashRouter>
|
||||
|
||||
{ error || loading ? <Container> <p>{ error || loading }</p> </Container> : <Router /> }
|
||||
|
||||
</HashRouter>
|
||||
</Theme>
|
||||
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zM6.023 15.416C7.491 17.606 9.695 19 12.16 19c2.464 0 4.669-1.393 6.136-3.584A8.968 8.968 0 0 0 12.16 13a8.968 8.968 0 0 0-6.137 2.416zM12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>
|
Before Width: | Height: | Size: 374 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-4.987-3.744A7.966 7.966 0 0 0 12 20c1.97 0 3.773-.712 5.167-1.892A6.979 6.979 0 0 0 12.16 16a6.981 6.981 0 0 0-5.147 2.256zM5.616 16.82A8.975 8.975 0 0 1 12.16 14a8.972 8.972 0 0 1 6.362 2.634 8 8 0 1 0-12.906.187zM12 13a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
Before Width: | Height: | Size: 495 B |
@ -1,10 +0,0 @@
|
||||
<svg width="71" height="55" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" fill="#23272A"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="71" height="55" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.0 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M3 21v-2h2V4c0-.552.448-1 1-1h12c.552 0 1 .448 1 1v15h2v2H3zm12-10h-2v2h2v-2z"/></svg>
|
Before Width: | Height: | Size: 215 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></svg>
|
Before Width: | Height: | Size: 471 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19z"/></svg>
|
Before Width: | Height: | Size: 257 B |
@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 318.6 318.6"
|
||||
style="enable-background:new 0 0 318.6 318.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#E2761B;stroke:#E2761B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#E4761B;stroke:#E4761B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st2{fill:#D7C1B3;stroke:#D7C1B3;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st3{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st4{fill:#CD6116;stroke:#CD6116;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st5{fill:#E4751F;stroke:#E4751F;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st6{fill:#F6851B;stroke:#F6851B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st7{fill:#C0AD9E;stroke:#C0AD9E;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st8{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st9{fill:#763D16;stroke:#763D16;stroke-linecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<polygon class="st0" points="274.1,35.5 174.6,109.4 193,65.8 "/>
|
||||
<g>
|
||||
<polygon class="st1" points="44.4,35.5 143.1,110.1 125.6,65.8 "/>
|
||||
<polygon class="st1" points="238.3,206.8 211.8,247.4 268.5,263 284.8,207.7 "/>
|
||||
<polygon class="st1" points="33.9,207.7 50.1,263 106.8,247.4 80.3,206.8 "/>
|
||||
<polygon class="st1" points="103.6,138.2 87.8,162.1 144.1,164.6 142.1,104.1 "/>
|
||||
<polygon class="st1" points="214.9,138.2 175.9,103.4 174.6,164.6 230.8,162.1 "/>
|
||||
<polygon class="st1" points="106.8,247.4 140.6,230.9 111.4,208.1 "/>
|
||||
<polygon class="st1" points="177.9,230.9 211.8,247.4 207.1,208.1 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st2" points="211.8,247.4 177.9,230.9 180.6,253 180.3,262.3 "/>
|
||||
<polygon class="st2" points="106.8,247.4 138.3,262.3 138.1,253 140.6,230.9 "/>
|
||||
</g>
|
||||
<polygon class="st3" points="138.8,193.5 110.6,185.2 130.5,176.1 "/>
|
||||
<polygon class="st3" points="179.7,193.5 188,176.1 208,185.2 "/>
|
||||
<g>
|
||||
<polygon class="st4" points="106.8,247.4 111.6,206.8 80.3,207.7 "/>
|
||||
<polygon class="st4" points="207,206.8 211.8,247.4 238.3,207.7 "/>
|
||||
<polygon class="st4" points="230.8,162.1 174.6,164.6 179.8,193.5 188.1,176.1 208.1,185.2 "/>
|
||||
<polygon class="st4" points="110.6,185.2 130.6,176.1 138.8,193.5 144.1,164.6 87.8,162.1 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st5" points="87.8,162.1 111.4,208.1 110.6,185.2 "/>
|
||||
<polygon class="st5" points="208.1,185.2 207.1,208.1 230.8,162.1 "/>
|
||||
<polygon class="st5" points="144.1,164.6 138.8,193.5 145.4,227.6 146.9,182.7 "/>
|
||||
<polygon class="st5" points="174.6,164.6 171.9,182.6 173.1,227.6 179.8,193.5 "/>
|
||||
</g>
|
||||
<polygon class="st6" points="179.8,193.5 173.1,227.6 177.9,230.9 207.1,208.1 208.1,185.2 "/>
|
||||
<polygon class="st6" points="110.6,185.2 111.4,208.1 140.6,230.9 145.4,227.6 138.8,193.5 "/>
|
||||
<polygon class="st7" points="180.3,262.3 180.6,253 178.1,250.8 140.4,250.8 138.1,253 138.3,262.3 106.8,247.4 117.8,256.4
|
||||
140.1,271.9 178.4,271.9 200.8,256.4 211.8,247.4 "/>
|
||||
<polygon class="st8" points="177.9,230.9 173.1,227.6 145.4,227.6 140.6,230.9 138.1,253 140.4,250.8 178.1,250.8 180.6,253 "/>
|
||||
<g>
|
||||
<polygon class="st9" points="278.3,114.2 286.8,73.4 274.1,35.5 177.9,106.9 214.9,138.2 267.2,153.5 278.8,140 273.8,136.4
|
||||
281.8,129.1 275.6,124.3 283.6,118.2 "/>
|
||||
<polygon class="st9" points="31.8,73.4 40.3,114.2 34.9,118.2 42.9,124.3 36.8,129.1 44.8,136.4 39.8,140 51.3,153.5 103.6,138.2
|
||||
140.6,106.9 44.4,35.5 "/>
|
||||
</g>
|
||||
<polygon class="st6" points="267.2,153.5 214.9,138.2 230.8,162.1 207.1,208.1 238.3,207.7 284.8,207.7 "/>
|
||||
<polygon class="st6" points="103.6,138.2 51.3,153.5 33.9,207.7 80.3,207.7 111.4,208.1 87.8,162.1 "/>
|
||||
<polygon class="st6" points="174.6,164.6 177.9,106.9 193.1,65.8 125.6,65.8 140.6,106.9 144.1,164.6 145.3,182.8 145.4,227.6
|
||||
173.1,227.6 173.3,182.8 "/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.9 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 2.05V13h10.95c-.501 5.053-4.765 9-9.95 9-5.523 0-10-4.477-10-10 0-5.185 3.947-9.449 9-9.95zm2 0A10.003 10.003 0 0 1 21.95 11H13V2.05z"/></svg>
|
Before Width: | Height: | Size: 275 B |
@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9.06,1.93C7.17,1.92 5.33,3.74 6.17,6H3A2,2 0 0,0 1,8V10A1,1 0 0,0 2,11H11V8H13V11H22A1,1 0 0,0 23,10V8A2,2 0 0,0 21,6H17.83C19,2.73 14.6,0.42 12.57,3.24L12,4L11.43,3.22C10.8,2.33 9.93,1.94 9.06,1.93M9,4C9.89,4 10.34,5.08 9.71,5.71C9.08,6.34 8,5.89 8,5A1,1 0 0,1 9,4M15,4C15.89,4 16.34,5.08 15.71,5.71C15.08,6.34 14,5.89 14,5A1,1 0 0,1 15,4M2,12V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V12H13V20H11V12H2Z" /></svg>
|
Before Width: | Height: | Size: 695 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5.33 15.929A13.064 13.064 0 0 1 5 13c0-5.088 2.903-9.436 7-11.182C16.097 3.564 19 7.912 19 13c0 1.01-.114 1.991-.33 2.929l2.02 1.796a.5.5 0 0 1 .097.63l-2.458 4.096a.5.5 0 0 1-.782.096l-2.254-2.254a1 1 0 0 0-.707-.293H9.414a1 1 0 0 0-.707.293l-2.254 2.254a.5.5 0 0 1-.782-.096l-2.458-4.095a.5.5 0 0 1 .097-.631l2.02-1.796zM12 13a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
Before Width: | Height: | Size: 496 B |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 8.1 KiB |
@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 750 750" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.01064,0,0,1.00261,1.06145,1.00001)">
|
||||
<g id="BG" transform="matrix(-0.981428,-0.00278963,0.00280316,-0.986189,957.595,822.459)">
|
||||
<path d="M977,451.82C977.172,637.872 839.464,797.573 655.41,824.77L553.3,824.77C553.14,824.91 547.48,825.18 547.48,825.18C546.53,825.05 545.57,824.91 544.62,824.77C537.35,823.7 530.147,822.423 523.01,820.94C495.065,815.155 467.877,806.18 441.98,794.19C441.71,794.07 441.45,793.94 441.18,793.82C426.749,787.122 412.763,779.503 399.31,771.01C398.97,770.8 398.64,770.59 398.31,770.38C397.74,770.02 397.18,769.67 396.62,769.3C396.44,769.19 396.26,769.08 396.08,768.96C384.053,761.213 372.477,752.786 361.41,743.72C360.87,743.29 360.33,742.84 359.79,742.4C357.62,740.6 355.47,738.78 353.34,736.94C353.32,736.93 353.31,736.91 353.29,736.9C342.406,727.467 332.066,717.424 322.32,706.82C322.29,706.78 322.25,706.75 322.22,706.71C279.41,660.144 249.027,603.526 233.89,542.11C233.77,541.67 233.67,541.24 233.56,540.8C226.526,511.663 222.981,481.794 223,451.82C223,243.61 391.79,74.82 600,74.82C808.21,74.82 977,243.61 977,451.82Z" style="fill:rgb(63,61,86);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g id="BG1" serif:id="BG" transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M977,451.82C977.172,637.872 839.464,797.573 655.41,824.77L553.3,824.77C553.14,824.91 547.48,825.18 547.48,825.18C546.53,825.05 545.57,824.91 544.62,824.77C537.35,823.7 530.147,822.423 523.01,820.94C495.065,815.155 467.877,806.18 441.98,794.19C441.71,794.07 441.45,793.94 441.18,793.82C426.749,787.122 412.763,779.503 399.31,771.01C398.97,770.8 398.64,770.59 398.31,770.38C397.74,770.02 397.18,769.67 396.62,769.3C396.44,769.19 396.26,769.08 396.08,768.96C384.053,761.213 372.477,752.786 361.41,743.72C360.87,743.29 360.33,742.84 359.79,742.4C357.62,740.6 355.47,738.78 353.34,736.94C353.32,736.93 353.31,736.91 353.29,736.9C342.406,727.467 332.066,717.424 322.32,706.82C322.29,706.78 322.25,706.75 322.22,706.71C279.41,660.144 249.027,603.526 233.89,542.11C233.77,541.67 233.67,541.24 233.56,540.8C226.526,511.663 222.981,481.794 223,451.82C223,243.61 391.79,74.82 600,74.82C808.21,74.82 977,243.61 977,451.82Z" style="fill:rgb(63,61,86);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M634.853,507.436L667.824,468.838L700.795,430.239L719.407,408.45C720.437,407.244 718.848,405.363 717.818,406.569L684.847,445.167L651.877,483.766L633.264,505.555C632.234,506.761 633.824,508.642 634.853,507.436L634.853,507.436Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="386.81" y="336.41" width="34.1" height="413.54" style="fill:rgb(47,46,65);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M409.95,488.23L409.88,500.61L409.05,650.87L408.95,668.78L408.73,709.14L408.51,749.95L406.31,749.95L406.53,709.14L406.74,671.44L406.85,650.5L407.68,500.64L407.75,488.22L409.95,488.23Z" style="fill:rgb(108,99,255);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M650.031,416.73L603.827,416.73C603.156,409.492 604.825,402.202 609.121,394.854L610.427,385.927L642.33,385.927L643.835,394.741C648.305,400.697 650.037,408.217 650.031,416.73Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M643.318,387.027L610.396,387.027C609.189,387.027 608.196,386.034 608.196,384.827C608.196,384.654 608.216,384.482 608.257,384.314L613.801,361.212C614.038,360.226 614.927,359.525 615.941,359.525L637.774,359.525C638.788,359.525 639.676,360.226 639.913,361.212L645.458,384.314C645.498,384.482 645.518,384.654 645.518,384.827C645.518,386.034 644.525,387.027 643.318,387.027Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M626.86,347.97C620.523,347.977 615.312,353.193 615.31,359.53L615.31,371.08L638.41,371.08L638.41,359.53C638.408,353.193 633.197,347.977 626.86,347.97Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M391.051,622.109L312.765,723.714L323.592,726.213L384.388,647.927L391.051,647.927L391.051,622.109Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M419.831,622.109L498.117,723.714L487.29,726.213L426.494,647.927L419.831,647.927L419.831,622.109Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M872.38,320.07L872.38,712.47C848.053,737.888 820.303,759.792 789.93,777.55L789.93,320.07L872.38,320.07Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="634.387" y="295.224" width="54.134" height="54.134" style="fill:rgb(47,46,65);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="601.906" y="114.5" width="12.492" height="141.581" style="fill:rgb(47,46,65);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M796.93,320.07L789.93,320.07L789.93,777.55C792.282,776.176 794.615,774.774 796.93,773.345L796.93,320.07Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="601.906" y="114.5" width="7" height="130.75" style="fill:rgb(230,230,230);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="583.168" y="309.798" width="44.973" height="24.985" style="fill:rgb(108,99,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="146.933" cy="244.847" r="66.193" style="fill:rgb(230,230,230);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="140.423" cy="199.271" r="7.596" style="fill:rgb(203,203,203);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="153.444" cy="275.23" r="7.596" style="fill:rgb(203,203,203);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="182.742" cy="232.91" r="5.426" style="fill:rgb(203,203,203);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="120.89" cy="250.272" r="17.362" style="fill:rgb(203,203,203);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="252.015" cy="349.213" r="3.576" style="fill:rgb(108,99,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M283.012,401.093L281.227,402.866L279.454,401.081L278.264,402.263L280.037,404.048L278.252,405.821L279.434,407.011L281.219,405.238L282.992,407.023L284.182,405.841L282.409,404.056L284.194,402.283L283.012,401.093Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M297.318,297.375L295.533,299.148L293.76,297.363L292.57,298.545L294.343,300.33L292.558,302.103L293.74,303.293L295.525,301.52L297.298,303.305L298.488,302.123L296.715,300.338L298.5,298.565L297.318,297.375Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M565.279,175.376C561.417,177.949 557.464,171.825 561.401,169.366C565.262,166.793 569.215,172.917 565.279,175.376Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M118.557,366.728L116.1,366.189L116.638,363.731L115,363.372L114.461,365.83L112.004,365.292L111.645,366.93L114.102,367.469L113.564,369.926L115.202,370.285L115.741,367.827L118.198,368.366L118.557,366.728Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M494.557,71.728L492.1,71.189L492.638,68.731L491,68.372L490.461,70.83L488.004,70.292L487.645,71.93L490.102,72.469L489.564,74.926L491.202,75.285L491.741,72.827L494.198,73.366L494.557,71.728Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M686.557,230.728L684.1,230.189L684.638,227.731L683,227.372L682.461,229.83L680.004,229.292L679.645,230.93L682.102,231.469L681.564,233.926L683.202,234.285L683.741,231.827L686.198,232.366L686.557,230.728Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M222.46,110.506L220.003,109.967L220.541,107.51L218.903,107.151L218.364,109.608L215.906,109.07L215.547,110.708L218.005,111.247L217.467,113.705L219.105,114.063L219.644,111.606L222.101,112.144L222.46,110.506Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="455.34" cy="188.551" r="3.576" style="fill:rgb(108,99,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M428.557,242.728L426.1,242.189L426.638,239.731L425,239.372L424.461,241.83L422.004,241.292L421.645,242.93L424.102,243.469L423.564,245.926L425.202,246.285L425.741,243.827L428.198,244.366L428.557,242.728Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M523.46,198.506L521.003,197.967L521.541,195.51L519.903,195.151L519.364,197.608L516.906,197.07L516.547,198.708L519.005,199.247L518.467,201.705L520.105,202.063L520.644,199.606L523.101,200.144L523.46,198.506Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M580.853,293.436L613.824,254.838L646.795,216.239L665.407,194.45C666.437,193.244 664.848,191.363 663.818,192.569L630.847,231.167L597.877,269.766L579.264,291.555C578.234,292.761 579.824,294.642 580.853,293.436L580.853,293.436Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="385.494" y="646.969" width="6" height="102.023" style="fill:rgb(230,230,230);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M628.54,347.42C623.517,348.779 620,353.367 619.99,358.57L619.99,358.67C619.244,358.893 618.669,359.495 618.48,360.25L612.94,383.36C612.9,383.526 612.88,383.697 612.88,383.868C612.88,385.023 613.796,385.989 614.95,386.05L614.91,386.32L614.76,387.32L612.5,394.32C608.21,401.66 607.84,408.53 608.51,415.77L614.49,415.77L614.49,696.28L536.45,797.58L542.19,798.9L541.28,800.08L530.45,797.58L608.49,696.28L608.49,415.77L603.5,415.32C602.83,408.08 604.21,401.66 608.5,394.32L608.76,387.32L608.91,386.32L608.95,386.05C607.796,385.989 606.88,385.023 606.88,383.868C606.88,383.697 606.9,383.526 606.94,383.36L612.48,360.25C612.669,359.495 613.244,358.893 613.99,358.67L613.99,358.57C613.992,352.233 619.203,347.017 625.54,347.01C626.554,347.013 627.563,347.151 628.54,347.42Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<path d="M431.177,646.969L425.594,646.969L425.594,647.506L485.974,725.255L491.062,724.081L431.177,646.969Z" style="fill:rgb(230,230,230);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M737.88,783.96L737.88,802.81C711.317,813.249 683.647,820.616 655.41,824.77L532.41,824.77C529.26,824.2 526.12,823.59 523.01,822.94L523.01,783.96L737.88,783.96Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M632.05,725.69C631.35,725.57 630.61,725.45 629.85,725.32C625.38,724.6 619.99,723.78 613.81,722.93C613.39,722.87 612.96,722.81 612.52,722.75C607.42,722.06 601.81,721.36 595.76,720.67C562.59,716.94 516.26,713.95 468.98,718.06C470.15,713.36 471.23,708.66 472.25,703.98L426.97,710.99L475.83,686.14C480.433,660.611 483.598,634.844 485.31,608.96L460.85,612.74L485.86,600.02C487.65,567.99 486.81,546.67 486.81,546.67C486.81,546.67 405,589.2 339.58,654.2C337.18,645.74 334.55,637.39 331.75,629.19L296.61,658.59L325.58,612.07C316.331,587.834 305.721,564.14 293.8,541.1L274.82,556.98L289.66,533.16C274.64,504.81 262.9,486.99 262.9,486.99C262.9,486.99 249.51,508.93 233.56,542.8C248.601,604.763 279.148,661.897 322.32,708.82C332.08,719.44 342.437,729.497 353.34,738.94C355.47,740.78 357.62,742.6 359.79,744.4C371.348,753.967 383.466,762.836 396.08,770.96C396.82,771.44 397.56,771.91 398.31,772.38C436.489,796.611 478.736,813.741 523.01,822.94C526.12,823.59 529.26,824.2 532.41,824.77C533.18,824.91 533.95,825.05 534.72,825.18L547.48,825.18C547.48,825.18 553.14,824.91 553.3,824.77C569.15,810.9 583.52,796.88 595.91,783.96C598.73,781.03 601.44,778.15 604.05,775.35L582.67,762.88L610.11,768.76C611.37,767.37 612.61,766 613.81,764.66C619.97,757.78 625.31,751.55 629.74,746.26C630.5,745.34 631.24,744.46 631.95,743.6C640.02,733.86 644.46,727.92 644.46,727.92C644.46,727.92 640,727.01 632.05,725.69Z" style="fill:rgb(47,46,65);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-218.859,-73.787)">
|
||||
<path d="M608.49,415.77L602.51,415.77C601.84,408.53 603.51,401.24 607.8,393.9L608.76,387.32L608.91,386.32L614.91,386.32L614.76,387.32L613.8,393.9C609.51,401.24 607.84,408.53 608.51,415.77L614.49,415.77" style="fill:rgb(108,99,255);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<rect x="385.49" y="427.5" width="6" height="73" style="fill:rgb(108,99,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.981432,0,0,0.986193,-1.41099e-06,-1.38067e-07)">
|
||||
<circle cx="606.5" cy="114.5" r="11" style="fill:rgb(108,99,255);"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(86.81,0,0,86.81,632.931,457.002)"><stop offset="0" style="stop-color:white;stop-opacity:0.8"/><stop offset="1" style="stop-color:white;stop-opacity:0.24"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7535.97,0,0,7535.97,50835.9,24891.7)"><stop offset="0" style="stop-color:white;stop-opacity:0.8"/><stop offset="1" style="stop-color:white;stop-opacity:0.24"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 8.5 KiB |
@ -1,36 +0,0 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const DynamicButton = ( { to='', onClick, ...props } ) => to && !to.includes( 'http' ) ? <Link { ...props } to={ to } /> : <button onClick={ onClick || ( () => window.open( to, '_blank' ).focus() ) } { ...props } />
|
||||
|
||||
|
||||
const PrettyButton = styled( DynamicButton )`
|
||||
|
||||
display: flex;
|
||||
flex-direction: ${ ( { direction='row' } ) => direction };
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid ${ ( { theme } ) => theme.colors.text };
|
||||
background: none;
|
||||
color: ${ ( { theme } ) => theme.colors.text };
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 1.1rem .5rem 1rem;
|
||||
margin: 1rem .5rem;
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& img {
|
||||
height: 50px;
|
||||
width: auto;
|
||||
margin: ${ ( { direction='row' } ) => direction == 'row' ? '0 1rem 0 0' : '1rem' };
|
||||
}
|
||||
`
|
||||
|
||||
export default ( { icon, ...props } ) => !icon ? <PrettyButton { ...props } /> : <PrettyButton { ...props }>
|
||||
<img alt="Button icon" src={ icon } />
|
||||
{ props.children }
|
||||
</PrettyButton>
|