Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from Emurgo/feature/endpoints
adding basic api endpoints and basic tests.
- Loading branch information
Showing
21 changed files
with
4,236 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
./node_modules | ||
*.swp |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
apps: | ||
- script: ./dist/index.js | ||
instances: max | ||
exec_mode: cluster |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}...`) | ||
); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' }; | ||
}; | ||
|
Oops, something went wrong.