├── public └── static │ ├── .gitkeep │ ├── social.png │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── JetBrainsMono-Regular.woff ├── common ├── environment.js ├── svg.js ├── styles │ └── global.js ├── node-cache.js ├── node-data-parse.js └── strings.js ├── .babelrc ├── .gitignore ├── .prettierrc ├── index.js ├── components ├── Loader.js └── Page.js ├── README.md ├── pages ├── _app.js └── index.js ├── LICENSE-MIT ├── package.json └── server.js /public/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimpick/file.app/master/public/static/social.png -------------------------------------------------------------------------------- /public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimpick/file.app/master/public/static/favicon.ico -------------------------------------------------------------------------------- /public/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimpick/file.app/master/public/static/favicon-16x16.png -------------------------------------------------------------------------------- /public/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimpick/file.app/master/public/static/favicon-32x32.png -------------------------------------------------------------------------------- /public/static/JetBrainsMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimpick/file.app/master/public/static/JetBrainsMono-Regular.woff -------------------------------------------------------------------------------- /common/environment.js: -------------------------------------------------------------------------------- 1 | export const NODE = process.env.NODE_ENV || "development"; 2 | export const IS_PRODUCTION = NODE === "production"; 3 | export const PORT = process.env.PORT || 4443; 4 | 5 | if (!IS_PRODUCTION) { 6 | require("dotenv").config(); 7 | } 8 | 9 | export const SENDGRID_KEY = process.env.SENDGRID_KEY; 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "transform-runtime": { 7 | "useESModules": false 8 | } 9 | } 10 | ], 11 | "@emotion/babel-preset-css-prop" 12 | ], 13 | "plugins": [ 14 | [ 15 | "module-resolver", 16 | { 17 | "alias": { 18 | "~": "./" 19 | } 20 | } 21 | ] 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .env 3 | .env-production 4 | .env.local 5 | .DS_STORE 6 | DS_STORE 7 | package-lock.json 8 | yarn.lock 9 | node_modules 10 | dist 11 | 12 | /**/*/package-lock.json 13 | /**/*/.DS_STORE 14 | /**/*/node_modules 15 | /**/*/.next 16 | /**/*/.data 17 | 18 | .data/* 19 | !.data/.gitkeep 20 | 21 | public/static/files/* 22 | !public/static/files/.gitkeep 23 | 24 | public/static/system/* 25 | !public/static/system/.gitkeep -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth":180, 3 | "tabWidth":2, 4 | "useTabs":false, 5 | "semi":true, 6 | "singleQuote":true, 7 | "trailingComma":"es5", 8 | "bracketSpacing":true, 9 | "jsxBracketSameLine":false, 10 | "arrowParens":"always", 11 | "requirePragma":false, 12 | "insertPragma":false, 13 | "proseWrap":"preserve", 14 | "parser":"babel", 15 | "overrides": [ 16 | { 17 | "files": "*.js", 18 | "options": { 19 | "parser": "babel" 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const dirPath = path.join(__dirname); 3 | 4 | require("@babel/register")({ 5 | presets: [ 6 | [require.resolve("@babel/preset-env")], 7 | [require.resolve("next/babel")], 8 | ], 9 | plugins: [ 10 | [ 11 | require.resolve("babel-plugin-module-resolver"), 12 | { 13 | alias: { 14 | "~": dirPath, 15 | }, 16 | }, 17 | ], 18 | ], 19 | ignore: ["node_modules", ".next"], 20 | }); 21 | 22 | module.exports = require("./server.js"); 23 | -------------------------------------------------------------------------------- /components/Loader.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { css } from "@emotion/react"; 4 | 5 | const STYLES_SPINNER = css` 6 | display: inline-block; 7 | width: 24px; 8 | height: 24px; 9 | border: 2px solid #000; 10 | border-radius: 50%; 11 | border-top-color: #fff; 12 | animation: animation-spin 1s ease-in-out infinite; 13 | 14 | @keyframes animation-spin { 15 | to { 16 | -webkit-transform: rotate(360deg); 17 | } 18 | } 19 | `; 20 | 21 | const LoaderSpinner = (props) =>
; 22 | export default LoaderSpinner; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # https://file.app 2 | 3 | A hacked together calculator for understanding the cost of Filecoin. 4 | 5 | ## Setup 6 | 7 | You need to clone this repo, and then create a file called `.env.local` with: 8 | 9 | ```sh 10 | ATHENA_EXPLORER_APP_ID=XXX 11 | ATHENA_EXPLORER_SECRET=XXX 12 | IEXCLOUD_TOKEN=XXX 13 | ``` 14 | 15 | Ask me for these if you need then. 16 | 17 | ## Development 18 | 19 | Make sure NodeJS 10+ is installed. Then run: 20 | 21 | ```sh 22 | npm install 23 | npm run dev 24 | ``` 25 | 26 | View localhost:4443 in your browser. 27 | 28 | ## Questions? 29 | 30 | Twitter: [@wwwjim](https://twitter.com/wwwjim). 31 | ARG: [https://arg.protocol.ai](https://arg.protocol.ai) 32 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Global } from "@emotion/react"; 4 | import { injectGlobalStyles } from "~/common/styles/global"; 5 | 6 | import App from "next/app"; 7 | 8 | // NOTE(wwwjim): 9 | // https://nextjs.org/docs/advanced-features/custom-app 10 | function MyApp({ Component, pageProps }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | MyApp.getInitialProps = async (appContext) => { 20 | const appProps = await App.getInitialProps(appContext); 21 | return { ...appProps }; 22 | }; 23 | 24 | export default MyApp; 25 | -------------------------------------------------------------------------------- /common/svg.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const Logo = (props) => ( 4 | 12 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file.app", 3 | "description": "", 4 | "author": "application-research-group", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "16.x.x" 9 | }, 10 | "scripts": { 11 | "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 node . --unhandled-rejections=strict", 12 | "start": "NODE_ENV=production node . --unhandled-rejections=strict", 13 | "build": "NODE_ENV=production next build" 14 | }, 15 | "repository": "application-research/file.app", 16 | "dependencies": { 17 | "@babel/core": "^7.15.8", 18 | "@babel/preset-env": "^7.15.8", 19 | "@babel/register": "^7.15.3", 20 | "@emotion/babel-preset-css-prop": "11.2.0", 21 | "@emotion/react": "11.5.0", 22 | "@glif/filecoin-number": "^1.1.0-beta.17", 23 | "babel-plugin-module-resolver": "^4.1.0", 24 | "body-parser": "^1.19.0", 25 | "compression": "^1.7.4", 26 | "cors": "^2.8.5", 27 | "dotenv": "^10.0.0", 28 | "express": "^4.17.1", 29 | "moment": "^2.29.1", 30 | "next": "11.1.2", 31 | "react": "^17.0.2", 32 | "react-dom": "^17.0.2", 33 | "uuid": "^8.3.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/styles/global.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | /* prettier-ignore */ 4 | export const injectGlobalStyles = () => css` 5 | @font-face { 6 | font-family: "Mono"; 7 | src: url("/static/JetBrainsMono-Regular.woff") format("woff"); 8 | } 9 | 10 | html, body, div, span, applet, object, iframe, 11 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 12 | a, abbr, acronym, address, big, cite, code, 13 | del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, 15 | b, u, i, center, 16 | dl, dt, dd, ol, ul, li, 17 | fieldset, form, label, legend, 18 | table, caption, tbody, tfoot, thead, tr, th, td, 19 | article, aside, canvas, details, embed, 20 | figure, figcaption, footer, header, hgroup, 21 | menu, nav, output, ruby, section, summary, 22 | time, mark, audio, video { 23 | box-sizing: border-box; 24 | margin: 0; 25 | padding: 0; 26 | border: 0; 27 | vertical-align: baseline; 28 | } 29 | 30 | article, aside, details, figcaption, figure, 31 | footer, header, hgroup, menu, nav, section { 32 | display: block; 33 | } 34 | 35 | html, body { 36 | --color-primary: #0047ff; 37 | 38 | background: #fff; 39 | color: #000; 40 | font-family: -apple-system, BlinkMacSystemFont, helvetica neue, helvetica, sans-serif; 41 | font-size: 14px; 42 | scrollbar-width: none; 43 | -ms-overflow-style: -ms-autohiding-scrollbar; 44 | 45 | ::-webkit-scrollbar { 46 | display: none; 47 | } 48 | 49 | @media (max-width: 768px) { 50 | font-size: 12px; 51 | } 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /common/node-cache.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | class Cache { 5 | config = null; 6 | filename = null; 7 | 8 | constructor(cacheFileName, config) { 9 | console.log("[ process.cwd() ]", process.cwd()); 10 | this.filename = path.join(process.cwd(), cacheFileName); 11 | console.log("[ cache file ]", this.filename); 12 | } 13 | 14 | async put(key, value, ms) { 15 | let data = null; 16 | try { 17 | const response = await fs.promises.readFile(this.filename, "utf8"); 18 | data = JSON.parse(response); 19 | } catch (error) { 20 | console.log(error.message); 21 | data = {}; 22 | } 23 | 24 | data[key] = { 25 | data: value, 26 | validUntil: new Date().getTime() + ms, 27 | }; 28 | 29 | await fs.promises.writeFile(this.filename, JSON.stringify(data), "utf8"); 30 | this.schedule(key, ms); 31 | return data[key]; 32 | } 33 | 34 | async get(key) { 35 | try { 36 | const data = JSON.parse( 37 | await fs.promises.readFile(this.filename, "utf8") 38 | )[key]; 39 | 40 | if (data.validUntil) { 41 | if (new Date().getTime() >= data.validUntil) { 42 | return { ...data, __invalidated: true }; 43 | } 44 | } 45 | 46 | return data.data; 47 | } catch (error) { 48 | return null; 49 | } 50 | } 51 | 52 | schedule(key, ms) { 53 | const that = this; 54 | setTimeout(async () => { 55 | const data = JSON.parse( 56 | await fs.promises.readFile(that.filename, "utf8") 57 | ); 58 | delete data[key]; 59 | await fs.promises.writeFile(that.filename, JSON.stringify(data), "utf8"); 60 | }, ms); 61 | } 62 | } 63 | 64 | function accessCache(filename, config) { 65 | return new Cache(filename, config); 66 | } 67 | 68 | export { accessCache }; 69 | -------------------------------------------------------------------------------- /components/Page.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | import * as React from "react"; 4 | 5 | export default class Page extends React.Component { 6 | static defaultProps = { 7 | title: "https://file.app", 8 | description: "Filecoin miner performance, activity, and data.", 9 | url: "https://file.app", 10 | image: "/static/social.png", 11 | }; 12 | 13 | render() { 14 | return ( 15 | 16 | 17 | {this.props.title} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 42 | 48 | 49 | 50 | 51 | {this.props.children} 52 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /common/node-data-parse.js: -------------------------------------------------------------------------------- 1 | import * as Strings from "~/common/strings"; 2 | 3 | export const getMinersArray = (data) => { 4 | const miners = []; 5 | const added = {}; 6 | const estuary = {}; 7 | 8 | for (let m of data.estuary) { 9 | if (Strings.isEmpty(m.version)) { 10 | continue; 11 | } 12 | 13 | estuary[m.addr] = m; 14 | } 15 | 16 | for (let m of data.filrep) { 17 | if (!m.rawPower) { 18 | continue; 19 | } 20 | 21 | if (!m.price) { 22 | continue; 23 | } 24 | 25 | if (!m.freeSpace) { 26 | continue; 27 | } 28 | 29 | if (Number(m.storageDeals.total) < 1) { 30 | continue; 31 | } 32 | 33 | if (Strings.isEmpty(m.isoCode)) { 34 | continue; 35 | } 36 | 37 | added[m.address] = true; 38 | 39 | const newMiner = { 40 | address: m.address, 41 | space: { free: m.freeSpace, used: m.storageDeals.dataStored }, 42 | iso: m.isoCode, 43 | region: m.region, 44 | maxPieceSize: m.maxPieceSize, 45 | minPieceSize: m.minPieceSize, 46 | price: m.price, 47 | verified: m.verifiedPrice, 48 | totalCost: m.storageDeals.averagePrice * m.storageDeals.total, 49 | power: m.rawPower, 50 | deals: m.storageDeals.total, 51 | estuary: estuary[m.address] ? estuary[m.address] : null, 52 | }; 53 | 54 | miners.push(newMiner); 55 | } 56 | 57 | for (let m of data.textile) { 58 | if (!m.miner) { 59 | continue; 60 | } 61 | 62 | if (added[m.miner.minerAddr]) { 63 | continue; 64 | } 65 | 66 | if ( 67 | Strings.isEmpty(m.miner.filecoin.sectorSize) || 68 | String(m.miner.filecoin.sectorSize) === "0" 69 | ) { 70 | continue; 71 | } 72 | 73 | if ( 74 | Strings.isEmpty(m.miner.filecoin.activeSectors) || 75 | String(m.miner.filecoin.activeSectors) === "0" 76 | ) { 77 | continue; 78 | } 79 | 80 | if (Number(m.miner.textile.dealsSummary.total) < 1) { 81 | continue; 82 | } 83 | 84 | const newMiner = { 85 | address: m.miner.minerAddr, 86 | space: { 87 | free: m.miner.filecoin.sectorSize, 88 | used: m.miner.filecoin.activeSectors, 89 | }, 90 | iso: m.miner.metadata.location, 91 | region: null, 92 | maxPieceSize: m.miner.filecoin.maxPieceSize, 93 | minPieceSize: m.miner.filecoin.minPieceSize, 94 | price: m.miner.filecoin.askPrice, 95 | verified: m.miner.filecoin.askVerifiedPrice, 96 | totalCost: 97 | m.miner.filecoin.askVerifiedPrice * m.miner.textile.dealsSummary.total, 98 | power: m.miner.filecoin.relativePower, 99 | deals: m.miner.textile.dealsSummary.total, 100 | estuary: estuary[m.miner.minerAddr] ? estuary[m.miner.minerAddr] : null, 101 | }; 102 | 103 | miners.push(newMiner); 104 | } 105 | 106 | return miners; 107 | }; 108 | -------------------------------------------------------------------------------- /common/strings.js: -------------------------------------------------------------------------------- 1 | import { FilecoinNumber, Converter } from "@glif/filecoin-number"; 2 | 3 | export function formatAsFilecoin(number) { 4 | return `${number} FIL`; 5 | } 6 | 7 | export function attoFILtoFIL(number = 0) { 8 | const filecoinNumber = new FilecoinNumber(`${number}`, "attofil"); 9 | const inFil = filecoinNumber.toFil(); 10 | return inFil; 11 | } 12 | 13 | export function getAttoFILtoUSD(number = 0, price) { 14 | const conversion = attoFILtoFIL(number); 15 | const usd = Number(conversion) * Number(price); 16 | return usd; 17 | } 18 | 19 | export function percentageCheaper(attoFIL, priceAmazon, currentUSD) { 20 | const filecoin = getAttoFILtoUSD(attoFIL, currentUSD); 21 | if (filecoin <= 0) { 22 | return "0.00% the cost of Amazon"; 23 | } 24 | 25 | return `${((filecoin / priceAmazon) * 100).toFixed( 26 | 2 27 | )}% the cost of Amazon S3`; 28 | } 29 | 30 | export function compareFilecoinToAmazon( 31 | priceFilecoin, 32 | priceAmazon, 33 | currentUSD 34 | ) { 35 | const filecoin = getAttoFILtoUSD(priceFilecoin, currentUSD); 36 | 37 | if (filecoin <= 0) { 38 | return "Infinity times cheaper"; 39 | } 40 | 41 | let percentage = 0; 42 | if (filecoin < priceAmazon) { 43 | const calculation = ((priceAmazon - filecoin) / filecoin) * 100; 44 | return `${calculation}% cheaper`; 45 | } else { 46 | return "It is not cheaper."; 47 | } 48 | } 49 | 50 | export function inFIL(number = 0, price) { 51 | const filecoinNumber = new FilecoinNumber(`${number}`, "attofil"); 52 | const inFil = filecoinNumber.toFil(); 53 | 54 | let candidate = `${formatAsFilecoin(inFil)}`; 55 | 56 | if (!isEmpty(price)) { 57 | let usd = Number(inFil) * Number(price); 58 | if (usd >= 0.0000001) { 59 | usd = `$${usd.toFixed(7)} USD`; 60 | } else { 61 | usd = `$0.00 USD`; 62 | } 63 | 64 | candidate = `${candidate} ⇄ ${usd}`; 65 | } 66 | 67 | return candidate; 68 | } 69 | 70 | export function inUSD(number, price) { 71 | const filecoinNumber = new FilecoinNumber(`${number}`, "attofil"); 72 | const inFil = filecoinNumber.toFil(); 73 | 74 | let candidate = `${formatAsFilecoin(inFil)}`; 75 | 76 | if (!isEmpty(price)) { 77 | let usd = Number(inFil) * Number(price); 78 | 79 | candidate = `$${usd} USD`; 80 | } 81 | 82 | return candidate; 83 | } 84 | 85 | export const isEmpty = (string) => { 86 | // NOTE(jim): This is not empty when its coerced into a string. 87 | if (string === 0) { 88 | return false; 89 | } 90 | 91 | if (!string) { 92 | return true; 93 | } 94 | 95 | if (typeof string === "object") { 96 | return true; 97 | } 98 | 99 | if (string.length === 0) { 100 | return true; 101 | } 102 | 103 | string = string.toString(); 104 | 105 | return !string.trim(); 106 | }; 107 | 108 | export const toDate = (date) => { 109 | return date.toUTCString(); 110 | }; 111 | 112 | export const toDateSinceEpoch = (epoch) => { 113 | const d = new Date(1000 * (epoch * 30 + 1598306400)); 114 | 115 | return toDate(d); 116 | }; 117 | 118 | export const bytesToSize = (bytes, decimals = 2) => { 119 | if (bytes === 0) return "0 Bytes"; 120 | 121 | const k = 1024; 122 | const dm = decimals < 0 ? 0 : decimals; 123 | const sizes = [ 124 | "Bytes", 125 | "KiB", 126 | "MiB", 127 | "GiB", 128 | "TiB", 129 | "PiB", 130 | "EiB", 131 | "ZiB", 132 | "YiB", 133 | ]; 134 | 135 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 136 | 137 | return `${(bytes / Math.pow(k, i)).toFixed(dm)} ${sizes[i]}`; 138 | }; 139 | 140 | export const bytesToGigaByte = (n = 0) => { 141 | return n / (1024 * 1024 * 1024); 142 | }; 143 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import * as Environment from '~/common/environment'; 2 | import * as Strings from '~/common/strings'; 3 | import * as NodeCache from '~/common/node-cache'; 4 | import * as Data from '~/common/node-data-parse'; 5 | 6 | import express from 'express'; 7 | import next from 'next'; 8 | import bodyParser from 'body-parser'; 9 | import compression from 'compression'; 10 | import cors from 'cors'; 11 | import moment from 'moment'; 12 | import crypto from 'crypto'; 13 | 14 | import { v4 as uuidv4 } from 'uuid'; 15 | 16 | const app = next({ 17 | dev: !Environment.IS_PRODUCTION, 18 | dir: __dirname, 19 | quiet: false, 20 | }); 21 | 22 | const handler = app.getRequestHandler(); 23 | 24 | global.locks = { fetch: false }; 25 | 26 | const getAthenaDataAsJSON = async () => { 27 | let index = 1; 28 | const data = { page_index: index, page_size: 100 }; 29 | 30 | const headers = { 31 | AppId: process.env.ATHENA_EXPLORER_APP_ID, 32 | Timestamp: new moment().format('YYYY-MM-DDTHH:mm:ssZZ'), 33 | SignatureVersion: 'V1', 34 | SignatureMethod: 'HMAC-SHA256', 35 | SignatureNonce: uuidv4(), 36 | }; 37 | 38 | function sign(body, opts = {}) { 39 | let sign_str = `body=${body ? JSON.stringify(body || {}) : ''}×tamp=${opts.Timestamp}&signatureNonce=${opts.SignatureNonce}`; 40 | const hmac = crypto.createHmac('sha256', opts.secret); 41 | hmac.update(sign_str); 42 | const s = hmac.digest(); 43 | return s.toString('hex'); 44 | } 45 | 46 | const signature = sign(null, { ...headers, secret: process.env.ATHENA_EXPLORER_SECRET }); 47 | 48 | const response = await fetch('https://openapi.atpool.com/v1/data/miners', { 49 | method: 'GET', 50 | form: data, 51 | headers: { 52 | ...headers, 53 | Signature: signature, 54 | }, 55 | }); 56 | 57 | console.log(response); 58 | const json = await response.json(); 59 | console.log(json); 60 | return json; 61 | }; 62 | 63 | const getEstuaryStatsAsJSON = async () => { 64 | console.log('fetching ...'); 65 | const response = await fetch('https://api.estuary.tech/public/stats'); 66 | const json = await response.json(); 67 | return json; 68 | }; 69 | 70 | const getEstuaryMinersAsJSON = async () => { 71 | console.log('fetching ...'); 72 | const response = await fetch('https://api.estuary.tech/public/miners'); 73 | const json = await response.json(); 74 | return json; 75 | }; 76 | 77 | const getSlingshotDataAsJSON = async () => { 78 | console.log('fetching ...'); 79 | const response = await fetch('https://space-race-slingshot-phase2.s3.amazonaws.com/prod/unfiltered_basic_stats.json'); 80 | const json = await response.json(); 81 | return json; 82 | }; 83 | 84 | const getFilecoinPriceDataAsJSON = async () => { 85 | console.log('fetching ...'); 86 | const response = await fetch(`https://cloud.iexapis.com/stable/crypto/filusdt/price?token=${process.env.IEXCLOUD_TOKEN}`); 87 | const json = await response.json(); 88 | return json; 89 | }; 90 | 91 | const getMinerIndexDataAsJSON = async () => { 92 | let miners = []; 93 | let canContinue = true; 94 | let offset = 0; 95 | 96 | while (offset < 4000 && canContinue) { 97 | console.log(`fetching ... textile ... ${offset} + 50...`); 98 | const response = await fetch(`https://minerindex.hub.textile.io/v1/index/query?sort.ascending=true&sort.field=ACTIVE_SECTORS&offset=${offset}&limit=50`); 99 | const json = await response.json(); 100 | 101 | if (!json.miners.length) { 102 | canContinue = false; 103 | break; 104 | } 105 | 106 | miners = [...miners, ...json.miners]; 107 | offset = offset + 50; 108 | } 109 | 110 | return { miners }; 111 | }; 112 | 113 | const getFilRepMinerIndexDataAsJSON = async () => { 114 | console.log('fetching ...'); 115 | const response = await fetch('https://api.filrep.io/api/v1/miners'); 116 | 117 | const json = await response.json(); 118 | return json; 119 | }; 120 | 121 | app.prepare().then(async () => { 122 | const server = express(); 123 | 124 | server.use(cors()); 125 | server.use(bodyParser.json({ limit: '10mb' })); 126 | server.use( 127 | bodyParser.urlencoded({ 128 | extended: false, 129 | }) 130 | ); 131 | 132 | if (Environment.IS_PRODUCTION) { 133 | server.use(compression()); 134 | } 135 | 136 | server.use('/public', express.static('public')); 137 | 138 | server.get('/refresh', async (r, s) => { 139 | const cache = NodeCache.accessCache('data.cache'); 140 | 141 | if (global.locks.fetch) { 142 | return s.status(200).json({}); 143 | } 144 | 145 | console.log('[ CACHE ] FORCED REFRESH ...'); 146 | const { payload, epoch } = await getSlingshotDataAsJSON(); 147 | const { price, symbol } = await getFilecoinPriceDataAsJSON(); 148 | const { miners } = await getMinerIndexDataAsJSON(); 149 | //const athenaResponse = await getAthenaDataAsJSON(); 150 | const athenaResponse = null; 151 | const estuaryMiners = await getEstuaryMinersAsJSON(); 152 | const estuaryStats = await getEstuaryStatsAsJSON(); 153 | const response = await getFilRepMinerIndexDataAsJSON(); 154 | 155 | const data = { 156 | epoch, 157 | price, 158 | symbol, 159 | estuaryStats, 160 | athenaResponse, 161 | miners: Data.getMinersArray({ 162 | athena: athenaResponse && athenaResponse.data ? athenaResponse.data.miners : [], 163 | textile: miners, 164 | filrep: response.miners, 165 | estuary: estuaryMiners, 166 | }), 167 | count: { 168 | athena: athenaResponse && athenaResponse.data ? athenaResponse.data.total_count : 0, 169 | textile: miners.length, 170 | filrep: response.miners.length, 171 | estuary: estuaryMiners.length, 172 | }, 173 | athena: null, 174 | /* 175 | athena: { 176 | deals: athenaResponse && athenaResponse.data ? athenaResponse.data.storage_deals : 0, 177 | verifiedDeals: athenaResponse && athenaResponse.data ? athenaResponse.data.verified_storageDeals : 0, 178 | data: athenaResponse && athenaResponse.data ? athenaResponse.data.data_stored : 0, 179 | verifiedData: athenaResponse && athenaResponse.data ? athenaResponse.data.verified_dataStored : 0, 180 | }, 181 | */ 182 | ...payload, 183 | }; 184 | 185 | console.log('[ CACHE ] STORING NEW ', data); 186 | await cache.put('store', data, 1800000 * 12); 187 | 188 | return s.status(200).json(data); 189 | }); 190 | 191 | server.get('/data', async (r, s) => { 192 | console.log(NodeCache); 193 | const cache = NodeCache.accessCache('data.cache'); 194 | const store = await cache.get('store'); 195 | 196 | console.log('[ CACHE ]', !!store); 197 | 198 | if (store || global.locks.fetch) { 199 | if (!store) { 200 | return s.status(200).json({ rebuilding: true }); 201 | } 202 | 203 | console.log('[ CACHE ] RETRIEVING ...'); 204 | 205 | // NOTE(jim): If invalid, we fire a request off but not async 206 | if (store.__invalidated) { 207 | const protocol = r.headers['x-forwarded-proto'] || 'http'; 208 | const baseURL = r ? `${protocol}://${r.headers.host}` : ''; 209 | fetch(`${baseURL}/refresh`); 210 | } 211 | 212 | return s.status(200).json(store); 213 | } 214 | 215 | global.locks.fetch = true; 216 | console.log('[ LOCK ] ENFORCED'); 217 | 218 | console.log('[ CACHE ] REBUILDING ...'); 219 | const { payload, epoch } = await getSlingshotDataAsJSON(); 220 | const { price, symbol } = await getFilecoinPriceDataAsJSON(); 221 | const { miners } = await getMinerIndexDataAsJSON(); 222 | // const athenaResponse = await getAthenaDataAsJSON(); 223 | const athenaResponse = null; 224 | const estuaryMiners = await getEstuaryMinersAsJSON(); 225 | const estuaryStats = await getEstuaryStatsAsJSON(); 226 | const response = await getFilRepMinerIndexDataAsJSON(); 227 | 228 | const data = { 229 | epoch, 230 | price, 231 | symbol, 232 | estuaryStats, 233 | athenaResponse, 234 | miners: Data.getMinersArray({ 235 | athena: athenaResponse && athenaResponse.data ? athenaResponse.data.miners : [], 236 | textile: miners, 237 | filrep: response.miners, 238 | estuary: estuaryMiners, 239 | }), 240 | count: { 241 | athena: athenaResponse && athenaResponse.data ? athenaResponse.data.total_count : 0, 242 | textile: miners.length, 243 | filrep: response.miners.length, 244 | estuary: estuaryMiners.length, 245 | }, 246 | athena: null, 247 | /* 248 | athena: { 249 | deals: athenaResponse && athenaResponse.data ? athenaResponse.data.storage_deals : 0, 250 | verifiedDeals: athenaResponse && athenaResponse.data ? athenaResponse.data.verified_storageDeals : 0, 251 | data: athenaResponse && athenaResponse.data ? athenaResponse.data.data_stored : 0, 252 | verifiedData: athenaResponse && athenaResponse.data ? athenaResponse.data.verified_dataStored : 0, 253 | }, 254 | */ 255 | ...payload, 256 | }; 257 | 258 | console.log('[ CACHE ] STORING NEW ', data); 259 | await cache.put('store', data, 1800000 * 12); 260 | 261 | console.log('[ LOCK ] LIFTED'); 262 | global.locks.fetch = false; 263 | 264 | return s.status(200).json(data); 265 | }); 266 | 267 | server.get('/', async (r, s) => { 268 | const protocol = r.headers['x-forwarded-proto'] || 'http'; 269 | const baseURL = r ? `${protocol}://${r.headers.host}` : ''; 270 | const dataRequest = await fetch(`${baseURL}/data`); 271 | const data = await dataRequest.json(); 272 | 273 | return app.render(r, s, '/', { 274 | ...data, 275 | }); 276 | }); 277 | 278 | server.all('*', async (r, s) => handler(r, s, r.url)); 279 | 280 | server.listen(Environment.PORT, async (e) => { 281 | if (e) throw e; 282 | 283 | console.log(`[ application-research/miners ] http://localhost:${Environment.PORT}`); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as Strings from '~/common/strings'; 3 | 4 | import { css } from '@emotion/react'; 5 | 6 | import Page from '~/components/Page'; 7 | import Loader from '~/components/Loader'; 8 | 9 | const ONE_GIB = 1073741824; 10 | const ONE_GB = 1000000000; 11 | const AMAZON_MONTHLY = 0.0125; 12 | const AMAZON_MONTHLY_IN_BYTES = AMAZON_MONTHLY / ONE_GB; 13 | const AMAZON_MONTHLY_IN_GIB = AMAZON_MONTHLY_IN_BYTES * 1073741824; 14 | 15 | const STYLES_GITHUB = css` 16 | background: #071908; 17 | color: #ffffff; 18 | padding: 8px 24px 8px 24px; 19 | font-size: 10px; 20 | font-family: 'Mono'; 21 | text-transform: uppercase; 22 | display: block; 23 | text-decoration: none; 24 | transition: 200ms ease color; 25 | 26 | &:visited { 27 | color: #ffffff; 28 | } 29 | 30 | &:hover { 31 | color: rgba(255, 255, 255, 0.8); 32 | } 33 | `; 34 | 35 | const STYLES_BODY = css` 36 | font-weight: 400; 37 | overflow-wrap: break-word; 38 | white-space: pre-wrap; 39 | width: 100%; 40 | display: flex; 41 | align-items: flex-start; 42 | justify-content: space-between; 43 | 44 | u { 45 | font-weight: 600; 46 | text-decoration: none; 47 | } 48 | 49 | @media (max-width: 1024px) { 50 | display: block; 51 | } 52 | `; 53 | 54 | const STYLES_H1 = css` 55 | color: #071908; 56 | font-size: 1.425rem; 57 | font-weight: 700; 58 | padding: 0px 24px 0px 24px; 59 | `; 60 | 61 | const STYLES_H2 = css` 62 | color: rgba(0, 0, 0, 0.8); 63 | font-size: 1.15rem; 64 | font-weight: 600; 65 | padding: 0px 24px 0px 24px; 66 | margin-top: 24px; 67 | display: block; 68 | text-decoration: none; 69 | `; 70 | 71 | const STYLES_H2_LINK = css` 72 | font-size: 1.15rem; 73 | font-weight: 600; 74 | padding: 0px 24px 0px 24px; 75 | margin-top: 24px; 76 | display: block; 77 | text-decoration: none; 78 | color: var(--color-primary); 79 | transition: 200ms ease opacity; 80 | 81 | &:visited { 82 | color: var(--color-primary); 83 | } 84 | 85 | &:hover { 86 | color: var(--color-primary); 87 | opacity: 0.8; 88 | } 89 | `; 90 | 91 | const STYLES_H3 = css` 92 | color: var(--color-primary); 93 | font-size: 1rem; 94 | font-weight: 500; 95 | padding: 0px 24px 0px 24px; 96 | margin-top: 24px; 97 | `; 98 | 99 | const STYLES_LINK = css` 100 | color: var(--color-primary); 101 | transition: 200ms ease all; 102 | 103 | :hover { 104 | opacity: 0.7; 105 | color: var(--color-primary); 106 | } 107 | 108 | :visited { 109 | color: var(--color-primary); 110 | } 111 | `; 112 | 113 | const STYLES_SUBHEADING = css` 114 | color: var(--color-primary); 115 | font-weight: 700; 116 | display: block; 117 | `; 118 | 119 | const STYLES_TEXT = css` 120 | padding: 0px 24px 0px 24px; 121 | margin-top: 16px; 122 | max-width: 640px; 123 | width: 100%; 124 | line-height: 1.5; 125 | 126 | a { 127 | text-decoration: none; 128 | color: var(--color-primary); 129 | font-weight: 600; 130 | transition: 200ms ease opacity; 131 | 132 | &:visited { 133 | color: var(--color-primary); 134 | } 135 | 136 | &:hover { 137 | color: var(--color-primary); 138 | opacity: 0.8; 139 | } 140 | } 141 | 142 | @media (max-width: 1024px) { 143 | max-width: none; 144 | } 145 | `; 146 | 147 | const STYLES_BODY_LEFT = css` 148 | width: 500px; 149 | padding: 0 0 24px 0; 150 | flex-shrink: 0; 151 | 152 | @media (max-width: 1024px) { 153 | width: 100%; 154 | } 155 | `; 156 | 157 | const STYLES_BODY_RIGHT = css` 158 | min-width: 10%; 159 | width: 100%; 160 | padding: 0 0 88px 0; 161 | min-height: 100vh; 162 | border-left: 1px solid #ececec; 163 | 164 | @media (max-width: 1024px) { 165 | border-left: 0px; 166 | } 167 | `; 168 | 169 | const STYLES_TABLE = css` 170 | font-size: 12px; 171 | font-family: 'Mono'; 172 | border: 0px; 173 | outline: 0px; 174 | padding: 0px; 175 | margin: 0px; 176 | border-top: 1px solid #ececec; 177 | width: 100%; 178 | border-collapse: collapse; 179 | margin-bottom: 32px; 180 | color: rgba(0, 0, 0, 0.8); 181 | 182 | th { 183 | text-transform: uppercase; 184 | padding: 8px 24px 8px 24px; 185 | border: 0px; 186 | outline: 0px; 187 | margin: 0px; 188 | overflow-wrap: break-word; 189 | white-space: pre-wrap; 190 | font-weight: 400; 191 | } 192 | 193 | tr { 194 | text-align: left; 195 | border: 0px; 196 | outline: 0px; 197 | margin: 0px; 198 | border-top: 1px solid #ececec; 199 | } 200 | 201 | td { 202 | text-align: left; 203 | border: 0px; 204 | outline: 0px; 205 | margin: 0px; 206 | padding: 8px 24px 8px 24px; 207 | overflow-wrap: break-word; 208 | white-space: pre-wrap; 209 | } 210 | `; 211 | 212 | const STYLES_STAT = css` 213 | display: flex; 214 | align-items: flex-start; 215 | justify-content: space-between; 216 | width: 100%; 217 | border-bottom: 1px solid #ececec; 218 | padding: 0 0 0 0; 219 | margin-bottom: 8px; 220 | 221 | &:last-child { 222 | border-bottom: 0px; 223 | margin-bottom: 0px; 224 | } 225 | `; 226 | 227 | const STYLES_STAT_LEFT = css` 228 | flex-shrink: 0; 229 | margin-bottom: 8px; 230 | color: #000; 231 | 232 | &:hover { 233 | color: #000; 234 | } 235 | 236 | &:visited { 237 | color: #000; 238 | } 239 | `; 240 | 241 | const STYLES_STAT_RIGHT = css` 242 | min-width: 10%; 243 | width: 100%; 244 | padding-left: 24px; 245 | text-align: right; 246 | `; 247 | 248 | const STYLES_BOX = css` 249 | min-height: 156px; 250 | background: rgba(0, 0, 0, 0.02); 251 | padding-top: 24px; 252 | border-bottom: 1px solid #ececec; 253 | `; 254 | 255 | const H1 = (props) =>

; 256 | const H2 = (props) => (props.href ? :

); 257 | const H3 = (props) =>

; 258 | const P = (props) =>

; 259 | 260 | export const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms)); 261 | 262 | export const getServerSideProps = async (context) => { 263 | return { 264 | props: { ...context.query }, 265 | }; 266 | }; 267 | 268 | export default class IndexPage extends React.Component { 269 | state = { 270 | miners: [], 271 | }; 272 | 273 | async componentDidMount() { 274 | await delay(2000); 275 | 276 | this.setState({ 277 | ...this.state, 278 | miners: this.props.miners, 279 | }); 280 | } 281 | 282 | render() { 283 | if (this.props.rebuilding) { 284 | return ( 285 | 286 |

287 |
288 |
289 |

file.app

290 |

Rebuilding your data, come back in a few minutes.

291 |
292 |
293 |
294 |
295 |

Want to use Filecoin to store data?

296 |

297 | Check out https://estuary.tech, our custom Filecoin⇄IPFS node designed to make storing large public data sets easier. 298 |

299 |
300 |
301 |
302 | 303 | ); 304 | } 305 | 306 | let totalCost = 0; 307 | let dealCount = 0; 308 | let dataStored = 0; 309 | let dataAvailable = 0; 310 | 311 | const minerElements = this.state.miners.length 312 | ? this.state.miners.map((each) => { 313 | dataStored = dataStored + Number(each.space.used); 314 | dataAvailable = dataAvailable + Number(each.space.free); 315 | dealCount = dealCount + Number(each.deals); 316 | totalCost = totalCost + Number(each.totalCost); 317 | 318 | return ( 319 | 320 | 321 |
322 | {each.address} 323 |
324 | {each.estuary ? ( 325 |
326 | 327 | View on Estuary 328 | 329 |
330 | ) : null} 331 | 332 | 333 | {each.region} [{each.iso}] 334 | 335 | 336 | {each.estuary ? ( 337 |
338 | Version 339 | {each.estuary.version} 340 |
341 | ) : null} 342 | 343 |
344 | Power 345 | {Strings.bytesToSize(each.power, 2)} 346 |
347 | 348 |
349 | Free space 350 | {Strings.bytesToSize(each.space.free, 2)} 351 |
352 | 353 |
354 | Used space 355 | {Strings.bytesToSize(each.space.used, 2)} 356 |
357 | 358 |
359 | Cost 360 | {Strings.inFIL(each.price * 2880, this.props.price)} per GiB per day 361 |
362 | 363 |
364 | Verified cost 365 | {Strings.inFIL(Number(each.verifiedPrice) > 0 ? each.verifiedPrice * 2880 : 0, this.props.price)} per GiB per day 366 |
367 | 368 |
369 | Total deal count 370 | {each.deals ? each.deals : '-'} 371 |
372 | 373 |
374 | 375 | 376 | 377 | 378 | 379 | 380 |
381 | 382 |
383 | Total storage cost 384 | {Strings.inFIL(each.totalCost, this.props.price)} 385 |
386 | 387 | 388 | ); 389 | }) 390 | : null; 391 | 392 | const averageCost = totalCost / this.state.miners.length; 393 | const averageAttoFILByByte = averageCost / dataStored; 394 | const averageAttoFILByGiB = averageAttoFILByByte * 1073741824; 395 | 396 | let storage = { 397 | day: averageAttoFILByGiB * 2880, 398 | month: averageAttoFILByGiB * 2880 * 30, 399 | year: averageAttoFILByGiB * 2880 * 365, 400 | }; 401 | 402 | let averages = { 403 | day: Strings.inFIL(storage.day, this.props.price), 404 | month: Strings.inFIL(storage.month, this.props.price), 405 | year: Strings.inFIL(storage.year, this.props.price), 406 | }; 407 | 408 | const { total_unique_cids = 0, total_unique_providers = 0, total_unique_clients = 0, epoch = 0 } = this.props; 409 | 410 | console.log(this.props); 411 | 412 | return ( 413 | 414 | 415 | View source on GitHub 416 | 417 |
418 |
419 |
420 |

file.app

421 |

Filecoin miner analytics

422 |
423 |

424 | storage.filecoin.io 425 |

426 |

{Strings.toDateSinceEpoch(epoch)}

427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 |
CIDsProvidersClients
{total_unique_cids ? total_unique_cids.toLocaleString() : 0}{total_unique_providers ? total_unique_providers.toLocaleString() : 0}{total_unique_clients ? total_unique_clients.toLocaleString() : 0}
442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 455 | 456 | 457 |
All dealsData
{this.props.total_num_deals ? this.props.total_num_deals.toLocaleString() : 0} 452 | {this.props.total_stored_data_size ? this.props.total_stored_data_size.toLocaleString() : 0} Bytes ⇄{' '} 453 | 454 |
458 | 459 |

460 | plus.fil.org 461 |

462 |

{Strings.toDateSinceEpoch(epoch)}

463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 476 | 477 | 478 |
Verified dealsData
{this.props.filplus_total_num_deals ? this.props.filplus_total_num_deals.toLocaleString() : 0} 473 | {this.props.filplus_total_stored_data_size ? this.props.filplus_total_stored_data_size.toLocaleString() : 0} bytes ⇄{' '} 474 | 475 |
479 | 480 |

481 | estuary.tech 482 |

483 |

{Strings.toDateSinceEpoch(epoch)}

484 | 485 | {this.props.estuaryStats ? ( 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 500 | 501 | 502 |
Verified dealsFilesStorage
{this.props.estuaryStats.dealsOnChain ? this.props.estuaryStats.dealsOnChain.toLocaleString() : 0}{this.props.estuaryStats.totalFiles ? this.props.estuaryStats.totalFiles.toLocaleString() : 0} 497 | {this.props.estuaryStats.totalStorage ? this.props.estuaryStats.totalStorage.toLocaleString() : 0} bytes ⇄{' '} 498 | 499 |
503 | ) : null} 504 | 505 | {this.props.athena && this.props.athenaResponse ? ( 506 | 507 |

508 | atpool.com/en-US/ 509 |

510 |

{Strings.toDateSinceEpoch(epoch)}

511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 526 | 529 | 530 | 531 |
DealsVerifiedStorageVerified
{this.props.athena.deals ? this.props.athena.deals.toLocaleString() : 0}{this.props.athena.verifiedDeals ? this.props.athena.verifiedDeals.toLocaleString() : 0} 524 | 525 | 527 | 528 |
532 |
533 | ) : null} 534 | 535 |

536 | iexcloud.io 537 |

538 |

{Strings.toDateSinceEpoch(epoch)}

539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 |
Price
${this.props.price} FIL / USDT
550 | 551 |

miners

552 |

Miner sources for our analysis.

553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | {this.props.athena && this.props.athenaResponse ? : null} 561 | 562 | 563 | 564 | 565 | 566 | {this.props.athena && this.props.athenaResponse ? : null} 567 | 568 | 569 |
textile.iofilrep.ioestuary.techatpool
{this.props.count.textile}{this.props.count.filrep}{this.props.count.estuary}{this.props.count.athena}
570 |
571 |
572 |
573 |

Want to use Filecoin to store data?

574 |

575 | Check out https://estuary.tech, our custom Filecoin⇄IPFS node designed to make storing large public data sets easier. 576 |

577 |
578 | 579 | {this.state.miners.length ? ( 580 | 581 |

Comparisons

582 |

All prices are based off of these values.

583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 |
GIBMIBKIBFIL-epoch24 Hours
1073741824 bytes1048576 bytes1024 bytes30 seconds2880 FIL-epochs
602 | 603 |

Average storage cost

604 |

Calculations are based off a FIL-epoch which is 30 seconds.

605 | 606 |

Calculation

607 |

608 | Filecoin cost per byte * 1073741824 = {averageAttoFILByGiB} attoFIL per GiB 609 |

610 | 611 | 612 | 613 | 614 | 615 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 628 | 629 | 630 |
per GiB per Day 616 | per GiB per Month 617 | per GiB per YearAverage deal cost
{averages.day}{averages.month}{averages.year} 626 | {Strings.inFIL(averageCost / dealCount)} ⇄ {Strings.inUSD(averageCost / dealCount, this.props.price)} 627 |
631 | 632 |

Comparison to Amazon S3 - Infrequent Access

633 |

634 | Determining the percentage of how much cheaper or expensive Filecoin storage is compared to Amazon S3 - Infrequent Access tier. That tier costs{' '} 635 | $0.0134217728 per GiB per month. Amazon recommends this pricing tier for long lived but infrequently accessed data that needs millisecond access. 636 |

637 | 638 |

Calculation

639 |

640 | Filecoin cost / Amazon cost = {Strings.percentageCheaper(storage.day, AMAZON_MONTHLY_IN_GIB / 30, this.props.price)} 641 |

642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 |
per GiB per dayper GiB per month (30 days)per GiB per year
{Strings.percentageCheaper(storage.day, AMAZON_MONTHLY_IN_GIB / 30, this.props.price)}{Strings.percentageCheaper(storage.month, AMAZON_MONTHLY_IN_GIB, this.props.price)}{Strings.percentageCheaper(storage.year, AMAZON_MONTHLY_IN_GIB * 12, this.props.price)}
657 |
658 | ) : null} 659 | 660 |

Filtered miners

661 |

Updated on {Strings.toDateSinceEpoch(epoch)}. Requires successful storage deals to be listed.

662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 |
MinersTotal storage dealsTotal storage sizeTotal available size
{this.state.miners ? this.state.miners.length.toLocaleString() : 0}{dealCount ? dealCount.toLocaleString() : 0}{dataStored ? Strings.bytesToSize(dataStored, 2) : 0}{dataAvailable ? Strings.bytesToSize(dataAvailable, 2) : 0}
680 |
681 | 682 | {minerElements ? ( 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | {minerElements} 691 | 692 |
AddressLocationData
693 | ) : ( 694 | 695 |

696 | 697 |

698 |

Loading... There are a lot of miners to show...

699 |
700 | )} 701 |
702 |
703 |
704 | ); 705 | } 706 | } 707 | --------------------------------------------------------------------------------