├── 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 |
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 | {Strings.inFIL(each.totalCost / each.deals, this.props.price)} per deal
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 | CIDs
432 | Providers
433 | Clients
434 |
435 |
436 | {total_unique_cids ? total_unique_cids.toLocaleString() : 0}
437 | {total_unique_providers ? total_unique_providers.toLocaleString() : 0}
438 | {total_unique_clients ? total_unique_clients.toLocaleString() : 0}
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 | All deals
447 | Data
448 |
449 |
450 | {this.props.total_num_deals ? this.props.total_num_deals.toLocaleString() : 0}
451 |
452 | {this.props.total_stored_data_size ? this.props.total_stored_data_size.toLocaleString() : 0} Bytes ⇄{' '}
453 | {Strings.bytesToSize(this.props.total_stored_data_size, 4)}
454 |
455 |
456 |
457 |
458 |
459 |
460 | plus.fil.org
461 |
462 |
{Strings.toDateSinceEpoch(epoch)}
463 |
464 |
465 |
466 |
467 | Verified deals
468 | Data
469 |
470 |
471 | {this.props.filplus_total_num_deals ? this.props.filplus_total_num_deals.toLocaleString() : 0}
472 |
473 | {this.props.filplus_total_stored_data_size ? this.props.filplus_total_stored_data_size.toLocaleString() : 0} bytes ⇄{' '}
474 | {Strings.bytesToSize(this.props.filplus_total_stored_data_size, 4)}
475 |
476 |
477 |
478 |
479 |
480 |
481 | estuary.tech
482 |
483 |
{Strings.toDateSinceEpoch(epoch)}
484 |
485 | {this.props.estuaryStats ? (
486 |
487 |
488 |
489 | Verified deals
490 | Files
491 | Storage
492 |
493 |
494 | {this.props.estuaryStats.dealsOnChain ? this.props.estuaryStats.dealsOnChain.toLocaleString() : 0}
495 | {this.props.estuaryStats.totalFiles ? this.props.estuaryStats.totalFiles.toLocaleString() : 0}
496 |
497 | {this.props.estuaryStats.totalStorage ? this.props.estuaryStats.totalStorage.toLocaleString() : 0} bytes ⇄{' '}
498 | {Strings.bytesToSize(this.props.estuaryStats.totalStorage, 4)}
499 |
500 |
501 |
502 |
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 | Deals
516 | Verified
517 | Storage
518 | Verified
519 |
520 |
521 | {this.props.athena.deals ? this.props.athena.deals.toLocaleString() : 0}
522 | {this.props.athena.verifiedDeals ? this.props.athena.verifiedDeals.toLocaleString() : 0}
523 |
524 | {Strings.bytesToSize(this.props.athena.data, 4)}
525 |
526 |
527 | {Strings.bytesToSize(this.props.athena.verifiedData, 4)}
528 |
529 |
530 |
531 |
532 |
533 | ) : null}
534 |
535 |
536 | iexcloud.io
537 |
538 |
{Strings.toDateSinceEpoch(epoch)}
539 |
540 |
541 |
542 |
543 | Price
544 |
545 |
546 | ${this.props.price} FIL / USDT
547 |
548 |
549 |
550 |
551 |
miners
552 |
Miner sources for our analysis.
553 |
554 |
555 |
556 |
557 | textile.io
558 | filrep.io
559 | estuary.tech
560 | {this.props.athena && this.props.athenaResponse ? atpool : null}
561 |
562 |
563 | {this.props.count.textile}
564 | {this.props.count.filrep}
565 | {this.props.count.estuary}
566 | {this.props.athena && this.props.athenaResponse ? {this.props.count.athena} : null}
567 |
568 |
569 |
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 | GIB
588 | MIB
589 | KIB
590 | FIL-epoch
591 | 24 Hours
592 |
593 |
594 | 1073741824 bytes
595 | 1048576 bytes
596 | 1024 bytes
597 | 30 seconds
598 | 2880 FIL-epochs
599 |
600 |
601 |
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 | per GiB per Day
615 |
616 | per GiB per Month
617 |
618 | per GiB per Year
619 | Average deal cost
620 |
621 |
622 | {averages.day}
623 | {averages.month}
624 | {averages.year}
625 |
626 | {Strings.inFIL(averageCost / dealCount)} ⇄ {Strings.inUSD(averageCost / dealCount, this.props.price)}
627 |
628 |
629 |
630 |
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 | per GiB per day
647 | per GiB per month (30 days)
648 | per GiB per year
649 |
650 |
651 | {Strings.percentageCheaper(storage.day, AMAZON_MONTHLY_IN_GIB / 30, this.props.price)}
652 | {Strings.percentageCheaper(storage.month, AMAZON_MONTHLY_IN_GIB, this.props.price)}
653 | {Strings.percentageCheaper(storage.year, AMAZON_MONTHLY_IN_GIB * 12, this.props.price)}
654 |
655 |
656 |
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 | Miners
668 | Total storage deals
669 | Total storage size
670 | Total available size
671 |
672 |
673 | {this.state.miners ? this.state.miners.length.toLocaleString() : 0}
674 | {dealCount ? dealCount.toLocaleString() : 0}
675 | {dataStored ? Strings.bytesToSize(dataStored, 2) : 0}
676 | {dataAvailable ? Strings.bytesToSize(dataAvailable, 2) : 0}
677 |
678 |
679 |
680 |
681 |
682 | {minerElements ? (
683 |
684 |
685 |
686 | Address
687 | Location
688 | Data
689 |
690 | {minerElements}
691 |
692 |
693 | ) : (
694 |
695 |
696 |
697 |
698 | Loading... There are a lot of miners to show...
699 |
700 | )}
701 |
702 |
703 |
704 | );
705 | }
706 | }
707 |
--------------------------------------------------------------------------------