feat: migrate to Typescript (#5)

This commit is contained in:
Cezary Haliniarz
2022-06-06 09:13:33 +02:00
committed by GitHub
parent 0e584f2126
commit 857f3c84f4
46 changed files with 1612 additions and 1501 deletions

23
app.js
View File

@@ -1,23 +0,0 @@
const express = require("express");
const cors = require("cors");
const errorhandler = require("errorhandler");
const { getRouter } = require("./routes");
const logger = require("./helpers/logger");
const app = express();
// This allows to get request ip address from req.ip
app.set("trust proxy", true);
app.use(cors());
app.use(express.json({ limit: "20mb", extended: true }));
app.use(express.urlencoded({ extended: true }));
app.use("/", getRouter(express));
app.use(errorhandler({ log: errorNotification }));
function errorNotification(err, str, req) {
const title = `Error in ${req.method} ${req.url}`;
logger.error(title, str, err.stack);
}
module.exports = app;

22
app.ts Normal file
View File

@@ -0,0 +1,22 @@
import express, { Request } from "express";
import cors from "cors";
import errorhandler from "errorhandler";
import { getRouter } from "./routes";
import { logger } from "./helpers/logger";
export const app = express();
// This allows to get request ip address from req.ip
app.set("trust proxy", true);
app.use(cors());
app.use(express.json({ limit: "20mb" }));
app.use(express.urlencoded({ extended: true }));
app.use("/", getRouter());
const errorNotification = (error: Error, string: string, request: Request) => {
const title = `Error in ${request.method} ${request.url}`;
logger.error(title, string, error.stack);
}
app.use(errorhandler({ log: errorNotification }));

View File

@@ -1,44 +0,0 @@
const cacheTTLMilliseconds = 3 * 60 * 1000; // 3 minutes
const enableLiteMode = !!getEnv("LIGHT_MODE", false);
const dbUrls = {
local: "mongodb://localhost:27017/redstone",
};
if (!enableLiteMode) {
const secrets = require("./.secrets.json");
dbUrls["prod"] = secrets.dbUrl;
}
function getDbUrl() {
if (getMode() === "LOCAL") {
return dbUrls.local;
} else {
return dbUrls.prod;
}
}
function getEnv(name, defaultValue) {
return process.env[name] || defaultValue;
}
function getMode() {
return getEnv("MODE", "LOCAL");
}
function isProd() {
return getMode() === "PROD";
}
module.exports = {
enableLiteMode,
dbUrl: getDbUrl(),
bigLimitWithMargin: 1200,
defaultLimit: 1,
defaultLocalPort: 9000,
awsSesRegion: "eu-north-1",
enableJsonLogs: isProd(),
maxLimitForPrices: 3000,
enableAmplitudeLogging: getEnv("ENABLE_AMPLITUDE_LOGGING", false),
isProd,
cacheTTLMilliseconds,
};

55
config.ts Normal file
View File

@@ -0,0 +1,55 @@
const getEnv = (name: string, defaultValue: any) => {
return process.env[name] || defaultValue;
}
const getMode = () => {
return getEnv("MODE", "LOCAL");
}
const isProd = () => {
return getMode() === "PROD";
}
const isTest = process.env.NODE_ENV === "test";
const enableLiteMode = getEnv("LIGHT_MODE", false) === "true";
const dbUrls = {
local: "mongodb://localhost:27017/redstone",
prod: ""
};
if (!enableLiteMode && !isTest) {
const secrets = require("./.secrets.json");
dbUrls["prod"] = secrets.dbUrl;
}
function getDbUrl() {
if (getMode() === "LOCAL") {
return dbUrls.local;
} else {
return dbUrls.prod;
}
}
const cacheTTLMilliseconds = !isTest ? 3 * 60 * 1000 : 0; // 3 minutes
const dbUrl = getDbUrl();
const bigLimitWithMargin = 1200;
const defaultLimit = 1;
const defaultLocalPort = 9000;
const awsSesRegion = "eu-north-1";
const isProduction = isProd();
const maxLimitForPrices = 3000;
const enableAmplitudeLogging = getEnv("ENABLE_AMPLITUDE_LOGGING", false);
export {
enableLiteMode,
dbUrl,
bigLimitWithMargin,
defaultLimit,
defaultLocalPort,
awsSesRegion,
isProduction,
maxLimitForPrices,
enableAmplitudeLogging,
cacheTTLMilliseconds
};

View File

@@ -1,6 +1,6 @@
const Amplitude = require("@amplitude/node"); import Amplitude from "@amplitude/node";
const logger = require("./logger"); import { logger } from "./logger";
const config = require("../config"); import { enableAmplitudeLogging } from "../config";
// Amplitude is a web analytics system. Learn more at https://amplitude.com // Amplitude is a web analytics system. Learn more at https://amplitude.com
// We use it to simply measure get and post requests to the RedStone Http Api // We use it to simply measure get and post requests to the RedStone Http Api
@@ -9,14 +9,15 @@ const config = require("../config");
// Check the analytics dashboard using the link below // Check the analytics dashboard using the link below
// https://analytics.amplitude.com/limestone/dashboard/ttoropr // https://analytics.amplitude.com/limestone/dashboard/ttoropr
const client = Amplitude.init("4990f7285c58e8a009f7818b54fc01eb");
function logEvent({ export const logEvent = ({
eventName, eventName,
eventProps, eventProps,
ip, ip,
}) { }) => {
if (config.enableAmplitudeLogging) { if (enableAmplitudeLogging) {
const client = Amplitude.init("4990f7285c58e8a009f7818b54fc01eb");
logger.info( logger.info(
`Logging event "${eventName}" in amplitude for ip: "${ip}". With props: ` `Logging event "${eventName}" in amplitude for ip: "${ip}". With props: `
+ JSON.stringify(eventProps)); + JSON.stringify(eventProps));
@@ -30,8 +31,4 @@ function logEvent({
event_properties: eventProps, event_properties: eventProps,
}); });
} }
}
module.exports = {
logEvent,
}; };

View File

