Skip to content

Commit

Permalink
feat: add basic indexer api (#12)
Browse files Browse the repository at this point in the history
Signed-off-by: david <david@umaproject.org>
  • Loading branch information
daywiss authored Aug 26, 2024
1 parent 9ca225e commit ab2a65c
Show file tree
Hide file tree
Showing 8 changed files with 7,479 additions and 9,667 deletions.
2 changes: 1 addition & 1 deletion apps/node/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async function run() {
void (await PersistenceExample.Main(process.env));
return "Example persistence app running";
case "indexer-api":
void (await IndexerApi.Main(process.env));
void (await IndexerApi.Main(process.env, logger));
return "Indexer API running";
default:
throw new Error(`Unable to start, unknown app: ${APP}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/indexer-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.2",
"superstruct": "2.0.3-1"
"superstruct": "2.0.3-1",
"winston": "^3.13.1"
},
"exports": {
".": "./src/index.ts"
Expand Down
71 changes: 52 additions & 19 deletions packages/indexer-api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,67 @@
import { object, string, assert } from "superstruct";
import { ExpressApp } from "./express-app";
import { createDataSource, DatabaseConfig } from "@repo/indexer-database";
import * as services from "./services";
import winston from "winston";
import { type Router } from "express";

const databaseConfigStruct = object({
host: string(),
port: string(),
user: string(),
password: string(),
dbName: string(),
});
export async function connectToDatabase(
databaseConfig: DatabaseConfig,
logger: winston.Logger,
) {
try {
const database = await createDataSource(databaseConfig).initialize();
logger.info("Postgres connection established");
return database;
} catch (error) {
logger.error("Unable to connect to database", error);
throw error;
}
}
function getPostgresConfig(
env: Record<string, string | undefined>,
): DatabaseConfig | undefined {
return env.DATABASE_HOST &&
env.DATABASE_PORT &&
env.DATABASE_USER &&
env.DATABASE_PASSWORD &&
env.DATABASE_NAME
? {
host: env.DATABASE_HOST,
port: env.DATABASE_PORT,
user: env.DATABASE_USER,
password: env.DATABASE_PASSWORD,
dbName: env.DATABASE_NAME,
}
: undefined;
}

export async function Main(env: Record<string, string | undefined>) {
export async function Main(
env: Record<string, string | undefined>,
logger: winston.Logger,
) {
const { PORT = "8080" } = env;
const port = Number(PORT);

// Validate database config
const databaseConfig = {
host: env.DATABASE_HOST,
port: env.DATABASE_PORT,
user: env.DATABASE_USER,
password: env.DATABASE_PASSWORD,
dbName: env.DATABASE_NAME,
};
assert(databaseConfig, databaseConfigStruct);
const postgresConfig = getPostgresConfig(env);
const postgres = postgresConfig
? await connectToDatabase(postgresConfig, logger)
: undefined;

const exampleRouter = services.example.getRouter();
const app = ExpressApp({ example: exampleRouter });
const allServices: Record<string, Router> = {
example: exampleRouter,
};
if (postgres) {
const indexerRouter = services.indexer.getRouter(postgres);
allServices.indexer = indexerRouter;
}
const app = ExpressApp(allServices);

logger.info({
message: `Starting indexer api on port ${port}`,
});
void (await new Promise((res) => {
app.listen(port, () => res(app));
}));
console.log(`Indexer api listening on port ${port}`);
}
1 change: 1 addition & 0 deletions packages/indexer-api/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * as example from "./example";
export * as indexer from "./indexer";
179 changes: 179 additions & 0 deletions packages/indexer-api/src/services/indexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import assert from "assert";
import * as s from "superstruct";
import { Request, Response, NextFunction, Router } from "express";
import { JSON } from "../types";
import { DataSource, entities } from "@repo/indexer-database";

type APIHandler = (
params?: JSON,
) => Promise<JSON> | JSON | never | Promise<never> | void | Promise<void>;
export function Indexer(db: DataSource): Record<string, APIHandler> {
const DepositParams = s.object({
id: s.string(),
});
// get a single deposit by an id, regardless of chain
async function deposit(params: JSON) {
s.assert(params, DepositParams);
throw new Error(`Deposit id not found ${params.id}`);
}
// possible filter options
const DepositsParams = s.object({
depositor: s.optional(s.string()),
recipient: s.optional(s.string()),
inputToken: s.optional(s.string()),
outputToken: s.optional(s.string()),
integrator: s.optional(s.string()),
status: s.optional(s.string()),
// some kind of pagination options, skip could be the start point
skip: s.optional(
s.coerce(s.number(), s.string(), (value) => parseInt(value)),
),
// pagination limit, how many to return after the start, note we convert string to number
limit: s.optional(
s.coerce(s.number(), s.string(), (value) => parseInt(value)),
),
});
// get a list of deposits filtered by options
async function deposits(queryParams: JSON) {
// this coerces any string numbers into numbers that we defined in our params
const params = s.create(queryParams, DepositsParams);
const repo = db.getRepository(entities.Deposit);
const queryBuilder = repo.createQueryBuilder("deposit");

if (params.depositor) {
queryBuilder.andWhere("deposit.depositor = :depositor", {
depositor: params.depositor,
});
}

if (params.recipient) {
queryBuilder.andWhere("deposit.recipient = :recipient", {
recipient: params.recipient,
});
}

if (params.inputToken) {
queryBuilder.andWhere("deposit.inputToken = :inputToken", {
inputToken: params.inputToken,
});
}

if (params.outputToken) {
queryBuilder.andWhere("deposit.outputToken = :outputToken", {
outputToken: params.outputToken,
});
}

if (params.integrator) {
queryBuilder.andWhere("deposit.integrator = :integrator", {
integrator: params.integrator,
});
}

if (params.status) {
queryBuilder.andWhere("deposit.status = :status", {
status: params.status,
});
}

if (params.skip) {
queryBuilder.skip(params.skip);
}

if (params.limit) {
// using take rather than limit
queryBuilder.take(params.limit);
}

return (await queryBuilder.getMany()) as unknown as JSON;
}
// query hub pools by chainId? default to 1 if not specified. will leave option in case of testnets?
const HubPoolBalanceParams = s.object({
chainId: s.defaulted(s.number(), 1),
l1Token: s.string(),
});
function hubPoolBalance(params: JSON) {
s.assert(params, HubPoolBalanceParams);
return 0;
}
// query spokepools by chainId, must specify
const SpokePoolBalanceParams = s.object({
chainId: s.number(),
// unsure why we have timestamp, implies we are storign history of balances? this is in the spec.
timestamp: s.number(),
// unsure why specified as l2Token in spec, don't we have spoke pool on L1?
l2Token: s.optional(s.number()),
});
function spokePoolBalance(params: JSON) {
s.assert(params, SpokePoolBalanceParams);
return 0;
}

const RelayerRefundParams = s.object({
relayHash: s.string(),
});
function relayerRefund(params: JSON) {
s.assert(params, RelayerRefundParams);
throw new Error("Relayer refund not found");
}

const RelayerRefundsParams = s.object({
relayer: s.string(),
// some kind of pagination options, start could be start id or start index
start: s.optional(s.string()),
// pagination limit, how many to return after the start
limit: s.optional(s.number()),
});
function relayerRefunds(params: JSON) {
s.assert(params, RelayerRefundsParams);
return [];
}

// TODO:
function bundles() {
return [];
}

return {
// POC
deposit,
deposits,
hubPoolBalance,
spokePoolBalance,
// Future endpoints
relayerRefund,
relayerRefunds,
bundles,
};
}

// build up express style calls to our example api
export function getRouter(db: DataSource): Router {
const router = Router();
const api = Indexer(db);
// example call: curl localhost:8080/example/now -> timestamp
router.get(
"/:action",
async (req: Request, res: Response, next: NextFunction) => {
const params = req.query;
const action = req.params.action;
console.log(params, action);
try {
assert(action, "No api call specified");
// extract method from api calls
const method = api[action];
//check if it exists
if (method) {
// call and return result
const result = await method(params);
return res.json(result);
}
throw new Error(`Unknown api call: ${action}`);
} catch (err) {
next(err);
}
},
);
// return the router to be included in the greater express app
return router;
}
3 changes: 3 additions & 0 deletions packages/indexer-database/src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./deposit.entity";
export * from "./fill.entity";
export * from "./slowFillRequest.entity";
1 change: 1 addition & 0 deletions packages/indexer-database/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./main";
export * as entities from "./entities";
Loading

0 comments on commit ab2a65c

Please sign in to comment.