diff --git a/.firebaserc b/.firebaserc deleted file mode 100644 index 46dc962..0000000 --- a/.firebaserc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "projects": { - "default": "rocketeer-nft" - }, - "targets": { - "rocketeer-nft": { - "hosting": { - "website": [ - "rocketeer" - ], - "minter": [ - "rocketeer-nft" - ], - "viewer": [ - "rocketeer-viewer" - ] - } - } - } -} \ No newline at end of file diff --git a/.github/workflows/deploy-functions.yml b/.github/workflows/deploy-functions.yml deleted file mode 100644 index 0ca083a..0000000 --- a/.github/workflows/deploy-functions.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/deploy-minter.yml b/.github/workflows/deploy-minter.yml deleted file mode 100644 index 37f02fe..0000000 --- a/.github/workflows/deploy-minter.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/deploy-viewer.yml b/.github/workflows/deploy-viewer.yml deleted file mode 100644 index de6600d..0000000 --- a/.github/workflows/deploy-viewer.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml deleted file mode 100644 index df8656e..0000000 --- a/.github/workflows/deploy-website.yml +++ /dev/null @@ -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 }} diff --git a/.gitignore b/.gitignore index f755f5d..dbf004d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .* node_modules build/ -docs/ *.log -.backups +notes.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ea38537..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 2954521..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,29 +0,0 @@ -# Rocketeer NFT - -This is the official repository of the [Rocketeers]( https://rocketeer.fans ) NFT collection. - -- [Mint Rocketeers here]( https://mint.rocketeer.fans/#/mint ) -- [View your Rocketeer portfolio here]( https://mint.rocketeer.fans/#/portfolio ) -- [Set your Rocketpool node avatar here]( https://mint.rocketeer.fans/#/avatar ) - -Do you want to contribute to this project? Read `CONTRIBUTING.md`. - -## Rocketeer components - -The Rocketeer project consists out of a Solidity `ERC721` contract and a number of `web2` interfaces. - -### Contract code - -You can find the contract source in `contracts`. The `migrations/*` files set the parameters used for deployment. - -### Minter code - -The minter interface hosted at [mint.rocketeer.fans]( https://mint.rocketeer.fans/ ) is a React app that connects to Metamask. The code is inside the `minter` folder. - -### Viewer code - -The Rocketeer viewer hosted at [viewer.rocketeer.fans]( https://viewer.rocketeer.fans/ ) is the official place to view your Rocketeers. It's code is inside the `viewer` folder. - -### Oracle code - -The metadata and image oracle generates the Rocketeer data when one is minted. The code is inside the `functions` folder. diff --git a/functions/assets/master.afdesign b/assets/master.afdesign similarity index 100% rename from functions/assets/master.afdesign rename to assets/master.afdesign diff --git a/functions/assets/master.svg b/assets/master.svg similarity index 100% rename from functions/assets/master.svg rename to assets/master.svg diff --git a/community/changing-room-poap.png b/community/changing-room-poap.png deleted file mode 100644 index 208dfbe..0000000 Binary files a/community/changing-room-poap.png and /dev/null differ diff --git a/community/changing-room-usage-poap.png b/community/changing-room-usage-poap.png deleted file mode 100644 index 3baa8f0..0000000 Binary files a/community/changing-room-usage-poap.png and /dev/null differ diff --git a/community/gratitude-poap.afdesign b/community/gratitude-poap.afdesign deleted file mode 100644 index 43d30f1..0000000 Binary files a/community/gratitude-poap.afdesign and /dev/null differ diff --git a/community/gratitude-poap.png b/community/gratitude-poap.png deleted file mode 100644 index bccfe27..0000000 Binary files a/community/gratitude-poap.png and /dev/null differ diff --git a/community/scraping.js b/community/scraping.js deleted file mode 100644 index e364e30..0000000 --- a/community/scraping.js +++ /dev/null @@ -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( ) \ No newline at end of file diff --git a/community/signer-launch.png b/community/signer-launch.png deleted file mode 100644 index 649b13f..0000000 Binary files a/community/signer-launch.png and /dev/null differ diff --git a/contracts/.nvmrc b/contracts/.nvmrc deleted file mode 100644 index da2d398..0000000 --- a/contracts/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -14 \ No newline at end of file diff --git a/contracts/ERC721Tradable.sol b/contracts/ERC721Tradable.sol deleted file mode 100644 index b297521..0000000 --- a/contracts/ERC721Tradable.sol +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol deleted file mode 100644 index 9aac975..0000000 --- a/contracts/Migrations.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/Rocketeer.sol b/contracts/Rocketeer.sol deleted file mode 100644 index ae49348..0000000 --- a/contracts/Rocketeer.sol +++ /dev/null @@ -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 ); - } - -} diff --git a/contracts/common/meta-transactions/ContentMixin.sol b/contracts/common/meta-transactions/ContentMixin.sol deleted file mode 100644 index e427bd4..0000000 --- a/contracts/common/meta-transactions/ContentMixin.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/common/meta-transactions/EIP712Base.sol b/contracts/common/meta-transactions/EIP712Base.sol deleted file mode 100644 index 0a4145f..0000000 --- a/contracts/common/meta-transactions/EIP712Base.sol +++ /dev/null @@ -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) - ); - } -} \ No newline at end of file diff --git a/contracts/common/meta-transactions/Initializable.sol b/contracts/common/meta-transactions/Initializable.sol deleted file mode 100644 index 1b75140..0000000 --- a/contracts/common/meta-transactions/Initializable.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/common/meta-transactions/NativeMetaTransaction.sol b/contracts/common/meta-transactions/NativeMetaTransaction.sol deleted file mode 100644 index 2d62b2e..0000000 --- a/contracts/common/meta-transactions/NativeMetaTransaction.sol +++ /dev/null @@ -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 - ); - } -} diff --git a/firebase.json b/firebase.json deleted file mode 100644 index d7fffb7..0000000 --- a/firebase.json +++ /dev/null @@ -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" - } -} diff --git a/firestore.indexes.json b/firestore.indexes.json deleted file mode 100644 index 415027e..0000000 --- a/firestore.indexes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "indexes": [], - "fieldOverrides": [] -} diff --git a/firestore.rules b/firestore.rules deleted file mode 100644 index 4d0126f..0000000 --- a/firestore.rules +++ /dev/null @@ -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; - - } - } - -} diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js deleted file mode 100644 index e3866cf..0000000 --- a/functions/.eslintrc.js +++ /dev/null @@ -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 - } - -} \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore deleted file mode 100644 index 92b391c..0000000 --- a/functions/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -.* \ No newline at end of file diff --git a/functions/.nvmrc b/functions/.nvmrc deleted file mode 100644 index 19c7bdb..0000000 --- a/functions/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -16 \ No newline at end of file diff --git a/functions/README.md b/functions/README.md deleted file mode 100644 index 2eec4f8..0000000 --- a/functions/README.md +++ /dev/null @@ -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` diff --git a/functions/assets/demo.svg b/functions/assets/demo.svg deleted file mode 100644 index e653231..0000000 --- a/functions/assets/demo.svg +++ /dev/null @@ -1,2678 +0,0 @@ - - \ No newline at end of file diff --git a/functions/endpoints/express.js b/functions/endpoints/express.js deleted file mode 100644 index 215ecf5..0000000 --- a/functions/endpoints/express.js +++ /dev/null @@ -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 - -} \ No newline at end of file diff --git a/functions/endpoints/mainnet.js b/functions/endpoints/mainnet.js deleted file mode 100644 index 88f4604..0000000 --- a/functions/endpoints/mainnet.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/functions/endpoints/testnet.js b/functions/endpoints/testnet.js deleted file mode 100644 index 01b7797..0000000 --- a/functions/endpoints/testnet.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/functions/index.js b/functions/index.js deleted file mode 100644 index 02a5e61..0000000 --- a/functions/index.js +++ /dev/null @@ -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 ) \ No newline at end of file diff --git a/functions/integrations/avatar.js b/functions/integrations/avatar.js deleted file mode 100644 index 1605458..0000000 --- a/functions/integrations/avatar.js +++ /dev/null @@ -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 - } ) - - } - -} \ No newline at end of file diff --git a/functions/integrations/changingroom.js b/functions/integrations/changingroom.js deleted file mode 100644 index 5424313..0000000 --- a/functions/integrations/changingroom.js +++ /dev/null @@ -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 ) - } - -} \ No newline at end of file diff --git a/functions/integrations/discord.js b/functions/integrations/discord.js deleted file mode 100644 index f07a741..0000000 --- a/functions/integrations/discord.js +++ /dev/null @@ -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 ) - } - -} \ No newline at end of file diff --git a/functions/integrations/merch.js b/functions/integrations/merch.js deleted file mode 100644 index 018ba84..0000000 --- a/functions/integrations/merch.js +++ /dev/null @@ -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 - } ) - } - -} \ No newline at end of file diff --git a/functions/integrations/notifier.js b/functions/integrations/notifier.js deleted file mode 100644 index d78533d..0000000 --- a/functions/integrations/notifier.js +++ /dev/null @@ -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() } ) - - } - -} \ No newline at end of file diff --git a/functions/integrations/opensea.js b/functions/integrations/opensea.js deleted file mode 100644 index 8e34644..0000000 --- a/functions/integrations/opensea.js +++ /dev/null @@ -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 ) - } - -} \ No newline at end of file diff --git a/functions/integrations/rocketeers.js b/functions/integrations/rocketeers.js deleted file mode 100644 index 4c3bae8..0000000 --- a/functions/integrations/rocketeers.js +++ /dev/null @@ -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() } ) - } - -} \ No newline at end of file diff --git a/functions/integrations/ses.js b/functions/integrations/ses.js deleted file mode 100644 index d7433b0..0000000 --- a/functions/integrations/ses.js +++ /dev/null @@ -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 ) - - -} \ No newline at end of file diff --git a/functions/integrations/signer_is.js b/functions/integrations/signer_is.js deleted file mode 100644 index 9005cfb..0000000 --- a/functions/integrations/signer_is.js +++ /dev/null @@ -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 || [] - -} \ No newline at end of file diff --git a/functions/modules/contract.js b/functions/modules/contract.js deleted file mode 100644 index 514627e..0000000 --- a/functions/modules/contract.js +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/functions/modules/email.js b/functions/modules/email.js deleted file mode 100644 index edd1d2c..0000000 --- a/functions/modules/email.js +++ /dev/null @@ -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 ) - - } - -} \ No newline at end of file diff --git a/functions/modules/firebase.js b/functions/modules/firebase.js deleted file mode 100644 index f48f8ea..0000000 --- a/functions/modules/firebase.js +++ /dev/null @@ -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 -} diff --git a/functions/modules/templates/css-resets/LICENSE.md b/functions/modules/templates/css-resets/LICENSE.md deleted file mode 100644 index bac7830..0000000 --- a/functions/modules/templates/css-resets/LICENSE.md +++ /dev/null @@ -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. diff --git a/functions/modules/templates/css-resets/README.md b/functions/modules/templates/css-resets/README.md deleted file mode 100644 index 5ba1300..0000000 --- a/functions/modules/templates/css-resets/README.md +++ /dev/null @@ -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 ` - - -
- - - | -- |
---|