improve web-app config

This commit is contained in:
johba 2025-10-11 10:55:49 +00:00
parent 280e2973cd
commit 371a8557b7
28 changed files with 831 additions and 344 deletions

View file

@ -28,7 +28,7 @@
## Testing & Tooling
- Contracts: run `forge build`, `forge test`, and `forge snapshot` inside `onchain/`.
- Fuzzing: scripts under `onchain/analysis/` (e.g., `./analysis/run-fuzzing.sh [optimizer] debugCSV`) generate replayable scenarios.
- Integration: after the stack boots, inspect Anvil logs, hit `http://localhost:42069/graphql` for Ponder, and poll `http://127.0.0.1:43069/status` for txnBot health.
- Integration: after the stack boots, inspect Anvil logs, hit `http://localhost:8081/api/graphql` for Ponder, and poll `http://localhost:8081/api/txn/status` for txnBot health.
- **E2E Tests**: Playwright-based full-stack tests in `tests/e2e/` verify complete user journeys (mint ETH → swap KRK → stake). Run with `npm run test:e2e` from repo root. Tests use mocked wallet provider with Anvil accounts and automatically start/stop the stack. See `INTEGRATION_TEST_STATUS.md` and `SWAP_VERIFICATION.md` for details.
## Version Validation System
@ -65,8 +65,8 @@
- `anvil --fork-url https://sepolia.base.org` - manual fork when diagnosing outside the helper script.
- `cast call <POOL> "slot0()"` - inspect pool state.
- `PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK npm run dev` (inside `services/ponder/`) - focused indexer debugging when the full stack is already running.
- `curl -X POST http://localhost:42069/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
- `curl http://127.0.0.1:43069/status`
- `curl -X POST http://localhost:8081/api/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
- `curl http://localhost:8081/api/txn/status`
## References
- Deployment history: `onchain/deployments-local.json`, `onchain/broadcast/`.

View file

@ -2,18 +2,19 @@
route /app* {
reverse_proxy webapp:5173
}
route /graphql* {
route /api/graphql* {
uri strip_prefix /api
reverse_proxy ponder:42069
}
route /health* {
reverse_proxy ponder:42069
}
route /rpc/anvil* {
uri strip_prefix /rpc/anvil
route /api/rpc* {
uri strip_prefix /api/rpc
reverse_proxy anvil:8545
}
route /txn* {
uri strip_prefix /txn
route /api/txn* {
uri strip_prefix /api/txn
reverse_proxy txn-bot:43069
}
reverse_proxy landing:5174

View file

@ -218,13 +218,15 @@ EOPONDER
}
write_txn_bot_env() {
local provider_url=${TXNBOT_PROVIDER_URL:-$ANVIL_RPC}
local graphql_endpoint=${TXNBOT_GRAPHQL_ENDPOINT:-http://ponder:42069/graphql}
cat >"$TXNBOT_ENV" <<EOTXNBOT
ENVIRONMENT=BASE_SEPOLIA_LOCAL_FORK
PROVIDER_URL=$ANVIL_RPC
PROVIDER_URL=$provider_url
PRIVATE_KEY=$TXNBOT_PRIVATE_KEY
LM_CONTRACT_ADDRESS=$LIQUIDITY_MANAGER
STAKE_CONTRACT_ADDRESS=$STAKE
GRAPHQL_ENDPOINT=http://ponder:42069/graphql
GRAPHQL_ENDPOINT=$graphql_endpoint
WALLET_ADDRESS=$TXNBOT_ADDRESS
PORT=43069
EOTXNBOT

View file

@ -54,13 +54,15 @@ else
fi
export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337}
export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/rpc/anvil}
export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/api/rpc}
export VITE_LOCAL_RPC_PROXY_TARGET=${VITE_LOCAL_RPC_PROXY_TARGET:-http://anvil:8545}
export VITE_LOCAL_GRAPHQL_PROXY_TARGET=${VITE_LOCAL_GRAPHQL_PROXY_TARGET:-http://ponder:42069}
export VITE_LOCAL_TXN_PROXY_TARGET=${VITE_LOCAL_TXN_PROXY_TARGET:-http://txn-bot:43069}
export VITE_KRAIKEN_ADDRESS=$KRAIKEN
export VITE_STAKE_ADDRESS=$STAKE
export VITE_SWAP_ROUTER=$SWAP_ROUTER
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/app/graphql}
export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/app/txn}
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/api/graphql}
export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn}
export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1}
exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/

View file

@ -8,7 +8,7 @@ The Podman stack mirrors `scripts/dev.sh` using long-lived containers. Every boo
- `ponder` `npm run dev` for the indexer (port 42069 inside the pod)
- `frontend` Vite dev server for `web-app` (port 5173 inside the pod)
- `txn-bot` automation loop plus Express status API (port 43069 inside the pod)
- `caddy` front door at `http://<host>:80`, routing `/graphql`, `/health`, `/rpc/anvil`, and `/txn` to the internal services
- `caddy` front door at `http://<host>:80`, routing `/api/graphql`, `/health`, `/api/rpc`, and `/api/txn` to the internal services
All containers mount the repository so code edits hot-reload exactly as the local script. Named volumes keep `node_modules` caches between restarts.
@ -26,9 +26,9 @@ podman-compose -f podman-compose.yml up
### Access Points (via Caddy)
- Frontend: `http://<host>/`
- GraphQL: `http://<host>/graphql`
- RPC passthrough: `http://<host>/rpc/anvil`
- Txn bot status: `http://<host>/txn/status`
- GraphQL: `http://<host>/api/graphql`
- RPC passthrough: `http://<host>/api/rpc`
- Txn bot status: `http://<host>/api/txn/status`
## Configuration Knobs
Set environment variables before `podman-compose up`:

View file

@ -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"
}
@ -3192,7 +3190,6 @@
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.45.0",
"@typescript-eslint/types": "8.45.0",
@ -3520,7 +3517,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3943,7 +3939,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
@ -4780,7 +4775,6 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -5521,7 +5515,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 +5599,6 @@
"integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"workspaces": [
"website"
],
@ -6266,7 +6258,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 +9240,6 @@
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9572,7 +9562,6 @@
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},

View file

@ -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"

