Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add api endpoint to generate and serve cached file #1

Merged
merged 44 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a4862e4
feat: add API endpoint to return a cached file
wa0x6e Apr 19, 2023
fff3ae7
feat: compress response
wa0x6e Apr 19, 2023
f1dedbd
feat: define precise error codes
wa0x6e Apr 19, 2023
e7174fa
chore: rename file to reflect its purpose
wa0x6e Apr 19, 2023
a757ee6
fix: fix missing new line in CSV file
wa0x6e Apr 19, 2023
ed221dc
fix: fix choices columns for votes with multiple choices
wa0x6e Apr 19, 2023
1a8150b
refactor: avoid passing `proposal` object
wa0x6e Apr 19, 2023
2c0fef4
fix: use different name for incomplete file
wa0x6e Apr 19, 2023
474b250
fix: prevent race condition on file generation
wa0x6e Apr 19, 2023
fd8a3c9
Merge branch 'main' into add-api
wa0x6e Apr 19, 2023
07fe282
feat: use AWS as cached file storage
wa0x6e Apr 20, 2023
399c8d4
feat: separate cache fetcher and generator in API endpoint
wa0x6e Apr 20, 2023
f7e8809
fix: do not return an error code on cache generation success
wa0x6e Apr 21, 2023
dbe874b
feat: add additional file storage engine
wa0x6e Apr 21, 2023
8d78400
chore: update README
wa0x6e Apr 21, 2023
a523f2a
fix: use inferred env variables for S3 client setup
wa0x6e Apr 21, 2023
57cb712
feat: custimize location of cached files
wa0x6e Apr 21, 2023
c7bd2cd
feat: protect generate endpoint behind authentication
wa0x6e Apr 21, 2023
cc31a4d
chore: add LICENCE file
wa0x6e Apr 22, 2023
d83e00a
feat: add webhook support for `generate` endpoint
wa0x6e Apr 22, 2023
8d0a8ed
feat: add basic queue system to handle cache file generation
wa0x6e Apr 22, 2023
e18cc3b
chore: update README
wa0x6e Apr 22, 2023
3eae769
chore: add tests
wa0x6e Apr 22, 2023
d0c7733
chore: fix github worflow
wa0x6e Apr 22, 2023
dee6171
fix: fix folder creation
wa0x6e Apr 22, 2023
9821586
chore: rename github workflow
wa0x6e Apr 22, 2023
fbdefa3
chore: update README
wa0x6e Apr 22, 2023
519a499
chorel; add test for weighted votes
wa0x6e Apr 22, 2023
9a36731
feat: add reason to votes report
wa0x6e Apr 22, 2023
9c91d29
chore: fix false positives in linter
wa0x6e Apr 24, 2023
3a391de
chore: dependencies upgrade
wa0x6e Apr 24, 2023
7b5404c
chore: fix CI job name
wa0x6e Apr 24, 2023
57e4400
chore: typecheck task should not also build
wa0x6e Apr 24, 2023
d2343d0
refactor: always throw Error objects instead of literal
wa0x6e Apr 25, 2023
b9d9f9a
chore: set minimum required node version
wa0x6e May 3, 2023
07cef1f
Update readme
ChaituVR May 3, 2023
4e5acda
Minor updates
ChaituVR May 3, 2023
f9e18ce
fix: add default route
wa0x6e May 4, 2023
447cb91
fix: use await syntax
wa0x6e May 4, 2023
4d026bc
chore: remove unecessary rule
wa0x6e May 4, 2023
2b0fb49
refactor: set the storage engine via an env variable
wa0x6e May 4, 2023
6d76940
refactor: keep private method to bottom of files
wa0x6e May 4, 2023
5469f06
refactor: set the StorageEngine and its related setting to environmen…
wa0x6e May 4, 2023
71864d3
chore: lint task should fix by default
wa0x6e May 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
HUB_URL=https://hub.snapshot.org
AWS_ACCESS_KEY_ID=
AWS_REGION=
AWS_SECRET_ACCESS_KEY=
AWS_BUCKET_NAME=
WEBHOOK_AUTH_TOKEN=
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Build and Run Tests
name: Run Lint
on: [push]
jobs:
build-test:
build-lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Build and Run Tests
on: [push]
jobs:
build-test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn install
- run: echo -en "HUB_URL=https://hub.snapshot.org" > .env
- run: yarn test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dist
build
.env
coverage
tmp