@@ -1,9 +1,9 @@
const AWS = require("aws-sdk"); import AWS from "aws-sdk";
const logger = require("../helpers/logger"); import { logger } from "../helpers/logger";
const { isProd } = require("../config"); import { isProduction } from "../config";
module.exports.saveMetric = async ({ label, value }) => { export const saveMetric = async ({ label, value }) => {
if (isProd()) { if (isProduction) {
const cloudwatch = new AWS.CloudWatch({apiVersion: "2010-08-01"}); const cloudwatch = new AWS.CloudWatch({apiVersion: "2010-08-01"});
const metricParams = { const metricParams = {

View File

@@ -1,8 +0,0 @@
const consola = require("consola");
const config = require("../config");
if (config.enableJsonLogs) {
consola.setReporters([new consola.JSONReporter()]);
}
module.exports = consola;

8
helpers/logger.ts Normal file
View File

@@ -0,0 +1,8 @@
import consola, { JSONReporter } from "consola";
import { isProduction } from "../config";
if (isProduction) {
consola.setReporters([new JSONReporter()]);
}
export const logger = consola;

View File

@@ -1,7 +1,7 @@
const aws = require("aws-sdk"); import aws from "aws-sdk";
const ses = new aws.SES({ region: config.awsSesRegion }); const ses = new aws.SES();
module.exports.sendEmail = async ({ to, subject, text }) => { export const sendEmail = async ({ to, subject, text }) => {
var params = { var params = {
Destination: { Destination: {
ToAddresses: [to], ToAddresses: [to],

View File

@@ -1,43 +0,0 @@
const mongoose = require("mongoose");
const { MongoMemoryServer } = require('mongodb-memory-server');
const logger = require("./logger");
const MAX_COLLECTION_SIZE_TO_CLEAN = 5000;
async function tryCleanCollection(model, query) {
const collectionSize = await model.countDocuments(query).exec();
if (collectionSize > MAX_COLLECTION_SIZE_TO_CLEAN) {
logger.warn('Unsafe collection cleaning skipped: '
+ JSON.stringify({collectionSize, MAX_COLLECTION_SIZE_TO_CLEAN}));
} else {
logger.info(
`Cleaning collection: ${model.collection.collectionName}. `
+ `Query: ${JSON.stringify(query)}. Items to be removed: ${collectionSize}`);
await model.deleteMany(query);
}
}
async function connectToRemoteMongo(url) {
await mongoose.connect(url, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true,
}).then("Connected to mongoDB");
}
async function connectToMongoMemoryServer() {
mongo = await MongoMemoryServer.create();
const uri = mongo.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
});
}
module.exports = {
tryCleanCollection,
connectToRemoteMongo,
connectToMongoMemoryServer,
};

29
helpers/mongo.ts Normal file
View File

@@ -0,0 +1,29 @@
import mongoose, { FilterQuery, Model } from "mongoose";
import { MongoMemoryServer } from 'mongodb-memory-server';
import { logger } from "./logger";
const MAX_COLLECTION_SIZE_TO_CLEAN = 5000;
export const tryCleanCollection = async (model: Model<any>, query: FilterQuery<any>) => {
const collectionSize = await model.countDocuments(query).exec();
if (collectionSize > MAX_COLLECTION_SIZE_TO_CLEAN) {
logger.warn('Unsafe collection cleaning skipped: '
+ JSON.stringify({collectionSize, MAX_COLLECTION_SIZE_TO_CLEAN}));
} else {
logger.info(
`Cleaning collection: ${model.collection.collectionName}. `
+ `Query: ${JSON.stringify(query)}. Items to be removed: ${collectionSize}`);
await model.deleteMany(query);
}
}
export const connectToRemoteMongo = async (url: string) => {
await mongoose.connect(url)
logger.info("Connected to mongoDB");
}
export const connectToMongoMemoryServer = async () => {
const mongo = await MongoMemoryServer.create();
const uri = mongo.getUri();
await mongoose.connect(uri);
}

View File

@@ -1,11 +1,12 @@
const _ = require("lodash"); import _ from "lodash";
const Arweave = require("arweave/node"); import Arweave from "arweave/node";
const deepSortObject = require("deep-sort-object"); import deepSortObject from "deep-sort-object";
const Price = require("../models/price"); import { Price } from "../models/price";
const { getPublicKeyForProviderAddress } = require("../providers"); import { getPublicKeyForProviderAddress } from "../providers";
const logger = require("./logger"); import { logger } from "./logger";
import { PriceWithParams } from "../routes/prices";
async function assertValidSignature(price, skipNewerPricesCheck = true) { export const assertValidSignature = async (price: PriceWithParams, skipNewerPricesCheck = true) => {
if (!skipNewerPricesCheck) { if (!skipNewerPricesCheck) {
// Checking if price with a greater timestamp is already in DB // Checking if price with a greater timestamp is already in DB
// If so, then we raise an Error, because we allow to publish // If so, then we raise an Error, because we allow to publish
@@ -33,7 +34,7 @@ async function assertValidSignature(price, skipNewerPricesCheck = true) {
} }
} }
async function verifySignature(price) { export const verifySignature = async (price: PriceWithParams) => {
// Time measurement: start // Time measurement: start
const startTime = Date.now(); const startTime = Date.now();
@@ -66,9 +67,9 @@ async function verifySignature(price) {
`Signature verification time elapsed: ${signVerificationTime} ms`); `Signature verification time elapsed: ${signVerificationTime} ms`);
return validSignature; return validSignature;
} };
function getPriceSignedData(price) { const getPriceSignedData = (price: PriceWithParams) => {
const priceWithPickedProps = _.pick(price, [ const priceWithPickedProps = _.pick(price, [
"id", "id",
"source", "source",
@@ -86,12 +87,8 @@ function getPriceSignedData(price) {
} else { } else {
return JSON.stringify(priceWithPickedProps); return JSON.stringify(priceWithPickedProps);
} }
} };
function shouldApplyDeepSort(price) { const shouldApplyDeepSort = (price: PriceWithParams) => {
return price.version && (price.version === "3" || price.version.includes(".")); return price.version && (price.version === "3" || price.version.includes("."));
}
module.exports = {
assertValidSignature,
}; };

View File

@@ -1,32 +0,0 @@
const awsServerlessExpress = require("aws-serverless-express");
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const config = require("./config");
const app = require("./app");
const logger = require("./helpers/logger");
const {
connectToMongoMemoryServer,
connectToRemoteMongo,
} = require("./helpers/mongo");
const argv = yargs(hideBin(process.argv)).argv;
// Connecting to mongoDB
if (config.enableLiteMode) {
connectToMongoMemoryServer();
} else {
connectToRemoteMongo(argv.db || config.dbUrl);
}
// Exporting method for docker container for AWS lambda
const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) =>
awsServerlessExpress.proxy(server, event, context);
// Method for locals server execution
exports.runLocalServer = () => {
const port = argv.port || config.defaultLocalPort;
app.listen(port, () => {
logger.info(`Express api listening at http://localhost:${port}`);
});
};

30
index.ts Normal file
View File

@@ -0,0 +1,30 @@
import awsServerlessExpress from "aws-serverless-express";
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import yargs from "yargs/yargs";
import { hideBin } from "yargs/helpers";
import { enableLiteMode, dbUrl, defaultLocalPort } from "./config";
import { app } from "./app";
import { logger } from "./helpers/logger";
import { connectToMongoMemoryServer, connectToRemoteMongo } from "./helpers/mongo";
const argv = yargs(hideBin(process.argv)).argv;
// Connecting to mongoDB
if (enableLiteMode) {
connectToMongoMemoryServer();
} else {
connectToRemoteMongo(argv["db"] || dbUrl);
}
// Exporting method for docker container for AWS lambda
const server = awsServerlessExpress.createServer(app);
exports.handler = (event: APIGatewayProxyEvent, context: Context) =>
awsServerlessExpress.proxy(server, event, context);
// Method for locals server execution
exports.runLocalServer = () => {
const port = argv["port"] || defaultLocalPort;
app.listen(port, () => {
logger.info(`Express api listening at http://localhost:${port}`);
});
};

View File

@@ -1,5 +1,9 @@
module.exports = { module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true, verbose: true,
testTimeout: 20000, testTimeout: 20000,
testEnvironment: "node", testEnvironment: "node",
modulePathIgnorePatterns: ['./dist'],
setupFiles: ['./jest.setup.ts']
}; };

1
jest.setup.ts Normal file
View File

@@ -0,0 +1 @@
process.env.LIGHT_MODE="true"

View File

@@ -1,7 +1,17 @@
const mongoose = require("mongoose"); import mongoose from "mongoose";
import { Price, PriceSchema } from "./price";
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
const PackageSchema = new Schema({ export interface Package {
timestamp: number;
signature?: string;
liteSignature?: string;
provider: string;
signer: string;
prices: Price[];
}
const PackageSchema = new Schema<Package>({
timestamp: { timestamp: {
type: Number, type: Number,
required: true, required: true,
@@ -23,9 +33,9 @@ const PackageSchema = new Schema({
required: false, required: false,
}, },
prices: { prices: {
type: Array, type: [PriceSchema],
required: true, required: true,
}, },
}); });
module.exports = mongoose.model("Package", PackageSchema); export const Package = mongoose.model("Package", PackageSchema);

View File

@@ -1,9 +1,23 @@
const _ = require("lodash"); import _ from "lodash";
const mongoose = require("mongoose"); import mongoose, { Document, Schema } from "mongoose";
const Schema = mongoose.Schema; import { getPublicKeyForProviderAddress } from "../providers";
const { getPublicKeyForProviderAddress } = require("../providers");
const PriceSchema = new Schema({ export interface Price {
id: string;
symbol: string;
provider: string;
value: number;
signature?: Buffer;
evmSignature?: Buffer;
liteEvmSignature?: Buffer;
permawebTx: string;
version: string;
source: object;
timestamp: number;
minutes?: number;
}
export const PriceSchema = new Schema<Price>({
id: { id: {
type: String, type: String,
required: true, required: true,
@@ -54,16 +68,16 @@ const PriceSchema = new Schema({
}, },
}); });
export const priceToObject = (price: Document<unknown, any, Price> & Price) => {
PriceSchema.statics.toObj = function(price) {
let result = price; let result = price;
if (result.toObject !== undefined) { if (result.toObject !== undefined) {
result = result.toObject(); result = result.toObject();
} }
result.providerPublicKey = getPublicKeyForProviderAddress(result.provider); const providerPublicKey = { providerPublicKey: getPublicKeyForProviderAddress(result.provider) }
const resultWithPublicKey = Object.assign(result, providerPublicKey);
return _.omit(result, ["__v", "_id"]); return _.omit(resultWithPublicKey, ["__v", "_id"]);
}; }
module.exports = mongoose.model("Price", PriceSchema); export const Price = mongoose.model("Price", PriceSchema);

View File

@@ -7,7 +7,8 @@
"start": "node -e 'require(\"./index\").runLocalServer()'", "start": "node -e 'require(\"./index\").runLocalServer()'",
"dev": "MODE=LOCAL node -e 'require(\"./index\").runLocalServer()'", "dev": "MODE=LOCAL node -e 'require(\"./index\").runLocalServer()'",
"test": "jest --coverage", "test": "jest --coverage",
"test:ci": "NODE_ENV=test jest --ci --reporters='default' --reporters='./helpers/GithubActionsReporter'" "test:ci": "NODE_ENV=test jest --ci --reporters='default' --reporters='./helpers/GithubActionsReporter'",
"build": "tsc"
}, },
"dependencies": { "dependencies": {
"@amplitude/node": "^1.9.1", "@amplitude/node": "^1.9.1",
@@ -23,16 +24,25 @@
"express-async-handler": "^1.1.4", "express-async-handler": "^1.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongodb-memory-server": "^7.5.1", "mongodb-memory-server": "^7.5.1",
"mongoose": "^5.12.3", "mongoose": "^6.3.4",
"redstone-node": "^0.4.20", "redstone-node": "^0.4.20",
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/aws-serverless-express": "^3.3.5",
"@types/cors": "^2.8.12",
"@types/errorhandler": "^1.5.0",
"@types/express": "^4.17.13",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182",
"@types/supertest": "^2.0.12",
"express": "^4.17.1", "express": "^4.17.1",
"jest": "^27.0.4", "jest": "^28.0.1",
"mongodb": "^3.6.6", "mongodb": "^3.6.6",
"sleep-promise": "^9.1.0", "sleep-promise": "^9.1.0",
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "^28.0.3",
"typescript": "^4.7.2",
"uuid-random": "^1.3.2" "uuid-random": "^1.3.2"
} }
} }

View File

@@ -1,20 +1,20 @@
const providers = require("redstone-node/dist/src/config/providers.json"); import providers from "redstone-node/dist/src/config/providers.json";
function getProviders() { export const getProviders = () => {
return providers; return providers;
} }
function getPublicKeyForProviderAddress(address) { export const getPublicKeyForProviderAddress = (address: string) => {
const details = findProviderDetailsByAddress(address); const details = findProviderDetailsByAddress(address);
return details.publicKey; return details.publicKey;
} }
function getEvmAddressForProviderAddress(address) { export const getEvmAddressForProviderAddress = (address: string) => {
const details = findProviderDetailsByAddress(address); const details = findProviderDetailsByAddress(address);
return details.evmAddress; return details.evmAddress;
} }
function findProviderDetailsByAddress(address) { export const findProviderDetailsByAddress = (address: string) => {
for (const providerName in providers) { for (const providerName in providers) {
const details = providers[providerName]; const details = providers[providerName];
if (details.address === address) { if (details.address === address) {
@@ -25,10 +25,3 @@ function findProviderDetailsByAddress(address) {
// throw new Error(`Public key not found for provider address: ${address}`); // throw new Error(`Public key not found for provider address: ${address}`);
return {}; return {};
} }
module.exports = {
getProviders,
getPublicKeyForProviderAddress,
getEvmAddressForProviderAddress,
};

View File

@@ -1,6 +1,7 @@
const tokens = require("redstone-node/dist/src/config/tokens.json"); import { Router } from "express";
import tokens from "redstone-node/dist/src/config/tokens.json";
module.exports = (router) => { export const configs = (router: Router) => {
/** /**
* This endpoint is used for returning tokens config file * This endpoint is used for returning tokens config file

View File

@@ -1,8 +1,9 @@
const asyncHandler = require("express-async-handler"); import { Router } from "express";
const logger = require("../helpers/logger"); import asyncHandler from "express-async-handler";
import { logger } from "../helpers/logger";
// const { sendEmail } = require("../helpers/mail-sender"); // const { sendEmail } = require("../helpers/mail-sender");
module.exports = (router) => { export const errors = (router: Router) => {
/** /**
* This endpoint is used for error saving * This endpoint is used for error saving

View File

@@ -1,23 +0,0 @@
const prices = require("./prices");
const packages = require("./packages");
const metrics = require("./metrics");
const errors = require("./errors");
const configs = require("./configs");
const providers = require("./providers");
const config = require("../config");
module.exports.getRouter = (express) => {
const router = express.Router();
prices(router);
packages(router);
configs(router);
providers(router);
if (!config.enableLiteMode) {
metrics(router);
errors(router);
}
return router;
};

24
routes/index.ts Normal file
View File

@@ -0,0 +1,24 @@
import express from "express";
import { prices } from "./prices";
import { packages } from "./packages";
import { metrics } from "./metrics";
import { errors } from "./errors";
import { configs } from "./configs";
import { providers } from "./providers";
import { enableLiteMode } from "../config";
export const getRouter = () => {
const router = express.Router();
prices(router);
packages(router);
configs(router);
providers(router);
if (!enableLiteMode) {
metrics(router);
errors(router);
}
return router;
};

View File

@@ -1,7 +1,8 @@
const asyncHandler = require("express-async-handler"); import { Router } from "express";
const cloudwatch = require("../helpers/cloudwatch"); import asyncHandler from "express-async-handler";
import { saveMetric } from "../helpers/cloudwatch";
module.exports = (router) => { export const metrics = (router: Router) => {
/** /**
* This endpoint is used for saving metric values in AWS Cloudwatch. * This endpoint is used for saving metric values in AWS Cloudwatch.
@@ -10,7 +11,7 @@ module.exports = (router) => {
*/ */
router.post("/metrics", asyncHandler(async (req, res) => { router.post("/metrics", asyncHandler(async (req, res) => {
const { label, value } = req.body; const { label, value } = req.body;
await cloudwatch.saveMetric({ label, value }); await saveMetric({ label, value });
return res.json({ return res.json({
msg: "Metric saved", msg: "Metric saved",

View File

@@ -1,16 +1,18 @@
const asyncHandler = require("express-async-handler"); import asyncHandler from "express-async-handler";
const _ = require("lodash"); import _ from "lodash";
const Package = require("../models/package"); import { Package } from "../models/package";
const Price = require("../models/price"); import { Price } from "../models/price";
const { getProviderFromParams } = require("../utils"); import { getProviderFromParams } from "../utils";
const { tryCleanCollection } = require("../helpers/mongo"); import { tryCleanCollection } from "../helpers/mongo";
const config = require("../config"); import { enableLiteMode, cacheTTLMilliseconds } from "../config";
import { Router } from "express";
import { Document } from "mongoose";
function dbItemToObj(item) { const dbItemToObj = (item: Document<unknown, any, Package> & Package) => {
return _.omit(item.toObject(), ["_id", "__v"]); return _.omit(item.toObject(), ["_id", "__v"]);
} }
module.exports = (router) => { export const packages = (router: Router) => {
/** /**
* This endpoint is used for publishing a new price package * This endpoint is used for publishing a new price package
*/ */
@@ -20,10 +22,10 @@ module.exports = (router) => {
await newPackage.save(); await newPackage.save();
// Cleaning older packages of the same provider before in the lite mode // Cleaning older packages of the same provider before in the lite mode
if (config.enableLiteMode) { if (enableLiteMode) {
await tryCleanCollection(Package, { await tryCleanCollection(Package, {
signer: req.body.signer, signer: req.body.signer,
timestamp: { $lt: newPackage.timestamp - config.cacheTTLMilliseconds }, timestamp: { $lt: newPackage.timestamp - cacheTTLMilliseconds },
}); });
} }
@@ -39,14 +41,13 @@ module.exports = (router) => {
* packages for the specified provider * packages for the specified provider
*/ */
router.get("/packages/latest", asyncHandler(async (req, res) => { router.get("/packages/latest", asyncHandler(async (req, res) => {
const provider = await getProviderFromParams(req.query); const provider = await getProviderFromParams(req.query as { provider: string; });
if (!provider.address) { if (!provider.address) {
throw new Error("Provider address is required"); throw new Error("Provider address is required");
} }
let responseObj; const symbol = req.query.symbol as string;
const symbol = req.query.symbol;
if (symbol) { if (symbol) {
// Fetching latest price for symbol from DB // Fetching latest price for symbol from DB
@@ -60,13 +61,15 @@ module.exports = (router) => {
throw new Error(`Value not found for symbol: ${symbol}`); throw new Error(`Value not found for symbol: ${symbol}`);
} }
responseObj = { const responseObj = {
..._.pick(price, ["timestamp", "provider"]), ..._.pick(price, ["timestamp", "provider"]),
signature: price.evmSignature?.toString("base64"), signature: price.evmSignature?.toString("base64"),
liteSignature: price.liteEvmSignature.toString("base64"), liteSignature: price.liteEvmSignature.toString("base64"),
prices: [{ symbol: req.query.symbol, value: price.value }], prices: [{ symbol, value: price.value }],
signer: provider.evmAddress, // TODO: we don't really need signer, as it must be fetched from a trusted source or hardcoded in the redstone-evm-connector signer: provider.evmAddress, // TODO: we don't really need signer, as it must be fetched from a trusted source or hardcoded in the redstone-evm-connector
}; };
return res.json(responseObj);
} else { } else {
// Fetching latest package from DB // Fetching latest package from DB
const packageFromDB = await Package.findOne({ const packageFromDB = await Package.findOne({
@@ -78,11 +81,8 @@ module.exports = (router) => {
throw new Error(`Latest package not found`); throw new Error(`Latest package not found`);
} }
responseObj = dbItemToObj(packageFromDB); const responseObj = dbItemToObj(packageFromDB);
return res.json(responseObj);
} }
return res.json(responseObj);
})); }));
}; };

View File

@@ -1,20 +1,34 @@
const asyncHandler = require("express-async-handler"); import { Request, Router } from "express";
const _ = require("lodash"); import asyncHandler from "express-async-handler";
import { FilterQuery, PipelineStage, Document } from "mongoose";
import _ from "lodash";
import { bigLimitWithMargin, defaultLimit, cacheTTLMilliseconds, maxLimitForPrices, enableLiteMode } from "../config";
import { Price, priceToObject } from "../models/price";
import { logEvent } from "../helpers/amplitude-event-logger";
import { assertValidSignature } from "../helpers/signature-verifier";
import { priceParamsToPriceObj, getProviderFromParams } from "../utils";
import { logger } from "../helpers/logger";
import { tryCleanCollection } from "../helpers/mongo";
const config = require("../config"); export interface PriceWithParams extends Omit<Price, "signature" | "evmSignature" | "liteEvmSignature"> {
const Price = require("../models/price"); limit?: number;
const { logEvent } = require("../helpers/amplitude-event-logger"); offset?: number;
const { assertValidSignature } = require("../helpers/signature-verifier"); fromTimestamp?: number;
const { priceParamsToPriceObj, getProviderFromParams } = require("../utils"); toTimestamp?: number;
const logger = require("../helpers/logger"); interval?: number;
const { tryCleanCollection } = require("../helpers/mongo"); providerPublicKey?: string;
symbols?: string;
signature?: string;
evmSignature?: string;
liteEvmSignature?: string;
}
async function addSinglePrice(params) { const addSinglePrice = async (params: PriceWithParams) => {
const price = new Price(priceParamsToPriceObj(params)); const price = new Price(priceParamsToPriceObj(params));
await price.save(); await price.save();
} }
async function getLatestPricesForSingleToken(params) { const getLatestPricesForSingleToken = async (params: PriceWithParams) => {
validateParams(params, ["symbol"]); validateParams(params, ["symbol"]);
const prices = await getPrices({ const prices = await getPrices({
filters: { filters: {
@@ -24,10 +38,10 @@ async function getLatestPricesForSingleToken(params) {
limit: params.limit, limit: params.limit,
offset: params.offset, offset: params.offset,
}); });
return prices.map(Price.toObj); return prices.map(priceToObject);
} }
async function addSeveralPrices(params) { const addSeveralPrices = async (params: PriceWithParams[]) => {
const ops = []; const ops = [];
for (const price of params) { for (const price of params) {
ops.push({ ops.push({
@@ -39,7 +53,7 @@ async function addSeveralPrices(params) {
await Price.bulkWrite(ops); await Price.bulkWrite(ops);
} }
async function getPriceForManyTokens(params) { const getPriceForManyTokens = async (params: PriceWithParams) => {
// Parsing symbols params // Parsing symbols params
let tokens = []; let tokens = [];
if (params.symbols !== undefined) { if (params.symbols !== undefined) {
@@ -55,7 +69,8 @@ async function getPriceForManyTokens(params) {
// Fetching prices from DB // Fetching prices from DB
const prices = await getPrices({ const prices = await getPrices({
filters, filters,
limit: config.bigLimitWithMargin, limit: bigLimitWithMargin,
offset: 0,
}); });
// Building tokens object // Building tokens object
@@ -65,10 +80,10 @@ async function getPriceForManyTokens(params) {
// We currently filter here // We currently filter here
if (tokens.length === 0 || tokens.includes(price.symbol)) { if (tokens.length === 0 || tokens.includes(price.symbol)) {
if (tokensResponse[price.symbol] === undefined) { if (tokensResponse[price.symbol] === undefined) {
tokensResponse[price.symbol] = Price.toObj(price); tokensResponse[price.symbol] = priceToObject(price);
} else { } else {
if (tokensResponse[price.symbol].timestamp < price.timestamp) { if (tokensResponse[price.symbol].timestamp < price.timestamp) {
tokensResponse[price.symbol] = Price.toObj(price); tokensResponse[price.symbol] = priceToObject(price);
} }
} }
} }
@@ -77,12 +92,12 @@ async function getPriceForManyTokens(params) {
return tokensResponse; return tokensResponse;
} }
async function getHistoricalPricesForSingleToken(params) { const getHistoricalPricesForSingleToken = async (params: PriceWithParams) => {
validateParams(params, ["symbol"]); validateParams(params, ["symbol"]);
const filters = { const filters = {
symbol: params.symbol, symbol: params.symbol,
provider: params.provider, provider: params.provider,
timestamp: { $lte: params.toTimestamp }, timestamp: { $lte: params.toTimestamp } as { $lte: number, $gte?: number },
}; };
if (params.fromTimestamp) { if (params.fromTimestamp) {
@@ -92,13 +107,13 @@ async function getHistoricalPricesForSingleToken(params) {
const prices = await getPrices({ const prices = await getPrices({
filters, filters,
offset: Number(params.offset || 0), offset: Number(params.offset || 0),
limit: params.limit || config.defaultLimit, limit: params.limit || defaultLimit,
}); });
return prices.map(Price.toObj); return prices.map(priceToObject);
} }
// This function is used to return data for charts // This function is used to return data for charts
async function getPricesInTimeRangeForSingleToken(params) { const getPricesInTimeRangeForSingleToken = async (params: PriceWithParams) => {
validateParams(params, validateParams(params,
["symbol", "fromTimestamp", "toTimestamp", "interval", "provider"]); ["symbol", "fromTimestamp", "toTimestamp", "interval", "provider"]);
const { symbol, provider, fromTimestamp, toTimestamp, interval, offset, limit } = params; const { symbol, provider, fromTimestamp, toTimestamp, interval, offset, limit } = params;
@@ -113,7 +128,7 @@ async function getPricesInTimeRangeForSingleToken(params) {
}, },
}, },
}, },
]; ] as PipelineStage[];
if (interval >= 3600 * 1000) { if (interval >= 3600 * 1000) {
pipeline.push({ pipeline.push({
@@ -138,7 +153,7 @@ async function getPricesInTimeRangeForSingleToken(params) {
} }
const fetchedPrices = await Price.aggregate(pipeline); const fetchedPrices = await Price.aggregate(pipeline);
let prices = fetchedPrices.map(Price.toObj); let prices = fetchedPrices.map(priceToObject);
// TODO: sorting may be moved to aggregation pipeline later // TODO: sorting may be moved to aggregation pipeline later
// it caused performance problems, that's why now we do it here // it caused performance problems, that's why now we do it here
@@ -164,16 +179,20 @@ async function getPricesInTimeRangeForSingleToken(params) {
return prices; return prices;
} }
async function getPrices({ const getPrices = async ({
filters = {}, filters = {},
limit = config.defaultLimit, limit = defaultLimit,
offset, offset,
}) { }: {
filters: FilterQuery<Price>;
limit: number;
offset: number;
}) => {
// Query building // Query building
let pricesQuery = Price let pricesQuery = Price
.find(filters) .find(filters)
.sort({ timestamp: -1 }) .sort({ timestamp: -1 })
.limit(Math.min(Number(limit), config.maxLimitForPrices)); .limit(Math.min(Number(limit), maxLimitForPrices));
if (offset) { if (offset) {
pricesQuery = pricesQuery.skip(Number(offset)); pricesQuery = pricesQuery.skip(Number(offset));
} }
@@ -184,7 +203,7 @@ async function getPrices({
return prices; return prices;
} }
function validateParams(params, requiredParams) { const validateParams = (params: Record<string, any>, requiredParams: string[]) => {
const errors = []; const errors = [];
for (const requiredParam of requiredParams) { for (const requiredParam of requiredParams) {
if (params[requiredParam] === undefined) { if (params[requiredParam] === undefined) {
@@ -197,7 +216,7 @@ function validateParams(params, requiredParams) {
} }
} }
function getPricesCount(reqBody) { const getPricesCount = (reqBody: PriceWithParams) => {
if (Array.isArray(reqBody)) { if (Array.isArray(reqBody)) {
return reqBody.length; return reqBody.length;
} else { } else {
@@ -205,20 +224,27 @@ function getPricesCount(reqBody) {
} }
} }
function getIp(req) { const getIp = (req: Request) => {
const ip = req.ip; const ip = req.ip;
logger.info("Request IP address: " + ip); logger.info("Request IP address: " + ip);
return ip; return ip;
} }
module.exports = (router) => { interface QueryParams extends PriceWithParams{
provider: string;
symbols?: string;
tokens?: string[];
providerPublicKey?: string;
}
export const prices = (router: Router) => {
/** /**
* This endpoint is used for fetching prices data. * This endpoint is used for fetching prices data.
* It is used in redstone-api * It is used in redstone-api
*/ */
router.get("/prices", asyncHandler(async (req, res) => { router.get("/prices", asyncHandler(async (req, res) => {
// Request validation // Request validation
const params = req.query; const params = req.query as unknown as QueryParams;
// Saving API read event in amplitude // Saving API read event in amplitude
logEvent({ logEvent({
@@ -234,6 +260,7 @@ module.exports = (router) => {
// If query params contain "symbol" we fetch price for this symbol // If query params contain "symbol" we fetch price for this symbol
if (params.symbol !== undefined) { if (params.symbol !== undefined) {
let body: _.Omit<Document<unknown, any, Price> & Price & { providerPublicKey: any; }, "_id" | "__v">[];
if (params.interval !== undefined) { if (params.interval !== undefined) {
body = await getPricesInTimeRangeForSingleToken(params); body = await getPricesInTimeRangeForSingleToken(params);
} else if (params.toTimestamp !== undefined) { } else if (params.toTimestamp !== undefined) {
@@ -241,6 +268,7 @@ module.exports = (router) => {
} else { } else {
body = await getLatestPricesForSingleToken(params); body = await getLatestPricesForSingleToken(params);
} }
return res.json(body);
} }
// Otherwise we fetch prices for many symbols // Otherwise we fetch prices for many symbols
else { else {
@@ -250,18 +278,18 @@ module.exports = (router) => {
} }
params.tokens = tokens; params.tokens = tokens;
body = await getPriceForManyTokens(params); const body = await getPriceForManyTokens(params);
return res.json(body);
} }
return res.json(body);
})); }));
/** /**
* This endpoint is used for posting a new price data. * This endpoint is used for posting a new price data.
* It supports posting a single price and several prices * It supports posting a single price and several prices
*/ */
router.post("/prices", asyncHandler(async (req, res) => { router.post("/prices", asyncHandler(async (req, res) => {
const reqBody = req.body; const reqBody = req.body as PriceWithParams;
let pricesSavedCount = 0; let pricesSavedCount = 0;
// Saving API post event in amplitude // Saving API post event in amplitude
@@ -290,10 +318,10 @@ module.exports = (router) => {
// Cleaning older prices for the same provider after posting // Cleaning older prices for the same provider after posting
// new ones in the lite mode // new ones in the lite mode
if (config.enableLiteMode) { if (enableLiteMode) {
await tryCleanCollection(Price, { await tryCleanCollection(Price, {
provider: reqBody[0].provider, provider: reqBody[0].provider,
timestamp: { $lt: Number(reqBody[0].timestamp) - config.cacheTTLMilliseconds }, timestamp: { $lt: Number(reqBody[0].timestamp) - cacheTTLMilliseconds },
}); });
} }
@@ -308,11 +336,11 @@ module.exports = (router) => {
// Cleaning prices for the same provider and symbol before posting // Cleaning prices for the same provider and symbol before posting
// a new one in the lite mode // a new one in the lite mode
if (config.enableLiteMode) { if (enableLiteMode) {
await tryCleanCollection(Price, { await tryCleanCollection(Price, {
provider: reqBody.provider, provider: reqBody.provider,
symbol: reqBody.symbol, symbol: reqBody.symbol,
timestamp: { $lt: Number(reqBody.timestamp) - config.cacheTTLMilliseconds }, timestamp: { $lt: Number(reqBody.timestamp) - cacheTTLMilliseconds },
}); });
} }
} }

View File

@@ -1,12 +0,0 @@
const providers = require("redstone-node/dist/src/config/providers.json");
module.exports = (router) => {
/**
* This endpoint is used for returning providers details
*/
router.get("/providers", (req, res) => {
res.json(providers);
});
};

12
routes/providers.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Router } from "express";
import providersJson from "redstone-node/dist/src/config/providers.json";
export const providers = (router: Router) => {
/**
* This endpoint is used for returning providers details
*/
router.get("/providers", (req, res) => {
res.json(providersJson);
});
};

View File

@@ -1,9 +0,0 @@
module.exports = {
bigLimitWithMargin: 1200,
defaultLimit: 1,
defaultLocalPort: 9000,
enableJsonLogs: false,
maxLimitForPrices: 3000,
enableLiteMode: true,
cacheTTLMilliseconds: 0, // for tests
};

View File

@@ -1,7 +1,7 @@
const { assertValidSignature } = require("../../helpers/signature-verifier"); import Arweave from "arweave";
const testJwk = require("./test-jwk.json"); import deepSortObject from "deep-sort-object";
const Arweave = require("arweave"); import { assertValidSignature } from "../../helpers/signature-verifier";
const deepSortObject = require("deep-sort-object"); import testJwk from "./test-jwk.json";
const mockProviders = { const mockProviders = {
"redstone": { "redstone": {
@@ -52,7 +52,7 @@ describe("Testing signature verifier", () => {
signature: await getSignature(initialPriceData), signature: await getSignature(initialPriceData),
}; };
const skipLatestPriceCheck = true; const skipLatestPriceCheck = true;
await expect(assertValidSignature(price, skipLatestPriceCheck)).resolves; expect(assertValidSignature(price, skipLatestPriceCheck)).resolves;
}); });
test("Should not verify invalid signature", async () => { test("Should not verify invalid signature", async () => {

16
test/helpers/test-db.ts Normal file
View File

@@ -0,0 +1,16 @@
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
let mongo: MongoMemoryServer;
export const connect = async () => {
mongo = await MongoMemoryServer.create();
const uri = mongo.getUri();
await mongoose.connect(uri);
};
export const closeDatabase = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongo.stop();
};

View File

@@ -1,12 +1,10 @@
const request = require("supertest"); import request from "supertest";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
jest.mock("../../config", () => require("../helpers/lite-mode-config")); describe.only("Testing configs route", () => {
beforeAll(async () => await connect());
describe("Testing configs route", () => { afterAll(async () => await closeDatabase());
beforeAll(async () => await testDB.connect());
afterAll(async () => await testDB.closeDatabase());
test("Should return tokens config", async () => { test("Should return tokens config", async () => {

View File

@@ -1,18 +1,16 @@
const request = require("supertest"); import request from "supertest";
const _ = require("lodash"); import _ from "lodash";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
const Price = require("../../models/price"); import { Price } from "../../models/price";
const Package = require("../../models/package"); import { Package } from "../../models/package";
const { getProviders } = require("../../providers"); import { getProviders } from "../../providers";
jest.mock("../../config", () => require("../helpers/lite-mode-config"));
const provider = getProviderForTests(); const provider = getProviderForTests();
describe("Testing packages route", () => { describe("Testing packages route", () => {
beforeEach(async () => await testDB.connect()); beforeEach(async () => await connect());
afterEach(async () => await testDB.closeDatabase()); afterEach(async () => await closeDatabase());
const testTimestamp = Date.now(); const testTimestamp = Date.now();
const arweaveSignature = "dGVzdC1zaWduYXR1cmU="; // test-signature in Base64 const arweaveSignature = "dGVzdC1zaWduYXR1cmU="; // test-signature in Base64

View File

@@ -1,21 +1,18 @@
const request = require("supertest"); import request from "supertest";
const _ = require("lodash"); import _ from "lodash";
const sleep = require("sleep-promise"); import { app } from "../../app";
const app = require("../../app"); import uuid from "uuid-random";
const uuid = require("uuid-random"); import { connect, closeDatabase } from "../helpers/test-db";
const testDB = require("../test-db"); import { getProviders } from "../../providers";
const { getProviders } = require("../../providers"); import { Price } from "../../models/price";
const Price = require("../../models/price");
jest.mock("../../config", () => require("../helpers/lite-mode-config"));
const provider = getProviderForTests(); const provider = getProviderForTests();
jest.mock("../../helpers/signature-verifier"); jest.mock("../../helpers/signature-verifier");
describe("Testing prices route", () => { describe("Testing prices route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
test("Should post and get a single price", async () => { test("Should post and get a single price", async () => {
// Posting a price // Posting a price

View File

@@ -1,6 +1,6 @@
const request = require("supertest"); import request from "supertest";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
const providers = [ const providers = [
"redstone", "redstone",
@@ -9,8 +9,8 @@ const providers = [
]; ];
describe("Testing providers route", () => { describe("Testing providers route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
test("Should return providers config", async () => { test("Should return providers config", async () => {
const response = await request(app) const response = await request(app)

View File

@@ -1,10 +1,10 @@
const request = require("supertest"); import request from "supertest";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
describe("Testing configs route", () => { describe("Testing configs route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
test("Should return tokens config", async () => { test("Should return tokens config", async () => {
const response = await request(app) const response = await request(app)

View File

@@ -1,10 +1,11 @@
const request = require("supertest"); process.env.LIGHT_MODE = "false";
const app = require("../../app"); import request from "supertest";
const testDB = require("../test-db"); import { app } from "../../app";
import { connect, closeDatabase } from "../helpers/test-db";
describe("Testing errors route", () => { describe("Testing errors route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
const testError = { const testError = {
error: "test-error", error: "test-error",

View File

@@ -1,11 +1,12 @@
const request = require("supertest"); process.env.LIGHT_MODE = "false";
const app = require("../../app"); import request from "supertest";
const testDB = require("../test-db"); import { app } from "../../app";
const cloudwatch = require("../../helpers/cloudwatch"); import { connect, closeDatabase } from "../helpers/test-db";
import * as cloudwatch from "../../helpers/cloudwatch";
describe("Testing metrics route", () => { describe("Testing metrics route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
const testMetricValue = { const testMetricValue = {
label: "test-metric", label: "test-metric",

View File

@@ -1,16 +1,15 @@
const request = require("supertest"); import request from "supertest";
const _ = require("lodash"); import _ from "lodash";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
const Price = require("../../models/price"); import { Price } from "../../models/price";
const Package = require("../../models/package"); import { getProviders } from "../../providers";
const { getProviders } = require("../../providers");
const provider = getProviderForTests(); const provider = getProviderForTests();
describe("Testing packages route", () => { describe("Testing packages route", () => {
beforeEach(async () => await testDB.connect()); beforeEach(async () => await connect());
afterEach(async () => await testDB.closeDatabase()); afterEach(async () => await closeDatabase());
const testTimestamp = Date.now(); const testTimestamp = Date.now();
const arweaveSignature = "dGVzdC1zaWduYXR1cmU="; // test-signature in Base64 const arweaveSignature = "dGVzdC1zaWduYXR1cmU="; // test-signature in Base64

View File

@@ -1,18 +1,18 @@
const request = require("supertest"); import request from "supertest";
const _ = require("lodash"); import _ from "lodash";
const sleep = require("sleep-promise"); import sleep from "sleep-promise";
const app = require("../../app"); import { app } from "../../app";
const uuid = require("uuid-random"); import uuid from "uuid-random";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
const { getProviders } = require("../../providers"); import { getProviders } from "../../providers";
const provider = getProviderForTests(); const provider = getProviderForTests();
jest.mock("../../helpers/signature-verifier"); jest.mock("../../helpers/signature-verifier");
describe("Testing prices route", () => { describe("Testing prices route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
test("Should post and get a single price", async () => { test("Should post and get a single price", async () => {
// Posting a price // Posting a price
@@ -183,10 +183,10 @@ describe("Testing prices route", () => {
describe("Testing EVM signature", () => { describe("Testing EVM signature", () => {
beforeEach(async () => { beforeEach(async () => {
await testDB.connect(); await connect();
// TODO: put intial data to DB // TODO: put intial data to DB
}); });
afterEach(async () => await testDB.closeDatabase()); afterEach(async () => await closeDatabase());
test("Should post and get a price without EVM signature", async () => { test("Should post and get a price without EVM signature", async () => {
const price = getMockPriceData(); const price = getMockPriceData();
@@ -208,17 +208,19 @@ describe("Testing EVM signature", () => {
expect(getResponse).toHaveProperty("body"); expect(getResponse).toHaveProperty("body");
expect(getResponse.body.length).toBe(1); expect(getResponse.body.length).toBe(1);
expect(getResponse.body[0]).toHaveProperty("signature"); expect(getResponse.body[0]).toHaveProperty("signature", "dGVzdC1zaWduYXR1cmU=");
expect(getResponse.body[0]).not.toHaveProperty("evmSignature"); expect(getResponse.body[0]).not.toHaveProperty("evmSignature");
}); });
test("Should post and get a price with EVM signature", async () => { test("Should post and get a price with EVM signature", async () => {
const price = getMockPriceData(); const price = getMockPriceData();
price.evmSignature = "dGVzdC1ldm0tc2lnbmF0dXJl"; // "test-evm-signature" in base64 const priceWithEvmSignature = Object.assign(price, {
evmSignature: "dGVzdC1ldm0tc2lnbmF0dXJl"
}); // "test-evm-signature" in base64
const postResponse = await request(app) const postResponse = await request(app)
.post("/prices") .post("/prices")
.send([price]) .send([priceWithEvmSignature])
.expect(200); .expect(200);
expect(postResponse.body).toHaveProperty("msg", "Prices saved. count: 1"); expect(postResponse.body).toHaveProperty("msg", "Prices saved. count: 1");
@@ -226,15 +228,15 @@ describe("Testing EVM signature", () => {
const getResponse = await request(app) const getResponse = await request(app)
.get("/prices") .get("/prices")
.query({ .query({
symbol: price.symbol, symbol: priceWithEvmSignature.symbol,
provider: provider.name, provider: priceWithEvmSignature.provider,
limit: 1, limit: 1,
}); });
expect(getResponse).toHaveProperty("body"); expect(getResponse).toHaveProperty("body");
expect(getResponse.body.length).toBe(1); expect(getResponse.body.length).toBe(1);
expect(getResponse.body[0]).toHaveProperty("signature"); expect(getResponse.body[0]).toHaveProperty("signature");
expect(getResponse.body[0]).toHaveProperty("evmSignature", price.evmSignature); expect(getResponse.body[0]).toHaveProperty("evmSignature", priceWithEvmSignature.evmSignature);
}); });
}); });

View File

@@ -1,8 +1,6 @@
const request = require("supertest"); import request from "supertest";
const app = require("../../app"); import { app } from "../../app";
const testDB = require("../test-db"); import { connect, closeDatabase } from "../helpers/test-db";
jest.mock("../../config", () => require("../helpers/lite-mode-config"));
const providers = [ const providers = [
"redstone", "redstone",
@@ -11,8 +9,8 @@ const providers = [
]; ];
describe("Testing providers route", () => { describe("Testing providers route", () => {
beforeAll(async () => await testDB.connect()); beforeAll(async () => await connect());
afterAll(async () => await testDB.closeDatabase()); afterAll(async () => await closeDatabase());
test("Should return providers config", async () => { test("Should return providers config", async () => {
const response = await request(app) const response = await request(app)

View File

@@ -1,20 +0,0 @@
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongo;
module.exports.connect = async () => {
mongo = await MongoMemoryServer.create();
const uri = mongo.getUri();
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
});
};
module.exports.closeDatabase = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongo.stop();
};

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"allowJs": true,
"resolveJsonModule": true
}
}

View File

@@ -1,6 +1,7 @@
const { getProviders } = require("./providers"); import { getProviders } from "./providers";
import { PriceWithParams } from "./routes/prices";
function priceParamsToPriceObj(price) { export const priceParamsToPriceObj = (price: PriceWithParams) => {
const priceObj = { const priceObj = {
...price, ...price,
minutes: getMinutesFromTimestamp(price.timestamp), minutes: getMinutesFromTimestamp(price.timestamp),
@@ -15,11 +16,11 @@ function priceParamsToPriceObj(price) {
return priceObj; return priceObj;
} }
function getMinutesFromTimestamp(timestamp) { export const getMinutesFromTimestamp = (timestamp: number) => {
return new Date(timestamp).getMinutes(); return new Date(timestamp).getMinutes();
} }
async function getProviderFromParams(params) { export const getProviderFromParams = async (params: { provider: string; }) => {
// TODO: load this mapping directly from arweave // TODO: load this mapping directly from arweave
// TODO: we also can implement caching // TODO: we also can implement caching
const providers = getProviders(); const providers = getProviders();
@@ -42,7 +43,7 @@ async function getProviderFromParams(params) {
} }
function formatDate(date) { export const formatDate = (date: number) => {
const d = new Date(Number(date)); const d = new Date(Number(date));
const year = String(d.getFullYear()); const year = String(d.getFullYear());
let month = String((d.getMonth() + 1)); let month = String((d.getMonth() + 1));
@@ -57,9 +58,3 @@ function formatDate(date) {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
module.exports = {
formatDate,
getProviderFromParams,
priceParamsToPriceObj,
};

2165
yarn.lock

File diff suppressed because it is too large Load Diff