├── .gitbook.yaml ├── web ├── backup │ ├── react-app-env.d.ts │ ├── utils │ │ ├── constants.ts │ │ ├── parse.test.ts │ │ ├── stats.ts │ │ └── parse.ts │ ├── pages │ │ ├── data.tsx │ │ ├── home.tsx │ │ ├── stats.tsx │ │ └── privacy.tsx │ ├── index.tsx │ ├── services │ │ ├── PriceService.ts │ │ ├── AirtableService.ts │ │ ├── EmailService.ts │ │ ├── MailjetService.ts │ │ ├── AlertService.ts │ │ └── GasService.ts │ ├── components │ │ ├── layout │ │ │ ├── alert.tsx │ │ │ ├── loading.tsx │ │ │ ├── header.tsx │ │ │ └── footer.tsx │ │ ├── index.ts │ │ ├── gaspricerow.tsx │ │ ├── gastablerow.tsx │ │ ├── gastable.tsx │ │ ├── alertcard.tsx │ │ ├── alertstats.tsx │ │ ├── gasprices.tsx │ │ ├── gaspricecard.tsx │ │ ├── heatmap.tsx │ │ ├── register.tsx │ │ └── gaschart.tsx │ ├── setupProxy.js │ ├── migrate.ts │ ├── functions │ │ ├── alerts.ts │ │ ├── gas.ts │ │ ├── cancel.ts │ │ ├── confirm.ts │ │ ├── registrations.ts │ │ ├── trend.ts │ │ ├── register.ts │ │ ├── save.ts │ │ ├── average.ts │ │ └── notify.ts │ ├── collectors │ │ ├── Collector.ts │ │ ├── MyCryptoCollector.ts │ │ ├── NetworkCollector.ts │ │ ├── UpvestCollector.ts │ │ ├── EtherchainCollector.ts │ │ ├── GasStationCollector.ts │ │ ├── EtherscanCollector.ts │ │ ├── GasCollector.ts │ │ └── GasNowCollector.ts │ ├── config │ │ └── app.ts │ ├── assets │ │ └── index.css │ ├── types │ │ └── index.ts │ └── layout │ │ └── main.tsx ├── src │ ├── react-app-env.d.ts │ ├── index.tsx │ └── setupProxy.js ├── public │ ├── fuel.png │ └── index.html ├── example.env ├── .gitignore ├── .babelrc ├── tsconfig.json ├── netlify.toml └── package.json ├── docs ├── .gitbook │ └── assets │ │ ├── image.png │ │ └── image (1).png ├── SUMMARY.md ├── README.md └── api.md ├── .travis.yml ├── README.md ├── LICENSE └── CODE_OF_CONDUCT.md /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ -------------------------------------------------------------------------------- /web/backup/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/backup/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const AVERAGE_NAME = "AVERAGE"; 2 | -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/public/fuel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/ethgaswatch/HEAD/web/public/fuel.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/ethgaswatch/HEAD/docs/.gitbook/assets/image.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/image (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/ethgaswatch/HEAD/docs/.gitbook/assets/image (1).png -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | * [API](api.md) 5 | 6 | ## ETH Gas.watch 7 | 8 | * [Visit website](https://ethgas.watch/) 9 | 10 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | ReactDOM.render( 5 | 6 |
7 |
, 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /web/backup/pages/data.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Heatmap } from '../components/heatmap'; 3 | 4 | function Data() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default Data; 13 | -------------------------------------------------------------------------------- /web/example.env: -------------------------------------------------------------------------------- 1 | REACT_APP_AIRTABLE_APIKEY="" 2 | REACT_APP_AIRTABLE_BASEID="" 3 | 4 | REACT_APP_ETHERSCAN_APIKEY="" 5 | REACT_APP_GASSTATION_APIKEY="" 6 | REACT_APP_SENDGRID_APIKEY="" 7 | 8 | REACT_APP_MONGODB_CONNECTIONSTRING="" 9 | REACT_APP_MONGODB_DB="" 10 | -------------------------------------------------------------------------------- /web/backup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Main from './layout/main'; 4 | 5 | ReactDOM.render( 6 | 7 |
8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | branches: 6 | only: 7 | - master 8 | cache: 9 | directories: 10 | - node_modules 11 | before_install: 12 | - npm update 13 | - cd web 14 | install: 15 | - npm install 16 | script: 17 | - npm run test -------------------------------------------------------------------------------- /web/backup/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GasPrices} from '../components'; 3 | import { Heatmap } from '../components/heatmap'; 4 | 5 | function Home() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default Home; 15 | -------------------------------------------------------------------------------- /web/backup/pages/stats.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AlertStats, GasChart } from '../components'; 3 | 4 | function Stats() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default Stats; 15 | -------------------------------------------------------------------------------- /web/backup/services/PriceService.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | require('encoding'); 3 | 4 | export async function GetSpotPrice() : Promise { 5 | 6 | const response = await fetch(`https://api.coinbase.com/v2/prices/ETH-USD/spot`); 7 | const body = await response.json(); 8 | 9 | return parseFloat(body.data.amount); 10 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /functions 14 | 15 | # misc 16 | .env 17 | .env.prod 18 | .env.dev 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /web/backup/components/layout/alert.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | interface AlertProps { 5 | type: "danger" | "warning" | "success" | "info" 6 | message: string 7 | } 8 | 9 | export const Alert = (props: AlertProps) => { 10 | return ( 11 |
12 | {props.message} 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /web/backup/components/layout/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Loading = () => { 4 | return ( 5 | <> 6 |
7 |
8 | Loading... 9 |
10 |
11 | 12 | ); 13 | } -------------------------------------------------------------------------------- /web/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | console.log("setup proxy...") 5 | 6 | app.use('/.netlify/functions/', 7 | createProxyMiddleware({ 8 | target: 'http://localhost:9000', 9 | pathRewrite: { 10 | '^/\\.netlify/functions': '' 11 | } 12 | })); 13 | }; -------------------------------------------------------------------------------- /web/backup/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | console.log("setup proxy...") 5 | 6 | app.use('/.netlify/functions/', 7 | createProxyMiddleware({ 8 | target: 'http://localhost:9000', 9 | pathRewrite: { 10 | '^/\\.netlify/functions': '' 11 | } 12 | })); 13 | }; -------------------------------------------------------------------------------- /web/backup/utils/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { WeiToGwei } from './parse'; 2 | 3 | it('parses wei to gwei', () => { 4 | const wei = 1000000000; 5 | const gwei = WeiToGwei(wei) 6 | 7 | expect(gwei).toEqual(1000000000 / 1e9); 8 | }); 9 | 10 | it('parses gwei to ether', () => { 11 | const gwei = 1000000000; 12 | const ether = WeiToGwei(gwei) 13 | 14 | expect(ether).toEqual(1000000000 / 1e9); 15 | }); -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "8.10" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-class-properties", 15 | "@babel/plugin-transform-object-assign", 16 | "@babel/plugin-proposal-object-rest-spread" 17 | ] 18 | } -------------------------------------------------------------------------------- /web/backup/migrate.ts: -------------------------------------------------------------------------------- 1 | import { GetAllUsersIterative } from "./services/AirtableService"; 2 | import { RegisterMany } from "./services/AlertService"; 3 | import { RegisteredEmailAddress } from "./types"; 4 | 5 | console.log("TESTING MIGRATE..") 6 | Run(); 7 | 8 | export async function Run() { 9 | const users = await GetAllUsersIterative(new Array()); 10 | console.log("TOTAL USERS", users.length); 11 | 12 | await RegisterMany(users); 13 | console.log("ALL DONE!") 14 | } 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: An aggregated Ethereum gas price feed. 3 | --- 4 | 5 | # Introduction 6 | 7 | UPDATE FEB 2022: This API service is no longer publicly available. Please use any the alternative services instead. Documentation will be removed soon. 8 | 9 | 10 | ETH Gas.watch is an aggregated gas price feed that checks multiple data sources for the latest gas prices. By aggregating these data sources, it provides a more reliable, median gas price. 11 | 12 | Visit [ethgas.watch](https://ethgas.watch/) 13 | 14 | ![](.gitbook/assets/image%20%281%29.png) 15 | 16 | -------------------------------------------------------------------------------- /web/backup/functions/alerts.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, GetUserAlertsData } from '../services/AlertService'; 3 | 4 | Connect().then(() => console.log("AlertService Connected")); 5 | 6 | export async function handler(event: APIGatewayEvent, context: Context) { 7 | context.callbackWaitsForEmptyEventLoop = false; 8 | 9 | const data = await GetUserAlertsData(); 10 | 11 | return { 12 | statusCode: 200, 13 | body: JSON.stringify(data), 14 | headers: { 15 | 'Cache-Control': 'public, max-age=1800', 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /web/backup/components/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Alert } from "./layout/alert"; 3 | export { Footer } from "./layout/footer"; 4 | export { Header } from "./layout/header"; 5 | export { Loading } from './layout/loading'; 6 | 7 | export { AlertStats } from "./alertstats"; 8 | export { AlertCard } from "./alertcard"; 9 | export { GasChart } from "./gaschart"; 10 | export { GasPriceCard } from './gaspricecard'; 11 | export { GasPriceRow } from './gaspricerow'; 12 | export { GasPrices } from './gasprices'; 13 | export { GasTable } from './gastable'; 14 | export { GasTableRow } from './gastablerow'; 15 | export { Register } from './register'; -------------------------------------------------------------------------------- /web/backup/functions/gas.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, GetLatestGasData } from '../services/GasService'; 3 | import { GasPriceData } from '../types'; 4 | 5 | Connect().then(() => console.log("GasService Connected")); 6 | 7 | export async function handler(event: APIGatewayEvent, context: Context) { 8 | context.callbackWaitsForEmptyEventLoop = false; 9 | 10 | const data = await GetLatestGasData(); 11 | 12 | return { 13 | statusCode: 200, 14 | body: JSON.stringify(data), 15 | headers: { 16 | 'Cache-Control': 'public, max-age=300', 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "exclude": ["src/setupProxy.js", "src/functions"], 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /web/backup/components/gaspricerow.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { GasPriceData } from '../types'; 4 | import { GasPriceCard } from './'; 5 | 6 | interface GasPriceRowProps { 7 | data: GasPriceData 8 | } 9 | 10 | export const GasPriceRow = (props: GasPriceRowProps) => { 11 | return ( 12 | <> 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /web/backup/collectors/Collector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import fetch from 'node-fetch'; 3 | 4 | 5 | export class Collector { 6 | protected name: string = ""; 7 | protected url: string = ""; 8 | 9 | async collect(): Promise { 10 | try{ 11 | const response = await fetch(this.url); 12 | return this.MapGas(await response.json()); 13 | } 14 | catch (ex) { 15 | console.log("Couldn't retrieve data from " + this.name, ex); 16 | return null; 17 | } 18 | } 19 | 20 | protected async MapGas(body: string) : Promise{ 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/backup/collectors/MyCryptoCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | 4 | export class MyCryptoCollector extends Collector{ 5 | name = "MyCrypto"; 6 | url = `https://gas.mycryptoapi.com/`; 7 | async MapGas(body: any): Promise { 8 | 9 | return { 10 | name: this.name, 11 | source: this.url, 12 | instant: Math.round(body.fastest), 13 | fast: Math.round(body.fast), 14 | standard: Math.round(body.standard), 15 | slow: Math.round(body.safeLow), 16 | lastBlock: Number(body.blockNum) 17 | } as RecommendedGasPrices; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/backup/collectors/NetworkCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | 4 | export class NetworkCollector extends Collector{ 5 | name = "POA Network"; 6 | url = `https://gasprice.poa.network/`; 7 | 8 | async MapGas(body: any): Promise { 9 | 10 | return { 11 | name: this.name, 12 | source: this.url, 13 | instant: Math.round(body.instant), 14 | fast: Math.round(body.fast), 15 | standard: Math.round(body.standard), 16 | slow: Math.round(body.slow), 17 | lastBlock: Number(body.block_number) 18 | } as RecommendedGasPrices; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/backup/functions/cancel.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, UpdateUserAlert } from '../services/AlertService'; 3 | 4 | Connect().then(() => console.log("AlertService Connected")); 5 | 6 | export async function handler(event: APIGatewayEvent, context: Context) { 7 | context.callbackWaitsForEmptyEventLoop = false; 8 | const data = event.queryStringParameters; 9 | console.log(data); 10 | 11 | if (!data.email || !data.id) 12 | return { statusCode: 400, body: "Bad Request" }; 13 | 14 | const result = await UpdateUserAlert(data.id, { disabled: true }); 15 | if (!result) return { statusCode: 500, body: "Error updating user" }; 16 | 17 | return { 18 | statusCode: 200, 19 | body: `Ok. Email removed` 20 | } 21 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS PROJECT IS MIGRATED 2 | 3 | This project has been migrated and is now part of [useWeb3](https://github.com/wslyvh/useWeb3). Live version available at https://www.useweb3.xyz/gas/ 4 | 5 | 6 | 7 | --- 8 | 9 | 10 | ETH Gas.watch is an aggregated gas price feed that checks multiple data sources for the latest gas prices. By aggregating these data sources, it provides a more reliable, median gas price. The service includes email alerts to get notified when the price drops. 11 | 12 | - Donations https://gitcoin.co/grants/4143/wslyvh-useweb3-ethgas-tokenlog-more 13 | 14 | ## License 15 | 16 | ETH Gas.watch is released under the [MIT License](https://opensource.org/licenses/MIT). 17 | 18 | --- 19 | 20 | ![Deploys by Netlify](https://www.netlify.com/img/global/badges/netlify-color-accent.svg) 21 | -------------------------------------------------------------------------------- /web/backup/collectors/UpvestCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | 4 | export class UpvestCollector extends Collector{ 5 | name = "Upvest"; 6 | url = `https://fees.upvest.co/estimate_eth_fees`; 7 | 8 | async MapGas(body: any): Promise { 9 | 10 | return { 11 | name: this.name, 12 | source: "https://doc.upvest.co/reference#ethereum-fees", 13 | instant: Math.round(body.estimates.fastest), 14 | fast: Math.round(body.estimates.fast), 15 | standard: Math.round(body.estimates.medium), 16 | slow: Math.round(body.estimates.slow), 17 | lastUpdate: Date.now() 18 | } as RecommendedGasPrices; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/backup/functions/confirm.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, UpdateUserAlert } from '../services/AlertService'; 3 | 4 | Connect().then(() => console.log("AlertService Connected")); 5 | 6 | export async function handler(event: APIGatewayEvent, context: Context) { 7 | context.callbackWaitsForEmptyEventLoop = false; 8 | const data = event.queryStringParameters; 9 | console.log(data); 10 | 11 | if (!data.email || !data.id) 12 | return { statusCode: 400, body: "Bad Request" }; 13 | 14 | const result = await UpdateUserAlert(data.id, { confirmed: true }); 15 | if (!result) return { statusCode: 500, body: "Error updating user" }; 16 | 17 | return { 18 | statusCode: 200, 19 | body: `Ok. Email confirmed` 20 | } 21 | } -------------------------------------------------------------------------------- /web/backup/functions/registrations.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, GetDailyUserAlertsRegistrations } from '../services/AlertService'; 3 | 4 | Connect().then(() => console.log("AlertService Connected")); 5 | 6 | export async function handler(event: APIGatewayEvent, context: Context) { 7 | context.callbackWaitsForEmptyEventLoop = false; 8 | const qs = event.queryStringParameters; 9 | 10 | let data: any = null; 11 | const days = parseInt(qs.days); 12 | if (!isNaN(days)) { 13 | data = await GetDailyUserAlertsRegistrations(days); 14 | } 15 | 16 | if (isNaN(days)) 17 | return { statusCode: 400, body: "Bad Request" }; 18 | 19 | return { 20 | statusCode: 200, 21 | body: JSON.stringify(data) 22 | } 23 | } -------------------------------------------------------------------------------- /web/backup/utils/stats.ts: -------------------------------------------------------------------------------- 1 | 2 | export function GetAverage(values: number[]): number { 3 | 4 | return values.reduce((a, v) => a + v) / values.length; 5 | } 6 | 7 | export function GetMedian(values: number[]): number { 8 | const prices = values.sort(); 9 | const mid = Math.ceil(prices.length / 2); 10 | 11 | return prices.length % 2 == 0 ? (prices[mid] + prices[mid - 1]) / 2 : prices[mid - 1]; 12 | } 13 | 14 | export function GetMode(values: number[]): number { 15 | const mode: any = {}; 16 | let max = 0, count = 0; 17 | 18 | values.forEach((i: any) => { 19 | if (mode[i]) mode[i]++; 20 | else mode[i] = 1; 21 | 22 | if (count < mode[i]) { 23 | max = i; 24 | count = mode[i]; 25 | } 26 | }); 27 | 28 | return max; 29 | } -------------------------------------------------------------------------------- /web/backup/collectors/EtherchainCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | 4 | export class EtherchainCollector extends Collector{ 5 | name = "Etherchain"; 6 | url = `https://www.etherchain.org/api/gasPriceOracle`; 7 | 8 | async MapGas(body: any): Promise { 9 | 10 | return { 11 | name: this.name, 12 | source: "https://etherchain.org/tools/gasPriceOracle", 13 | instant: Math.round(Number(body.fastest)), 14 | fast: Math.round(Number(body.fast)), 15 | standard: Math.round(Number(body.standard)), 16 | slow: Math.round(Number(body.safeLow)), 17 | lastUpdate: Date.now() 18 | } as RecommendedGasPrices; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/backup/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export const Header = () => { 5 | return ( 6 | <> 7 | 22 | 23 | ); 24 | } -------------------------------------------------------------------------------- /web/backup/config/app.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | 3 | dotenv.config(); 4 | 5 | export const AppConfig = { 6 | NODE_ENV: process.env.NODE_ENV, 7 | 8 | HOST: process.env.REACT_APP_HOST || "http://localhost:3000/", 9 | AIRTABLE_APIKEY: process.env.REACT_APP_AIRTABLE_APIKEY || "", 10 | AIRTABLE_BASEID: process.env.REACT_APP_AIRTABLE_BASEID || "", 11 | ETHERSCAN_APIKEY: process.env.REACT_APP_ETHERSCAN_APIKEY || "", 12 | GASSTATION_APIKEY: process.env.REACT_APP_GASSTATION_APIKEY || "", 13 | SENDGRID_APIKEY: process.env.REACT_APP_SENDGRID_APIKEY || "", 14 | MONGODB_CONNECTIONSTRING: process.env.REACT_APP_MONGODB_CONNECTIONSTRING || "", 15 | MONGODB_DB: process.env.REACT_APP_MONGODB_DB || "ethgas", 16 | MAILJET_APIKEY: process.env.REACT_APP_MAILJET_APIKEY || "", 17 | MAILJET_PASSWORD: process.env.REACT_APP_MAILJET_PASSWORD || "", 18 | }; -------------------------------------------------------------------------------- /web/backup/collectors/GasStationCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | import { AppConfig } from "../config/app"; 4 | 5 | export class GasStationCollector extends Collector{ 6 | name = "Gas station"; 7 | url = `https://ethgasstation.info/api/ethgasAPI.json?api-key=${AppConfig.GASSTATION_APIKEY}`; 8 | async MapGas(body: any): Promise { 9 | 10 | return { 11 | name: this.name, 12 | source: "https://ethgasstation.info/", 13 | instant: Math.round(body.fastest / 10), 14 | fast: Math.round(body.fast / 10), 15 | standard: Math.round(body.average / 10), 16 | slow: Math.round(body.safeLow / 10), 17 | lastBlock: Number(body.blockNum) 18 | } as RecommendedGasPrices; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/backup/collectors/EtherscanCollector.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from "../config/app"; 2 | import { RecommendedGasPrices } from "../types"; 3 | import { Collector } from "./Collector"; 4 | 5 | export class EtherscanCollector extends Collector{ 6 | name = "Etherscan"; 7 | url = `https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=${AppConfig.ETHERSCAN_APIKEY}`; 8 | 9 | async MapGas(body: any): Promise { 10 | 11 | return { 12 | name: this.name, 13 | source: "https://etherscan.io/gastracker", 14 | // NO instant 15 | fast: Math.round(Number(body.result.FastGasPrice)), 16 | standard: Math.round(Number(body.result.ProposeGasPrice)), 17 | slow: Math.round(Number(body.result.SafeGasPrice)), 18 | lastBlock: Number(body.result.LastBlock) 19 | } as RecommendedGasPrices; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/backup/components/gastablerow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecommendedGasPrices } from '../types'; 3 | 4 | interface GasTableRowProps { 5 | data: RecommendedGasPrices 6 | } 7 | 8 | export const GasTableRow = (props: GasTableRowProps) => { 9 | 10 | let renderSource = <> 11 | if (props.data.source) { 12 | renderSource = ( 13 | 14 | ℹ️ 15 | 16 | ) 17 | } 18 | 19 | return ( 20 | 21 | 22 | {props.data.name} {renderSource} 23 | 24 | {props.data.slow || "-"} 25 | {props.data.standard || "-"} 26 | {props.data.fast || "-"} 27 | {props.data.instant || "-"} 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /web/backup/functions/trend.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { TrendChartData } from '../types'; 3 | import { Connect, GetDailyAverageGasData, GetHourlyAverageGasData } from '../services/GasService'; 4 | 5 | Connect().then(() => console.log("GasService Connected")); 6 | 7 | export async function handler(event: APIGatewayEvent, context: Context) { 8 | context.callbackWaitsForEmptyEventLoop = false; 9 | const qs = event.queryStringParameters; 10 | 11 | let data: TrendChartData = null; 12 | 13 | const days = parseInt(qs.days); 14 | if (!isNaN(days)) { 15 | data = await GetDailyAverageGasData(days); 16 | } 17 | 18 | const hours = parseInt(qs.hours); 19 | if (!isNaN(hours)) { 20 | data = await GetHourlyAverageGasData(hours); 21 | } 22 | 23 | if (isNaN(days) && isNaN(hours)) 24 | return { statusCode: 400, body: "Bad Request" }; 25 | 26 | return { 27 | statusCode: 200, 28 | body: JSON.stringify(data), 29 | } 30 | } -------------------------------------------------------------------------------- /web/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | functions = "functions" 4 | publish = "build" 5 | 6 | [[redirects]] 7 | from = '/' 8 | to = 'https://www.useweb3.xyz/gas?source=ethgas.watch&referrer=ethgas.watch' 9 | force = true 10 | status = 301 11 | 12 | [[redirects]] 13 | from = '/*' 14 | to = 'https://www.useweb3.xyz/gas?source=ethgas.watch&referrer=ethgas.watch' 15 | force = true 16 | status = 301 17 | 18 | [[redirects]] 19 | from = '*' 20 | to = 'https://www.useweb3.xyz/gas?source=ethgas.watch&referrer=ethgas.watch' 21 | force = true 22 | status = 301 23 | 24 | # [[redirects]] 25 | # from = '/api/gas/trend' 26 | # to = '/.netlify/functions/trend' 27 | # status = 200 28 | 29 | # [[redirects]] 30 | # from = '/api/alerts/stats' 31 | # to = '/.netlify/functions/alerts' 32 | # status = 200 33 | 34 | # [[redirects]] 35 | # from = '/api/*' 36 | # to = '/.netlify/functions/:splat' 37 | # status = 200 38 | 39 | # [[redirects]] 40 | # from = '/*' 41 | # to = '/index.html' 42 | # status = 200 -------------------------------------------------------------------------------- /web/backup/collectors/GasCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { EtherscanCollector } from "./EtherscanCollector"; 3 | import { GasStationCollector } from "./GasStationCollector"; 4 | import { MyCryptoCollector } from "./MyCryptoCollector"; 5 | import { NetworkCollector } from "./NetworkCollector"; 6 | import { UpvestCollector } from "./UpvestCollector"; 7 | import { Collector } from "./Collector"; 8 | 9 | export class GasCollector { 10 | 11 | async Get() : Promise { 12 | const collectors : (typeof Collector)[] = [EtherscanCollector, GasStationCollector, MyCryptoCollector, NetworkCollector, UpvestCollector]; 13 | const results = new Array(); 14 | const gases = await Promise.all(collectors 15 | .map(async collector => { 16 | return new collector().collect(); 17 | })); 18 | 19 | gases.map(gas => { 20 | if(gas) results.push(gas); 21 | }); 22 | 23 | return results; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/backup/collectors/GasNowCollector.ts: -------------------------------------------------------------------------------- 1 | import { RecommendedGasPrices } from "../types"; 2 | import { Collector } from "./Collector"; 3 | import { WeiToGwei } from "../utils/parse"; 4 | 5 | export class GasNowCollector extends Collector{ 6 | name = "GAS Now"; 7 | url = "https://www.gasnow.org/api/v2/gas/price"; 8 | async MapGas(body: any): Promise { 9 | 10 | return { 11 | name: this.name, 12 | source: "https://www.gasnow.org/", 13 | instant: Math.round(WeiToGwei(body.data.list.find((i: any) => i.index === 50).gasPrice)), // instant 14 | fast: Math.round(WeiToGwei(body.data.list.find((i: any) => i.index === 200).gasPrice)), // 1 min 15 | standard: Math.round(WeiToGwei(body.data.list.find((i: any) => i.index === 500).gasPrice)), // 3 min 16 | slow: Math.round(WeiToGwei(body.data.list.find((i: any) => i.index === 1000).gasPrice)), // > 10 17 | lastUpdate: Number(body.data.timestamp) 18 | } as RecommendedGasPrices; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wesley van Heije 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/backup/components/gastable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecommendedGasPrices } from '../types'; 3 | import { GasTableRow } from './'; 4 | 5 | interface GasPriceTableProps { 6 | sources: RecommendedGasPrices[] 7 | } 8 | 9 | export const GasTable = (props: GasPriceTableProps) => { 10 | 11 | let renderTableRows = props.sources.map((member: RecommendedGasPrices, id: number) => 12 | 13 | ); 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {renderTableRows} 29 | 30 |
SlowNormalFastInstant
31 |
32 | ); 33 | } -------------------------------------------------------------------------------- /web/backup/components/alertcard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface AlertCardProps { 4 | title: string 5 | description?: string 6 | value: number 7 | } 8 | 9 | export const AlertCard = (props: AlertCardProps) => { 10 | 11 | function icon(title: string): string { 12 | if (title === "Alerts") { 13 | return "🛎️"; 14 | } 15 | if (title === "Users") { 16 | return "👤"; 17 | } 18 | if (title === "Average") { 19 | return "➗"; 20 | } 21 | if (title === "Mode") { 22 | return "✖️"; 23 | } 24 | 25 | return ""; 26 | } 27 | 28 | return ( 29 |
30 |
31 |

{props.title}

32 |

{props.value} {props.description}

33 |
34 | {icon(props.title)} 35 |
36 |
37 |
38 | ); 39 | } -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ETH Gas.watch 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /web/backup/assets/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0 auto; 3 | } 4 | 5 | body { 6 | background: #222629; 7 | color: #D4D4DC; 8 | margin: 0 auto; 9 | text-align: center; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | h1 { 15 | font-size: 2em; 16 | } 17 | 18 | h2 { 19 | font-size: 1.5em; 20 | } 21 | 22 | h3 { 23 | font-size: 1.17em; 24 | } 25 | 26 | h4 { 27 | font-size: 1em; 28 | } 29 | 30 | h5 { 31 | font-size: .83em; 32 | } 33 | 34 | table, tr, td { 35 | color: #D4D4DC; 36 | } 37 | 38 | .table-index { 39 | text-align: left; 40 | } 41 | 42 | .container { 43 | margin-top: 20px; 44 | max-width: 960px; 45 | } 46 | 47 | .card { 48 | border: none; 49 | background: #393F4D; 50 | border-radius: 1rem; 51 | transition: all 0.2s; 52 | box-shadow: 0 0.5rem 1rem 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .card-title { 56 | letter-spacing: .1rem; 57 | font-weight: bold; 58 | } 59 | 60 | .card-price { 61 | font-size: 1.5rem; 62 | } 63 | 64 | .period { 65 | font-size: 1rem; 66 | } 67 | 68 | .card-icon { 69 | font-size: 1.5em; 70 | } 71 | 72 | /* 73 | Yellow: #FEDA6A 74 | Light gray: #D4D4DC 75 | Gray: #393F4D 76 | Dark gray: #222629 77 | Dark Slate: #1D1E22 78 | */ -------------------------------------------------------------------------------- /web/backup/functions/register.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, RegisterUserAlert } from '../services/AlertService'; 3 | import { SendConfirmationEmail } from '../services/MailjetService'; 4 | 5 | Connect().then(() => console.log("AlertService Connected")); 6 | 7 | export async function handler(event: APIGatewayEvent, context: Context) { 8 | return { 9 | statusCode: 200, 10 | body: JSON.stringify({ 11 | message: 'Ok', 12 | data: '', 13 | }) 14 | } 15 | context.callbackWaitsForEmptyEventLoop = false; 16 | 17 | if (event.httpMethod !== "POST") 18 | return { statusCode: 405, body: "Method Not Allowed" }; 19 | 20 | const data = JSON.parse(event.body); 21 | if (!data.email || !data.gasprice) 22 | return { statusCode: 400, body: "Bad Request" }; 23 | 24 | const id = await RegisterUserAlert(data.email, data.gasprice); 25 | if (!id) return { statusCode: 500, body: "Error registering user" }; 26 | 27 | await SendConfirmationEmail(data.email, id); 28 | 29 | return { 30 | statusCode: 200, 31 | body: JSON.stringify({ 32 | message: `${data.email} registered.`, 33 | data, 34 | }) 35 | } 36 | } -------------------------------------------------------------------------------- /web/backup/components/layout/footer.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export const Footer = () => { 5 | return ( 6 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /web/backup/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb" 2 | 3 | export type RecommendedGasPrices = { 4 | name: string; 5 | source: string; 6 | instant: number; 7 | fast: number; 8 | standard: number; 9 | slow: number; 10 | lastBlock?: number; 11 | lastUpdate?: number; 12 | } 13 | 14 | export type RegisteredEmailAddress = { 15 | _id?: ObjectId; 16 | email: string; 17 | price: number; 18 | confirmed?: boolean; 19 | disabled?: boolean; 20 | emailSent?: boolean; 21 | lastModified?: number; 22 | } 23 | 24 | export type GasPriceData = { 25 | slow: GasPriceValues, 26 | normal: GasPriceValues, 27 | fast: GasPriceValues, 28 | instant: GasPriceValues, 29 | ethPrice: number, 30 | lastUpdated: number, 31 | sources: RecommendedGasPrices[] 32 | } 33 | 34 | export type GasPriceValues = { 35 | gwei: number, 36 | usd: number 37 | } 38 | 39 | export type AlertsData = { 40 | alerts: number, 41 | unique: number, 42 | average: number, 43 | mode: number, 44 | } 45 | 46 | export type AlertsChartData = { 47 | labels: Array, 48 | registrations: Array, 49 | } 50 | 51 | export type TrendChartData = { 52 | labels: Array, 53 | slow: Array, 54 | normal: Array, 55 | fast: Array, 56 | instant: Array 57 | } -------------------------------------------------------------------------------- /web/backup/services/AirtableService.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { AppConfig } from "../config/app"; 3 | import { RegisteredEmailAddress } from '../types'; 4 | require('encoding'); 5 | 6 | export async function GetAllUsersIterative(users: Array, offset?: string): Promise { 7 | 8 | let offsetFilter = offset ? `&offset=${offset}` : ""; 9 | const response = await fetch(`https://api.airtable.com/v0/${AppConfig.AIRTABLE_BASEID}/Users?fields%5B%5D=Email&fields%5B%5D=Price&fields%5B%5D=EmailSent&view=Migrate${offsetFilter}`, { 10 | method: 'GET', 11 | headers: { 12 | 'Authorization': `Bearer ${AppConfig.AIRTABLE_APIKEY}` 13 | } 14 | }) 15 | 16 | const body = await response.json(); 17 | body.records.forEach((i: any) => { 18 | const user = { 19 | email: i.fields.Email, 20 | price: Number(i.fields.Price), 21 | confirmed: true, 22 | emailSent: i.fields.EmailSent || false 23 | } as RegisteredEmailAddress; 24 | 25 | users.push(user); 26 | }); 27 | 28 | if (body.offset) { 29 | console.log("GetAllUsersIterative", body.offset) 30 | const offsetUsers = await GetAllUsersIterative(users, body.offset); 31 | users.concat(offsetUsers); 32 | } 33 | 34 | return users; 35 | } 36 | -------------------------------------------------------------------------------- /web/backup/pages/privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Privacy() { 4 | return ( 5 |
6 |

Privacy Policy

7 | Last updated: Oct 2020 8 |

9 | A more detailed, in-depth privacy policy will come soon. In the meantime: 10 |

11 |
    12 |
  • this website uses no cookies
  • 13 |
  • this website uses Clicky (https://clicky.com/) website analytics, a privacy-friendly alternative to Google analytics. 14 | By default, no "personal data" of visitors is monitored by this service. This means all IP addresses are anonymized, and global opt out cookies are honored
  • 15 |
  • this website does not collect anu personal information from its visitors, unless you sign up for alerts. In that case, your email address is stored in our cloud-hosted database (Azure, West Europe)
  • 16 |
  • your email address will not be sold, shared with any other parties or used for purposed beyond gas prices alerts and occassional information regarding ETH Gas.watch
  • 17 |
  • if you have any further questions, please don't hesitate to reach out to me at wesley [at] ethgas.watch
  • 18 |
19 |
20 | ); 21 | } 22 | 23 | export default Privacy; 24 | -------------------------------------------------------------------------------- /web/backup/components/alertstats.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { AlertsData } from '../types'; 3 | import { Alert, AlertCard, Loading } from '.'; 4 | 5 | export const AlertStats = () => { 6 | const [loading, setLoading] = useState(true); 7 | const [alertsData, setAlertsData] = useState(); 8 | 9 | useEffect(() => { 10 | async function asyncEffect() { 11 | try { 12 | const response = await fetch(`/.netlify/functions/alerts`); 13 | const body = await response.json() as AlertsData; 14 | 15 | setAlertsData(body); 16 | } catch (ex) { 17 | console.log("Couldn't retrieve alert data", ex); 18 | } 19 | 20 | setLoading(false); 21 | } 22 | 23 | asyncEffect(); 24 | }, []); 25 | 26 | if (loading) { 27 | return 28 | } 29 | 30 | if (!alertsData) { 31 | return 32 | } 33 | 34 | return ( 35 |
36 | 37 | 38 | 39 |
40 | ); 41 | } -------------------------------------------------------------------------------- /web/backup/functions/save.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect, GetAllPrices, SaveGasData } from '../services/GasService'; 3 | import { GetSpotPrice } from '../services/PriceService'; 4 | import { GweiToUsdTransfer } from '../utils/parse'; 5 | import { GasPriceData } from '../types'; 6 | 7 | Connect().then(() => console.log("GasService Connected")); 8 | 9 | export async function handler(event: APIGatewayEvent, context: Context) { 10 | context.callbackWaitsForEmptyEventLoop = false; 11 | 12 | const ethPrice = await GetSpotPrice(); 13 | const results = await GetAllPrices(true); 14 | const average = results[results.length - 1]; 15 | results.pop(); 16 | 17 | const data = { 18 | slow: { 19 | gwei: average.slow, 20 | usd: parseFloat(GweiToUsdTransfer(average.slow, ethPrice)), 21 | }, 22 | normal: { 23 | gwei: average.standard, 24 | usd: parseFloat(GweiToUsdTransfer(average.standard, ethPrice)), 25 | }, 26 | fast: { 27 | gwei: average.fast, 28 | usd: parseFloat(GweiToUsdTransfer(average.fast, ethPrice)), 29 | }, 30 | instant: { 31 | gwei: average.instant, 32 | usd: parseFloat(GweiToUsdTransfer(average.instant, ethPrice)), 33 | }, 34 | ethPrice, 35 | lastUpdated: Date.now(), 36 | sources: results 37 | } as GasPriceData; 38 | 39 | await SaveGasData(data); 40 | 41 | return { 42 | statusCode: 200, 43 | body: `Ok.` 44 | } 45 | } -------------------------------------------------------------------------------- /web/backup/functions/average.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import moment from 'moment'; 3 | import { Connect, GetHourlyAverage } from '../services/GasService'; 4 | 5 | Connect().then(() => console.log("GasService Connected")); 6 | 7 | export async function handler(event: APIGatewayEvent, context: Context) { 8 | context.callbackWaitsForEmptyEventLoop = false; 9 | 10 | const days = 7 11 | const hours = 24 12 | const total = days * hours 13 | const gasData = await GetHourlyAverage(total) 14 | const xDayOfTheWeek = new Array(days).fill(0).map((_, i) => moment().subtract(i, 'd').format('dddd')).reverse() 15 | const weekDays = xDayOfTheWeek.map((i) => moment().day(i).weekday()) 16 | const yHoursInTheDay = new Array(hours).fill(0).map((_, i) => `${i}:00`) 17 | const hoursInTheDay = new Array(hours).fill(0).map((_, i) => i) 18 | 19 | // mongo days = 1-7 (starting at Sunday) 20 | // moment days = 0-6 (starting at Sunday) 21 | 22 | const current = moment() 23 | const data = hoursInTheDay 24 | .map((hourOfTheDay) => { 25 | return weekDays 26 | .map((weekDay) => { 27 | // timezones? GMT ? 28 | if (weekDay === current.weekday() && hourOfTheDay > current.hour() - 1) { 29 | return null 30 | } 31 | 32 | const value = gasData.find(i => i.day === (weekDay + 1) && i.hour === hourOfTheDay) 33 | return value ? Math.floor(value.avg) : undefined 34 | }) 35 | } 36 | ) 37 | 38 | const response = { 39 | x: xDayOfTheWeek, 40 | y: yHoursInTheDay, 41 | data 42 | } 43 | 44 | return { 45 | statusCode: 200, 46 | body: JSON.stringify(response), 47 | } 48 | } -------------------------------------------------------------------------------- /web/backup/utils/parse.ts: -------------------------------------------------------------------------------- 1 | 2 | export function WeiToGwei(value: number): number { 3 | return value / 1e9; 4 | } 5 | 6 | export function GweiToEther(value: number): number { 7 | return value / 1e9; 8 | } 9 | 10 | export function GweiToUsdTransfer(value: number, spotPrice: number): string { 11 | return (value * 21000 / 1e9 * spotPrice).toFixed(2); 12 | } 13 | 14 | export function TimestampToTimeAgo(timestamp: number): string { 15 | const currentDate = new Date(); 16 | const currentTimestamp = Math.floor(currentDate.getTime() / 1000); 17 | var seconds = currentTimestamp - (timestamp / 1000); 18 | 19 | if (seconds > 360 * 24 * 3600) { 20 | return "more than a year ago"; 21 | } 22 | 23 | if (seconds > 60 * 24 * 3600) { 24 | return Math.floor(seconds / (60 * 12 * 3600)) + " months ago"; 25 | } 26 | 27 | if (seconds > 30 * 24 * 3600) { 28 | return "a month ago"; 29 | } 30 | 31 | if (seconds > 14 * 24 * 3600) { 32 | return Math.floor(seconds / (24 * 3600) / 7) + " weeks ago"; 33 | } 34 | 35 | if (seconds > 7 * 24 * 3600) { 36 | return "a week ago"; 37 | } 38 | 39 | if (seconds > 2 * 24 * 3600) { 40 | return Math.floor(seconds / (60 * 3600)) + " days ago"; 41 | } 42 | 43 | if (seconds > 24 * 3600) { 44 | return "yesterday"; 45 | } 46 | 47 | if (seconds > 3600) { 48 | return Math.floor(seconds / 3600) + " hours ago"; 49 | } 50 | 51 | if (seconds > 60) { 52 | return Math.floor(seconds / 60) + " minutes ago"; 53 | } 54 | 55 | if (seconds < 60) { 56 | return "a few seconds ago"; 57 | } 58 | 59 | return new Date(timestamp).toUTCString(); 60 | } -------------------------------------------------------------------------------- /web/backup/components/gasprices.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { GasPriceData } from '../types'; 3 | import { TimestampToTimeAgo } from '../utils/parse'; 4 | import { Alert, GasPriceRow, Loading } from './'; 5 | import { GasTable } from './gastable'; 6 | 7 | export const GasPrices = () => { 8 | const [loading, setLoading] = useState(true); 9 | const [gasData, setGasData] = useState(); 10 | 11 | useEffect(() => { 12 | async function asyncEffect() { 13 | try { 14 | const response = await fetch(`/.netlify/functions/gas`); 15 | const body = await response.json() as GasPriceData; 16 | 17 | setGasData(body); 18 | } catch (ex) { 19 | console.log("Couldn't retrieve gas prices", ex); 20 | } 21 | 22 | setLoading(false); 23 | } 24 | 25 | asyncEffect(); 26 | }, []); 27 | 28 | useEffect(() => { 29 | if (gasData) { 30 | document.title = `ETH Gas.watch | ${gasData.normal.gwei} gwei` 31 | } 32 | }, [gasData]); 33 | 34 | if (loading) { 35 | return 36 | } 37 | 38 | if (!gasData || gasData.sources.length < 1) { 39 | return 40 | } 41 | 42 | return ( 43 | <> 44 | 45 |
46 | Last update: {TimestampToTimeAgo(gasData.lastUpdated)} 47 |
48 |
49 | 50 | 51 | ); 52 | } -------------------------------------------------------------------------------- /web/backup/components/gaspricecard.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { GasPriceValues } from '../types'; 4 | 5 | interface GasPriceProps { 6 | title: string 7 | values: GasPriceValues 8 | } 9 | 10 | export const GasPriceCard = (props: GasPriceProps) => { 11 | 12 | function waitTime(title: string): string { 13 | if (title === "Instant") { 14 | return "few secs"; 15 | } 16 | if (title === "Fast") { 17 | return "<2 min"; 18 | } 19 | if (title === "Normal") { 20 | return "<5 min"; 21 | } 22 | if (title === "Slow") { 23 | return "<30 min"; 24 | } 25 | 26 | return ""; 27 | } 28 | 29 | function speedIcon(title: string): string { 30 | if (title === "Instant") { 31 | return "⚡"; 32 | } 33 | if (title === "Fast") { 34 | return "🚀"; 35 | } 36 | if (title === "Normal") { 37 | return "⏳"; 38 | } 39 | if (title === "Slow") { 40 | return "🐌"; 41 | } 42 | 43 | return ""; 44 | } 45 | 46 | if (!props.values) { 47 | return <> 48 | } 49 | 50 | return ( 51 | 52 |
53 |
54 |

{props.title}

55 |

{props.values.gwei} gwei

56 |
${props.values.usd}
57 |
{waitTime(props.title)}
58 |
59 | {speedIcon(props.title)} 60 |
61 |
62 |
63 | ) 64 | } -------------------------------------------------------------------------------- /web/backup/services/EmailService.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from "../config/app"; 2 | 3 | const sendgridMailer = require('@sendgrid/mail'); 4 | 5 | export async function SendConfirmationEmail(email: string, id: string) : Promise { 6 | sendgridMailer.setApiKey(AppConfig.SENDGRID_APIKEY); 7 | const activationLink = `${AppConfig.HOST}.netlify/functions/confirm?email=${email}&id=${id}` 8 | const message = { 9 | to: email, 10 | from: { 11 | email: 'notifier@ethgas.watch', 12 | name: 'ETH Gas.watch notifier' 13 | }, 14 | subject: 'Confirm ETH Gas.watch registration', 15 | text: ` 16 | To confirm your registration for ETH Gas.watch notifications, please click on the link below. 17 | 18 | ${activationLink}` 19 | }; 20 | 21 | try { 22 | await sendgridMailer.send(message); 23 | } catch (ex) { 24 | console.log("Error sending confirmation email", ex) 25 | } 26 | } 27 | 28 | export async function SendEmailNotification(email: string, id: string, price: number, currentPrice: number) : Promise { 29 | sendgridMailer.setApiKey(AppConfig.SENDGRID_APIKEY); 30 | const cancellationLink = `${AppConfig.HOST}.netlify/functions/cancel?email=${email}&id=${id}` 31 | const message = { 32 | to: email, 33 | from: { 34 | email: 'notifier@ethgas.watch', 35 | name: 'ETH Gas.watch notifier' 36 | }, 37 | subject: `ETH Gas.price <${price} gwei`, 38 | text: ` 39 | The Ethereum gas price is currently ${currentPrice} gwei. 40 | 41 | For up-to-date prices, check out at ${AppConfig.HOST} 42 | 43 | To unsubscribe from notifications at this price level, click the link below. 44 | ${cancellationLink}` 45 | }; 46 | 47 | try { 48 | await sendgridMailer.send(message); 49 | } catch (ex) { 50 | console.log("Error sending notification email", ex) 51 | } 52 | } -------------------------------------------------------------------------------- /web/backup/layout/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; 3 | import Home from '../pages/home'; 4 | import Stats from '../pages/stats'; 5 | import Data from '../pages/data'; 6 | import 'bootstrap/dist/css/bootstrap.min.css'; 7 | import '../assets/index.css'; 8 | import { Footer, Header } from '../components'; 9 | import Privacy from '../pages/privacy'; 10 | 11 | function Main() { 12 | return ( 13 | <> 14 |
15 | 16 |
17 |
18 | If you enjoy using ETH Gas.watch? Please support my work at Gitcoin Grants. Even a small donation can go a long way 💰 19 |
20 |
21 | NOTE: ETHGas.watch is migrating it's service. Visit useWeb3 Gas tracker or follow @wslyvh for updates. 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | { 30 | window.location.href = 'https://docs.ethgas.watch/'; 31 | return null; 32 | }} /> 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | ); 42 | } 43 | 44 | export default Main; 45 | -------------------------------------------------------------------------------- /web/backup/functions/notify.ts: -------------------------------------------------------------------------------- 1 | import { Context, APIGatewayEvent } from 'aws-lambda' 2 | import { Connect as AlertConnect, GetUserAlerts, UpdateMultipleUserAlerts, UpdateUserAlert } from '../services/AlertService'; 3 | import { SendEmailNotification } from '../services/MailjetService'; 4 | import { Connect as GasConnect, GetLatestGasData } from '../services/GasService'; 5 | import { RegisteredEmailAddress } from '../types'; 6 | 7 | AlertConnect().then(() => console.log("AlertService Connected")); 8 | GasConnect().then(() => console.log("GasService Connected")); 9 | 10 | export async function handler(event: APIGatewayEvent, context: Context) { 11 | return { 12 | statusCode: 200, 13 | body: JSON.stringify({ 14 | message: 'Ok', 15 | data: '', 16 | }) 17 | } 18 | 19 | context.callbackWaitsForEmptyEventLoop = false; 20 | 21 | const data = await GetLatestGasData(); 22 | const normal = data.normal.gwei; 23 | console.log("Current avg normal", normal); 24 | 25 | const activeUsers = await GetUserAlerts("Active", normal); 26 | const uniques = activeUsers.filter((item: RegisteredEmailAddress, index: number, array: RegisteredEmailAddress[]) => 27 | array.findIndex(i => i.email === item.email) === index); 28 | 29 | console.log("Notifying", uniques.length, "users", activeUsers.length, "total"); 30 | uniques.map(async i => { 31 | console.log("Notify", i.email, i.price, i._id.toString()); 32 | await SendEmailNotification(i.email, i._id.toString(), i.price, normal); 33 | }); 34 | 35 | console.log("Flagging", uniques.length ,"users. Emails sent"); 36 | await UpdateMultipleUserAlerts(uniques, { emailSent: true }); 37 | 38 | const flaggedUsers = await GetUserAlerts("Flagged", normal); 39 | console.log("Unflagging", flaggedUsers.length, "users"); 40 | await UpdateMultipleUserAlerts(flaggedUsers, { emailSent: false }); 41 | 42 | return { statusCode: 200, body: `Ok. ${uniques.length} notifications sent. ${flaggedUsers.length} unflagged.` } 43 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "description": "", 4 | "author": "wslyvh", 5 | "version": "0.1.0", 6 | "scripts": { 7 | "start": "concurrently \"react-scripts start\" \"netlify-lambda serve src/functions\"", 8 | "functions": "netlify-lambda serve src/functions", 9 | "build:all": "react-scripts build && netlify-lambda build src/functions", 10 | "build": "react-scripts build", 11 | "test": "react-scripts test --coverage --verbose --detectOpenHandles --forceExit" 12 | }, 13 | "dependencies": { 14 | "@sendgrid/mail": "^7.2.5", 15 | "base-64": "^1.0.0", 16 | "bootstrap": "^4.5.2", 17 | "chart.js": "^2.9.3", 18 | "dotenv": "^8.2.0", 19 | "encoding": "^0.1.13", 20 | "http-proxy-middleware": "^1.0.5", 21 | "moment": "^2.28.0", 22 | "mongodb": "^3.6.2", 23 | "mongodb-client-encryption": "^1.1.0", 24 | "node-fetch": "^2.6.1", 25 | "react": "^16.13.1", 26 | "react-chartjs-2": "^2.10.0", 27 | "react-dom": "^16.13.1", 28 | "react-grid-heatmap": "^1.0.1", 29 | "react-router-dom": "^5.2.0", 30 | "saslprep": "^1.0.3" 31 | }, 32 | "devDependencies": { 33 | "@babel/preset-typescript": "^7.10.4", 34 | "@testing-library/jest-dom": "^5.11.4", 35 | "@testing-library/react": "^11.0.2", 36 | "@testing-library/user-event": "^12.1.4", 37 | "@types/aws-lambda": "^8.10.62", 38 | "@types/chart.js": "^2.9.24", 39 | "@types/dotenv": "^8.2.0", 40 | "@types/jest": "^26.0.13", 41 | "@types/mongodb": "^3.5.27", 42 | "@types/node": "^14.10.1", 43 | "@types/node-fetch": "^2.5.7", 44 | "@types/react": "^16.9.49", 45 | "@types/react-dom": "^16.9.8", 46 | "@types/react-router-dom": "^5.1.5", 47 | "concurrently": "^5.3.0", 48 | "netlify-lambda": "^2.0.1", 49 | "react-scripts": "3.4.3", 50 | "typescript": "^4.0.2" 51 | }, 52 | "eslintConfig": { 53 | "extends": "react-app" 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">0.2%", 58 | "not dead", 59 | "not op_mini all" 60 | ], 61 | "development": [ 62 | "last 1 chrome version", 63 | "last 1 firefox version", 64 | "last 1 safari version" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /web/backup/components/heatmap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Alert, Loading } from '.'; 3 | import { HeatMapGrid } from "react-grid-heatmap"; 4 | 5 | interface Props { 6 | 7 | } 8 | 9 | export const Heatmap = (props: Props) => { 10 | const [loading, setLoading] = useState(true) 11 | const [data, setData] = useState(); 12 | 13 | useEffect(() => { 14 | async function asyncEffect() { 15 | try { 16 | const response = await fetch(`/.netlify/functions/average`); 17 | const body = await response.json() 18 | 19 | if (body === null) { 20 | console.log("Error retrieving gas chart data"); 21 | return; 22 | } 23 | 24 | setData(body) 25 | } catch (ex) { 26 | console.log("Couldn't retrieve gas chart data", ex); 27 | } 28 | 29 | setLoading(false); 30 | } 31 | 32 | asyncEffect(); 33 | }, []); 34 | 35 | if (loading) { 36 | return 37 | } 38 | 39 | if (!data) { 40 | return 41 | } 42 | 43 | return ( 44 |
45 |

Average gas prices

46 | 47 |
48 | ( 53 |
{value}
54 | )} 55 | xLabelsStyle={(index) => ({ 56 | fontSize: ".65rem" 57 | })} 58 | yLabelsStyle={() => ({ 59 | fontSize: ".65rem", 60 | })} 61 | cellStyle={(_x, _y, ratio) => ({ 62 | border: 0, 63 | borderRadius: 0, 64 | background: `rgb(255, 51, 51, ${ratio})`, 65 | color: 'white' 66 | })} 67 | cellHeight="1.5rem" 68 | xLabelsPos="bottom" 69 | /> 70 |
71 | * timezone in GMT 72 |
73 |
74 |
75 | ); 76 | } -------------------------------------------------------------------------------- /web/backup/components/register.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Alert } from './'; 3 | 4 | export const Register = () => { 5 | const [error, setError] = useState(""); 6 | const [message, setMessage] = useState(""); 7 | const [email, setEmail] = useState(""); 8 | const [gasprice, setGasprice] = useState(""); 9 | 10 | const registerEmail = async () => { 11 | 12 | var emailRegex = /\S+@\S+\.\S+/; 13 | if (!emailRegex.test(email)) { 14 | setMessage(""); 15 | setError("Email not valid"); 16 | } 17 | else if (isNaN(parseInt(gasprice)) || parseInt(gasprice) <= 0) { 18 | setMessage(""); 19 | setError("No valid gasprice. Please use a number >0") 20 | } 21 | else { 22 | setError(""); 23 | 24 | const body = { 25 | email: email.trim(), 26 | gasprice: gasprice.trim() 27 | }; 28 | 29 | try { 30 | const response = await fetch('/.netlify/functions/register', { 31 | method: 'POST', 32 | body: JSON.stringify(body), 33 | headers: {'Content-Type': 'application/json'} 34 | }); 35 | 36 | const result = await response.json(); 37 | if (result) { 38 | setMessage("Registration received. Please confirm your email address (check your spam folder)."); 39 | } 40 | } catch { 41 | setMessage(""); 42 | setError("Error registering email. Check your input and try again."); 43 | } 44 | } 45 | } 46 | 47 | let renderAlertMessage = <> 48 | if (error) { 49 | renderAlertMessage = 50 | } 51 | if (message) { 52 | renderAlertMessage = 53 | } 54 | 55 | return ( 56 | <> 57 |
58 |

59 | ETH Gas.watch is an aggregated gas price feed that checks multiple data sources for the latest gas prices. By aggregating these data sources, it provides a more reliable average gas price. 60 |

61 |

Sign-up to receive a notification when the price drops.

62 | 63 | {renderAlertMessage} 64 | 65 |
66 |
67 | setEmail(e.target.value)} /> 69 |
70 | 71 |
72 | setGasprice(e.target.value)} /> 74 |
75 | 76 |
77 |
78 |
79 |
80 | 81 | ); 82 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at wesley [at] ethgas.watch. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /web/backup/services/MailjetService.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from "../config/app"; 2 | import fetch from 'node-fetch'; 3 | require('encoding'); 4 | let base64 = require('base-64'); 5 | 6 | export async function SendConfirmationEmail(email: string, id: string) : Promise { 7 | const activationLink = `${AppConfig.HOST}.netlify/functions/confirm?email=${email}&id=${id}` 8 | const body = `To confirm your registration for ETH Gas.watch notifications, please click on the link below. 9 | 10 | ${activationLink}` 11 | 12 | const message = { 13 | "Messages": [ 14 | { 15 | "From": { 16 | "Email": "notifier@ethgas.watch", 17 | "Name": "ETH Gas.watch notifier" 18 | }, 19 | "To": [ 20 | { 21 | "Email": email, 22 | "Name": email 23 | } 24 | ], 25 | "Subject": "Confirm ETH Gas.watch registration", 26 | "TextPart": body, 27 | "HTMLPart": body, 28 | } 29 | ] 30 | } 31 | 32 | try { 33 | const response = await fetch(`https://api.mailjet.com/v3.1/send`, { 34 | method: 'POST', 35 | body: JSON.stringify(message), 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | 'Authorization': 'Basic ' + base64.encode(AppConfig.MAILJET_APIKEY + ":" + AppConfig.MAILJET_PASSWORD) 39 | } 40 | }); 41 | 42 | await response.json(); 43 | } catch (ex) { 44 | console.log("Error sending confirmation email", ex) 45 | } 46 | } 47 | 48 | 49 | // * Want to learn more about Web3? Check out https://www.useweb3.xyz/.
50 | // A curated overview of the best and latest resources on Ethereum, blockchain and Web3 development.` 51 | 52 | // * If you're using this service, please considering donating in Gitcoin Round 12.
53 | // All your donations are matched using quadratic funding and help keep this service running (for free).