124
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "harb",
"name": "harb-wa-conf",
"lockfileVersion": 3,
"requires": true,
"packages": {
@ -30,6 +30,7 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -47,6 +48,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -64,6 +66,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -81,6 +84,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -98,6 +102,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -115,6 +120,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -132,6 +138,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -149,6 +156,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -166,6 +174,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -183,6 +192,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -200,6 +210,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -217,6 +228,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -234,6 +246,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -251,6 +264,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -268,6 +282,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -285,6 +300,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -302,6 +318,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -319,6 +336,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -336,6 +354,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -353,6 +372,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -370,6 +390,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -387,6 +408,7 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -404,6 +426,7 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -421,6 +444,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -438,6 +462,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -455,6 +480,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@ -1269,7 +1295,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.2",
@ -1283,7 +1310,8 @@
"optional": true,
"os": [
"android"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.2",
@ -1297,7 +1325,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.2",
@ -1311,7 +1340,8 @@
"optional": true,
"os": [
"darwin"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.2",
@ -1325,7 +1355,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.2",
@ -1339,7 +1370,8 @@
"optional": true,
"os": [
"freebsd"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.2",
@ -1353,7 +1385,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.2",
@ -1367,7 +1400,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.2",
@ -1381,7 +1415,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.2",
@ -1395,7 +1430,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.2",
@ -1409,7 +1445,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.2",
@ -1423,7 +1460,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.2",
@ -1437,7 +1475,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.2",
@ -1451,7 +1490,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.2",
@ -1465,7 +1505,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.2",
@ -1479,7 +1520,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.2",
@ -1493,7 +1535,8 @@
"optional": true,
"os": [
"linux"
]
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.2",
@ -1507,7 +1550,8 @@
"optional": true,
"os": [
"openharmony"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.2",
@ -1521,7 +1565,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.2",
@ -1535,7 +1580,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.2",
@ -1549,7 +1595,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.2",
@ -1563,7 +1610,8 @@
"optional": true,
"os": [
"win32"
]
],
"peer": true
},
"node_modules/@skorotkiewicz/snowflake-id": {
"version": "1.0.1",
@ -1858,7 +1906,8 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/accepts": {
"version": "2.0.0",
@ -2208,6 +2257,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -2338,7 +2388,6 @@
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
@ -2412,6 +2461,7 @@
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@ -2688,7 +2738,6 @@
"integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@ -2713,7 +2762,6 @@
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"dev": true,
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@ -3085,6 +3133,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -3184,7 +3233,8 @@
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/picomatch": {
"version": "4.0.3",
@ -3296,6 +3346,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -3416,7 +3467,6 @@
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -3441,7 +3491,6 @@
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -3555,6 +3604,7 @@
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@ -3824,8 +3874,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@ -3874,6 +3923,7 @@
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
@ -3917,13 +3967,6 @@
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
"dev": true,
"optional": true
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -4005,6 +4048,7 @@
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -4085,6 +4129,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
@ -4169,7 +4214,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -15,6 +15,11 @@ readonly POLL_INTERVAL=2 # Check health every N seconds
PID_FILE=/tmp/kraiken-watcher.pid
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}
container_name() {
local service="$1"
echo "${PROJECT_NAME}_${service}_1"
}
cleanup_existing() {
# Kill any existing watch scripts
pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true
@ -24,11 +29,11 @@ cleanup_existing() {
rm -f "$PID_FILE"
# Kill zombie podman processes
pkill -9 -f "podman wait.*harb_" 2>/dev/null || true
pkill -9 -f "podman wait.*${PROJECT_NAME}_" 2>/dev/null || true
# Remove any existing containers (suppress errors if they don't exist)
echo " Cleaning up existing containers..."
podman ps -a --filter "name=harb_" --format "{{.Names}}" 2>/dev/null | \
podman ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" 2>/dev/null | \
xargs -r podman rm -f 2>&1 | grep -v "Error.*no container" || true
}
@ -94,32 +99,32 @@ start_stack() {
echo " Starting anvil & postgres..."
podman-compose up -d anvil postgres 2>&1 | grep -v "STEP\|Copying\|Writing\|Getting\|fetch\|Installing\|Executing" || true
wait_for_healthy harb_anvil_1 "$ANVIL_TIMEOUT" || exit 1
wait_for_healthy harb_postgres_1 "$POSTGRES_TIMEOUT" || exit 1
wait_for_healthy "$(container_name anvil)" "$ANVIL_TIMEOUT" || exit 1
wait_for_healthy "$(container_name postgres)" "$POSTGRES_TIMEOUT" || exit 1
# Phase 2: Start bootstrap (depends on anvil & postgres healthy)
echo " Starting bootstrap..."
podman-compose up -d bootstrap >/dev/null 2>&1
wait_for_exited harb_bootstrap_1 "$BOOTSTRAP_TIMEOUT" || exit 1
wait_for_exited "$(container_name bootstrap)" "$BOOTSTRAP_TIMEOUT" || exit 1
# Phase 3: Start ponder (depends on bootstrap completed)
echo " Starting ponder..."
podman-compose up -d ponder >/dev/null 2>&1
wait_for_healthy harb_ponder_1 "$PONDER_TIMEOUT" || exit 1
wait_for_healthy "$(container_name ponder)" "$PONDER_TIMEOUT" || exit 1
# Phase 4: Start frontend services (depend on ponder healthy)
echo " Starting webapp, landing, txn-bot..."
podman-compose up -d webapp landing txn-bot >/dev/null 2>&1
wait_for_healthy harb_webapp_1 "$WEBAPP_TIMEOUT" || exit 1
wait_for_healthy "$(container_name webapp)" "$WEBAPP_TIMEOUT" || exit 1
# Phase 5: Start caddy (depends on frontend services)
echo " Starting caddy..."
podman-compose up -d caddy >/dev/null 2>&1
wait_for_healthy harb_caddy_1 "$CADDY_TIMEOUT" || exit 1
wait_for_healthy "$(container_name caddy)" "$CADDY_TIMEOUT" || exit 1
if [[ -z "${SKIP_WATCH:-}" ]]; then
echo "Watching for kraiken-lib changes..."
@ -131,7 +136,8 @@ start_stack() {
echo ""
echo "[ok] Stack started in ${total_time}s"
echo " Web App: http://localhost:8081/app/"
echo " GraphQL: http://localhost:8081/graphql"
echo " RPC Proxy: http://localhost:8081/api/rpc"
echo " GraphQL: http://localhost:8081/api/graphql"
}
stop_stack() {

View file

@ -3,9 +3,9 @@ import {
formatHealthCheckError,
} from './health-checks.js';
const DEFAULT_RPC_URL = 'http://127.0.0.1:8545';
const DEFAULT_RPC_URL = 'http://localhost:8081/api/rpc';
const DEFAULT_WEBAPP_URL = 'http://localhost:8081';
const DEFAULT_GRAPHQL_URL = 'http://localhost:8081/graphql';
const DEFAULT_GRAPHQL_URL = 'http://localhost:8081/api/graphql';
export interface StackConfig {
rpcUrl: string;

278
web-app/AGENTS.md Normal file
View file

@ -0,0 +1,278 @@
# Web App - Agent Guide
Vue 3 + TypeScript staking interface for KRAIKEN, enabling users to stake tokens, manage positions, and interact with Harberger-tax mechanics.
## Technology Snapshot
- Vue 3 (Composition API) with TypeScript and Vite toolchain
- Wagmi/Viem for wallet connection and blockchain interaction
- Vue Router for navigation
- Axios for GraphQL queries to Ponder indexer
- Sass-based component styling
## Architecture Overview
### Chain Configuration Service
**Location**: `src/services/chainConfig.ts`
Centralizes all endpoint resolution for different blockchain networks. This service eliminates scattered configuration and provides a single source of truth for network-specific URLs.
**Key Method**:
```typescript
chainConfigService.getEndpoint(chainId: number, type: 'graphql' | 'rpc' | 'txnBot'): string
```
**Supported Chains**:
- `31337` - Local Anvil fork (development)
- `84532` - Base Sepolia (testnet)
- `8453` - Base Mainnet (production)
**Usage Pattern**:
```typescript
// Composables receive chainId and resolve endpoints internally
const endpoint = chainConfigService.getEndpoint(chainId, 'graphql');
```
**Benefits**:
- Single source of truth for all endpoint configuration
- Easy to test composables with different chainIds
- No hidden global state dependencies
- Clear error messages when endpoints aren't configured
### Wagmi Integration
**Configuration**: `src/wagmi.ts`
Wagmi manages wallet connection and tracks the user's active blockchain:
- Wallet providers: WalletConnect, Coinbase Wallet
- Supported chains: Local fork (31337), Base Sepolia (84532)
- State persistence via localStorage
**Chain Determination Flow**:
1. Wagmi detects wallet's current chain via `watchChainId()`
2. `useWallet` exposes `account.chainId` from wagmi state
3. Components read `account.chainId` and pass to composables
4. Composables watch for chain changes independently and reload data
5. `ChainConfigService` maps chainId → endpoint URLs
**Key Insight**: Wagmi is the source of truth for *which chain the wallet is on*, but composables don't import wallet state directly. They accept `chainId` as a parameter for better testability and explicit dependencies.
## Key Composables
### `useWallet()`
**Purpose**: Manages wallet connection, balance, and account state
**Exports**: `balance`, `account`, `loadBalance()`
**Watchers**:
- `watchAccount()` - Reloads balance when account/chain changes
- `watchChainId()` - Reloads balance on chain switch
**Note**: Also exports `chainData` computed property for UI metadata (Uniswap links, chain names). This is separate from endpoint resolution, which goes through `ChainConfigService`.
### `usePositions(chainId: number)`
**Purpose**: Loads and manages staking positions from Ponder GraphQL
**Parameters**: `chainId` - Which chain to query positions from
**Exports**: `activePositions`, `myActivePositions`, `myClosedPositions`, `tresholdValue`, `positionsError`, `loading`
**Key Features**:
- Watches contract events (`PositionCreated`, `PositionRemoved`) to auto-refresh
- Independent `watchChainId()` listener reloads on chain switch
- Exponential backoff retry on GraphQL failures (1.5s → 60s max)
**Data Flow**:
```
Component (chainId) → usePositions(chainId)
→ resolveGraphqlEndpoint(chainId)
→ chainConfigService.getEndpoint(chainId, 'graphql')
→ axios.post(endpoint, query)
```
### `useStatCollection(chainId: number)`
**Purpose**: Loads protocol-wide statistics (total supply, outstanding stake, inflation, etc.)
**Parameters**: `chainId` - Which chain to query stats from
**Exports**: `kraikenTotalSupply`, `stakeTotalSupply`, `outstandingStake`, `profit7d`, `inflation7d`, `maxSlots`, `claimedSlots`, `statsError`, `loading`
**Retry Logic**: Same exponential backoff as `usePositions`
### `useSnatchSelection(demo, taxRateIndex, chainId)`
**Purpose**: Calculates which positions can be "snatched" based on staking amount and tax rate
**Parameters**:
- `demo` - Include own positions in selection
- `taxRateIndex` - Maximum tax rate index to snatch
- `chainId` - Optional chain ID
**Dependencies**: `usePositions`, `useStatCollection`, `useStake`
**Exports**: `snatchablePositions`, `shortfallShares`, `floorTax`, `openPositionsAvailable`
### `useStake()`
**Purpose**: Executes staking transactions (stake, snatch-and-stake)
**Contract Interaction**: Calls `Stake.sol` via wagmi/viem
**State Management**: Tracks transaction states (StakeAble, SignTransaction, Waiting)
### `useAdjustTaxRate()`
**Purpose**: Provides tax rate options and handles tax rate adjustments
**Key Data**: `taxRates` array with pre-calculated yearly/daily rates
### `useUnstake()`
**Purpose**: Handles position exit transactions
**Contract Interaction**: Calls `exitPosition()` on Stake contract
## Key Components
### `StakeView.vue`
**Route**: `/dashboard`
**Purpose**: Main staking dashboard showing chart, statistics, active positions
**Key Features**:
- Passes `initialChainId` to all composables for consistent data loading
- Displays chain support status
- Shows "Connect Wallet" prompt when disconnected
### `StakeHolder.vue`
**Purpose**: Staking form with accessibility-focused UI
**Key Elements**:
- Token amount slider with ARIA labels
- Tax rate selector dropdown
- Real-time feedback (floor tax, positions buyout count)
- Action button with state-driven labels (Stake / Snatch and Stake / Sign Transaction / Waiting)
**Accessibility**:
- Semantic HTML with proper ARIA attributes
- Screen reader announcements for dynamic content
- Keyboard navigation support
**Test Hooks** (for Playwright):
- `page.getByRole('slider', { name: 'Token Amount' })`
- `page.getByLabel('Staking Amount')`
- `page.getByLabel('Tax')`
### `ConnectWallet.vue`
**Purpose**: Wallet connection modal with connector selection
**Features**:
- Shows connected wallet with avatar (blockies) and address
- Token balance display ($KRK, Staked $KRK)
- Buy button with chain-specific Uniswap link (uses `chainData.uniswap`)
### `ChartComplete.vue`
**Purpose**: Position visualization showing tax rates and snatchable positions
**Dependencies**: `usePositions`, `useStatCollection`, `useStake`
### `CollapseActive.vue`
**Purpose**: Expandable position card with profit calculation and actions
**Actions**: Pay Tax, Adjust Tax Rate, Unstake
## Shared Utilities
### `src/utils/logger.ts`
Structured logging with namespaces (contract, info, error)
### `src/utils/helper.ts`
Common helpers:
- `bigInt2Number(value, decimals)` - Convert Wei to human-readable
- `formatBigIntDivision(a, b)` - Safe BigInt division
- `compactNumber(n)` - Format large numbers (1.5M, 3.2K)
### `src/config.ts`
Chain configuration data:
- `chainsData` - Array of chain metadata (id, graphql, contracts, etc.)
- `getChain(id)` - Lookup chain by ID
- `DEFAULT_CHAIN_ID` - Auto-detected based on environment/hostname
## Contract Interfaces
### `src/contracts/harb.ts`
**Contract**: Kraiken.sol
**Methods**: `getMinStake()`, `getAllowance()`, `getNonce()`, `approve()`
**Setup**: `setHarbContract()` updates contract address when chain changes
### `src/contracts/stake.ts`
**Contract**: Stake.sol
**Methods**: `assetsToShares()`, `getTaxDue()`, `payTax(positionId)`
**Setup**: `setStakeContract()` updates contract address when chain changes
## Retry Logic & Error Handling
Both `usePositions` and `useStatCollection` implement **exponential backoff retry** for GraphQL failures:
**Configuration**:
- Base delay: 1.5 seconds
- Max delay: 60 seconds
- Backoff multiplier: 2x (1.5s → 3s → 6s → 12s → 24s → 48s → 60s)
**Why Retry**:
- Ponder indexer may not be ready during stack startup
- Temporary network issues shouldn't permanently break UI
- Chain switching endpoints may have brief response delays
**Implementation** (duplicated in both files):
- `formatGraphqlError()` - Parse Axios errors into user-friendly messages
- `clearXxxRetryTimer()` - Cancel pending retry
- `scheduleXxxRetry()` - Queue retry with exponential backoff
- `resolveGraphqlEndpoint()` - Resolve chainId to GraphQL URL
**Refactoring Opportunity**: ~41 lines of duplicate code could be extracted to `src/utils/graphqlRetry.ts`
## Development Workflow
### Running Locally
Boot the full stack with `./scripts/dev.sh start` (see root `CLAUDE.md` for details)
### Targeted Development
- `npm run dev` - Vite dev server (assumes Ponder/Anvil already running)
- `npm run build` - Production build with type checking
- `npm run preview` - Preview production build
- `npm run test:e2e` - Playwright E2E tests (from repo root)
### Live Reload
Use `./scripts/watch-kraiken-lib.sh` to rebuild `kraiken-lib` on file changes and auto-restart dependent containers
## Testing
### E2E Tests
**Location**: `tests/e2e/` (repo root)
**Framework**: Playwright
**Coverage**: Complete user journeys (mint ETH → swap KRK → stake)
**References**: See `INTEGRATION_TEST_STATUS.md` and `SWAP_VERIFICATION.md`
**Test Strategy**:
- Use mocked wallet provider with Anvil accounts
- Tests automatically start/stop the full stack
- Rely on semantic HTML and ARIA attributes (not private selectors)
### Manual Testing Checklist
1. Connect wallet (WalletConnect / Coinbase Wallet)
2. Switch chains and verify data reloads
3. Stake tokens with different tax rates
4. Snatch positions with higher tax rate
5. Adjust tax rate on existing position
6. Pay tax manually
7. Unstake position
8. Verify GraphQL error retry behavior (kill Ponder, observe retries)
## Quality Guidelines
- **Composables**: Accept `chainId` parameter instead of importing wallet state directly
- **Watchers**: Each composable maintains its own `watchChainId()` listener for independence
- **Error States**: Always expose `xxxError` and `loading` refs for UI feedback
- **Type Safety**: Use strongly typed interfaces for Position, StatsRecord, etc.
- **Accessibility**: Use semantic HTML, ARIA attributes, and keyboard navigation
- **Testability**: Design components to work with Playwright role/label selectors
## Performance Tips
- Lazy load routes with Vue Router dynamic imports
- Debounce `assetsToShares()` calls in StakeHolder (500ms)
- Use `computed()` for derived state to avoid recalculation
- Clear watchers in `onUnmounted()` to prevent memory leaks
- Cancel retry timers on unmount or manual actions
## Common Pitfalls
1. **Don't** import `chainData` from `useWallet` in composables - pass `chainId` explicitly
2. **Don't** forget to pass `chainId` to `loadPositions()` / `loadStats()` on manual refresh
3. **Don't** call composables conditionally - they must run on every component render
4. **Don't** forget to clear timers/watchers in `onUnmounted()`
5. **Do** reset retry delays on successful loads (`retryDelayMs.value = BASE_DELAY`)
## Future Enhancements
- Extract retry logic to shared utility (`src/utils/graphqlRetry.ts`)
- Remove dead code in `useChain.ts` (entire file is commented out)
- Clean up commented debug `console.log()` statements
- Implement three-way version validation (contract VERSION ↔ Ponder ↔ web-app)
- Add GraphQL query caching layer for better performance

View file

@ -159,6 +159,7 @@ import { getMinStake } from '@/contracts/harb';
import { useWallet } from '@/composables/useWallet';
import { ref, onMounted, watch, computed, watchEffect, getCurrentInstance } from 'vue';
import { useStatCollection, loadStats } from '@/composables/useStatCollection';
import { DEFAULT_CHAIN_ID } from '@/config';
const demo = sessionStorage.getItem('demo') === 'true';
@ -168,8 +169,10 @@ const taxRateIndex = ref<number>(defaultTaxRateIndex);
const stake = useStake();
const _claim = useClaim();
const wallet = useWallet();
const statCollection = useStatCollection();
const { activePositions: _activePositions } = usePositions();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const statCollection = useStatCollection(initialChainId);
const { activePositions: _activePositions } = usePositions(initialChainId);
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
const instance = getCurrentInstance();
const uid = instance?.uid ?? Math.floor(Math.random() * 10000);
@ -330,7 +333,7 @@ const taxRateAnnouncement = computed(() => {
return `Selected tax rate index ${selectedTaxOption.value.index}. You will pay ${formatNumber(selectedTaxOption.value.year, 2)} percent yearly.`;
});
const snatchSelection = useSnatchSelection(demo, taxRateIndex);
const snatchSelection = useSnatchSelection(demo, taxRateIndex, initialChainId);
const floorTaxDisplay = computed(() => `${formatNumber(snatchSelection.floorTax.value ?? 0, 2)} %`);
const floorTaxHelpText = 'Your tax needs to exceed this value to displace an existing position.';
@ -487,8 +490,8 @@ async function stakeSnatch() {
const snatchAblePositionsIds = snatchSelection.snatchablePositions.value.map((p: Position) => p.positionId);
await stake.snatch(stake.stakingAmount, taxRateIndex.value, snatchAblePositionsIds);
}
await loadPositions();
await loadStats();
await loadPositions(currentChainId.value);
await loadStats(currentChainId.value);
}
async function handleSubmit() {

View file

@ -10,9 +10,13 @@ import { useStatCollection } from '@/composables/useStatCollection';
import { useStake } from '@/composables/useStake';
import { usePositions, type Position } from '@/composables/usePositions';
import { useDark } from '@/composables/useDark';
import { useWallet } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
const { darkTheme } = useDark();
const { activePositions } = usePositions();
const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { activePositions } = usePositions(initialChainId);
const ignoreOwner = ref(false);
const taxRate = ref<number>(1.0);
@ -26,7 +30,7 @@ const stakeAbleHarbAmount = computed(() => statCollection.kraikenTotalSupply / 5
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
const stake = useStake();
const statCollection = useStatCollection();
const statCollection = useStatCollection(initialChainId);
const snatchPositions = computed(() => {
if (
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=

View file

@ -83,12 +83,17 @@ import { computed, ref, onMounted } from 'vue';
import { getTaxDue, payTax } from '@/contracts/stake';
import { type Position, loadPositions } from '@/composables/usePositions';
import { useStatCollection } from '@/composables/useStatCollection';
import { useWallet } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
import { formatUnits } from 'viem';
const unstake = useUnstake();
const adjustTaxRate = useAdjustTaxRate();
const statCollection = useStatCollection();
const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const statCollection = useStatCollection(initialChainId);
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
const props = defineProps<{
taxRate: number;
@ -134,7 +139,7 @@ async function unstakePosition() {
await unstake.exitPosition(props.id);
loading.value = true;
await new Promise(resolve => setTimeout(resolve, 5000));
await loadPositions();
await loadPositions(currentChainId.value);
loading.value = false;
}

View file

@ -16,7 +16,7 @@
<div class="connected-tokens">
<FOutput name="$KRK" :price="harbAmount" :variant="3">
<template #end>
<FButton size="small" dense><a :href="chain.chainData?.uniswap" target="_blank">Buy</a></FButton>
<FButton size="small" dense><a :href="chain?.uniswap" target="_blank">Buy</a></FButton>
</template>
</FOutput>
<FOutput name="Staked $KRK" :price="compactNumber(stakedAmount)" :variant="3">
@ -64,8 +64,8 @@ import FOutput from '@/components/fcomponents/FOutput.vue';
// import { usePositions } from "@/composables/usePositions";
// import { useWallet } from "@/composables/useWallet";
import { usePositions } from '@/composables/usePositions';
import { useWallet } from '@/composables/useWallet';
import { useChain } from '@/composables/useChain';
import { useWallet, chainData as walletChainData } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
import { useAccount, useDisconnect, useConnect, useChainId, type CreateConnectorFn, type Connector } from '@wagmi/vue';
@ -74,9 +74,10 @@ const { disconnect } = useDisconnect();
const { connectors, connect } = useConnect();
const router = useRouter();
const chainId = useChainId();
const { myActivePositions } = usePositions();
const initialChainId = chainId.value ?? DEFAULT_CHAIN_ID;
const { myActivePositions } = usePositions(initialChainId);
const wallet = useWallet();
const chain = useChain();
const chain = walletChainData;
function loadConnectorImage(connector: Connector) {
if (connector.icon) {
return connector.icon;

View file

@ -1,45 +0,0 @@
import { ref, reactive, computed } from 'vue';
// import { getChainId, watchChainId, getAccount, watchAccount } from "@wagmi/core";
// import { config } from "@/wagmi";
// import { setHarbContract } from "@/contracts/harb";
// import { setStakeContract } from "@/contracts/stake";
import { chainsData } from '@/config';
// import logger from "@/utils/logger";
const activeChain = ref();
export const chainData = computed(() => {
return chainsData.find(obj => obj.id === activeChain.value);
});
export function useChain() {
// if (!unwatch) {
// console.log("useChain function");
// const chain = getChainId(config as Config)
// activeChain.value = chain
// unwatch = watchChainId(config as Config, {
// async onChange(chainId) {
// console.log("Chain changed", chainId);
// activeChain.value = chainId
// setHarbContract()
// setStakeContract()
// },
// });
// }
// if (!unwatch) {
// console.log("useWallet function");
// unwatch = watchAccount(config as Config, {
// async onChange(data) {
// console.log("watchaccount-useChain", data);
// if(!data.address) {
// } else if (activeChain.value !== data.chainId) {
// logger.info(`Chain changed!:`, data.chainId);
// }
// },
// });
// }
return reactive({ chainData });
}

View file

@ -9,17 +9,17 @@ import { HarbContract } from '@/contracts/harb';
import { bytesToUint256 } from 'kraiken-lib';
import { bigInt2Number } from '@/utils/helper';
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
import { chainData } from '@/composables/useWallet';
import logger from '@/utils/logger';
import { DEFAULT_CHAIN_ID } from '@/config';
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
const rawActivePositions = ref<Array<Position>>([]);
const rawClosedPositoins = ref<Array<Position>>([]);
const loading = ref(false);
const positionsError = ref<string | null>(null);
const POSITIONS_RETRY_BASE_DELAY = 1_500;
const POSITIONS_RETRY_MAX_DELAY = 60_000;
const positionsRetryDelayMs = ref(POSITIONS_RETRY_BASE_DELAY);
const GRAPHQL_TIMEOUT_MS = 15_000;
let positionsRetryTimer: number | null = null;
const activeChainId = ref<number>(DEFAULT_CHAIN_ID);
const retryManager = createRetryManager(loadPositions, activeChainId);
const activePositions = computed(() => {
const account = getAccount(config as Config);
@ -108,9 +108,7 @@ const myActivePositions: ComputedRef<Position[]> = computed(() =>
const tresholdValue = computed(() => {
// Compute average tax rate index instead of percentage to avoid floating-point issues
const validIndices = activePositions.value
.map(obj => obj.taxRateIndex)
.filter((idx): idx is number => typeof idx === 'number');
const validIndices = activePositions.value.map(obj => obj.taxRateIndex).filter((idx): idx is number => typeof idx === 'number');
if (validIndices.length === 0) return 0;
@ -121,14 +119,11 @@ const tresholdValue = computed(() => {
return Math.floor(avgIndex / 2);
});
export async function loadActivePositions(endpoint?: string) {
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`);
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
if (!targetEndpoint) {
throw new Error('GraphQL endpoint not configured for this chain.');
}
export async function loadActivePositions(chainId: number, endpointOverride?: string) {
const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
logger.info(`loadActivePositions for chainId: ${chainId}`);
// console.log("chainData.value?.graphql", targetEndpoint);
// console.log("graphql endpoint", targetEndpoint);
const res = await axios.post(
targetEndpoint,
@ -173,12 +168,9 @@ function formatId(id: Hex) {
return bigIntId;
}
export async function loadMyClosedPositions(endpoint: string | undefined, account: GetAccountReturnType) {
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`);
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
if (!targetEndpoint) {
throw new Error('GraphQL endpoint not configured for this chain.');
}
export async function loadMyClosedPositions(chainId: number, endpointOverride: string | undefined, account: GetAccountReturnType) {
const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
logger.info(`loadMyClosedPositions for chainId: ${chainId}`);
const res = await axios.post(
targetEndpoint,
{
@ -214,36 +206,41 @@ export async function loadMyClosedPositions(endpoint: string | undefined, accoun
})) as Position[];
}
export async function loadPositions() {
export async function loadPositions(chainId?: number) {
loading.value = true;
const endpoint = chainData.value?.graphql?.trim();
if (!endpoint) {
const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
activeChainId.value = targetChainId;
let endpoint: string;
try {
endpoint = resolveGraphqlEndpoint(targetChainId);
} catch (error) {
rawActivePositions.value = [];
rawClosedPositoins.value = [];
positionsError.value = 'GraphQL endpoint not configured for this chain.';
clearPositionsRetryTimer();
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
positionsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
retryManager.clear();
retryManager.reset();
loading.value = false;
return;
}
try {
rawActivePositions.value = await loadActivePositions(endpoint);
rawActivePositions.value = await loadActivePositions(targetChainId, endpoint);
const account = getAccount(config as Config);
if (account.address) {
rawClosedPositoins.value = await loadMyClosedPositions(endpoint, account);
rawClosedPositoins.value = await loadMyClosedPositions(targetChainId, endpoint, account);
} else {
rawClosedPositoins.value = [];
}
positionsError.value = null;
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
clearPositionsRetryTimer();
retryManager.reset();
retryManager.clear();
} catch (error) {
rawActivePositions.value = [];
rawClosedPositoins.value = [];
positionsError.value = formatGraphqlError(error);
schedulePositionsRetry();
retryManager.schedule();
} finally {
loading.value = false;
}
@ -253,48 +250,9 @@ let unwatch: WatchEventReturnType | null;
let unwatchPositionRemovedEvent: WatchEventReturnType | null;
let unwatchChainSwitch: WatchChainIdReturnType | null;
let unwatchAccountChanged: WatchAccountReturnType | null;
export function usePositions(chainId: number = DEFAULT_CHAIN_ID) {
activeChainId.value = chainId;
function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown GraphQL error';
}
function clearPositionsRetryTimer() {
if (positionsRetryTimer !== null) {
clearTimeout(positionsRetryTimer);
positionsRetryTimer = null;
}
}
function schedulePositionsRetry() {
if (typeof window === 'undefined') {
return;
}
if (positionsRetryTimer !== null) {
return;
}
const delay = positionsRetryDelayMs.value;
positionsRetryTimer = window.setTimeout(async () => {
positionsRetryTimer = null;
await loadPositions();
}, delay);
positionsRetryDelayMs.value = Math.min(positionsRetryDelayMs.value * 2, POSITIONS_RETRY_MAX_DELAY);
}
export function usePositions() {
function watchEvent() {
unwatch = watchContractEvent(config as Config, {
address: HarbContract.contractAddress,
@ -325,7 +283,7 @@ export function usePositions() {
//initial loading positions
if (activePositions.value.length < 1 && loading.value === false) {
await loadPositions();
await loadPositions(activeChainId.value);
// await getMinStake();
}
@ -338,8 +296,10 @@ export function usePositions() {
if (!unwatchChainSwitch) {
unwatchChainSwitch = watchChainId(config as Config, {
async onChange(_chainId) {
await loadPositions();
async onChange(nextChainId) {
const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
activeChainId.value = resolvedChainId;
await loadPositions(resolvedChainId);
},
});
}
@ -347,7 +307,7 @@ export function usePositions() {
if (!unwatchAccountChanged) {
unwatchAccountChanged = watchAccount(config as Config, {
async onChange() {
await loadPositions();
await loadPositions(activeChainId.value);
},
});
}
@ -370,7 +330,7 @@ export function usePositions() {
unwatchAccountChanged();
unwatchAccountChanged = null;
}
clearPositionsRetryTimer();
retryManager.clear();
});
function createRandomPosition(amount: number = 1) {

View file

@ -6,6 +6,7 @@ import { useStatCollection } from './useStatCollection';
import { useAdjustTaxRate } from './useAdjustTaxRates';
import { calculateSnatchShortfall } from 'kraiken-lib/staking';
import { selectSnatchPositions, minimumTaxRate, type SnatchablePosition } from 'kraiken-lib/snatch';
import { DEFAULT_CHAIN_ID } from '@/config';
/**
* Converts Kraiken token assets to shares using the same formula as Stake.sol:
@ -24,11 +25,12 @@ function assetsToSharesLocal(assets: bigint, kraikenTotalSupply: bigint, stakeTo
return (assets * stakeTotalSupply) / kraikenTotalSupply;
}
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
const { activePositions } = usePositions();
const stake = useStake();
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>, chainId?: number) {
const wallet = useWallet();
const statCollection = useStatCollection();
const resolvedChainId = chainId ?? wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { activePositions } = usePositions(resolvedChainId);
const stake = useStake();
const statCollection = useStatCollection(resolvedChainId);
const adjustTaxRate = useAdjustTaxRate();
const snatchablePositions = ref<Position[]>([]);
@ -78,7 +80,7 @@ export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex;
const maxTaxRateDecimal =
typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex)
? adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY
? (adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY)
: Number.POSITIVE_INFINITY;
const includeOwned = demo;
const recipient = wallet.account.address ?? null;

View file

@ -1,17 +1,16 @@
import { ref, reactive, computed } from 'vue';
import axios from 'axios';
import { chainData } from './useWallet';
import { watchChainId } from '@wagmi/core';
import type { Config } from '@wagmi/core';
import { config } from '@/wagmi';
import logger from '@/utils/logger';
import type { WatchBlocksReturnType } from 'viem';
import { bigInt2Number } from '@/utils/helper';
import { DEFAULT_CHAIN_ID } from '@/config';
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
const demo = sessionStorage.getItem('demo') === 'true';
const GRAPHQL_TIMEOUT_MS = 15_000;
const RETRY_BASE_DELAY_MS = 1_500;
const RETRY_MAX_DELAY_MS = 60_000;
interface StatsRecord {
burnNextHourProjected: string;
@ -33,52 +32,13 @@ const rawStatsCollections = ref<Array<StatsRecord>>([]);
const loading = ref(false);
const initialized = ref(false);
const statsError = ref<string | null>(null);
const statsRetryDelayMs = ref(RETRY_BASE_DELAY_MS);
let statsRetryTimer: number | null = null;
const activeChainId = ref<number>(DEFAULT_CHAIN_ID);
function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown GraphQL error';
}
const retryManager = createRetryManager(loadStats, activeChainId);
function clearStatsRetryTimer() {
if (statsRetryTimer !== null) {
clearTimeout(statsRetryTimer);
statsRetryTimer = null;
}
}
function scheduleStatsRetry() {
if (typeof window === 'undefined') {
return;
}
if (statsRetryTimer !== null) {
return;
}
const delay = statsRetryDelayMs.value;
statsRetryTimer = window.setTimeout(async () => {
statsRetryTimer = null;
await loadStats();
}, delay);
statsRetryDelayMs.value = Math.min(statsRetryDelayMs.value * 2, RETRY_MAX_DELAY_MS);
}
export async function loadStatsCollection(endpoint: string) {
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
export async function loadStatsCollection(chainId: number, endpointOverride?: string) {
const endpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
logger.info(`loadStatsCollection for chainId: ${chainId}`);
const res = await axios.post(
endpoint,
{
@ -223,30 +183,34 @@ const claimedSlots = computed(() => {
}
});
export async function loadStats() {
export async function loadStats(chainId?: number) {
loading.value = true;
const endpoint = chainData.value?.graphql?.trim();
if (!endpoint) {
const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
activeChainId.value = targetChainId;
let endpoint: string;
try {
endpoint = resolveGraphqlEndpoint(targetChainId);
} catch (error) {
rawStatsCollections.value = [];
statsError.value = 'GraphQL endpoint not configured for this chain.';
clearStatsRetryTimer();
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
statsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
retryManager.clear();
retryManager.reset();
loading.value = false;
initialized.value = true;
return;
}
try {
rawStatsCollections.value = await loadStatsCollection(endpoint);
rawStatsCollections.value = await loadStatsCollection(targetChainId, endpoint);
statsError.value = null;
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
clearStatsRetryTimer();
retryManager.reset();
retryManager.clear();
} catch (error) {
// console.warn('[stats] loadStats() failed', error);
rawStatsCollections.value = [];
statsError.value = formatGraphqlError(error);
scheduleStatsRetry();
retryManager.schedule();
} finally {
loading.value = false;
initialized.value = true;
@ -256,11 +220,13 @@ export async function loadStats() {
import { onMounted, onUnmounted } from 'vue';
let unwatch: WatchBlocksReturnType | null = null;
export function useStatCollection() {
export function useStatCollection(chainId: number = DEFAULT_CHAIN_ID) {
activeChainId.value = chainId;
onMounted(async () => {
//initial loading stats
if (rawStatsCollections.value?.length === 0 && !loading.value) {
await loadStats();
await loadStats(activeChainId.value);
}
});
if (!unwatch) {
@ -268,8 +234,10 @@ export function useStatCollection() {
//chain Switch reload stats for other chain
unwatch = watchChainId(config as Config, {
async onChange(_chainId) {
await loadStats();
async onChange(nextChainId) {
const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
activeChainId.value = resolvedChainId;
await loadStats(resolvedChainId);
},
});
@ -281,7 +249,7 @@ export function useStatCollection() {
// })
}
onUnmounted(() => {
clearStatsRetryTimer();
retryManager.clear();
if (unwatch) {
unwatch();
}

View file

@ -1,12 +1,14 @@
import { ref, reactive, computed } from 'vue';
import { getAccount, getBalance, watchAccount, watchChainId, type Config } from '@wagmi/core';
import { getAccount, watchAccount, watchChainId, type Config } from '@wagmi/core';
import { type WatchAccountReturnType, type GetAccountReturnType, type GetBalanceReturnType } from '@wagmi/core';
import { config } from '@/wagmi';
import { config, KRAIKEN_LOCAL_CHAIN } from '@/wagmi';
import { getAllowance, HarbContract, getNonce } from '@/contracts/harb';
import logger from '@/utils/logger';
import { setHarbContract } from '@/contracts/harb';
import { setStakeContract } from '@/contracts/stake';
import { chainsData, DEFAULT_CHAIN_ID } from '@/config';
import { createPublicClient, custom, formatUnits, type EIP1193Provider, type PublicClient, type Transport, type WalletClient } from 'viem';
import { getWalletPublicClient, setWalletPublicClient } from '@/services/walletRpc';
const balance = ref<GetBalanceReturnType>({
value: 0n,
@ -25,24 +27,81 @@ export const chainData = computed(() => {
return chainsData.find(obj => obj.id === selectedChainId.value);
});
const TOKEN_DECIMALS = 18;
const TOKEN_SYMBOL = 'KRK';
async function syncWalletPublicClient(data: GetAccountReturnType | undefined = account.value): Promise<PublicClient | null> {
if (!data?.address || !data.connector) {
setWalletPublicClient(null);
return null;
}
try {
const connector = data.connector;
if (!connector || typeof connector.getProvider !== 'function') {
setWalletPublicClient(null);
return null;
}
const connectorAny = connector as {
getProvider?: () => Promise<unknown>;
getWalletClient?: (args: { chainId: number }) => Promise<WalletClient | null>;
};
const provider = connectorAny.getProvider ? await connectorAny.getProvider() : null;
const walletClient = connectorAny.getWalletClient
? await connectorAny.getWalletClient({ chainId: data.chainId ?? selectedChainId.value })
: null;
if (!provider || typeof (provider as { request?: unknown }).request !== 'function') {
setWalletPublicClient(null);
return null;
}
const chain = (walletClient as WalletClient | null)?.chain ?? KRAIKEN_LOCAL_CHAIN;
const transport = custom(provider as EIP1193Provider) as Transport;
const publicClient = createPublicClient({
chain,
transport,
}) as PublicClient;
setWalletPublicClient(publicClient);
return publicClient;
} catch (_error) {
logger.contract('Failed to create wallet public client');
setWalletPublicClient(null);
return null;
}
}
let unwatch: WatchAccountReturnType | null = null;
let unwatchChain: WatchAccountReturnType | null = null;
export function useWallet() {
async function loadBalance() {
logger.contract('loadBalance');
if (account.value.address) {
// console.log("HarbContract",HarbContract );
balance.value = await getBalance(config as Config, {
address: account.value.address,
token: HarbContract.contractAddress,
});
// console.log("balance.value", balance.value);
return balance.value;
} else {
const userAddress = account.value.address;
if (!userAddress) {
return 0n;
}
let publicClient = getWalletPublicClient();
if (!publicClient) {
publicClient = await syncWalletPublicClient();
}
if (!publicClient) {
return 0n;
}
const value = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'balanceOf',
args: [userAddress],
})) as bigint;
balance.value = {
value,
decimals: TOKEN_DECIMALS,
symbol: TOKEN_SYMBOL,
formatted: formatUnits(value, TOKEN_DECIMALS),
} as GetBalanceReturnType;
return value;
}
if (!unwatch) {
@ -54,6 +113,7 @@ export function useWallet() {
if (!data.address) {
logger.info(`disconnected`);
setWalletPublicClient(null);
balance.value = {
value: 0n,
decimals: 0,
@ -63,6 +123,7 @@ export function useWallet() {
} else if (account.value.address !== data.address || account.value.chainId !== data.chainId) {
logger.info(`Account changed!:`, data.address);
account.value = data;
await syncWalletPublicClient(data);
await loadBalance();
await getAllowance();
// await loadPositions();
@ -75,11 +136,16 @@ export function useWallet() {
}
//funzt nicht mehr-> library Änderung?
if (account.value.address) {
void syncWalletPublicClient(account.value);
}
if (!unwatchChain) {
// console.log("unwatchChain");
unwatchChain = watchChainId(config as Config, {
async onChange(_chainId) {
await syncWalletPublicClient(account.value);
await loadBalance();
await getAllowance();
await getNonce();

View file

@ -2,9 +2,9 @@ import deploymentsLocal from '../../onchain/deployments-local.json';
const env = import.meta.env;
const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? 'http://127.0.0.1:42069/graphql';
const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? 'http://127.0.0.1:43069';
const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? '/rpc/anvil';
const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? '/api/graphql';
const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? '/api/txn';
const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? '/api/rpc';
interface DeploymentContracts {
Kraiken?: string;

View file

@ -7,8 +7,7 @@ import { type Abi, type Address, type Hash } from 'viem';
import { StakeContract } from '@/contracts/stake';
import { getChain } from '@/config';
import logger from '@/utils/logger';
// const chain1 = useChain();
// console.log("chain1", chain1);
import { getWalletPublicClient } from '@/services/walletRpc';
interface Contract {
abi: Abi;
@ -36,16 +35,9 @@ function getHarbJson() {
}
export function setHarbContract() {
// console.log("setHarbContract");
HarbContract = getHarbJson();
}
// watch(chainData, async (newQuestion, oldQuestion) => {
// console.log("log harb update");
// });
export async function getAllowance() {
logger.contract('getAllowance');
@ -53,12 +45,18 @@ export async function getAllowance() {
if (!account.address) {
return 0n;
}
const result = await readContract(config as Config, {
const publicClient = getWalletPublicClient();
if (!publicClient) {
throw new Error('Wallet public client unavailable');
}
const result = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'allowance',
args: [account.address, StakeContract.contractAddress],
});
})) as bigint;
allowance.value = result;
return result;
}
@ -66,6 +64,18 @@ export async function getAllowance() {
export async function getMinStake() {
logger.contract('getMinStake');
const publicClient = getWalletPublicClient();
if (publicClient) {
const result = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'minStake',
args: [],
})) as bigint;
allowance.value = result;
return result;
}
const result: bigint = (await readContract(config as Config, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
@ -83,14 +93,19 @@ export async function getNonce() {
if (!account.address) {
return 0n;
}
// console.log("HarbContract.contractAddress", HarbContract.contractAddress);
// console.log('HarbContract.contractAddress', HarbContract.contractAddress);
const result = await readContract(config as Config, {
const publicClient = getWalletPublicClient();
if (!publicClient) {
throw new Error('Wallet public client unavailable');
}
const result = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'nonces',
args: [account.address],
});
})) as bigint;
nonce.value = result;
return result;
@ -99,15 +114,19 @@ export async function getNonce() {
export async function getName() {
logger.contract('getName');
const result = await readContract(config as Config, {
const publicClient = getWalletPublicClient();
if (!publicClient) {
throw new Error('Wallet public client unavailable');
}
const result = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'name',
args: [],
});
})) as string;
name.value = result;
return result as string;
return result;
}
export async function approve(amount: bigint): Promise<Hash> {

View file

@ -0,0 +1,38 @@
import { getChain } from '@/config';
type EndpointType = 'graphql' | 'rpc' | 'txnBot';
class ChainConfigService {
getEndpoint(chainId: number, type: EndpointType): string {
const chain = getChain(chainId);
if (!chain) {
throw new Error(`Chain ${chainId} is not configured.`);
}
switch (type) {
case 'graphql':
return this.ensureEndpoint(chain.graphql, chainId, 'GraphQL');
case 'rpc':
return this.ensureEndpoint(chain.cheats?.rpc, chainId, 'RPC');
case 'txnBot':
return this.ensureEndpoint(chain.cheats?.txnBot, chainId, 'txnBot');
default:
// Exhaustiveness check
assertNever(type);
}
}
private ensureEndpoint(value: string | null | undefined, chainId: number, label: string): string {
const normalized = value?.trim();
if (!normalized) {
throw new Error(`${label} endpoint not configured for chain ${chainId}.`);
}
return normalized;
}
}
function assertNever(_x: never): never {
throw new Error('Unsupported endpoint type');
}
export const chainConfigService = new ChainConfigService();

View file

@ -0,0 +1,11 @@
import type { PublicClient } from 'viem';
let currentPublicClient: PublicClient | null = null;
export function setWalletPublicClient(client: PublicClient | null): void {
currentPublicClient = client;
}
export function getWalletPublicClient(): PublicClient | null {
return currentPublicClient;
}

View file

@ -0,0 +1,107 @@
import { ref, type Ref } from 'vue';
import axios from 'axios';
import { chainConfigService } from '@/services/chainConfig';
const RETRY_BASE_DELAY_MS = 1_500;
const RETRY_MAX_DELAY_MS = 60_000;
export interface RetryManager {
schedule: () => void;
clear: () => void;
reset: () => void;
retryDelayMs: Ref<number>;
}
/**
* Creates a retry manager with exponential backoff for async operations.
*
* @param loadFn - The async function to retry (receives activeChainId as argument)
* @param activeChainId - Ref containing the current chain ID to pass to loadFn
* @param baseDelay - Initial retry delay in milliseconds (default: 1500ms)
* @param maxDelay - Maximum retry delay in milliseconds (default: 60000ms)
* @returns RetryManager with schedule, clear, and reset methods
*
* @example
* const retry = createRetryManager(loadPositions, activeChainId);
* try {
* await loadPositions(chainId);
* retry.reset(); // Reset on success
* } catch (error) {
* retry.schedule(); // Schedule retry with backoff
* }
*/
export function createRetryManager(
loadFn: (chainId: number) => Promise<void>,
activeChainId: Ref<number>,
baseDelay = RETRY_BASE_DELAY_MS,
maxDelay = RETRY_MAX_DELAY_MS
): RetryManager {
const retryDelayMs = ref(baseDelay);
let retryTimer: number | null = null;
function clear() {
if (retryTimer !== null) {
clearTimeout(retryTimer);
retryTimer = null;
}
}
function schedule() {
if (typeof window === 'undefined' || retryTimer !== null) {
return;
}
const delay = retryDelayMs.value;
retryTimer = window.setTimeout(async () => {
retryTimer = null;
await loadFn(activeChainId.value);
}, delay);
retryDelayMs.value = Math.min(retryDelayMs.value * 2, maxDelay);
}
function reset() {
retryDelayMs.value = baseDelay;
}
return { schedule, clear, reset, retryDelayMs };
}
/**
* Formats GraphQL/Axios errors into user-friendly messages.
*
* @param error - The error object from axios or other source
* @returns Formatted error message string
*/
export function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown GraphQL error';
}
/**
* Resolves a GraphQL endpoint URL for a given chain ID.
*
* @param chainId - The blockchain network ID
* @param endpointOverride - Optional endpoint URL to use instead of default
* @returns The GraphQL endpoint URL
* @throws Error if endpoint is not configured for the chain
*/
export function resolveGraphqlEndpoint(chainId: number, endpointOverride?: string): string {
const normalized = endpointOverride?.trim();
if (normalized) {
return normalized;
}
return chainConfigService.getEndpoint(chainId, 'graphql');
}

View file

@ -81,10 +81,11 @@ const { status } = useAccount();
const showPanel = inject('showPanel');
import { InsertCommaNumber } from '@/utils/helper';
const { myActivePositions, tresholdValue, activePositions } = usePositions();
const stats = useStatCollection();
const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { myActivePositions, tresholdValue, activePositions } = usePositions(initialChainId);
const stats = useStatCollection(initialChainId);
const chains = useChains();
function calculateAverageTaxRate(data: Array<{ taxRate: number | string }>): number {

View file

@ -3,9 +3,9 @@ import { baseSepolia } from '@wagmi/vue/chains';
import { coinbaseWallet, walletConnect } from '@wagmi/vue/connectors';
import { defineChain } from 'viem';
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? '/rpc/anvil';
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? '/api/rpc';
const kraikenLocalFork = defineChain({
export const KRAIKEN_LOCAL_CHAIN = defineChain({
id: 31337,
name: 'Kraiken Local Fork',
network: 'kraiken-local',
@ -21,7 +21,7 @@ const kraikenLocalFork = defineChain({
});
export const config = createConfig({
chains: [kraikenLocalFork, baseSepolia],
chains: [KRAIKEN_LOCAL_CHAIN, baseSepolia],
storage: createStorage({ storage: window.localStorage }),
connectors: [
@ -44,11 +44,11 @@ export const config = createConfig({
}),
],
transports: {
[kraikenLocalFork.id]: http(LOCAL_RPC_URL),
[KRAIKEN_LOCAL_CHAIN.id]: http(LOCAL_RPC_URL),
[baseSepolia.id]: http(),
},
});
if (typeof window !== 'undefined' && config.state.chainId !== kraikenLocalFork.id) {
config.setState(state => ({ ...state, chainId: kraikenLocalFork.id }));
if (typeof window !== 'undefined' && config.state.chainId !== KRAIKEN_LOCAL_CHAIN.id) {
config.setState(state => ({ ...state, chainId: KRAIKEN_LOCAL_CHAIN.id }));
}

View file

@ -7,6 +7,8 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig(() => {
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET
const localGraphqlProxyTarget = process.env.VITE_LOCAL_GRAPHQL_PROXY_TARGET ?? 'http://127.0.0.1:42069'
const localTxnProxyTarget = process.env.VITE_LOCAL_TXN_PROXY_TARGET ?? 'http://127.0.0.1:43069'
return {
// base: "/HarbergPublic/",
@ -21,19 +23,47 @@ export default defineConfig(() => {
},
},
server: {
proxy: localRpcProxyTarget
? {
'/rpc/anvil': {
target: localRpcProxyTarget,
changeOrigin: true,
secure: false,
rewrite: (path) => {
const rewritten = path.replace(/^\/rpc\/anvil/, '')
return rewritten.length === 0 ? '/' : rewritten
},
},
}
: undefined,
proxy:
localRpcProxyTarget || localGraphqlProxyTarget || localTxnProxyTarget
? {
...(localRpcProxyTarget
? {
'/api/rpc': {
target: localRpcProxyTarget,
changeOrigin: true,
secure: false,
rewrite: (path: string) => {
const rewritten = path.replace(/^\/api\/rpc/, '')
return rewritten.length === 0 ? '/' : rewritten
},
},
}
: {}),
...(localGraphqlProxyTarget
? {
'/api/graphql': {
target: localGraphqlProxyTarget,
changeOrigin: true,
secure: false,
rewrite: (path: string) => path.replace(/^\/api\/graphql/, '/graphql'),
},
}
: {}),
...(localTxnProxyTarget
? {
'/api/txn': {
target: localTxnProxyTarget,
changeOrigin: true,
secure: false,
rewrite: (path: string) => {
const rewritten = path.replace(/^\/api\/txn/, '')
return rewritten.length === 0 ? '/' : rewritten
},
},
}
: {}),
}
: undefined,
},
}
})