Skip to content

Commit

Permalink
Merge pull request #1 from Emurgo/feature/endpoints
Browse files Browse the repository at this point in the history
adding basic api endpoints and basic tests.
  • Loading branch information
SebastienGllmt committed Jul 6, 2020
2 parents ed6609d + 0546249 commit c5a4045
Show file tree
Hide file tree
Showing 21 changed files with 4,236 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1 +1,2 @@
./node_modules
*.swp
3,067 changes: 3,067 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions package.json
@@ -0,0 +1,42 @@
{
"name": "yoroi-be-mk3",
"version": "1.0.0",
"description": "like https://github.com/Emurgo/yoroi-backend-service but simpler and missing pieces.",
"main": "src/index.ts",
"scripts": {
"postinstall": "tsc",
"start": "pm2 start pm2.yaml",
"stop": "pm2 stop pm2.yaml",
"dev": "tsc-watch --onSuccess \"node ./dist/index.js\"",
"test": "mocha -r ts-node/register tests/**/*.test.ts --slow 0",
"testtxhist": "mocha -r ts-node/register tests/txHistory.test.ts"
},
"author": "",
"license": "MIT",
"dependencies": {
"@types/chai": "^4.2.11",
"@types/compression": "^1.7.0",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"@types/lodash": "^4.14.155",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.13",
"@types/pg": "^7.14.3",
"axios": "^0.19.2",
"chai": "^4.2.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^5.0.0-alpha.8",
"lodash": "^4.17.15",
"mocha": "^8.0.1",
"pg": "^8.2.1",
"pm2": "^4.4.0",
"ts-node": "^8.10.2",
"tsc-watch": "^4.2.8",
"typescript": "^3.9.5"
},
"devDependencies": {
"@types/ramda": "github:types/npm-ramda#dist",
"ramda": "^0.27.0"
}
}
4 changes: 4 additions & 0 deletions pm2.yaml
@@ -0,0 +1,4 @@
apps:
- script: ./dist/index.js
instances: max
exec_mode: cluster
242 changes: 242 additions & 0 deletions src/index.ts
@@ -0,0 +1,242 @@
import http from "http";
import express from "express";
import { Request, Response } from "express";

import axios from 'axios';

import * as _ from 'lodash';

import { Pool } from 'pg';

import { applyMiddleware, applyRoutes, contentTypeHeaders, graphqlEndpoint, Route } from "./utils";
import * as utils from "./utils";
import * as middleware from "./middleware";

import { askBestBlock } from "./services/bestblock";
import { askUtxoForAddresses } from "./services/utxoForAddress";
import { askBlockNumByHash, askBlockNumByTxHash, askTransactionHistory } from "./services/transactionHistory";
import { askFilterUsedAddresses } from "./services/filterUsedAddress";
import { askUtxoSumForAddresses } from "./services/utxoSumForAddress";


const pool = new Pool({user: 'hasura', host:'/tmp/', database: 'cexplorer'});

const router = express();

const middlewares = [ middleware.handleCors
, middleware.handleBodyRequestParsing
, middleware.handleCompression
];

applyMiddleware(middlewares, router);

const port = 8082;
const addressesRequestLimit = 50;
const apiResponseLimit = 50;

const bestBlock = async (req: Request, res: Response) => {
const result = await askBestBlock();
switch(result.kind) {
case "ok":
const cardano = result.value;
res.send({
epoch: cardano.currentEpoch.number,
slot: cardano.slotDuration,
hash: cardano.currentEpoch.blocks[0].hash,
height: cardano.blockHeight,
});

return;
case "error":
console.log(result.errMsg);
return;
default: return utils.assertNever(result);
};
};

const utxoForAddresses = async (req: Request, res: Response) => {
if(!req.body || !req.body.addresses) {
console.log("error, no addresses.");
return;
}
const verifiedAddresses = utils.validateAddressesReq(addressesRequestLimit
, req.body.addresses);
switch(verifiedAddresses.kind){
case "ok":
const result = await askUtxoForAddresses(verifiedAddresses.value);
switch(result.kind)
{
case "ok":
const utxos = result.value.map( utxo =>
({
utxo_id: `${utxo.txHash}:${utxo.index}`,
tx_hash: utxo.txHash,
tx_index: utxo.index,
receiver: utxo.address,
amount: utxo.value,
block_num: utxo.transaction.block.number,
}));
res.send(utxos);
return;
case "error":
console.log(result.errMsg);
return;
default: return utils.assertNever(result);

}
case "error":
console.log(verifiedAddresses.errMsg);
return;
default: return utils.assertNever(verifiedAddresses);
}
};


const filterUsedAddresses = async (req: Request, res: Response) => {
if(!req.body || !req.body.addresses) {
console.log("error, no addresses.");
return;
}
const verifiedAddresses = utils.validateAddressesReq(addressesRequestLimit
, req.body.addresses);
switch(verifiedAddresses.kind){
case "ok":
const result = await askFilterUsedAddresses(verifiedAddresses.value);
switch(result.kind){
case "ok":
const usedAddresses = _.chain(result.value)
.flatMap(tx => [...tx.inputs, ...tx.outputs])
.map('address')
.intersection(verifiedAddresses.value)
.value();

res.send(usedAddresses);
return;
case "error":
console.log(result.errMsg);
return;
default: return utils.assertNever(result);
}
return;
case "error":
console.log(verifiedAddresses.errMsg);
return;
default: return utils.assertNever(verifiedAddresses);
}
};



