Skip to content

Commit

Permalink
small backend for snapshot (#21)
Browse files Browse the repository at this point in the history
* small backend for snapshot

* readme.md

* fixed lint
  • Loading branch information
akorchyn committed Apr 2, 2024
1 parent db70a7f commit e965858
Show file tree
Hide file tree
Showing 10 changed files with 1,317 additions and 2 deletions.
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
"dependencies": {
"@supercharge/promise-pool": "^2.3.2",
"big.js": "^6.1.1",
"bn-sqrt": "^1.0.0",
"commander": "^12.0.0",
"dotenv": "^16.3.1",
"fs": "^0.0.1security",
"near-api-js": "3.0.4",
"near-api-js": "^3.0.4",
"p-retry": "^6.2.0",
"pg": "^8.11.3",
"secp256k1": "^5.0.0",
Expand Down
52 changes: 52 additions & 0 deletions snapshotter/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Snapshot API

This is a Node.js application that provides an API endpoint for retrieving paginated, sorted, and filtered snapshot data.

## Installation

1. Install the dependencies:

```bash
npm i
```

2. Start the application:

```bash
npm start:local
```

## API Endpoint

The API endpoint is available at `/api/snapshot`.

### Query Parameters

The following query parameters can be used to customize the API response:

- `page` (optional, default: 0): Page number of the paginated results.
- `limit` (optional, default: 100): Maximum number of items per page.
- `sortBy` (optional, default: "name"): Field to sort the results by.
- Possible values: "name", "stake", "active_months", "vote_power", "stake_power", "activity_power"
- `sortOrder` (optional, default: "asc"): Order in which the results should be sorted.
- Possible values: "asc" (ascending), "desc" (descending)
- `prefix` (optional, default: ""): Prefix to filter the `account_id` field by.

### Example Requests

- `/api/snapshot`: First page of data with default sorting and no filtering.
- `/api/snapshot?page=1&limit=50`: Second page of data with 50 items per page.
- `/api/snapshot?sortBy=stake&sortOrder=desc`: First page of data sorted by stake in descending order.
- `/api/snapshot?page=2&limit=20&sortBy=vote_power&sortOrder=asc`: Third page of data sorted by vote power in ascending order with 20 items per page.
- `/api/snapshot?prefix=00&sortBy=activity_power&sortOrder=desc`: First page of data filtered by `account_id` prefix "00" and sorted by activity power in descending order.

### Error Handling

If an invalid `sortBy` parameter is provided, the API will respond with a 400 status code and an error message indicating the invalid parameter.

## Configuration

The application can be configured using the following environment variables:

- `SERVER_PORT` (default: 3000): The port on which the server will run.
- `SNAPSHOT_FILE` (default: 'snapshot.json'): The path to the snapshot JSON file.
12 changes: 12 additions & 0 deletions snapshotter/server/api/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express from "express";
import cors from "cors";
import { corsConfig } from "./config/cors.config.js";
import { routes } from "./routes.js";

export const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors(corsConfig));

app.use("/api", routes);
9 changes: 9 additions & 0 deletions snapshotter/server/api/config/cors.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const corsConfig = {
origin: [
"http://localhost:3000",
"https://near.org",
"https://near.social",
],
methods: "GET,OPTION,HEAD",
optionsSuccessStatus: 200,
};
8 changes: 8 additions & 0 deletions snapshotter/server/api/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from "express";
import { GetSnapshot } from "./snapshot.js";

const routes = express.Router();

routes.get("/snapshot", GetSnapshot);

export { routes };
91 changes: 91 additions & 0 deletions snapshotter/server/api/snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { parseNearAmount } from "near-api-js/lib/utils/format.js";
import { snapshot } from "../index.js";
import { BN } from 'bn.js';

const NEAR_THRESHOLD = new BN(parseNearAmount('1000'));
const NEAR_TO_YOCTO = new BN(parseNearAmount('1'));

const stakePower = (stakeStr) => {
const stake = new BN(stakeStr);

let stake_power = stake.div(NEAR_TO_YOCTO).toNumber();

if (stake.gt(NEAR_THRESHOLD)) {
stake_power = 1000;
stake_power += Math.floor(Math.sqrt(stake.sub(NEAR_THRESHOLD).div(NEAR_TO_YOCTO).toNumber()));
}
return stake_power;

}

const activityPower = (activeMonths) => {
return activeMonths * 20;
}

const userToVotePower = (user) => {
const activeMonthsPower = activityPower(user.active_months);
const stake_power = stakePower(user.stake);

return activeMonthsPower + stake_power;
}

export const GetSnapshot = (req, res) => {
const { page = 0, limit = 100, sortBy = "name", sortOrder = "asc", prefix = "" } = req.query;

// Convert page and limit to numbers
const pageNumber = parseInt(page);
const limitNumber = parseInt(limit);

let filteredSnapshot = snapshot;
if (prefix) {
filteredSnapshot = snapshot.filter((item) => item.account_id.startsWith(prefix));
}

// Sort the snapshot data based on the sortBy and sortOrder parameters
let sortedSnapshot = [...filteredSnapshot];
switch (sortBy) {
case "name":
sortedSnapshot.sort((a, b) => a.account_id.localeCompare(b.account_id));
break;
case "stake":
sortedSnapshot.sort((a, b) => {
const stakeA = new BN(a.stake);
const stakeB = new BN(b.stake);
return stakeA.cmp(stakeB);
});
break;
case "active_months":
sortedSnapshot.sort((a, b) => a.active_months - b.active_months);
break;
case "vote_power":
sortedSnapshot = sortedSnapshot.map(a => ({ power: userToVotePower(a), ...a })).sort((a, b) => a.power - b.power);
break;
case "stake_power":
sortedSnapshot = sortedSnapshot.map(a => ({ power: stakePower(a.stake), ...a })).sort((a, b) => a.power - b.power);
break;
case "activity_power":
sortedSnapshot = sortedSnapshot.map(a => ({ power: activityPower(a.active_months), ...a })).sort((a, b) => a.power - b.power);
break;
default:
res.status(400).send(`Invalid sortBy parameter: ${sortBy}`);
break;
}

if (sortOrder === "desc") {
sortedSnapshot.reverse();
}

const startIndex = pageNumber * limitNumber;
const endIndex = startIndex + limitNumber;
const paginatedData = sortedSnapshot.slice(startIndex, endIndex);

// Prepare the response
const response = {
data: paginatedData,
currentPage: pageNumber + 1,
totalPages: Math.ceil(sortedSnapshot.length / limitNumber),
totalItems: sortedSnapshot.length,
};

res.json(response);
};
18 changes: 18 additions & 0 deletions snapshotter/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { app } from "./api/app.js";
import fs from 'fs';

const port = process.env.SERVER_PORT || 3000;

const configFile = process.env.SNAPSHOT_FILE || 'snapshot.json';
export let snapshot = {}

function loadSnapshot() {
const data = JSON.parse(fs.readFileSync(configFile, 'utf-8')).data;
snapshot = data;
}

app.listen(port, () => {
loadSnapshot();
console.log(`Dashboard server running on port :${port}`);
console.log(`------------------`);
});
Loading

0 comments on commit e965858

Please sign in to comment.