Skip to content

Commit

Permalink
Add transfers and approvals
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathieu Lefebvre committed Nov 20, 2023
1 parent 1265269 commit 104589c
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/fetch/GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import * as prometheus from "../prometheus.js";
import { logger } from "../logger.js";
import swaggerHtml from "../../swagger/index.html"
import swaggerFavicon from "../../swagger/favicon.png"
import transfers from "./transfers.js";
import approvals from "./approvals.js";

export default async function (req: Request) {
const { pathname } = new URL(req.url);
Expand All @@ -23,6 +25,8 @@ export default async function (req: Request) {
if (pathname === "/supply") return supply(req);
if (pathname === "/balance") return balance(req);
if (pathname === "/contract") return contract(req);
if (pathname === "/transfers") return transfers(req);
if (pathname === "/approvals") return approvals(req);
if (pathname === "/holders") return holders(req);
logger.warn(`Not found: ${pathname}`);
prometheus.request_error.inc({ pathname, status: 404 });
Expand Down
20 changes: 20 additions & 0 deletions src/fetch/approvals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// from: https://github.com/pinax-network/substreams-clock-api/blob/main/src/fetch/block.ts
import { makeQuery } from "../clickhouse/makeQuery.js";
import { logger } from "../logger.js";
import { getApprovals } from "../queries.js";
import * as prometheus from "../prometheus.js";
import { toJSON } from "./utils.js";

export default async function (req: Request) {
try {
const { searchParams } = new URL(req.url);
logger.info({ searchParams: Object.fromEntries(Array.from(searchParams)) });
const query = await getApprovals(searchParams);
const response = await makeQuery(query)
return toJSON(response.data);
} catch (e: any) {
logger.error(e);
prometheus.request_error.inc({ pathname: "/approvals", status: 400 });
return new Response(e.message, { status: 400 });
}
}
60 changes: 59 additions & 1 deletion src/fetch/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { config } from "../config.js";
import { registry } from "../prometheus.js";
import { supportedChainsQuery } from "./chains.js";
import { makeQuery } from "../clickhouse/makeQuery.js";
import { getBalanceChanges, getContracts, getHolders, getTotalSupply } from "../queries.js";
import { getApprovals, getBalanceChanges, getContracts, getHolders, getTotalSupply, getTransfers } from "../queries.js";
const TAGS = {
MONITORING: "Monitoring",
HEALTH: "Health",
Expand All @@ -16,11 +16,16 @@ const TAGS = {

const timestampExamplesArrayFilter = ["greater_or_equals_by_timestamp", "greater_by_timestamp", "less_or_equals_by_timestamp", "less_by_timestamp"];
const blockExamplesArrayFilter = ["greater_or_equals_by_block", "greater_by_block", "less_or_equals_by_block", "less_by_block"];
const amountExamplesArrayFilter = ["amount_greater_or_equals", "amount_greater", "amount_less_or_equals", "amount_less"];

const chains = await supportedChainsQuery();
const supply_example = (await makeQuery(await getTotalSupply(new URLSearchParams({ limit: "2" }), true))).data;
const contract_example = (await makeQuery(await getContracts(new URLSearchParams({ limit: "2" }), true))).data;
const balance_example = (await makeQuery(await getBalanceChanges(new URLSearchParams({ limit: "2" }), true))).data;
const holders_example = (await makeQuery(await getHolders(new URLSearchParams({ limit: "5" }), true))).data;
const transfers_example = (await makeQuery(await getTransfers(new URLSearchParams({ limit: "5" }), true))).data;
const approvals_example = (await makeQuery(await getApprovals(new URLSearchParams({ limit: "5" }), true))).data;

const timestampSchema: SchemaObject = {
anyOf: [
{ type: "number" },
Expand Down Expand Up @@ -88,6 +93,17 @@ const blockFilter = blockExamplesArrayFilter.map(name => {
} as ParameterObject
})

const amountFilter = amountExamplesArrayFilter.map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: { type: "number" },
} as ParameterObject
})


export default new OpenApiBuilder()
.addInfo({
title: pkg.name,
Expand Down Expand Up @@ -173,6 +189,48 @@ export default new OpenApiBuilder()
400: { description: "Bad request" },
},
},
}).addPath("/transfers", {
get: {
tags: [TAGS.USAGE],
summary: "ERC20 Transfers",
parameters: [
parameterChain,
parameterString("contract"),
parameterString("from"),
parameterString("to"),
parameterString("transaction_id"),
...amountFilter,
...timestampFilter,
...blockFilter,
parameterLimit,
parameterOffset,
],
responses: {
200: { description: "Array of supply", content: { "application/json": { example: transfers_example, schema: { type: "array" } } } },
400: { description: "Bad request" },
},
},
}).addPath("/approvals", {
get: {
tags: [TAGS.USAGE],
summary: "ERC20 Approvals",
parameters: [
parameterChain,
parameterString("contract"),
parameterString("owner"),
parameterString("spender"),
parameterString("transaction_id"),
...amountFilter,
...timestampFilter,
...blockFilter,
parameterLimit,
parameterOffset,
],
responses: {
200: { description: "Array of supply", content: { "application/json": { example: approvals_example, schema: { type: "array" } } } },
400: { description: "Bad request" },
},
},
}).addPath("/holders", {
get: {
tags: [TAGS.USAGE],
Expand Down
20 changes: 20 additions & 0 deletions src/fetch/transfers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// from: https://github.com/pinax-network/substreams-clock-api/blob/main/src/fetch/block.ts
import { makeQuery } from "../clickhouse/makeQuery.js";
import { logger } from "../logger.js";
import { getTransfers } from "../queries.js";
import * as prometheus from "../prometheus.js";
import { toJSON } from "./utils.js";

export default async function (req: Request) {
try {
const { searchParams } = new URL(req.url);
logger.info({ searchParams: Object.fromEntries(Array.from(searchParams)) });
const query = await getTransfers(searchParams);
const response = await makeQuery(query)
return toJSON(response.data);
} catch (e: any) {
logger.error(e);
prometheus.request_error.inc({ pathname: "/transfers", status: 400 });
return new Response(e.message, { status: 400 });
}
}
86 changes: 86 additions & 0 deletions src/queries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
getBalanceChanges,
addTimestampBlockFilter,
getHolders,
getTransfers,
getApprovals,
addAmountFilter,
} from "./queries.js";

const chain = "eth";
Expand Down Expand Up @@ -85,6 +88,24 @@ test("addTimestampBlockFilter", () => {
expect(where).toContain("toUnixTimestamp(timestamp) <= 1697587100");
});

//Test Amount Filter
test("addAmountFilter", () => {
let where: any[] = [];
const searchParams = new URLSearchParams({
address: address,
amount_greater_or_equals: "123123",
amount_less_or_equals: "123123",
amount_greater: "2323",
amount_less: "2332",
});
addAmountFilter(searchParams, where);
expect(where).toContain("amount >= 123123");
expect(where).toContain("amount <= 123123");
expect(where).toContain("amount > 2323");
expect(where).toContain("amount < 2332");
});


// Test TotalSupply
test("getTotalSupply", () => {
const parameters = new URLSearchParams({ chain, address });
Expand Down Expand Up @@ -198,6 +219,71 @@ test("getBalanceChanges with options", () => {
);
});

// Test getTransfers

test("getTransfers", () => {
const parameters = new URLSearchParams({ chain, contract: address, from: address, to: address, transaction_id });
expect(formatSQL(getTransfers(parameters))).toContain(
formatSQL(`SELECT
address as contract,
from,
to,
value as amount,
transaction as transaction_id,
block_number,
timestamp,
chain`)
);
expect(formatSQL(getTransfers(parameters))).toContain(
formatSQL(`FROM Transfers`)
);


expect(formatSQL(getTransfers(parameters))).toContain(
formatSQL(`WHERE(Transfers.chain == '${chain}' AND Transfers.address == '${address}' AND Transfers.from == '${address}' AND Transfers.to == '${address}' AND Transfers.transaction == '${transaction_id}')`)
);

expect(formatSQL(getTransfers(parameters))).toContain(
formatSQL(`ORDER BY block_number DESC`)
);

expect(formatSQL(getTransfers(parameters))).toContain(
formatSQL(`LIMIT 100`)
);
});

// Test Approvals

test("getApprovals", () => {
const parameters = new URLSearchParams({ chain, contract: address, owner: address, sender: address, transaction_id });
expect(formatSQL(getApprovals(parameters))).toContain(
formatSQL(`SELECT
address as contract,
owner,
spender,
value as amount,
transaction as transaction_id,
block_number,
timestamp,
chain`)
);
expect(formatSQL(getApprovals(parameters))).toContain(
formatSQL(`FROM Approvals`)
);


expect(formatSQL(getApprovals(parameters))).toContain(
formatSQL(`WHERE(Approvals.chain == '${chain}' AND Approvals.address == '${address}' AND Approvals.owner == '${address}' AND Approvals.sender == '${address}' AND Approvals.transaction == '${transaction_id}')`)
);

expect(formatSQL(getApprovals(parameters))).toContain(
formatSQL(`ORDER BY block_number DESC`)
);

expect(formatSQL(getApprovals(parameters))).toContain(
formatSQL(`LIMIT 100`)
);
});

// Test getHolders
test("getHolders", () => {
Expand Down
122 changes: 122 additions & 0 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ export function addTimestampBlockFilter(searchParams: URLSearchParams, where: an
}
}

export function addAmountFilter(searchParams: URLSearchParams, where: any[]) {
const operators = [
["greater_or_equals", ">="],
["greater", ">"],
["less_or_equals", "<="],
["less", "<"],
]
for (const [key, operator] of operators) {
const amount = searchParams.get(`amount_${key}`);
if (amount) where.push(`amount ${operator} ${amount}`);
}
}


export function getTotalSupply(searchParams: URLSearchParams, example?: boolean) {
// Params
const address = getAddress(searchParams, "address", false)?.toLowerCase();
Expand Down Expand Up @@ -215,6 +229,114 @@ export function getHolders(searchParams: URLSearchParams, example?: boolean) {
return query;
}

export function getTransfers(searchParams: URLSearchParams, example?: boolean) {
// Params
const contract = getAddress(searchParams, "contract", false)?.toLowerCase();
const from = getAddress(searchParams, "from", false)?.toLowerCase();
const to = getAddress(searchParams, "to", false)?.toLowerCase();
const chain = searchParams.get("chain");
const transaction_id = searchParams.get("transaction_id")?.toLowerCase();
const amount = searchParams.get("amount");
// Query
const table = 'Transfers'

let query = `SELECT
address as contract,
from,
to,
value as amount,
transaction as transaction_id,
block_number,
timestamp,
chain
FROM ${table} `;

if (!example) {
// WHERE statements
const where = [];

// equals
if (chain) where.push(`${table}.chain == '${chain}'`);
if (contract) where.push(`${table}.address == '${contract}'`);
if (from) where.push(`${table}.from == '${from}'`);
if (to) where.push(`${table}.to == '${to}'`);
if (transaction_id) where.push(`${table}.transaction == '${transaction_id}'`);

//add amount filter
addAmountFilter(searchParams, where);
// timestamp and block filter
addTimestampBlockFilter(searchParams, where);

// Join WHERE statements with AND
if (where.length) query += ` WHERE (${where.join(' AND ')})`;

// Sort and Limit
const sort_by = searchParams.get("sort_by");
query += ` ORDER BY block_number ${sort_by ?? DEFAULT_SORT_BY} `

}
const limit = parseLimit(searchParams.get("limit"), 100);
query += ` LIMIT ${limit} `
const offset = searchParams.get("offset");
if (offset) query += ` OFFSET ${offset} `
return query;
}


export function getApprovals(searchParams: URLSearchParams, example?: boolean) {
// Params
const contract = getAddress(searchParams, "contract", false)?.toLowerCase();
const owner = getAddress(searchParams, "owner", false)?.toLowerCase();
const sender = getAddress(searchParams, "sender", false)?.toLowerCase();
const chain = searchParams.get("chain");
const transaction_id = searchParams.get("transaction_id")?.toLowerCase();
const amount = searchParams.get("amount");
// Query
const table = 'Approvals'

let query = `SELECT
address as contract,
owner,
spender,
value as amount,
transaction as transaction_id,
block_number,
timestamp,
chain
FROM ${table} `;

if (!example) {
// WHERE statements
const where = [];

// equals
if (chain) where.push(`${table}.chain == '${chain}'`);
if (contract) where.push(`${table}.address == '${contract}'`);
if (owner) where.push(`${table}.owner == '${owner}'`);
if (sender) where.push(`${table}.sender == '${sender}'`);
if (transaction_id) where.push(`${table}.transaction == '${transaction_id}'`);

//add amount filter
addAmountFilter(searchParams, where);
// timestamp and block filter
addTimestampBlockFilter(searchParams, where);

// Join WHERE statements with AND
if (where.length) query += ` WHERE (${where.join(' AND ')})`;

// Sort and Limit
const sort_by = searchParams.get("sort_by");
query += ` ORDER BY block_number ${sort_by ?? DEFAULT_SORT_BY} `

}
const limit = parseLimit(searchParams.get("limit"), 100);
query += ` LIMIT ${limit} `
const offset = searchParams.get("offset");
if (offset) query += ` OFFSET ${offset} `
return query;
}



export function getChain() {
return `SELECT DISTINCT chain FROM module_hashes`;
Expand Down

0 comments on commit 104589c

Please sign in to comment.