const utxoSumForAddresses = async (req: Request, res:Response) => {
if(!req.body || !req.body.addresses) {
console.log("error, no addresses.");
return;
}
const verifiedAddresses = utils.validateAddressesReq(addressesRequestLimit
, req.body.addresses);
switch(verifiedAddresses.kind){
case "ok":
const result = await askUtxoSumForAddresses(verifiedAddresses.value);
switch(result.kind) {
case "ok":
res.send({ sum: result.value });
return;
case "error":
console.log(result.errMsg);
return;
default: return utils.assertNever(result);
}
return;
case "error":
console.log(verifiedAddresses.errMsg);
return;
default: return utils.assertNever(verifiedAddresses);
}
};

const txHistory = async (req: Request, res: Response) => {
if(!req.body){
console.log("error, no body");
return;
}
const verifiedBody = utils.validateHistoryReq(addressesRequestLimit, apiResponseLimit, req.body);
switch(verifiedBody.kind){
case "ok":
const body = verifiedBody.value;
const limit = body.limit || apiResponseLimit;
const [referenceTx, referenceBlock] = (body.after && [body.after.tx, body.after.block]) || [];
const referenceBestBlock = body.untilBlock;
const untilBlockNum = await askBlockNumByHash(referenceBestBlock);
const afterBlockNum = await askBlockNumByTxHash(referenceTx );

if(untilBlockNum.kind === 'error' && untilBlockNum.errMsg !== utils.errMsgs.noValue) {
console.log(`untilBlockNum failed: ${untilBlockNum.errMsg}`);
return;
}
if(afterBlockNum.kind === 'error' && afterBlockNum.errMsg !== utils.errMsgs.noValue) {
console.log(`afterBlockNum failed: ${afterBlockNum.errMsg}`);
return;
}

const maybeTxs = await askTransactionHistory(pool, limit, body.addresses, afterBlockNum, untilBlockNum);
switch(maybeTxs.kind) {
case "ok":
const txs = maybeTxs.value.map( tx => ({
hash: tx.hash,
tx_ordinal: tx.txIndex,
tx_state: 'Successful', // graphql doesn't handle pending/failed txs
last_update: tx.includedAt,
block_num: tx.block.number,
block_hash: tx.block.hash,
time: tx.includedAt,
epoch: tx.block.epochNo,
slot: tx.block.slotNo,
inputs: tx.inputs,
outputs: tx.outputs
}));

res.send(txs);
return;
case "error":
console.log(maybeTxs.errMsg);
return;
default: return utils.assertNever(maybeTxs);
}
return;
case "error":
console.log(verifiedBody.errMsg);
return;
default: return utils.assertNever(verifiedBody);
}
};

const routes : Route[] = [ { path: '/bestblock'
, method: "get"
, handler: bestBlock
}
, { path: '/addresses/filterUsed'
, method: "post"
, handler: filterUsedAddresses
}
, { path: '/txs/utxoForAddresses'
, method: "post"
, handler: utxoForAddresses
}
, { path: '/txs/utxoSumForAddresses'
, method: "post"
, handler: utxoSumForAddresses
}
, { path: '/txs/history'
, method: "post"
, handler: txHistory
}
]

applyRoutes(routes, router);

const server = http.createServer(router);

server.listen(port, () =>
console.log(`listening on ${port}...`)
);

16 changes: 16 additions & 0 deletions src/middleware/index.ts
@@ -0,0 +1,16 @@
import { Router } from "express";
import cors from "cors";
import parser from "body-parser";
import compression from "compression";

export const handleCors = (router: Router) =>
router.use(cors({ credentials: true, origin: true }));

export const handleBodyRequestParsing = (router: Router) => {
router.use(parser.urlencoded({ extended: true }));
router.use(parser.json());
};

export const handleCompression = (router: Router) => {
router.use(compression());
};
43 changes: 43 additions & 0 deletions src/services/bestblock.ts
@@ -0,0 +1,43 @@
import axios from 'axios';
import { Request, Response } from "express";

import { assertNever, contentTypeHeaders, graphqlEndpoint, UtilEither} from "../utils";

interface CardanoFrag {
blockHeight: number;
currentEpoch: EpochFrag;
slotDuration: number;
}

interface EpochFrag {
blocks: BlockFrag[];
number: number;
}

interface BlockFrag {
hash: string;
number: number;
}

export const askBestBlock = async () : Promise<UtilEither<CardanoFrag>> => {
const query = `
{
cardano {
blockHeight,
currentEpoch {
number
blocks(limit:1, order_by: { createdAt:desc}) {
hash
number
}
},
slotDuration,
},
}
`;
const ret = await axios.post(graphqlEndpoint, JSON.stringify({'query':query}), contentTypeHeaders);
if('data' in ret && 'data' in ret.data && 'cardano' in ret.data.data)
return { kind: 'ok', value: ret.data.data.cardano };
else return { kind: 'error', errMsg:'BestBlock, could not understand graphql response' };
};

0 comments on commit c5a4045

Please sign in to comment.