# Remove some common IDE working directories
.idea
Expand Down
21 changes: 21 additions & 0 deletions LICENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) Snapshot Labs

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
103 changes: 90 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,132 @@
# Snapshot/Sidekick

Sidekick is the service serving all proposals' votes CSV report
Sidekick is the service serving all proposal's votes CSV report

<hr>
-------------------

This service is exposing an API endpoint expecting a closed proposal ID, and will
return a CSV file with all the given proposal's votes.

NOTE: CSV files are generated only once, then cached, making this service a cache middleware
for snapshot-hub, for proposals' votes.
> NOTE: CSV files are generated only once, then cached, making this service a cache middleware between snapshot-hub and UI

## Project Setup

### Requirements

node >= 18.0.0

### Dependencies

Install the dependencies

```
```bash
yarn
```

_This project does not require a database, but may need larger server storage capacity for the cached files_
_This project does not require a database, but requires a [storage engine](#storage-engine)_

### Configuration

Edit the hub API url in the `.env` file if needed
Copy `.env.example`, rename it to `.env` and edit the hub API url in the `.env` file if needed

```
```bash
HUB_URL=https://hub.snapshot.org
```

## Compiles and hot-reloads for development
If you are using AWS as storage engine, set all the required `AWS_` config keys.

### Storage engine

This script is shipped with 2 storage engine:

- `AWS`: All cached files will be stored on Amazon S3 storage
- `File`: All cached files will be stored locally, in the `tmp` folder (used for dev environment and testing)

You can toggle the cache engine in `helpers/utils.ts`, when importing the storage engine

```typescript
// For File (default) storage engine
import StorageEngine from '../lib/storage/file';

// For AWS storage engine
import StorageEngine from '../lib/storage/aws';
```

### Compiles and hot-reloads for development

```bash
yarn dev
```

## Linting and typecheck
## Linting, typecheck and test

```
```bash
yarn lint
yarn typecheck
yarn test
```

## Build for production
## Usage

Retrieving and generating the cache file have their own respective endpoint

### Fetch a cache file

Send a POST request with a proposal ID

```bash
curl -X POST localhost:3000/votes/[PROPOSAL-ID]
```

When cached, this request will respond with a stream to a CSV file.

On all other cases, it will respond with a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) error response:

```bash
{
"jsonrpc":"2.0",
"error":{
"code": CODE,
"message": MESSAGE
},
"id": PROPOSAL-ID
}
```

| Description | `CODE` | `MESSAGE` |
| ----------------------------------- | ------ | ------------------- |
| When the proposal does not exist | -40001 | PROPOSAL_NOT_FOUND |
| When the proposal is not closed | -40004 | PROPOSAL_NOT_CLOSED |
| When the file is pending generation | -40010 | PENDING_GENERATION |
| Other/Unknown/Server Error | -32603 | INTERNAL_ERROR |

Furthermore, when votes report can be cached, but does not exist yet, a cache generation task will be queued. This enable cache to be generated on-demand.

### Generate a cache file

Send a POST request with a body following the [Webhook event object](https://docs.snapshot.org/tools/webhooks).

```bash
curl -X POST localhost:3000/votes/generate \
-H "Authenticate: WEBHOOK_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "proposal/[PROPOSAL-ID]", "event": "proposal/end"}'
```

- On success, will respond with a success [JSON-RPC 2.0](https://www.jsonrpc.org/specification) message
- On error, will respond with the same result and codes as the `fetch` endpoint above

The endpoint has been designed to receive events from snapshot webhook service.

Do not forget to set `WEBHOOK_AUTH_TOKEN` in the `.env` file

## Build for production

```bash
yarn build
yarn start
```

## License

[MIT](https://github.com/snapshot-labs/envelop-ui/blob/bootstrap-app/LICENSE)
[MIT](https://github.com/snapshot-labs/snapshot-sidekick/blob/main/LICENCE)
20 changes: 20 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/

export default {
clearMocks: true,
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/', '<rootDir>/test/fixtures/'],

preset: 'ts-jest',
testEnvironment: 'jest-environment-node-single-context',
setupFiles: ['dotenv/config'],
//setupFilesAfterEnv: ['<rootDir>src/setupTests.ts'],
moduleFileExtensions: ['js', 'ts']
};
26 changes: 22 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,45 @@
"build": "tsc -p tsconfig.json",
"dev": "nodemon src/index.ts -e js,ts",
"start": "node dist/src/index.js",
"typecheck": "tsc"
"typecheck": "tsc --noEmit",
"test": "jest"
},
"eslintConfig": {
"extends": "@snapshot-labs"
"extends": "@snapshot-labs",
"rules": {
"no-throw-literal": "error"
}
},
"engines": {
"node": ">= 18"
},
"prettier": "@snapshot-labs/prettier-config",
"dependencies": {
"@apollo/client": "^3.7.12",
"@aws-sdk/client-s3": "^3.316.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.18.2"
"express": "^4.18.2",
"graphql": "^16.6.0",
"winston": "^3.8.2"
},
"devDependencies": {
"@snapshot-labs/eslint-config": "^0.1.0-beta.0",
"@snapshot-labs/eslint-config": "^0.1.0-beta.9",
"@snapshot-labs/prettier-config": "^0.1.0-beta.7",
"@types/compression": "^1.7.2",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.1",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"dotenv": "^16.0.3",
"eslint": "^8.38.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"jest-environment-node-single-context": "^29.0.0",
"nodemon": "^2.0.22",
"prettier": "^2.8.8",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
Expand Down
68 changes: 68 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import express from 'express';
import { voteReportWithStorage, rpcError, rpcSuccess } from './helpers/utils';
import log from './helpers/log';
import { queues } from './lib/queue';

const router = express.Router();

router.post('/votes/generate', async (req, res) => {
log.info(`[http] POST /votes/generate`);

const body = req.body || {};
const event = body.event.toString();
const id = body.id.toString().replace('proposal/', '');

if (req.headers['authenticate'] !== process.env.WEBHOOK_AUTH_TOKEN?.toString()) {
return rpcError(res, 'UNAUTHORIZED', id);
}

if (!event || !id) {
return rpcError(res, 'Invalid Request', id);
}

if (event !== 'proposal/end') {
return rpcSuccess(res, 'Event skipped', id);
}

try {
await voteReportWithStorage(id).canBeCached();
queues.add(id);
return rpcSuccess(res, 'Cache file generation queued', id);
} catch (e) {
log.error(e);
return rpcError(res, 'INTERNAL_ERROR', id);
}
});

router.post('/votes/:id', async (req, res) => {
const { id } = req.params;
log.info(`[http] POST /votes/${id}`);

const votesReport = voteReportWithStorage(id);

try {
const file = await votesReport.cachedFile();

if (typeof file === 'string') {
res.header('Content-Type', 'text/csv');
res.attachment(votesReport.filename);
return res.send(Buffer.from(file));
}

votesReport
.canBeCached()
.then(() => {
queues.add(id);
return rpcError(res, 'PENDING_GENERATION', id);
})
.catch((e: any) => {
log.error(e);
rpcError(res, e, id);
});
} catch (e) {
log.error(e);
return rpcError(res, 'INTERNAL_ERROR', id);
}
});

export default router;
9 changes: 9 additions & 0 deletions src/helpers/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import winston from 'winston';

const log = winston.createLogger({
transports: new winston.transports.Console({
format: winston.format.combine(winston.format.colorize(), winston.format.simple())
})
});

export default log;
Loading