54 | // Thank you!` 55 | 56 | export async function SendEmailNotification(email: string, id: string, price: number, currentPrice: number) : Promise { 57 | const cancellationLink = `${AppConfig.HOST}.netlify/functions/cancel?email=${email}&id=${id}` 58 | const body = `The Ethereum gas price is currently ${currentPrice} gwei. 59 | 60 | For up-to-date prices, check out ${AppConfig.HOST} 61 | 62 | To unsubscribe from notifications at this price level, click the link below. 63 | ${cancellationLink}` 64 | 65 | const htmlBody = `The Ethereum gas price is currently ${currentPrice} gwei. 66 |

67 | 68 | For up-to-date prices, check out ${AppConfig.HOST}. 69 |

70 | 71 | Click here to unsubscribe from notifications at this price level. 72 |


73 | 74 | * Want to learn more about Web3? Check out https://www.useweb3.xyz/.
75 | A curated overview of the best and latest resources on Ethereum, blockchain and Web3 development.` 76 | 77 | const message = { 78 | "Messages": [ 79 | { 80 | "From": { 81 | "Email": "notifier@ethgas.watch", 82 | "Name": "ETH Gas.watch notifier" 83 | }, 84 | "To": [ 85 | { 86 | "Email": email, 87 | "Name": email 88 | } 89 | ], 90 | "Subject": `ETH Gas.price <${price} gwei`, 91 | "TextPart": body, 92 | "HTMLPart": htmlBody, 93 | } 94 | ] 95 | } 96 | 97 | try { 98 | const response = await fetch(`https://api.mailjet.com/v3.1/send`, { 99 | method: 'POST', 100 | body: JSON.stringify(message), 101 | headers: { 102 | 'Content-Type': 'application/json', 103 | 'Authorization': 'Basic ' + base64.encode(AppConfig.MAILJET_APIKEY + ":" + AppConfig.MAILJET_PASSWORD) 104 | } 105 | }); 106 | 107 | await response.json(); 108 | } catch (ex) { 109 | console.log("Error sending notification email", ex) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Access ETH Gas.watch data 3 | --- 4 | 5 | # API 6 | 7 | {% api-method method="get" host="http://ethgas.watch/api" path="/gas" %} 8 | {% api-method-summary %} 9 | Gas 10 | {% endapi-method-summary %} 11 | 12 | {% api-method-description %} 13 | This endpoint allows you to get the latest gas price data, similar to what is shown on the website. 14 | {% endapi-method-description %} 15 | 16 | {% api-method-spec %} 17 | {% api-method-request %} 18 | 19 | {% api-method-response %} 20 | {% api-method-response-example httpCode=200 %} 21 | {% api-method-response-example-description %} 22 | 23 | {% endapi-method-response-example-description %} 24 | 25 | ``` 26 | { 27 | "slow": { 28 | "gwei": 199, 29 | "usd": 12.84 30 | }, 31 | "normal": { 32 | "gwei": 208, 33 | "usd": 13.42 34 | }, 35 | "fast": { 36 | "gwei": 221, 37 | "usd": 14.26 38 | }, 39 | "instant": { 40 | "gwei": 243, 41 | "usd": 15.68 42 | }, 43 | "ethPrice": 3072.61, 44 | "lastUpdated": 1641851674313, 45 | "sources": [ 46 | { 47 | "name": "Etherscan", 48 | "source": "https://etherscan.io/gastracker", 49 | "fast": 219, 50 | "standard": 217, 51 | "slow": 217, 52 | "lastBlock": 13980268 53 | }, 54 | { 55 | "name": "Gas station", 56 | "source": "https://ethgasstation.info/", 57 | "instant": 312, 58 | "fast": 265, 59 | "standard": 215, 60 | "slow": 201, 61 | "lastBlock": 13980267 62 | }, 63 | { 64 | "name": "MyCrypto", 65 | "source": "https://gas.mycryptoapi.com/", 66 | "instant": 243, 67 | "fast": 223, 68 | "standard": 201, 69 | "slow": 181, 70 | "lastBlock": 13980267 71 | }, 72 | { 73 | "name": "Upvest", 74 | "source": "https://doc.upvest.co/reference#ethereum-fees", 75 | "instant": 226, 76 | "fast": 218, 77 | "standard": 201, 78 | "slow": 196, 79 | "lastUpdate": 1641851673884 80 | } 81 | ] 82 | } 83 | ``` 84 | {% endapi-method-response-example %} 85 | {% endapi-method-response %} 86 | {% endapi-method-spec %} 87 | {% endapi-method %} 88 | 89 | {% api-method method="get" host="http://ethgas.watch/api" path="/gas/trend" %} 90 | {% api-method-summary %} 91 | Trend 92 | {% endapi-method-summary %} 93 | 94 | {% api-method-description %} 95 | This endpoint allows you to get historical gas price data. Chose one of the query params below to retrieve either the daily or the hourly trend data. 96 | {% endapi-method-description %} 97 | 98 | {% api-method-spec %} 99 | {% api-method-request %} 100 | {% api-method-query-parameters %} 101 | {% api-method-parameter name="hours" type="number" required=false %} 102 | Amount of hours to return \(e.g. 24 for 1 day\) 103 | {% endapi-method-parameter %} 104 | 105 | {% api-method-parameter name="days" type="number" required=false %} 106 | Amount of days to return \(e.g. 7 for 1 week\) 107 | {% endapi-method-parameter %} 108 | {% endapi-method-query-parameters %} 109 | {% endapi-method-request %} 110 | 111 | {% api-method-response %} 112 | {% api-method-response-example httpCode=200 %} 113 | {% api-method-response-example-description %} 114 | 115 | {% endapi-method-response-example-description %} 116 | 117 | ``` 118 | { 119 | "labels": ["Sep 17, 2020", "Sep 18, 2020", "Sep 19, 2020", "Sep 20, 2020", "Sep 21, 2020", "Sep 22, 2020", "Sep 23, 2020", "Sep 24, 2020"], 120 | "slow": [431, 231.5, 165.5, 71, 64, 64, 63.5, 61], 121 | "normal": [491, 250, 151, 80, 290, 257.5, 66, 71], 122 | "fast": [500, 285, 149.5, 174, 242, 178.5, 178.5, 75], 123 | "instant": [559, 341.5, 159, 124, 163, 178, 150, 85] 124 | } 125 | ``` 126 | {% endapi-method-response-example %} 127 | {% endapi-method-response %} 128 | {% endapi-method-spec %} 129 | {% endapi-method %} 130 | 131 | {% api-method method="get" host="http://ethgas.watch/api" path="/alerts/stats" %} 132 | {% api-method-summary %} 133 | Stats 134 | {% endapi-method-summary %} 135 | 136 | {% api-method-description %} 137 | This endpoint allows you to get statistics on the amount of alerts and registrations. 138 | {% endapi-method-description %} 139 | 140 | {% api-method-spec %} 141 | {% api-method-request %} 142 | {% api-method-query-parameters %} 143 | {% api-method-parameter name="days" type="number" required=true %} 144 | Amount of days to return \(e.g. 7 for 1 week\) 145 | {% endapi-method-parameter %} 146 | {% endapi-method-query-parameters %} 147 | {% endapi-method-request %} 148 | 149 | {% api-method-response %} 150 | {% api-method-response-example httpCode=200 %} 151 | {% api-method-response-example-description %} 152 | 153 | {% endapi-method-response-example-description %} 154 | 155 | ``` 156 | { 157 | "alerts": 750, 158 | "unique": 548, 159 | "average": 125, 160 | "mode": 100 161 | } 162 | ``` 163 | {% endapi-method-response-example %} 164 | {% endapi-method-response %} 165 | {% endapi-method-spec %} 166 | {% endapi-method %} 167 | 168 | -------------------------------------------------------------------------------- /web/backup/components/gaschart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Alert, Loading } from '.'; 3 | import { AlertsChartData, TrendChartData } from '../types'; 4 | import { Line } from 'react-chartjs-2'; 5 | 6 | interface GasChartProps { 7 | type: "daily" | "hourly" 8 | } 9 | 10 | export const GasChart = (props: GasChartProps) => { 11 | const defaultTimePeriod = props.type === "daily" ? 7 : 24 12 | const [loading, setLoading] = useState(true); 13 | const [timePeriod, setTimePeriod] = useState(defaultTimePeriod); 14 | const [chartData, setChartData] = useState(); 15 | 16 | useEffect(() => { 17 | async function asyncEffect() { 18 | try { 19 | let body: TrendChartData | null = null; 20 | let registrations: AlertsChartData | null = null; 21 | 22 | if (props.type === "daily") { 23 | const response = await fetch(`/.netlify/functions/trend?days=${timePeriod}`); 24 | body = await response.json() as TrendChartData; 25 | 26 | const regResponse = await fetch(`/.netlify/functions/registrations?days=${timePeriod}`); 27 | registrations = await regResponse.json() as AlertsChartData; 28 | } 29 | if (props.type === "hourly") { 30 | const response = await fetch(`/.netlify/functions/trend?hours=${timePeriod}`); 31 | body = await response.json() as TrendChartData; 32 | } 33 | if (body === null) { 34 | console.log("Error retrieving gas chart data"); 35 | return; 36 | } 37 | 38 | const chartData = { 39 | labels: body.labels, 40 | datasets: [{ 41 | label: "Slow", 42 | borderColor: "#FFFF9D", 43 | borderWidth: "1", 44 | data: body.slow 45 | }, 46 | { 47 | label: "Normal", 48 | borderColor: "#FEDA6A", 49 | borderWidth: "1", 50 | data: body.normal, 51 | }, 52 | { 53 | label: "Fast", 54 | borderColor: "#CBA737", 55 | borderWidth: "1", 56 | data: body.fast, 57 | }, 58 | { 59 | label: "Instant", 60 | borderColor: "#654100", 61 | borderWidth: "1", 62 | data: body.instant, 63 | },], 64 | options: { 65 | legend: { 66 | display: true, 67 | position: 'top' 68 | }, 69 | tooltips: { 70 | mode: 'index', 71 | intersect: false, 72 | }, 73 | } 74 | }; 75 | 76 | if (registrations) { 77 | chartData.datasets.push( 78 | { 79 | label: "Registrations", 80 | borderColor: "#dc3545", 81 | borderWidth: "1", 82 | data: registrations.registrations, 83 | }) 84 | } 85 | 86 | setChartData(chartData); 87 | } catch (ex) { 88 | console.log("Couldn't retrieve gas chart data", ex); 89 | } 90 | 91 | setLoading(false); 92 | } 93 | 94 | asyncEffect(); 95 | }, [timePeriod, props.type]); 96 | 97 | if (loading) { 98 | return 99 | } 100 | 101 | if (!chartData) { 102 | return 103 | } 104 | 105 | const dailyOptions = (<> 106 | 107 | 108 | 109 | ); 110 | 111 | const hourlyOptions = (<> 112 | 113 | 114 | 115 | ); 116 | 117 | return ( 118 |
119 |

