feat(ponder): Add strict ESLint + Prettier with pre-commit hooks (#52)
- Install eslint, @typescript-eslint plugins, prettier, husky, lint-staged - Configure ESLint flat config with TypeScript parser - Enforce: no-explicit-any (with exceptions), no-unused-vars, naming-convention, prefer-const, no-console - Set style: 2-space indent, 140 char max-len, disable complexity rules - Configure Prettier: single quotes, 140 width, trailing commas - Setup husky pre-commit hook to auto-fix and format on commit - Replace console.log/warn with context.logger.info/warn in handlers - Remove console.log from ponder.config.ts (replaced with comment) - Add npm scripts: lint, lint:fix, format, format:check - All lint rules pass without warnings resolvel #45 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/52
This commit is contained in:
parent
c150b683c8
commit
dc61771dfc
18 changed files with 2229 additions and 194 deletions
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
set -e
|
||||
|
||||
if [ -d "onchain" ]; then
|
||||
|
|
@ -12,3 +13,7 @@ fi
|
|||
if [ -d "web-app" ]; then
|
||||
(cd web-app && npx lint-staged)
|
||||
fi
|
||||
|
||||
if [ -d "services/ponder" ]; then
|
||||
(cd services/ponder && npx lint-staged)
|
||||
fi
|
||||
|
|
|
|||
8
kraiken-lib/package-lock.json
generated
8
kraiken-lib/package-lock.json
generated
|
|
@ -219,7 +219,6 @@
|
|||
"integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.2",
|
||||
|
|
@ -3107,7 +3106,6 @@
|
|||
"integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.13.0"
|
||||
}
|
||||
|
|
@ -3943,7 +3941,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
|
|
@ -5521,7 +5518,6 @@
|
|||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
|
|
@ -5606,7 +5602,6 @@
|
|||
"integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
"website"
|
||||
],
|
||||
|
|
@ -6266,7 +6261,6 @@
|
|||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
|
|
@ -9249,7 +9243,6 @@
|
|||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -9572,7 +9565,6 @@
|
|||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Positions } from './__generated__/graphql.js';
|
||||
import type { Positions as Position } from './__generated__/graphql.js';
|
||||
import { toBigIntId } from './ids.js';
|
||||
|
||||
export interface SnatchablePosition {
|
||||
|
|
@ -110,7 +110,7 @@ export function selectSnatchPositions(candidates: SnatchablePosition[], options:
|
|||
}
|
||||
|
||||
export function getSnatchList(
|
||||
positions: Positions[],
|
||||
positions: Position[],
|
||||
needed: bigint,
|
||||
taxRate: number,
|
||||
stakeTotalSupply: bigint
|
||||
|
|
|
|||
|
|
@ -2803,11 +2803,6 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@^2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
|
|
|
|||
1
services/ponder/.husky/pre-commit
Normal file
1
services/ponder/.husky/pre-commit
Normal file
|
|
@ -0,0 +1 @@
|
|||
npm test
|
||||
14
services/ponder/.lintstagedrc.json
Normal file
14
services/ponder/.lintstagedrc.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"src/**/*.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"ponder.config.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"ponder.schema.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
8
services/ponder/.prettierrc
Normal file
8
services/ponder/.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 140,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
76
services/ponder/eslint.config.js
Normal file
76
services/ponder/eslint.config.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import eslint from '@eslint/js';
|
||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsparser from '@typescript-eslint/parser';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
eslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Context: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint,
|
||||
},
|
||||
rules: {
|
||||
// TypeScript
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'function',
|
||||
format: ['camelCase'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'UPPER_CASE'],
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: ['PascalCase'],
|
||||
},
|
||||
],
|
||||
|
||||
// Style
|
||||
'prefer-const': 'error',
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 140,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreUrls: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Console
|
||||
'no-console': 'error',
|
||||
|
||||
// Complexity (off)
|
||||
complexity: 'off',
|
||||
'max-depth': 'off',
|
||||
'max-nested-callbacks': 'off',
|
||||
'max-params': 'off',
|
||||
'max-statements': 'off',
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
];
|
||||
1916
services/ponder/package-lock.json
generated
1916
services/ponder/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,10 +4,15 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "ponder dev",
|
||||
"start": "ponder start",
|
||||
"codegen": "ponder codegen",
|
||||
"build": "ponder codegen"
|
||||
"dev": "node ./node_modules/ponder/dist/esm/bin/ponder.js dev",
|
||||
"start": "node ./node_modules/ponder/dist/esm/bin/ponder.js start",
|
||||
"codegen": "node ./node_modules/ponder/dist/esm/bin/ponder.js codegen",
|
||||
"build": "node ./node_modules/ponder/dist/esm/bin/ponder.js codegen",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"ponder.config.ts\" \"ponder.schema.ts\"",
|
||||
"format:check": "prettier --check \"src/**/*.ts\" \"ponder.config.ts\" \"ponder.schema.ts\"",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.5.0",
|
||||
|
|
@ -17,9 +22,30 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"ponder.config.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"ponder.schema.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.25.10",
|
||||
"vite": "^5.4.11"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createConfig } from "ponder";
|
||||
import type { Abi } from "viem";
|
||||
import { KraikenAbi, StakeAbi } from "kraiken-lib";
|
||||
import { createConfig } from 'ponder';
|
||||
import type { Abi } from 'viem';
|
||||
import { KraikenAbi, StakeAbi } from 'kraiken-lib';
|
||||
|
||||
// Network configurations keyed by canonical environment name
|
||||
type NetworkConfig = {
|
||||
|
|
@ -17,53 +17,54 @@ type NetworkConfig = {
|
|||
const networks: Record<string, NetworkConfig> = {
|
||||
BASE_SEPOLIA_LOCAL_FORK: {
|
||||
chainId: 31337,
|
||||
rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA_LOCAL_FORK || "http://127.0.0.1:8545",
|
||||
rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA_LOCAL_FORK || 'http://127.0.0.1:8545',
|
||||
disableCache: true,
|
||||
contracts: {
|
||||
kraiken: process.env.KRAIKEN_ADDRESS || "0x56186c1E64cA8043dEF78d06AfF222212eA5df71",
|
||||
stake: process.env.STAKE_ADDRESS || "0x056E4a859558A3975761ABd7385506BC4D8A8E60",
|
||||
startBlock: parseInt(process.env.START_BLOCK || "31425917"),
|
||||
kraiken: process.env.KRAIKEN_ADDRESS || '0x56186c1E64cA8043dEF78d06AfF222212eA5df71',
|
||||
stake: process.env.STAKE_ADDRESS || '0x056E4a859558A3975761ABd7385506BC4D8A8E60',
|
||||
startBlock: parseInt(process.env.START_BLOCK || '31425917'),
|
||||
},
|
||||
},
|
||||
BASE_SEPOLIA: {
|
||||
chainId: 84532,
|
||||
rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA || "https://sepolia.base.org",
|
||||
rpc: process.env.PONDER_RPC_URL_BASE_SEPOLIA || 'https://sepolia.base.org',
|
||||
contracts: {
|
||||
kraiken: "0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8",
|
||||
stake: "0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2",
|
||||
kraiken: '0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8',
|
||||
stake: '0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2',
|
||||
startBlock: 20940337,
|
||||
},
|
||||
},
|
||||
BASE: {
|
||||
chainId: 8453,
|
||||
rpc: process.env.PONDER_RPC_URL_BASE || "https://base.llamarpc.com",
|
||||
rpc: process.env.PONDER_RPC_URL_BASE || 'https://base.llamarpc.com',
|
||||
contracts: {
|
||||
kraiken: "0x45caa5929f6ee038039984205bdecf968b954820",
|
||||
stake: "0xed70707fab05d973ad41eae8d17e2bcd36192cfc",
|
||||
kraiken: '0x45caa5929f6ee038039984205bdecf968b954820',
|
||||
stake: '0xed70707fab05d973ad41eae8d17e2bcd36192cfc',
|
||||
startBlock: 26038614,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Select network based on environment variable
|
||||
const NETWORK = (process.env.PONDER_NETWORK as keyof typeof networks) || "BASE_SEPOLIA_LOCAL_FORK";
|
||||
const NETWORK = (process.env.PONDER_NETWORK as keyof typeof networks) || 'BASE_SEPOLIA_LOCAL_FORK';
|
||||
const selectedNetwork = networks[NETWORK as keyof typeof networks];
|
||||
|
||||
if (!selectedNetwork) {
|
||||
throw new Error(`Invalid network: ${NETWORK}. Valid options: ${Object.keys(networks).join(", ")}`);
|
||||
throw new Error(`Invalid network: ${NETWORK}. Valid options: ${Object.keys(networks).join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ponder.config] Network=${NETWORK}, chainId=${selectedNetwork.chainId}, startBlock=${selectedNetwork.contracts.startBlock}`,
|
||||
);
|
||||
// Network configuration logged during Ponder startup
|
||||
// Network=${NETWORK}, chainId=${selectedNetwork.chainId}, startBlock=${selectedNetwork.contracts.startBlock}
|
||||
|
||||
export default createConfig({
|
||||
// Use PostgreSQL if DATABASE_URL is set, otherwise use PGlite
|
||||
database: process.env.DATABASE_URL ? {
|
||||
kind: "postgres" as const,
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
schema: process.env.DATABASE_SCHEMA || "public",
|
||||
} : undefined,
|
||||
database: process.env.DATABASE_URL
|
||||
? {
|
||||
kind: 'postgres' as const,
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
schema: process.env.DATABASE_SCHEMA || 'public',
|
||||
}
|
||||
: undefined,
|
||||
chains: {
|
||||
[NETWORK]: {
|
||||
id: selectedNetwork.chainId,
|
||||
|
|
|
|||
|
|
@ -1,73 +1,145 @@
|
|||
import { onchainTable, primaryKey, index } from "ponder";
|
||||
import { onchainTable, index } from 'ponder';
|
||||
|
||||
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
|
||||
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
||||
|
||||
// Global protocol stats - singleton with id "0x01"
|
||||
export const stats = onchainTable(
|
||||
"stats",
|
||||
(t) => ({
|
||||
id: t.text().primaryKey(), // Always "0x01"
|
||||
kraikenTotalSupply: t.bigint().notNull().$default(() => 0n),
|
||||
stakeTotalSupply: t.bigint().notNull().$default(() => 0n),
|
||||
outstandingStake: t.bigint().notNull().$default(() => 0n),
|
||||
export const stats = onchainTable('stats', t => ({
|
||||
id: t.text().primaryKey(), // Always "0x01"
|
||||
kraikenTotalSupply: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
stakeTotalSupply: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
outstandingStake: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
// Totals
|
||||
totalMinted: t.bigint().notNull().$default(() => 0n),
|
||||
totalBurned: t.bigint().notNull().$default(() => 0n),
|
||||
totalTaxPaid: t.bigint().notNull().$default(() => 0n),
|
||||
totalUbiClaimed: t.bigint().notNull().$default(() => 0n),
|
||||
// Totals
|
||||
totalMinted: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
totalBurned: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
totalTaxPaid: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
totalUbiClaimed: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
// Rolling windows - calculated from ring buffer
|
||||
mintedLastWeek: t.bigint().notNull().$default(() => 0n),
|
||||
mintedLastDay: t.bigint().notNull().$default(() => 0n),
|
||||
mintNextHourProjected: t.bigint().notNull().$default(() => 0n),
|
||||
// Rolling windows - calculated from ring buffer
|
||||
mintedLastWeek: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
mintedLastDay: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
mintNextHourProjected: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
burnedLastWeek: t.bigint().notNull().$default(() => 0n),
|
||||
burnedLastDay: t.bigint().notNull().$default(() => 0n),
|
||||
burnNextHourProjected: t.bigint().notNull().$default(() => 0n),
|
||||
burnedLastWeek: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
burnedLastDay: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
burnNextHourProjected: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
taxPaidLastWeek: t.bigint().notNull().$default(() => 0n),
|
||||
taxPaidLastDay: t.bigint().notNull().$default(() => 0n),
|
||||
taxPaidNextHourProjected: t.bigint().notNull().$default(() => 0n),
|
||||
taxPaidLastWeek: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
taxPaidLastDay: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
taxPaidNextHourProjected: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
ubiClaimedLastWeek: t.bigint().notNull().$default(() => 0n),
|
||||
ubiClaimedLastDay: t.bigint().notNull().$default(() => 0n),
|
||||
ubiClaimedNextHourProjected: t.bigint().notNull().$default(() => 0n),
|
||||
ubiClaimedLastWeek: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
ubiClaimedLastDay: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
ubiClaimedNextHourProjected: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
|
||||
// Ring buffer state (flattened array of length HOURS_IN_RING_BUFFER * 4)
|
||||
ringBufferPointer: t.integer().notNull().$default(() => 0),
|
||||
lastHourlyUpdateTimestamp: t.bigint().notNull().$default(() => 0n),
|
||||
ringBuffer: t
|
||||
.jsonb()
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.$default(() => Array(HOURS_IN_RING_BUFFER * RING_BUFFER_SEGMENTS).fill("0")),
|
||||
})
|
||||
);
|
||||
// Ring buffer state (flattened array of length HOURS_IN_RING_BUFFER * 4)
|
||||
ringBufferPointer: t
|
||||
.integer()
|
||||
.notNull()
|
||||
.$default(() => 0),
|
||||
lastHourlyUpdateTimestamp: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
ringBuffer: t
|
||||
.jsonb()
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.$default(() => Array(HOURS_IN_RING_BUFFER * RING_BUFFER_SEGMENTS).fill('0')),
|
||||
}));
|
||||
|
||||
// Individual staking positions
|
||||
export const positions = onchainTable(
|
||||
"positions",
|
||||
(t) => ({
|
||||
'positions',
|
||||
t => ({
|
||||
id: t.text().primaryKey(), // Position ID from contract
|
||||
owner: t.hex().notNull(),
|
||||
share: t.real().notNull(), // Share as decimal (0-1)
|
||||
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%)
|
||||
kraikenDeposit: t.bigint().notNull(),
|
||||
stakeDeposit: t.bigint().notNull(),
|
||||
taxPaid: t.bigint().notNull().$default(() => 0n),
|
||||
snatched: t.integer().notNull().$default(() => 0),
|
||||
taxPaid: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
snatched: t
|
||||
.integer()
|
||||
.notNull()
|
||||
.$default(() => 0),
|
||||
creationTime: t.bigint().notNull(),
|
||||
lastTaxTime: t.bigint().notNull(),
|
||||
status: t.text().notNull().$default(() => "Active"), // "Active" or "Closed"
|
||||
status: t
|
||||
.text()
|
||||
.notNull()
|
||||
.$default(() => 'Active'), // "Active" or "Closed"
|
||||
createdAt: t.bigint().notNull(),
|
||||
closedAt: t.bigint(),
|
||||
totalSupplyInit: t.bigint().notNull(),
|
||||
totalSupplyEnd: t.bigint(),
|
||||
payout: t.bigint().notNull().$default(() => 0n),
|
||||
payout: t
|
||||
.bigint()
|
||||
.notNull()
|
||||
.$default(() => 0n),
|
||||
}),
|
||||
(table) => ({
|
||||
table => ({
|
||||
ownerIdx: index().on(table.owner),
|
||||
statusIdx: index().on(table.status),
|
||||
})
|
||||
|
|
@ -75,11 +147,10 @@ export const positions = onchainTable(
|
|||
|
||||
// Constants for tax rates (matches subgraph)
|
||||
export const TAX_RATES = [
|
||||
0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19,
|
||||
0.21, 0.25, 0.29, 0.33, 0.37, 0.41, 0.45, 0.49, 0.53, 0.57,
|
||||
0.61, 0.65, 0.69, 0.73, 0.77, 0.81, 0.85, 0.89, 0.93, 0.97
|
||||
0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19, 0.21, 0.25, 0.29, 0.33, 0.37, 0.41, 0.45, 0.49, 0.53, 0.57, 0.61, 0.65, 0.69,
|
||||
0.73, 0.77, 0.81, 0.85, 0.89, 0.93, 0.97,
|
||||
];
|
||||
|
||||
// Helper constants
|
||||
export const STATS_ID = "0x01";
|
||||
export const STATS_ID = '0x01';
|
||||
export const SECONDS_IN_HOUR = 3600;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { client, graphql } from "ponder";
|
||||
import { db } from "ponder:api";
|
||||
import schema from "ponder:schema";
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { client, graphql } from 'ponder';
|
||||
import { db } from 'ponder:api';
|
||||
import schema from 'ponder:schema';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
const allowedOrigins = process.env.PONDER_CORS_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
||||
const allowedOrigins = process.env.PONDER_CORS_ORIGINS?.split(',')
|
||||
.map(origin => origin.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
app.use("/*", cors({
|
||||
origin: allowedOrigins?.length ? allowedOrigins : "*",
|
||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
||||
allowHeaders: ["Content-Type", "Apollo-Require-Preflight"],
|
||||
}));
|
||||
app.use(
|
||||
'/*',
|
||||
cors({
|
||||
origin: allowedOrigins?.length ? allowedOrigins : '*',
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Apollo-Require-Preflight'],
|
||||
})
|
||||
);
|
||||
|
||||
// SQL endpoint
|
||||
app.use("/sql/*", client({ db, schema }));
|
||||
app.use('/sql/*', client({ db, schema }));
|
||||
|
||||
// GraphQL endpoints
|
||||
app.use("/graphql", graphql({ db, schema }));
|
||||
app.use("/", graphql({ db, schema }));
|
||||
app.use('/graphql', graphql({ db, schema }));
|
||||
app.use('/', graphql({ db, schema }));
|
||||
|
||||
export default app;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Abi } from 'viem'
|
||||
import type { Abi } from 'viem';
|
||||
|
||||
/**
|
||||
* Helper function to ensure an imported ABI matches the viem Abi type at compile time
|
||||
|
|
@ -6,7 +6,7 @@ import type { Abi } from 'viem'
|
|||
* @returns The validated ABI
|
||||
*/
|
||||
export function validateAbi<T extends Abi>(abi: T): T {
|
||||
return abi
|
||||
return abi;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -15,5 +15,5 @@ export function validateAbi<T extends Abi>(abi: T): T {
|
|||
* @returns The validated contract ABI
|
||||
*/
|
||||
export function validateContractAbi<T extends { abi: Abi }>(contract: T): T['abi'] {
|
||||
return contract.abi
|
||||
return contract.abi;
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
import { stats, STATS_ID, HOURS_IN_RING_BUFFER, SECONDS_IN_HOUR } from "ponder:schema";
|
||||
import { stats, STATS_ID, HOURS_IN_RING_BUFFER, SECONDS_IN_HOUR } from 'ponder:schema';
|
||||
|
||||
type Handler = Parameters<(typeof import('ponder:registry'))['ponder']['on']>[1];
|
||||
type HandlerArgs = Handler extends (...args: infer Args) => unknown ? Args[0] : never;
|
||||
export type StatsContext = HandlerArgs extends { context: infer C } ? C : never;
|
||||
type StatsEvent = HandlerArgs extends { event: infer E } ? E : never;
|
||||
|
||||
export const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
||||
export const MINIMUM_BLOCKS_FOR_RINGBUFFER = 100;
|
||||
|
|
@ -13,11 +18,11 @@ export function parseRingBuffer(raw?: string[] | null): bigint[] {
|
|||
if (!raw || raw.length === 0) {
|
||||
return makeEmptyRingBuffer();
|
||||
}
|
||||
return raw.map((value) => BigInt(value));
|
||||
return raw.map(value => BigInt(value));
|
||||
}
|
||||
|
||||
export function serializeRingBuffer(values: bigint[]): string[] {
|
||||
return values.map((value) => value.toString());
|
||||
return values.map(value => value.toString());
|
||||
}
|
||||
|
||||
function computeMetrics(ringBuffer: bigint[], pointer: number) {
|
||||
|
|
@ -62,12 +67,7 @@ function computeMetrics(ringBuffer: bigint[], pointer: number) {
|
|||
};
|
||||
}
|
||||
|
||||
function computeProjections(
|
||||
ringBuffer: bigint[],
|
||||
pointer: number,
|
||||
timestamp: bigint,
|
||||
metrics: ReturnType<typeof computeMetrics>
|
||||
) {
|
||||
function computeProjections(ringBuffer: bigint[], pointer: number, timestamp: bigint, metrics: ReturnType<typeof computeMetrics>) {
|
||||
const startOfHour = (timestamp / BigInt(SECONDS_IN_HOUR)) * BigInt(SECONDS_IN_HOUR);
|
||||
const elapsedSeconds = timestamp - startOfHour;
|
||||
|
||||
|
|
@ -96,13 +96,13 @@ function computeProjections(
|
|||
};
|
||||
}
|
||||
|
||||
export function checkBlockHistorySufficient(context: any, event: any): boolean {
|
||||
export function checkBlockHistorySufficient(context: StatsContext, event: StatsEvent): boolean {
|
||||
const currentBlock = event.block.number;
|
||||
const deployBlock = BigInt(context.network.contracts.Kraiken.startBlock);
|
||||
const blocksSinceDeployment = Number(currentBlock - deployBlock);
|
||||
|
||||
if (blocksSinceDeployment < MINIMUM_BLOCKS_FOR_RINGBUFFER) {
|
||||
context.log.warn(
|
||||
context.logger.warn(
|
||||
`Insufficient block history (only ${blocksSinceDeployment} blocks available, need ${MINIMUM_BLOCKS_FOR_RINGBUFFER})`
|
||||
);
|
||||
return false;
|
||||
|
|
@ -110,7 +110,7 @@ export function checkBlockHistorySufficient(context: any, event: any): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export async function ensureStatsExists(context: any, timestamp?: bigint) {
|
||||
export async function ensureStatsExists(context: StatsContext, timestamp?: bigint) {
|
||||
let statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (!statsData) {
|
||||
const { client, contracts } = context;
|
||||
|
|
@ -118,7 +118,7 @@ export async function ensureStatsExists(context: any, timestamp?: bigint) {
|
|||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
console.warn(`[stats.ensureStatsExists] Falling back for ${label}`, error);
|
||||
context.logger.warn(`[stats.ensureStatsExists] Falling back for ${label}`, error);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
|
@ -129,38 +129,36 @@ export async function ensureStatsExists(context: any, timestamp?: bigint) {
|
|||
client.readContract({
|
||||
abi: contracts.Kraiken.abi,
|
||||
address: contracts.Kraiken.address,
|
||||
functionName: "totalSupply",
|
||||
functionName: 'totalSupply',
|
||||
}),
|
||||
0n,
|
||||
"Kraiken.totalSupply",
|
||||
'Kraiken.totalSupply'
|
||||
),
|
||||
readWithFallback(
|
||||
() =>
|
||||
client.readContract({
|
||||
abi: contracts.Stake.abi,
|
||||
address: contracts.Stake.address,
|
||||
functionName: "totalSupply",
|
||||
functionName: 'totalSupply',
|
||||
}),
|
||||
0n,
|
||||
"Stake.totalSupply",
|
||||
'Stake.totalSupply'
|
||||
),
|
||||
readWithFallback(
|
||||
() =>
|
||||
client.readContract({
|
||||
abi: contracts.Stake.abi,
|
||||
address: contracts.Stake.address,
|
||||
functionName: "outstandingStake",
|
||||
functionName: 'outstandingStake',
|
||||
}),
|
||||
0n,
|
||||
"Stake.outstandingStake",
|
||||
'Stake.outstandingStake'
|
||||
),
|
||||
]);
|
||||
|
||||
cachedStakeTotalSupply = stakeTotalSupply;
|
||||
|
||||
const currentHour = timestamp
|
||||
? (timestamp / BigInt(SECONDS_IN_HOUR)) * BigInt(SECONDS_IN_HOUR)
|
||||
: 0n;
|
||||
const currentHour = timestamp ? (timestamp / BigInt(SECONDS_IN_HOUR)) * BigInt(SECONDS_IN_HOUR) : 0n;
|
||||
|
||||
await context.db.insert(stats).values({
|
||||
id: STATS_ID,
|
||||
|
|
@ -178,7 +176,7 @@ export async function ensureStatsExists(context: any, timestamp?: bigint) {
|
|||
return statsData;
|
||||
}
|
||||
|
||||
export async function updateHourlyData(context: any, timestamp: bigint) {
|
||||
export async function updateHourlyData(context: StatsContext, timestamp: bigint) {
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (!statsData) return;
|
||||
|
||||
|
|
@ -197,9 +195,7 @@ export async function updateHourlyData(context: any, timestamp: bigint) {
|
|||
|
||||
if (currentHour > lastUpdate) {
|
||||
const hoursElapsedBig = (currentHour - lastUpdate) / BigInt(SECONDS_IN_HOUR);
|
||||
const hoursElapsed = Number(hoursElapsedBig > BigInt(HOURS_IN_RING_BUFFER)
|
||||
? BigInt(HOURS_IN_RING_BUFFER)
|
||||
: hoursElapsedBig);
|
||||
const hoursElapsed = Number(hoursElapsedBig > BigInt(HOURS_IN_RING_BUFFER) ? BigInt(HOURS_IN_RING_BUFFER) : hoursElapsedBig);
|
||||
|
||||
for (let h = 0; h < hoursElapsed; h++) {
|
||||
pointer = (pointer + 1) % HOURS_IN_RING_BUFFER;
|
||||
|
|
@ -251,7 +247,7 @@ export async function updateHourlyData(context: any, timestamp: bigint) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getStakeTotalSupply(context: any): Promise<bigint> {
|
||||
export async function getStakeTotalSupply(context: StatsContext): Promise<bigint> {
|
||||
await ensureStatsExists(context);
|
||||
|
||||
if (cachedStakeTotalSupply !== null) {
|
||||
|
|
@ -261,7 +257,7 @@ export async function getStakeTotalSupply(context: any): Promise<bigint> {
|
|||
const totalSupply = await context.client.readContract({
|
||||
abi: context.contracts.Stake.abi,
|
||||
address: context.contracts.Stake.address,
|
||||
functionName: "totalSupply",
|
||||
functionName: 'totalSupply',
|
||||
});
|
||||
cachedStakeTotalSupply = totalSupply;
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
|
|
@ -270,12 +266,12 @@ export async function getStakeTotalSupply(context: any): Promise<bigint> {
|
|||
return totalSupply;
|
||||
}
|
||||
|
||||
export async function refreshOutstandingStake(context: any) {
|
||||
export async function refreshOutstandingStake(context: StatsContext) {
|
||||
await ensureStatsExists(context);
|
||||
const outstandingStake = await context.client.readContract({
|
||||
abi: context.contracts.Stake.abi,
|
||||
address: context.contracts.Stake.address,
|
||||
functionName: "outstandingStake",
|
||||
functionName: 'outstandingStake',
|
||||
});
|
||||
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Import all event handlers
|
||||
import "./kraiken";
|
||||
import "./stake";
|
||||
import './kraiken';
|
||||
import './stake';
|
||||
|
||||
// This file serves as the entry point for all indexing functions
|
||||
// Ponder will automatically register all event handlers from imported files
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ponder } from "ponder:registry";
|
||||
import { stats, STATS_ID } from "ponder:schema";
|
||||
import { ponder } from 'ponder:registry';
|
||||
import { stats, STATS_ID } from 'ponder:schema';
|
||||
import {
|
||||
ensureStatsExists,
|
||||
parseRingBuffer,
|
||||
|
|
@ -7,11 +7,11 @@ import {
|
|||
updateHourlyData,
|
||||
checkBlockHistorySufficient,
|
||||
RING_BUFFER_SEGMENTS,
|
||||
} from "./helpers/stats";
|
||||
} from './helpers/stats';
|
||||
|
||||
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
|
||||
|
||||
ponder.on("Kraiken:Transfer", async ({ event, context }) => {
|
||||
ponder.on('Kraiken:Transfer', async ({ event, context }) => {
|
||||
const { from, to, value } = event.args;
|
||||
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
|
@ -69,7 +69,7 @@ ponder.on("Kraiken:Transfer", async ({ event, context }) => {
|
|||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
||||
ponder.on("StatsBlock:block", async ({ event, context }) => {
|
||||
ponder.on('StatsBlock:block', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
// Only update hourly data if we have sufficient block history
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ponder } from "ponder:registry";
|
||||
import { positions, stats, STATS_ID, TAX_RATES } from "ponder:schema";
|
||||
import { ponder } from 'ponder:registry';
|
||||
import { positions, stats, STATS_ID, TAX_RATES } from 'ponder:schema';
|
||||
import {
|
||||
ensureStatsExists,
|
||||
getStakeTotalSupply,
|
||||
|
|
@ -9,15 +9,16 @@ import {
|
|||
updateHourlyData,
|
||||
checkBlockHistorySufficient,
|
||||
RING_BUFFER_SEGMENTS,
|
||||
} from "./helpers/stats";
|
||||
} from './helpers/stats';
|
||||
import type { StatsContext } from './helpers/stats';
|
||||
|
||||
const ZERO = 0n;
|
||||
|
||||
async function getKraikenTotalSupply(context: Context) {
|
||||
async function getKraikenTotalSupply(context: StatsContext) {
|
||||
return context.client.readContract({
|
||||
abi: context.contracts.Kraiken.abi,
|
||||
address: context.contracts.Kraiken.address,
|
||||
functionName: "totalSupply",
|
||||
functionName: 'totalSupply',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ function toShareRatio(share: bigint, stakeTotalSupply: bigint): number {
|
|||
return Number(share) / Number(stakeTotalSupply);
|
||||
}
|
||||
|
||||
ponder.on("Stake:PositionCreated", async ({ event, context }) => {
|
||||
ponder.on('Stake:PositionCreated', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const stakeTotalSupply = await getStakeTotalSupply(context);
|
||||
|
|
@ -44,7 +45,7 @@ ponder.on("Stake:PositionCreated", async ({ event, context }) => {
|
|||
snatched: 0,
|
||||
creationTime: event.block.timestamp,
|
||||
lastTaxTime: event.block.timestamp,
|
||||
status: "Active",
|
||||
status: 'Active',
|
||||
createdAt: event.block.timestamp,
|
||||
totalSupplyInit,
|
||||
totalSupplyEnd: null,
|
||||
|
|
@ -54,7 +55,7 @@ ponder.on("Stake:PositionCreated", async ({ event, context }) => {
|
|||
await refreshOutstandingStake(context);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionRemoved", async ({ event, context }) => {
|
||||
ponder.on('Stake:PositionRemoved', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const positionId = event.args.positionId.toString();
|
||||
|
|
@ -64,7 +65,7 @@ ponder.on("Stake:PositionRemoved", async ({ event, context }) => {
|
|||
const totalSupplyEnd = await getKraikenTotalSupply(context);
|
||||
|
||||
await context.db.update(positions, { id: positionId }).set({
|
||||
status: "Closed",
|
||||
status: 'Closed',
|
||||
closedAt: event.block.timestamp,
|
||||
totalSupplyEnd,
|
||||
payout: (position.payout ?? ZERO) + event.args.kraikenPayout,
|
||||
|
|
@ -79,7 +80,7 @@ ponder.on("Stake:PositionRemoved", async ({ event, context }) => {
|
|||
}
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionShrunk", async ({ event, context }) => {
|
||||
ponder.on('Stake:PositionShrunk', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const positionId = event.args.positionId.toString();
|
||||
|
|
@ -104,7 +105,7 @@ ponder.on("Stake:PositionShrunk", async ({ event, context }) => {
|
|||
}
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionTaxPaid", async ({ event, context }) => {
|
||||
ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const positionId = event.args.positionId.toString();
|
||||
|
|
@ -151,7 +152,7 @@ ponder.on("Stake:PositionTaxPaid", async ({ event, context }) => {
|
|||
await refreshOutstandingStake(context);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionRateHiked", async ({ event, context }) => {
|
||||
ponder.on('Stake:PositionRateHiked', async ({ event, context }) => {
|
||||
const positionId = event.args.positionId.toString();
|
||||
await context.db.update(positions, { id: positionId }).set({
|
||||
taxRate: TAX_RATES[Number(event.args.newTaxRate)] || 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue