diff --git a/.github/workflows/deploy-analytics.yml b/.github/workflows/deploy-analytics.yml index 3812e3e..4a8f7c4 100644 --- a/.github/workflows/deploy-analytics.yml +++ b/.github/workflows/deploy-analytics.yml @@ -13,7 +13,7 @@ jobs: - uses: akhileshns/heroku-deploy@v3.12.14 with: heroku_api_key: ${{secrets.HEROKU_API_KEY}} - heroku_app_name: ${{secrets.HEROKU_APP_NAME}} + heroku_app_name: ${{secrets.HEROKU_ANALYTICS_APP_NAME}} heroku_email: ${{secrets.HEROKU_EMAIL}} usedocker: true appdir: "analytics" \ No newline at end of file diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml new file mode 100644 index 0000000..72546ad --- /dev/null +++ b/.github/workflows/deploy-api.yml @@ -0,0 +1,19 @@ +name: Deploy API to Heroku + +on: + push: + branches: + - prod + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: akhileshns/heroku-deploy@v3.12.14 + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: ${{secrets.HEROKU_API_APP_NAME}} + heroku_email: ${{secrets.HEROKU_EMAIL}} + usedocker: true + appdir: "api" \ No newline at end of file diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..4bf037c --- /dev/null +++ b/api/.env.example @@ -0,0 +1,6 @@ +# Local development +POSTGRES_USER=postgres +POSTGRES_HOST=localhost +POSTGRES_DB=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..fe815ad --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,29 @@ +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ +dist + +# env +.env +.env.production + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# misc +.DS_Store diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..e3d5618 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,18 @@ +FROM oven/bun:1 as base +WORKDIR /app + +# Install dependencies only +FROM base AS deps +COPY package.json . +RUN bun install --frozen-lockfile + +# Production image +FROM base AS runner +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENTRYPOINT [ "/app/heroku-run.sh" ] +CMD ["bun", "run", "src/index.ts"] + diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..5e44119 --- /dev/null +++ b/api/README.md @@ -0,0 +1,24 @@ +# Cowllector API + +## Development + +```bash +npm install +npm run dev +``` + +The server will start at http://localhost:3000 + +## API Documentation + +OpenAPI documentation is available at: http://localhost:3000/api/docs +Swagger UI is available at: http://localhost:3000/swagger + +## Test URLs for Last Harvest Reports Endpoint + +``` +# Get all last harvest reports +http://localhost:3000/api/v1/last-harvest-reports +``` + +You can test this URL in your browser or using tools like curl or Postman. The API will return a JSON response with an array containing the last harvest report for each vault. diff --git a/api/biome.json b/api/biome.json new file mode 100644 index 0000000..60fc624 --- /dev/null +++ b/api/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/api/heroku-run.sh b/api/heroku-run.sh new file mode 100755 index 0000000..8a6a355 --- /dev/null +++ b/api/heroku-run.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +# use a pointer to the environment variable because it may change name +DB_URL=${!COWLLECTOR_DB_URL_ENV_VAR} + +[[ $DB_URL =~ ^postgres:\/\/([^:]+):([^@]+)@([^:]+):([^\/]+)\/(.+)$ ]] +export PG_USERNAME="${BASH_REMATCH[1]}" +export PG_PASSWORD="${BASH_REMATCH[2]}" +export PG_HOSTNAME="${BASH_REMATCH[3]}" +export PG_PORT="${BASH_REMATCH[4]}" +export PG_DATABASE="${BASH_REMATCH[5]}" + +if [[ -z "$PG_USERNAME" || -z "$PG_PASSWORD" || -z "$PG_HOSTNAME" || -z "$PG_PORT" || -z "$PG_DATABASE" ]]; then + echo "Invalid DATABASE_URL: $DB_URL" + echo "Found looking at the content of '$COWLLECTOR_DB_URL_ENV_VAR'" + echo "Expected format: postgres://username:password@hostname:port/database" + exit 1 +fi + +exec "$@" diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..d88733c --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,670 @@ +{ + "name": "cowllector-api", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cowllector-api", + "dependencies": { + "@hono/swagger-ui": "^0.5.0", + "@hono/zod-openapi": "^0.9.8", + "hono": "^4.6.12", + "hono-rate-limiter": "^0.4.2", + "pg": "^8.11.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/pg": "^8.11.0", + "bun": "^1.1.38", + "bun-types": "latest" + } + }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-5.5.0.tgz", + "integrity": "sha512-d5HwrvM6dOKr3XdeF+DmashGvfEc+1oiEfbscugsiwSTrFtuMa7ETpW9sTNnVgn+hJaz+PRxPQUYD7q9/5dUig==", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^3.20.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@hono/swagger-ui": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@hono/swagger-ui/-/swagger-ui-0.5.0.tgz", + "integrity": "sha512-MWYYSv9kC8IwFBLZdwgZZMT9zUq2C/4/ekuyEYOkHEgUMqu+FG3eebtBZ4ofMh60xYRxRR2BgQGoNIILys/PFg==", + "peerDependencies": { + "hono": "*" + } + }, + "node_modules/@hono/zod-openapi": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-0.9.10.tgz", + "integrity": "sha512-v/b/z0qPxDo952gjRyhJ0n9ifbPoIluR2KmXDL20np0hj99+XvakoIHK5/T/3+hUmXlTj1Kn3TiGsSV6hwZesg==", + "dependencies": { + "@asteasolutions/zod-to-openapi": "^5.5.0", + "@hono/zod-validator": "0.2.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "hono": ">=3.11.3", + "zod": "3.*" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.2.1.tgz", + "integrity": "sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==", + "peerDependencies": { + "hono": ">=3.9.0", + "zod": "^3.19.1" + } + }, + "node_modules/@oven/bun-darwin-aarch64": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.1.38.tgz", + "integrity": "sha512-6r+PgOE1s56h16wHs4Tg32ZOB9JQEgLi3V+FyIag/lIKS5FV9rUjfSZSwwI8UGfNqj7RrD5cB+1PT3IFpV6gmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.1.38.tgz", + "integrity": "sha512-eda41VCgQcYkrvRnob1xep8zlOm0Io3q1uiBGMaNL8aSrhpYaz3NhMH1NVlZEFahfIHhCfkin/gSLhJK0qK1fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64-baseline": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.1.38.tgz", + "integrity": "sha512-hqaAsJGdGXiwwN6Y7dvYWjYwgAB8r3fXFIecjmxeijbOIw8zfru+zKFCBQtHa5AglAUAw1fOSOsWGlu8rtGp7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-linux-aarch64": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.1.38.tgz", + "integrity": "sha512-YIyJ2cBEgvQAYUh1udxe6yximei2QUh6gpdGWmhHiWWRX0VhVxPpZ2E8n6NIlpM2TBy4h/hOndoImiD/XnSq5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.1.38.tgz", + "integrity": "sha512-foVXWa2/zRPMudxVpr+/COmcF1F849g4JJHTDDzpxIp30Xp7422nSk/c0NESveklrqhCvINq4CNcKnBh3WPFAw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-baseline": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.1.38.tgz", + "integrity": "sha512-7Sv4RHpWBVjmkGjER90e99bYYkPiiNPGVP02CTBo49JwHfogVl8md8oWKr9A6K3ZZ05HS5atOg7wrKolkbR0bA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-windows-x64": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.1.38.tgz", + "integrity": "sha512-bMo3o7lyfC8HlyaunUXBFZVbVrYCQHHQRPXsCtgtBKzKbe/r51piwtMl4wpcvd5VZUhBDXMPrm7/OR89XXteyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oven/bun-windows-x64-baseline": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.1.38.tgz", + "integrity": "sha512-iwvzUC59J/aMwEsCkKyPLVc2oNep2OhWL6VRp2d9Sx0g9hycBgxOfBfAhii0bDOBI/aQAVevcTRoQJ1V79PT9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/node": { + "version": "20.17.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", + "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/pg": { + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/bun": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.1.38.tgz", + "integrity": "sha512-cr+UDFiEasyw0kKEbbD7kDewrI2vTo17JssVVjzBv/eNskeL2wikJ+4RNgUfoRqgthCjDZux7r6ELGgIGq6aWw==", + "cpu": [ + "arm64", + "x64" + ], + "dev": true, + "hasInstallScript": true, + "os": [ + "darwin", + "linux", + "win32" + ], + "bin": { + "bun": "bin/bun.exe", + "bunx": "bin/bun.exe" + }, + "optionalDependencies": { + "@oven/bun-darwin-aarch64": "1.1.38", + "@oven/bun-darwin-x64": "1.1.38", + "@oven/bun-darwin-x64-baseline": "1.1.38", + "@oven/bun-linux-aarch64": "1.1.38", + "@oven/bun-linux-x64": "1.1.38", + "@oven/bun-linux-x64-baseline": "1.1.38", + "@oven/bun-windows-x64": "1.1.38", + "@oven/bun-windows-x64-baseline": "1.1.38" + } + }, + "node_modules/bun-types": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.38.tgz", + "integrity": "sha512-iglB2t9z1Hc6DIuwwscwWj/csx22QlCZ96QbcqQfiy1wmuZ38srQLI/fDVkFHAo2+KL7aJZGVWF+nAWrR6Njig==", + "dev": true, + "dependencies": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10" + } + }, + "node_modules/bun-types/node_modules/@types/node": { + "version": "20.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz", + "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/bun-types/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/hono": { + "version": "4.6.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.12.tgz", + "integrity": "sha512-eHtf4kSDNw6VVrdbd5IQi16r22m3s7mWPLd7xOMhg1a/Yyb1A0qpUFq8xYMX4FMuDe1nTKeMX5rTx7Nmw+a+Ag==", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hono-rate-limiter": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/hono-rate-limiter/-/hono-rate-limiter-0.4.2.tgz", + "integrity": "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==", + "peerDependencies": { + "hono": "^4.1.1" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "dependencies": { + "yaml": "^2.5.0" + } + }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + }, + "node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pg/node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..d93f953 --- /dev/null +++ b/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "cowllector-api", + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "dev": "bun --watch src/index.ts", + "format": "biome check --write --unsafe ./src" + }, + "dependencies": { + "@hono/swagger-ui": "^0.5.0", + "@hono/zod-openapi": "^0.9.8", + "hono": "^4.6.12", + "hono-rate-limiter": "^0.4.2", + "pg": "^8.11.3", + "zod": "^3.22.4" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/pg": "^8.11.0", + "bun": "^1.1.38", + "bun-types": "latest" + } +} diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000..f48114c --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,50 @@ +import { swaggerUI } from '@hono/swagger-ui'; +import { OpenAPIHono } from '@hono/zod-openapi'; +import type { Context, Next } from 'hono'; +import { rateLimiter } from 'hono-rate-limiter'; +import { db } from './lib/db.js'; +import { lastHarvestReportApi } from './routes/last-harvest-reports'; +import type { Variables } from './types'; + +// Initialize OpenAPIHono with the custom context type +const app = new OpenAPIHono<{ Variables: Variables }>(); + +// Add rate limiting middleware +app.use( + '*', + rateLimiter({ + windowMs: 15 * 60 * 1000, // 15 minutes + limit: 100, // Limit each IP to 100 requests per window + standardHeaders: 'draft-6', // RateLimit-* headers + keyGenerator: c => c.req.header('x-forwarded-for') || 'unknown', // Use IP address as key + }) +); + +// Add the pool to the Hono context +app.use('*', async (c: Context<{ Variables: Variables }>, next: Next) => { + c.set('db', db); + await next(); +}); + +// Add the routes +app.route('/api/v1', lastHarvestReportApi); + +// Add OpenAPI documentation +app.doc('/api/docs', { + openapi: '3.0.0', + info: { + title: 'Harvest Reports API', + version: '1.0.0', + }, +}); + +// Add Swagger UI +app.get('/swagger', swaggerUI({ url: '/api/docs' })); + +const port = process.env.PORT || 3000; +console.log(`Server is running on http://localhost:${port}`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/api/src/lib/db.ts b/api/src/lib/db.ts new file mode 100644 index 0000000..20cbb33 --- /dev/null +++ b/api/src/lib/db.ts @@ -0,0 +1,42 @@ +import { Pool } from 'pg'; + +// Define database types +export type DB = Pool; + +// Parse database URL for Heroku or use local config +const getDatabaseConfig = () => { + const connectionString = process.env.DATABASE_URL; + if (connectionString) { + return { + connectionString, + ssl: { + rejectUnauthorized: false, // Required for Heroku + }, + }; + } + const { POSTGRES_USER, POSTGRES_HOST, POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_PORT } = process.env; + + if (!POSTGRES_USER || !POSTGRES_HOST || !POSTGRES_DB || !POSTGRES_PASSWORD || !POSTGRES_PORT) { + throw new Error('Missing required database configuration'); + } + + return { + user: POSTGRES_USER, + host: POSTGRES_HOST, + database: POSTGRES_DB, + password: POSTGRES_PASSWORD, + port: Number.parseInt(POSTGRES_PORT, 10), + ssl: { + rejectUnauthorized: false, // Required for Heroku + }, + }; +}; + +// Create and export the database pool +export const db = new Pool(getDatabaseConfig()); + +// Handle shutdown gracefully +process.on('SIGTERM', async () => { + console.log('Shutting down database connection...'); + await db.end(); +}); diff --git a/api/src/routes/last-harvest-reports.ts b/api/src/routes/last-harvest-reports.ts new file mode 100644 index 0000000..9c37803 --- /dev/null +++ b/api/src/routes/last-harvest-reports.ts @@ -0,0 +1,133 @@ +import { OpenAPIHono, z } from '@hono/zod-openapi'; +import { createRoute } from '@hono/zod-openapi'; +import type { Variables } from '../types'; + +// Define the schema for a single harvest report +const LastHarvestReportSchema = z + .object({ + raw_report_id: z.number(), + chain: z.string(), + datetime: z.string(), + run_ok: z.boolean(), + vault_id: z.string(), + vault_is_clm_manager: z.boolean(), + vault_is_clm_vault: z.boolean(), + simulation_started: z.boolean(), + simulation_ok: z.boolean(), + simulation_ko_reason: z.string().nullable(), + simulation_last_harvest: z.string().nullable(), + simulation_hours_since_last_harvest: z.number().nullable(), + simulation_is_last_harvest_recent: z.boolean().nullable(), + simulation_paused: z.boolean().nullable(), + simulation_block_number: z.number().nullable(), + simulation_harvest_result_data: z.any().nullable(), + simulation_gas_raw_gas_price: z.number().nullable(), + simulation_gas_raw_gas_amount_estimation: z.number().nullable(), + simulation_gas_estimated_call_rewards_wei: z.number().nullable(), + simulation_gas_gas_price_multiplier: z.number().nullable(), + simulation_gas_gas_price: z.number().nullable(), + simulation_gas_transaction_cost_estimation_wei: z.number().nullable(), + simulation_gas_estimated_gain_wei: z.number().nullable(), + simulation_gas_would_be_profitable: z.boolean().nullable(), + decision_started: z.boolean(), + decision_ok: z.boolean(), + decision_ko_reason: z.string().nullable(), + decision_should_harvest: z.boolean().nullable(), + decision_level: z.string().nullable(), + decision_not_harvesting_reason: z.string().nullable(), + decision_might_need_eol: z.boolean(), + decision_harvest_return_data: z.string().nullable(), + decision_harvest_return_data_decoded: z.string().nullable(), + transaction_started: z.boolean(), + transaction_ok: z.boolean(), + transaction_ko_reason: z.string().nullable(), + transaction_hash: z.string().nullable(), + transaction_block_number: z.number().nullable(), + transaction_gas_used: z.number().nullable(), + transaction_effective_gas_price: z.number().nullable(), + transaction_cost_wei: z.number().nullable(), + transaction_balance_before_wei: z.number().nullable(), + transaction_estimated_profit_wei: z.number().nullable(), + summary_harvested: z.boolean().nullable(), + summary_skipped: z.boolean().nullable(), + summary_status: z.string().nullable(), + }) + .openapi('LastHarvestReport'); + +// Response schema +const ResponseSchema = z + .object({ + data: z.array(LastHarvestReportSchema), + }) + .openapi('LastHarvestReportsResponse'); + +export const lastHarvestReportApi = new OpenAPIHono<{ Variables: Variables }>(); + +// Export types for use in the handler +export type LastHarvestReportResponse = z.infer; + +// Route definition +const lastHarvestReportsRoute = createRoute({ + method: 'get', + path: '/last-harvest-reports', + responses: { + 200: { + content: { + 'application/json': { + schema: ResponseSchema, + }, + }, + description: 'List of last harvest reports for each vault', + }, + 500: { + content: { + 'application/json': { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: 'Server error', + }, + }, +}); + +lastHarvestReportApi.openapi(lastHarvestReportsRoute, async c => { + const query = ` + SELECT + raw_report_id, chain, datetime, run_ok, vault_id, + vault_is_clm_manager, vault_is_clm_vault, + simulation_started, simulation_ok, simulation_ko_reason, + simulation_last_harvest, simulation_hours_since_last_harvest, + simulation_is_last_harvest_recent, simulation_paused, + simulation_block_number, simulation_harvest_result_data, + simulation_gas_raw_gas_price, simulation_gas_raw_gas_amount_estimation, + simulation_gas_estimated_call_rewards_wei, simulation_gas_gas_price_multiplier, + simulation_gas_gas_price, simulation_gas_transaction_cost_estimation_wei, + simulation_gas_estimated_gain_wei, simulation_gas_would_be_profitable, + decision_started, decision_ok, decision_ko_reason, + decision_should_harvest, decision_level, decision_not_harvesting_reason, + decision_might_need_eol, decision_harvest_return_data, + decision_harvest_return_data_decoded, + transaction_started, transaction_ok, transaction_ko_reason, + transaction_hash, transaction_block_number, transaction_gas_used, + transaction_effective_gas_price, transaction_cost_wei, + transaction_balance_before_wei, transaction_estimated_profit_wei, + summary_harvested, summary_skipped, summary_status + FROM harvest_report_last_vault_details + ORDER BY datetime DESC + `; + + try { + const result = await c.get('db').query(query); + + const response: LastHarvestReportResponse = { + data: result.rows, + }; + + return c.json(response); + } catch (error) { + console.error('Database query failed:', error); + return c.json({ error: 'Database query failed' }, 500); + } +}); diff --git a/api/src/types.ts b/api/src/types.ts new file mode 100644 index 0000000..a04ad62 --- /dev/null +++ b/api/src/types.ts @@ -0,0 +1,5 @@ +import type { DB } from './lib/db'; + +export type Variables = { + db: DB; +}; diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..d84d293 --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["bun-types"], + "strict": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts index 5730951..4c85c68 100644 --- a/src/lib/db/migrate.ts +++ b/src/lib/db/migrate.ts @@ -552,12 +552,163 @@ export async function db_migrate() { drop view if exists harvest_report_last_vault_details cascade; CREATE OR REPLACE VIEW harvest_report_last_vault_details AS ( - with latest_report as ( - select - *, row_number() over (partition by vault_id order by datetime desc) as row_number - from harvest_report_vault_details - ) - select * from latest_report where row_number = 1 + with last_day_reports_by_chain as ( + select + *, row_number() over (partition by r.report_content->'chain' order by datetime desc) as row_number + from raw_harvest_report r + where r.datetime < now() - interval '1 day' + ), + latest_report_by_chain as ( + select r.* + from last_day_reports_by_chain r + where r.row_number = 1 + ), + vault_report_jsonb as ( + SELECT + r.raw_report_id, + r.chain, + r.datetime, + async_field_ok(d."fetchGasPrice") and async_field_ok(d."collectorBalanceBefore") and async_field_ok(d."collectorBalanceAfter") as run_ok, + async_field_ok(d."fetchGasPrice") as fetch_gas_price_ok, + async_field_ok(d."collectorBalanceBefore") as balance_before_ok, + async_field_ok(d."collectorBalanceAfter") as balance_after_ok, + jsonb_path_query(r.report_content, '$.details[*]') as vault_report + FROM latest_report_by_chain r, + jsonb_to_record(r.report_content) as d( + "timing" jsonb, + "fetchGasPrice" jsonb, + "collectorBalanceBefore" jsonb, + "collectorBalanceAfter" jsonb, + "summary" jsonb + ) + ) + select + r.raw_report_id, + r.chain, + r.datetime, + r.run_ok, + r.fetch_gas_price_ok, + r.balance_before_ok, + r.balance_after_ok, + d.vault->>'id' as vault_id, + ((d.vault->>'isClmManager') = 'true') as vault_is_clm_manager, + ((d.vault->>'isClmVault') = 'true') as vault_is_clm_vault, + d.simulation is not null as simulation_started, + async_field_ok(d.simulation) as simulation_ok, + d.simulation->'reason' as simulation_ko_reason, + sim_ok."lastHarvest" as simulation_last_harvest, + sim_ok."hoursSinceLastHarvest" as simulation_hours_since_last_harvest, + sim_ok."isLastHarvestRecent" as simulation_is_last_harvest_recent, + sim_ok."paused" as simulation_paused, + sim_ok."blockNumber" as simulation_block_number, + sim_ok."harvestResultData" as simulation_harvest_result_data, + gas."rawGasPrice" as simulation_gas_raw_gas_price, + gas."rawGasAmountEstimation" as simulation_gas_raw_gas_amount_estimation, + gas."estimatedCallRewardsWei" as simulation_gas_estimated_call_rewards_wei, + gas."gasPriceMultiplier" as simulation_gas_gas_price_multiplier, + gas."gasPrice" as simulation_gas_gas_price, + gas."transactionCostEstimationWei" as simulation_gas_transaction_cost_estimation_wei, + gas."estimatedGainWei" as simulation_gas_estimated_gain_wei, + gas."wouldBeProfitable" as simulation_gas_would_be_profitable, + d.decision is not null as decision_started, + async_field_ok(d.decision) as decision_ok, + d.decision->'reason' as decision_ko_reason, + dec_ok."shouldHarvest" as decision_should_harvest, + dec_ok."level" as decision_level, + dec_ok."notHarvestingReason" as decision_not_harvesting_reason, + (dec_ok."mightNeedEOL" is not null and dec_ok."mightNeedEOL") as decision_might_need_eol, + hexstr_to_bytea(dec_ok."harvestReturnData") as decision_harvest_return_data, + case + -- abi.encodeWithSignature('Error(string)', 'SOME TEXT') + when substr(hexstr_to_bytea(dec_ok."harvestReturnData"), 0, 5) = '\\x08c379a0' then + replace( + encode( + hexstr_to_bytea( + '0x' || substring( + dec_ok."harvestReturnData", + 1 /*?*/ + 2 /*"0x"*/ + (4 /*selector*/ + 32 /*offset to str*/ + 32 /*string len*/) * 2 /* bytes are 2 char long*/ + ) + ), + 'escape' + ), + '\\000', + '' + ) + else dec_ok."harvestReturnData" + end as decision_harvest_return_data_decoded, + d.transaction is not null as transaction_started, + async_field_ok(d.transaction) as transaction_ok, + d.transaction->'reason' as transaction_ko_reason, + hexstr_to_bytea(tx."transactionHash") as transaction_hash, + tx."blockNumber" as transaction_block_number, + tx."gasUsed" as transaction_gas_used, + tx."effectiveGasPrice" as transaction_effective_gas_price, + tx."transactionCostWei" as transaction_cost_wei, + tx."balanceBeforeWei" as transaction_balance_before_wei, + tx."estimatedProfitWei" as transaction_estimated_profit_wei, + summary.harvested as summary_harvested, + summary.skipped as summary_skipped, + summary.status as summary_status, + summary."discordMessage" as summary_discord_message, + summary."discordVaultLink" as summary_discord_vault_link, + summary."discordStrategyLink" as summary_discord_strategy_link, + summary."discordTransactionLink" as summary_discord_transaction_link, + r.vault_report + FROM + vault_report_jsonb r, + jsonb_to_record(r.vault_report) as d( + vault jsonb, + simulation jsonb, + decision jsonb, + transaction jsonb, + summary jsonb + ), + jsonb_to_record(d.simulation->'value') as sim_ok( + "estimatedCallRewardsWei" numeric, + "gas" jsonb, + "harvestWillSucceed" boolean, + "lastHarvest" timestamp with time zone, + "hoursSinceLastHarvest" double precision, + "isLastHarvestRecent" boolean, + "paused" boolean, + "blockNumber" numeric, + "harvestResultData" jsonb + ), + jsonb_to_record(sim_ok.gas) as gas( + "rawGasPrice" numeric, + "rawGasAmountEstimation" numeric, + "estimatedCallRewardsWei" numeric, + "gasPriceMultiplier" double precision, + "gasPrice" numeric, + "transactionCostEstimationWei" numeric, + "estimatedGainWei" numeric, + "wouldBeProfitable" boolean + ), + jsonb_to_record(d.decision->'value') as dec_ok( + "shouldHarvest" boolean, + "level" character varying, + "notHarvestingReason" character varying, + "mightNeedEOL" boolean, + "harvestReturnData" character varying + ), + jsonb_to_record(d.transaction->'value') as tx( + "transactionHash" character varying, + "blockNumber" numeric, + "gasUsed" numeric, + "effectiveGasPrice" numeric, + "transactionCostWei" numeric, + "balanceBeforeWei" numeric, + "estimatedProfitWei" numeric + ), + jsonb_to_record(d.summary) as summary( + harvested boolean, + skipped boolean, + status character varying, + "discordMessage" character varying, + "discordVaultLink" character varying, + "discordStrategyLink" character varying, + "discordTransactionLink" character varying + ) ); `); diff --git a/tsconfig.json b/tsconfig.json index 8804498..3c38562 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2022", - "module": "commonjs", + "module": "CommonJS", "strict": true, "esModuleInterop": true, "outDir": "dist",