{props.type} average gas prices

120 | 121 |
122 | 126 |
127 | 128 |
129 | 130 |
131 |
132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /web/backup/services/AlertService.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from 'mongodb'; 2 | import { AlertsChartData, AlertsData, RegisteredEmailAddress } from "../types"; 3 | import { AppConfig } from "../config/app"; 4 | import { GetAverage, GetMode } from '../utils/stats'; 5 | import moment from 'moment'; 6 | require('encoding'); 7 | require('mongodb-client-encryption'); 8 | const qs = require('querystring'); 9 | 10 | const db_collection = "alerts" 11 | let dbClient: MongoClient | null = null; 12 | 13 | export async function Connect(): Promise { 14 | if (!dbClient) { 15 | dbClient = await MongoClient.connect(AppConfig.MONGODB_CONNECTIONSTRING, { useNewUrlParser: true }); 16 | console.log("alerts Connected.."); 17 | } 18 | 19 | return dbClient; 20 | } 21 | 22 | export async function RegisterMany(alerts: RegisteredEmailAddress[]) { 23 | 24 | try { 25 | const collection = await getDatabaseCollection(); 26 | 27 | console.log(`Importing ${alerts.length} alerts...`); 28 | const result = await collection.insertMany(alerts); 29 | console.log(`${result.insertedCount} alerts inserted!`); 30 | } 31 | catch (ex) { 32 | console.log("ERROR insertMany", ex); 33 | } 34 | } 35 | 36 | export async function RegisterUserAlert(email: string, gasprice: string): Promise { 37 | 38 | try { 39 | const collection = await getDatabaseCollection(); 40 | const result = await collection.insertOne({ 41 | email: email, 42 | price: Number(gasprice), 43 | registered: new Date() 44 | }); 45 | 46 | return result.insertedId; 47 | } 48 | catch (ex) { 49 | console.log("ERROR registering user alert", ex); 50 | } 51 | 52 | return ""; 53 | } 54 | 55 | export async function UpdateUserAlert(id: string, fields: any): Promise { 56 | 57 | try { 58 | const collection = await getDatabaseCollection(); 59 | const result = await collection.updateOne({ _id : new ObjectId(id) }, { $set: fields }); 60 | 61 | return result.modifiedCount > 0; 62 | } 63 | catch (ex) { 64 | console.log("ERROR updating user alert", ex); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | export async function UpdateMultipleUserAlerts(addresses: RegisteredEmailAddress[], fields: any): Promise { 71 | 72 | try { 73 | const collection = await getDatabaseCollection(); 74 | const ids = addresses.map(i => new ObjectId(i._id)); 75 | const result = await collection.updateMany( 76 | { _id : { $in: ids } }, 77 | { $set: fields }); 78 | 79 | console.log("Update results", addresses.length === result.modifiedCount, addresses.length, result.modifiedCount) 80 | return result.modifiedCount > 0; 81 | } 82 | catch (ex) { 83 | console.log("ERROR updating user alert", ex); 84 | } 85 | 86 | return false; 87 | } 88 | export async function GetUserAlerts(view: "Active" | "Flagged", gasprice: number): Promise { 89 | 90 | let priceQuery = {}; 91 | if (view === "Active") { 92 | priceQuery = { 93 | price: { $gt: gasprice }, 94 | confirmed: true, 95 | emailSent: { $ne: true }, 96 | disabled: { $ne: true }, 97 | } 98 | } 99 | if (view === "Flagged") { 100 | priceQuery = { 101 | price: { $lt: gasprice }, 102 | confirmed: true, 103 | emailSent: true, 104 | disabled: { $ne: true }, 105 | } 106 | } 107 | 108 | try { 109 | const collection = await getDatabaseCollection(); 110 | return await collection.find(priceQuery).toArray(); 111 | } 112 | catch (ex) { 113 | console.log("ERROR getting user alerts", ex); 114 | } 115 | 116 | return []; 117 | } 118 | 119 | export async function ExportUserAlerts(): Promise> { 120 | 121 | try { 122 | const collection = await getDatabaseCollection(); 123 | const items = await collection.find().toArray(); 124 | const uniques = items.filter((item: RegisteredEmailAddress, index: number, array: RegisteredEmailAddress[]) => 125 | array.findIndex(i => i.email === item.email) === index); 126 | return uniques.map((i: RegisteredEmailAddress) => i.email); 127 | } 128 | catch (ex) { 129 | console.log("ERROR getting user alerts", ex); 130 | } 131 | 132 | return []; 133 | } 134 | 135 | export async function GetUserAlertsData(): Promise { 136 | 137 | try { 138 | const collection = await getDatabaseCollection(); 139 | const all = await collection.countDocuments() 140 | const uniques = await collection.distinct("email") 141 | console.log('Unique addresses', uniques.length) 142 | 143 | const stats = await collection.aggregate([ 144 | { "$match": { 145 | "confirmed": true, 146 | "price": { $lte: 10000 } 147 | }}, 148 | { "$group": { 149 | _id: null, 150 | count: { $sum: 1 }, 151 | min: { $min: "$price" }, 152 | max: { $max: "$price" }, 153 | avg: { $avg: "$price" } 154 | } 155 | } 156 | ]).toArray(); 157 | 158 | return { 159 | alerts: all, 160 | unique: stats[0].count, 161 | average: Math.round(stats[0].avg), 162 | mode: 0 //GetMode(values), 163 | } as AlertsData; 164 | } 165 | catch (ex) { 166 | console.log("ERROR getting user alerts", ex); 167 | } 168 | 169 | return null; 170 | } 171 | 172 | export async function GetLatestUserAlerts(count: number, uniques?: boolean): Promise { 173 | 174 | try { 175 | const collection = await getDatabaseCollection(); 176 | let items = await collection.find().sort({ _id: -1 }).limit(count).toArray(); 177 | 178 | if (uniques) { 179 | items = items.filter((item: RegisteredEmailAddress, index: number, array: RegisteredEmailAddress[]) => 180 | array.findIndex(i => i.email === item.email) === index); 181 | } 182 | 183 | return items; 184 | } 185 | catch (ex) { 186 | console.log("ERROR getting last user alerts", count, ex); 187 | } 188 | 189 | return []; 190 | } 191 | 192 | export async function GetDailyUserAlertsRegistrations(days: number): Promise { 193 | 194 | try { 195 | const collection = await getDatabaseCollection(); 196 | const since = moment().subtract(days, "days").valueOf(); 197 | 198 | const items = await collection.aggregate([ 199 | { $match: { "registered": { $gte: since } } }, 200 | { $project: { 201 | year: { $year: "$registered" }, 202 | month: { $month: "$registered" }, 203 | dayOfMonth: { $dayOfMonth: "$registered" } 204 | }}, 205 | { $group: { 206 | _id: { 207 | year: '$year', 208 | month: '$month', 209 | dayOfMonth: '$dayOfMonth' 210 | }, 211 | count: { 212 | $sum: 1 213 | } 214 | }} 215 | ]).toArray(); 216 | 217 | const results = { 218 | labels: Array(), 219 | registrations: Array(), 220 | } as AlertsChartData; 221 | 222 | items.forEach((i: any) => { 223 | const mt = moment(`${i._id.year} ${i._id.month} ${i._id.dayOfMonth}`, "YYYY MM DD"); 224 | results.labels.push(mt.format("ll")); 225 | results.registrations.push(i.count); 226 | }); 227 | 228 | return results; 229 | } 230 | catch (ex) { 231 | console.log("ERROR GetDailyUserAlertsRegistrations", ex); 232 | } 233 | 234 | return []; 235 | } 236 | 237 | async function getDatabaseCollection(): Promise { 238 | const client = await Connect(); 239 | const db = client.db(AppConfig.MONGODB_DB); 240 | return db.collection(db_collection); 241 | } -------------------------------------------------------------------------------- /web/backup/services/GasService.ts: -------------------------------------------------------------------------------- 1 | require('encoding'); 2 | require('mongodb-client-encryption'); 3 | import { MongoClient } from 'mongodb'; 4 | import { RecommendedGasPrices, GasPriceData, TrendChartData } from "../types"; 5 | import { AppConfig } from "../config/app"; 6 | import { AVERAGE_NAME } from '../utils/constants'; 7 | import moment from 'moment'; 8 | import { GetMedian } from '../utils/stats'; 9 | import { GasCollector } from '../collectors/GasCollector'; 10 | 11 | const db_collection = "gasdata" 12 | let dbClient: MongoClient | null = null; 13 | 14 | export async function Connect(): Promise { 15 | if (!dbClient) { 16 | dbClient = await MongoClient.connect(AppConfig.MONGODB_CONNECTIONSTRING, { useNewUrlParser: true }); 17 | console.log("gasdata connected.."); 18 | } 19 | 20 | return dbClient; 21 | } 22 | 23 | export async function GetAllPrices(includeAverage?: boolean): Promise { 24 | 25 | var factory = new GasCollector(); 26 | const results = await factory.Get(); 27 | 28 | if (includeAverage) { 29 | const medianPrices = Average(results); 30 | results.push(medianPrices); 31 | } 32 | 33 | return results; 34 | } 35 | 36 | export async function GetAveragePrice(): Promise { 37 | 38 | const results = await GetAllPrices(true); 39 | return Average(results); 40 | } 41 | 42 | export async function GetLatestGasData(): Promise { 43 | try { 44 | const dbCollection = await getDatabaseCollection(); 45 | const items = await dbCollection.find().sort({ _id: -1 }).limit(1).toArray(); 46 | 47 | if (items && items.length === 1) { 48 | return items[0].data as GasPriceData; 49 | } 50 | 51 | return null; 52 | 53 | } catch(ex) { 54 | console.log("Failed to retrieve gas data", ex); 55 | return null 56 | } 57 | } 58 | 59 | export async function SaveGasData(data: GasPriceData) { 60 | 61 | try { 62 | const dbCollection = await getDatabaseCollection(); 63 | 64 | await dbCollection.insertOne({ data }); 65 | } catch(ex) { 66 | console.log("Failed to save gas data", ex); 67 | } 68 | } 69 | 70 | export async function GetDailyAverageGasData(days: number): Promise { 71 | 72 | try { 73 | const dbCollection = await getDatabaseCollection(); 74 | const since = moment().subtract(days, "days"); 75 | 76 | const items = await dbCollection.find({ "data.lastUpdated": { $gte: since.valueOf() } }).toArray(); 77 | 78 | var reduced = items.reduce((accumulator: any, item: any) => { 79 | const day = moment(item.data.lastUpdated).startOf("day").format("ll"); 80 | accumulator[day] = accumulator[day] || []; 81 | accumulator[day].push(item); 82 | 83 | return accumulator; 84 | }, {}); 85 | 86 | const result = { 87 | labels: Array(), 88 | slow: Array(), 89 | normal: Array(), 90 | fast: Array(), 91 | instant: Array() 92 | } as TrendChartData; 93 | 94 | Object.keys(reduced).forEach(i => { 95 | let slow: number[] = []; 96 | let normal: number[] = []; 97 | let fast: number[] = []; 98 | let instant: number[] = []; 99 | 100 | reduced[i].forEach((gasdata: any) => { 101 | const gas = gasdata.data as GasPriceData; 102 | if (gas.slow) slow.push(gas.slow.gwei); 103 | if (gas.normal) normal.push(gas.normal.gwei); 104 | if (gas.fast) fast.push(gas.fast.gwei); 105 | if (gas.instant) instant.push(gas.instant.gwei); 106 | }); 107 | 108 | result.labels.push(i); 109 | result.slow.push(GetMedian(slow)); 110 | result.normal.push(GetMedian(normal)); 111 | result.fast.push(GetMedian(fast)); 112 | result.instant.push(GetMedian(instant)); 113 | }) 114 | 115 | return result; 116 | 117 | } catch(ex) { 118 | console.log("Failed to query daily avg gas data", ex); 119 | return null 120 | } 121 | } 122 | 123 | export async function GetHourlyAverage(hours: number): Promise | null> { 124 | 125 | try { 126 | const dbCollection = await getDatabaseCollection(); 127 | const since = moment().subtract(hours, "hours"); 128 | 129 | const items = await dbCollection.aggregate([ 130 | { "$match": { "data.lastUpdated": { $gte: since.valueOf() }}}, 131 | { "$group": { 132 | _id: { 133 | "day": { $dayOfWeek: { $toDate: "$data.lastUpdated" } }, 134 | "hour": { $hour: { $toDate: "$data.lastUpdated" } } 135 | }, 136 | avg: { $avg: "$data.fast.gwei" } 137 | } 138 | } 139 | ]).toArray(); 140 | 141 | return items.map((i: any) => { 142 | return { 143 | day: i._id.day, 144 | hour: i._id.hour, 145 | avg: i.avg 146 | } 147 | }) 148 | 149 | } catch(ex) { 150 | console.log("Failed to query daily avg gas data", ex); 151 | return null 152 | } 153 | } 154 | 155 | export async function GetHourlyAverageGasData(hours: number): Promise { 156 | 157 | try { 158 | const dbCollection = await getDatabaseCollection(); 159 | const since = moment().subtract(hours, "hours"); 160 | 161 | const items = await dbCollection.find({ "data.lastUpdated": { $gte: since.valueOf() } }).toArray(); 162 | 163 | var reduced = items.reduce((accumulator: any, item: any) => { 164 | const hour = moment(item.data.lastUpdated).startOf("hour").format("D/MM HH:mm"); 165 | accumulator[hour] = accumulator[hour] || []; 166 | accumulator[hour].push(item); 167 | 168 | return accumulator; 169 | }, {}); 170 | 171 | const result = { 172 | labels: Array(), 173 | slow: Array(), 174 | normal: Array(), 175 | fast: Array(), 176 | instant: Array() 177 | } as TrendChartData; 178 | 179 | Object.keys(reduced).forEach(i => { 180 | let slow: number[] = []; 181 | let normal: number[] = []; 182 | let fast: number[] = []; 183 | let instant: number[] = []; 184 | 185 | reduced[i].forEach((gasdata: any) => { 186 | const gas = gasdata.data as GasPriceData; 187 | if (gas.slow) slow.push(gas.slow.gwei); 188 | if (gas.normal) normal.push(gas.normal.gwei); 189 | if (gas.fast) fast.push(gas.fast.gwei); 190 | if (gas.instant) instant.push(gas.instant.gwei); 191 | }); 192 | 193 | result.labels.push(i); 194 | result.slow.push(GetMedian(slow)); 195 | result.normal.push(GetMedian(normal)); 196 | result.fast.push(GetMedian(fast)); 197 | result.instant.push(GetMedian(instant)); 198 | }) 199 | 200 | return result; 201 | 202 | } catch(ex) { 203 | console.log("Failed to query daily avg gas data", ex); 204 | return null 205 | } 206 | } 207 | 208 | export function Average(prices: RecommendedGasPrices[]): RecommendedGasPrices { 209 | 210 | const validPrices = prices.filter(i => ValidateGasPriceOrder(i)); 211 | console.log("Get average", prices.length, "data sources.", validPrices.length, "valid."); 212 | var instant = GetMedian(validPrices.filter(i => i.instant > 0).map(i => i.instant)); 213 | var fast = GetMedian(validPrices.filter(i => i.fast > 0).map(i => i.fast)); 214 | var standard = GetMedian(validPrices.filter(i => i.standard > 0).map(i => i.standard)); 215 | var slow = GetMedian(validPrices.filter(i => i.slow > 0).map(i => i.slow)); 216 | 217 | return { 218 | name: AVERAGE_NAME, 219 | instant: Math.round(instant), 220 | fast: Math.round(fast), 221 | standard: Math.round(standard), 222 | slow: Math.round(slow), 223 | } as RecommendedGasPrices; 224 | } 225 | 226 | function ValidateGasPriceOrder(prices: RecommendedGasPrices): boolean { 227 | let result = prices.slow <= prices.standard && prices.standard <= prices.fast; 228 | if (prices.instant) 229 | result = result && prices.fast <= prices.instant; 230 | 231 | if (!result) { 232 | console.log("NOT a valid gas prices", prices.name, prices.slow, prices.standard, prices.fast, prices.instant); 233 | } 234 | 235 | return result; 236 | } 237 | 238 | async function getDatabaseCollection(): Promise { 239 | const client = await Connect(); 240 | const db = client.db(AppConfig.MONGODB_DB); 241 | return db.collection(db_collection); 242 | } --------------------------------------------------------------------------------