Remove a bunch of stuff, we only want to serve the image generation part

This commit is contained in:
Marco van Dijk 2022-10-07 11:22:33 +02:00
parent 91c545f961
commit 329f9bc471
193 changed files with 124 additions and 129347 deletions

View File

@ -1,20 +0,0 @@
{
"projects": {
"default": "rocketeer-nft"
},
"targets": {
"rocketeer-nft": {
"hosting": {
"website": [
"rocketeer"
],
"minter": [
"rocketeer-nft"
],
"viewer": [
"rocketeer-viewer"
]
}
}
}
}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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
View File

@ -1,6 +1,5 @@
.*
node_modules
build/
docs/
*.log
.backups
notes.txt

View File

@ -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.

View File

@ -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.

View File

Before

Width:  |  Height:  |  Size: 14 MiB

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View File

@ -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( )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1 +0,0 @@
14

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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 );
}
}

View File

@ -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;
}
}

View File

@ -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)
);
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -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"
}
}

View File

@ -1,4 +0,0 @@
{
"indexes": [],
"fieldOverrides": []
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -1,2 +0,0 @@
node_modules/
.*

View File

@ -1 +0,0 @@
16

View File

@ -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`

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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
} )
}
}

View File

@ -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 )
}
}

View File

@ -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 )
}
}

View File

@ -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
} )
}
}

View File

@ -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() } )
}
}

View File

@ -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 )
}
}

View File

@ -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() } )
}
}

View File

@ -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 )
}

View File

@ -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 || []
}

View File

@ -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
}

View File

@ -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 )
}
}

View File

@ -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
}

View File

@ -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.

View File

@ -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>
```

View File

@ -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&nbsp;our&nbsp;<a href="http://blog.npmjs.org/post/161515829950/credentials-resets">blog&nbsp;post</a>.</li>
</ol>
<p>Npm loves you.</p>
</th>
<th></th>
</tr>
</table>
</div>
</body>
</html>

View File

@ -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%;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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%;
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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.

View File

@ -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>
```

View File

@ -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&nbsp;our&nbsp;<a href="http://blog.npmjs.org/post/161515829950/credentials-resets">blog&nbsp;post</a>.</li>
</ol>
<p>Npm loves you.</p>
</th>
<th></th>
</tr>
</table>
</div>
</body>
</html>

View File

@ -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%;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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
View 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);
}
})();

View File

@ -1,5 +0,0 @@
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};

View File

@ -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})
}

View File

@ -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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -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"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -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;
}*/

View File

@ -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;

View File

@ -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();
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More