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 ## Testing & Tooling
- Contracts: run `forge build`, `forge test`, and `forge snapshot` inside `onchain/`. - 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. - 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. - **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 ## Version Validation System
@ -65,8 +65,8 @@
- `anvil --fork-url https://sepolia.base.org` - manual fork when diagnosing outside the helper script. - `anvil --fork-url https://sepolia.base.org` - manual fork when diagnosing outside the helper script.
- `cast call <POOL> "slot0()"` - inspect pool state. - `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. - `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 -X POST http://localhost:8081/api/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
- `curl http://127.0.0.1:43069/status` - `curl http://localhost:8081/api/txn/status`
## References ## References
- Deployment history: `onchain/deployments-local.json`, `onchain/broadcast/`. - Deployment history: `onchain/deployments-local.json`, `onchain/broadcast/`.

View file

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

View file

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

View file

@ -54,13 +54,15 @@ else
fi fi
export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337} 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_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_KRAIKEN_ADDRESS=$KRAIKEN
export VITE_STAKE_ADDRESS=$STAKE export VITE_STAKE_ADDRESS=$STAKE
export VITE_SWAP_ROUTER=$SWAP_ROUTER export VITE_SWAP_ROUTER=$SWAP_ROUTER
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/app/graphql} 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:-/app/txn} export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn}
export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1} export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1}
exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/ 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) - `ponder` `npm run dev` for the indexer (port 42069 inside the pod)
- `frontend` Vite dev server for `web-app` (port 5173 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) - `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. 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) ### Access Points (via Caddy)
- Frontend: `http://<host>/` - Frontend: `http://<host>/`
- GraphQL: `http://<host>/graphql` - GraphQL: `http://<host>/api/graphql`
- RPC passthrough: `http://<host>/rpc/anvil` - RPC passthrough: `http://<host>/api/rpc`
- Txn bot status: `http://<host>/txn/status` - Txn bot status: `http://<host>/api/txn/status`
## Configuration Knobs ## Configuration Knobs
Set environment variables before `podman-compose up`: Set environment variables before `podman-compose up`:

View file

@ -219,7 +219,6 @@
"integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.2", "@babel/code-frame": "^7.24.2",
@ -3107,7 +3106,6 @@
"integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.13.0" "undici-types": "~7.13.0"
} }
@ -3192,7 +3190,6 @@
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/scope-manager": "8.45.0",
"@typescript-eslint/types": "8.45.0", "@typescript-eslint/types": "8.45.0",
@ -3520,7 +3517,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3943,7 +3939,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001587", "caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668", "electron-to-chromium": "^1.4.668",
@ -4780,7 +4775,6 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -5521,7 +5515,6 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
} }
@ -5606,7 +5599,6 @@
"integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"workspaces": [ "workspaces": [
"website" "website"
], ],
@ -6266,7 +6258,6 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@ -9249,7 +9240,6 @@
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -9572,7 +9562,6 @@
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "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" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 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: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" 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, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@ -30,6 +30,7 @@
"os": [ "os": [
"aix" "aix"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -47,6 +48,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -64,6 +66,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -81,6 +84,7 @@
"os": [ "os": [
"android" "android"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -98,6 +102,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -115,6 +120,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -132,6 +138,7 @@
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -149,6 +156,7 @@
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -166,6 +174,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -183,6 +192,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -200,6 +210,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -217,6 +228,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -234,6 +246,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -251,6 +264,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -268,6 +282,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -285,6 +300,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -302,6 +318,7 @@
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -319,6 +336,7 @@
"os": [ "os": [
"netbsd" "netbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -336,6 +354,7 @@
"os": [ "os": [
"netbsd" "netbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -353,6 +372,7 @@
"os": [ "os": [
"openbsd" "openbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -370,6 +390,7 @@
"os": [ "os": [
"openbsd" "openbsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -387,6 +408,7 @@
"os": [ "os": [
"openharmony" "openharmony"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -404,6 +426,7 @@
"os": [ "os": [
"sunos" "sunos"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -421,6 +444,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -438,6 +462,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -455,6 +480,7 @@
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@ -1269,7 +1295,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.2", "version": "4.52.2",
@ -1283,7 +1310,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.2", "version": "4.52.2",
@ -1297,7 +1325,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.2", "version": "4.52.2",
@ -1311,7 +1340,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.2", "version": "4.52.2",
@ -1325,7 +1355,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.2", "version": "4.52.2",
@ -1339,7 +1370,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.2", "version": "4.52.2",
@ -1353,7 +1385,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.2", "version": "4.52.2",
@ -1367,7 +1400,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1381,7 +1415,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.2", "version": "4.52.2",
@ -1395,7 +1430,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1409,7 +1445,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1423,7 +1460,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1437,7 +1475,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.2", "version": "4.52.2",
@ -1451,7 +1490,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1465,7 +1505,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1479,7 +1520,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.2", "version": "4.52.2",
@ -1493,7 +1535,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.2", "version": "4.52.2",
@ -1507,7 +1550,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"openharmony" "openharmony"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.2", "version": "4.52.2",
@ -1521,7 +1565,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.2", "version": "4.52.2",
@ -1535,7 +1580,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.2", "version": "4.52.2",
@ -1549,7 +1595,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"peer": true
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.2", "version": "4.52.2",
@ -1563,7 +1610,8 @@
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ],
"peer": true
}, },
"node_modules/@skorotkiewicz/snowflake-id": { "node_modules/@skorotkiewicz/snowflake-id": {
"version": "1.0.1", "version": "1.0.1",
@ -1858,7 +1906,8 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
@ -2208,6 +2257,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@ -2338,7 +2388,6 @@
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@ -2412,6 +2461,7 @@
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
}, },
@ -2688,7 +2738,6 @@
"integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
@ -2713,7 +2762,6 @@
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"dev": true, "dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true,
"dependencies": { "dependencies": {
"detect-libc": "^2.0.3" "detect-libc": "^2.0.3"
}, },
@ -3085,6 +3133,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@ -3184,7 +3233,8 @@
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC",
"peer": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.3",
@ -3296,6 +3346,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -3416,7 +3467,6 @@
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -3441,7 +3491,6 @@
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@ -3555,6 +3604,7 @@
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
@ -3824,8 +3874,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",
@ -3874,6 +3923,7 @@
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fdir": "^6.5.0", "fdir": "^6.5.0",
"picomatch": "^4.0.3" "picomatch": "^4.0.3"
@ -3917,13 +3967,6 @@
"node": ">= 0.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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -4005,6 +4048,7 @@
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -4085,6 +4129,7 @@
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
@ -4169,7 +4214,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "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 PID_FILE=/tmp/kraiken-watcher.pid
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")} PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}
container_name() {
local service="$1"
echo "${PROJECT_NAME}_${service}_1"
}
cleanup_existing() { cleanup_existing() {
# Kill any existing watch scripts # Kill any existing watch scripts
pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true
@ -24,11 +29,11 @@ cleanup_existing() {
rm -f "$PID_FILE" rm -f "$PID_FILE"
# Kill zombie podman processes # 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) # Remove any existing containers (suppress errors if they don't exist)
echo " Cleaning up existing containers..." 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 xargs -r podman rm -f 2>&1 | grep -v "Error.*no container" || true
} }
@ -94,32 +99,32 @@ start_stack() {
echo " Starting anvil & postgres..." echo " Starting anvil & postgres..."
podman-compose up -d anvil postgres 2>&1 | grep -v "STEP\|Copying\|Writing\|Getting\|fetch\|Installing\|Executing" || true 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 "$(container_name anvil)" "$ANVIL_TIMEOUT" || exit 1
wait_for_healthy harb_postgres_1 "$POSTGRES_TIMEOUT" || exit 1 wait_for_healthy "$(container_name postgres)" "$POSTGRES_TIMEOUT" || exit 1
# Phase 2: Start bootstrap (depends on anvil & postgres healthy) # Phase 2: Start bootstrap (depends on anvil & postgres healthy)
echo " Starting bootstrap..." echo " Starting bootstrap..."
podman-compose up -d bootstrap >/dev/null 2>&1 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) # Phase 3: Start ponder (depends on bootstrap completed)
echo " Starting ponder..." echo " Starting ponder..."
podman-compose up -d ponder >/dev/null 2>&1 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) # Phase 4: Start frontend services (depend on ponder healthy)
echo " Starting webapp, landing, txn-bot..." echo " Starting webapp, landing, txn-bot..."
podman-compose up -d webapp landing txn-bot >/dev/null 2>&1 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) # Phase 5: Start caddy (depends on frontend services)
echo " Starting caddy..." echo " Starting caddy..."
podman-compose up -d caddy >/dev/null 2>&1 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 if [[ -z "${SKIP_WATCH:-}" ]]; then
echo "Watching for kraiken-lib changes..." echo "Watching for kraiken-lib changes..."
@ -131,7 +136,8 @@ start_stack() {
echo "" echo ""
echo "[ok] Stack started in ${total_time}s" echo "[ok] Stack started in ${total_time}s"
echo " Web App: http://localhost:8081/app/" 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() { stop_stack() {

View file

@ -3,9 +3,9 @@ import {
formatHealthCheckError, formatHealthCheckError,
} from './health-checks.js'; } 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_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 { export interface StackConfig {
rpcUrl: string; 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 { useWallet } from '@/composables/useWallet';
import { ref, onMounted, watch, computed, watchEffect, getCurrentInstance } from 'vue'; import { ref, onMounted, watch, computed, watchEffect, getCurrentInstance } from 'vue';
import { useStatCollection, loadStats } from '@/composables/useStatCollection'; import { useStatCollection, loadStats } from '@/composables/useStatCollection';
import { DEFAULT_CHAIN_ID } from '@/config';
const demo = sessionStorage.getItem('demo') === 'true'; const demo = sessionStorage.getItem('demo') === 'true';
@ -168,8 +169,10 @@ const taxRateIndex = ref<number>(defaultTaxRateIndex);
const stake = useStake(); const stake = useStake();
const _claim = useClaim(); const _claim = useClaim();
const wallet = useWallet(); const wallet = useWallet();
const statCollection = useStatCollection(); const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { activePositions: _activePositions } = usePositions(); const statCollection = useStatCollection(initialChainId);
const { activePositions: _activePositions } = usePositions(initialChainId);
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const uid = instance?.uid ?? Math.floor(Math.random() * 10000); 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.`; 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 floorTaxDisplay = computed(() => `${formatNumber(snatchSelection.floorTax.value ?? 0, 2)} %`);
const floorTaxHelpText = 'Your tax needs to exceed this value to displace an existing position.'; 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); const snatchAblePositionsIds = snatchSelection.snatchablePositions.value.map((p: Position) => p.positionId);
await stake.snatch(stake.stakingAmount, taxRateIndex.value, snatchAblePositionsIds); await stake.snatch(stake.stakingAmount, taxRateIndex.value, snatchAblePositionsIds);
} }
await loadPositions(); await loadPositions(currentChainId.value);
await loadStats(); await loadStats(currentChainId.value);
} }
async function handleSubmit() { async function handleSubmit() {

View file

@ -10,9 +10,13 @@ import { useStatCollection } from '@/composables/useStatCollection';
import { useStake } from '@/composables/useStake'; import { useStake } from '@/composables/useStake';
import { usePositions, type Position } from '@/composables/usePositions'; import { usePositions, type Position } from '@/composables/usePositions';
import { useDark } from '@/composables/useDark'; import { useDark } from '@/composables/useDark';
import { useWallet } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
const { darkTheme } = useDark(); 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 ignoreOwner = ref(false);
const taxRate = ref<number>(1.0); const taxRate = ref<number>(1.0);
@ -26,7 +30,7 @@ const stakeAbleHarbAmount = computed(() => statCollection.kraikenTotalSupply / 5
const minStake = computed(() => stakeAbleHarbAmount.value / 600n); const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
const stake = useStake(); const stake = useStake();
const statCollection = useStatCollection(); const statCollection = useStatCollection(initialChainId);
const snatchPositions = computed(() => { const snatchPositions = computed(() => {
if ( if (
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <= 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 { getTaxDue, payTax } from '@/contracts/stake';
import { type Position, loadPositions } from '@/composables/usePositions'; import { type Position, loadPositions } from '@/composables/usePositions';
import { useStatCollection } from '@/composables/useStatCollection'; import { useStatCollection } from '@/composables/useStatCollection';
import { useWallet } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
import { formatUnits } from 'viem'; import { formatUnits } from 'viem';
const unstake = useUnstake(); const unstake = useUnstake();
const adjustTaxRate = useAdjustTaxRate(); 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<{ const props = defineProps<{
taxRate: number; taxRate: number;
@ -134,7 +139,7 @@ async function unstakePosition() {
await unstake.exitPosition(props.id); await unstake.exitPosition(props.id);
loading.value = true; loading.value = true;
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
await loadPositions(); await loadPositions(currentChainId.value);
loading.value = false; loading.value = false;
} }

View file

@ -16,7 +16,7 @@
<div class="connected-tokens"> <div class="connected-tokens">
<FOutput name="$KRK" :price="harbAmount" :variant="3"> <FOutput name="$KRK" :price="harbAmount" :variant="3">
<template #end> <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> </template>
</FOutput> </FOutput>
<FOutput name="Staked $KRK" :price="compactNumber(stakedAmount)" :variant="3"> <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 { usePositions } from "@/composables/usePositions";
// import { useWallet } from "@/composables/useWallet"; // import { useWallet } from "@/composables/useWallet";
import { usePositions } from '@/composables/usePositions'; import { usePositions } from '@/composables/usePositions';
import { useWallet } from '@/composables/useWallet'; import { useWallet, chainData as walletChainData } from '@/composables/useWallet';
import { useChain } from '@/composables/useChain'; import { DEFAULT_CHAIN_ID } from '@/config';
import { useAccount, useDisconnect, useConnect, useChainId, type CreateConnectorFn, type Connector } from '@wagmi/vue'; import { useAccount, useDisconnect, useConnect, useChainId, type CreateConnectorFn, type Connector } from '@wagmi/vue';
@ -74,9 +74,10 @@ const { disconnect } = useDisconnect();
const { connectors, connect } = useConnect(); const { connectors, connect } = useConnect();
const router = useRouter(); const router = useRouter();
const chainId = useChainId(); const chainId = useChainId();
const { myActivePositions } = usePositions(); const initialChainId = chainId.value ?? DEFAULT_CHAIN_ID;
const { myActivePositions } = usePositions(initialChainId);
const wallet = useWallet(); const wallet = useWallet();
const chain = useChain(); const chain = walletChainData;
function loadConnectorImage(connector: Connector) { function loadConnectorImage(connector: Connector) {
if (connector.icon) { if (connector.icon) {
return 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 { bytesToUint256 } from 'kraiken-lib';
import { bigInt2Number } from '@/utils/helper'; import { bigInt2Number } from '@/utils/helper';
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates'; import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
import { chainData } from '@/composables/useWallet';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { DEFAULT_CHAIN_ID } from '@/config';
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
const rawActivePositions = ref<Array<Position>>([]); const rawActivePositions = ref<Array<Position>>([]);
const rawClosedPositoins = ref<Array<Position>>([]); const rawClosedPositoins = ref<Array<Position>>([]);
const loading = ref(false); const loading = ref(false);
const positionsError = ref<string | null>(null); 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; 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 activePositions = computed(() => {
const account = getAccount(config as Config); const account = getAccount(config as Config);
@ -108,9 +108,7 @@ const myActivePositions: ComputedRef<Position[]> = computed(() =>
const tresholdValue = computed(() => { const tresholdValue = computed(() => {
// Compute average tax rate index instead of percentage to avoid floating-point issues // Compute average tax rate index instead of percentage to avoid floating-point issues
const validIndices = activePositions.value const validIndices = activePositions.value.map(obj => obj.taxRateIndex).filter((idx): idx is number => typeof idx === 'number');
.map(obj => obj.taxRateIndex)
.filter((idx): idx is number => typeof idx === 'number');
if (validIndices.length === 0) return 0; if (validIndices.length === 0) return 0;
@ -121,14 +119,11 @@ const tresholdValue = computed(() => {
return Math.floor(avgIndex / 2); return Math.floor(avgIndex / 2);
}); });
export async function loadActivePositions(endpoint?: string) { export async function loadActivePositions(chainId: number, endpointOverride?: string) {
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`); const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim(); logger.info(`loadActivePositions for chainId: ${chainId}`);
if (!targetEndpoint) {
throw new Error('GraphQL endpoint not configured for this chain.');
}
// console.log("chainData.value?.graphql", targetEndpoint); // console.log("graphql endpoint", targetEndpoint);
const res = await axios.post( const res = await axios.post(
targetEndpoint, targetEndpoint,
@ -173,12 +168,9 @@ function formatId(id: Hex) {
return bigIntId; return bigIntId;
} }
export async function loadMyClosedPositions(endpoint: string | undefined, account: GetAccountReturnType) { export async function loadMyClosedPositions(chainId: number, endpointOverride: string | undefined, account: GetAccountReturnType) {
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`); const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim(); logger.info(`loadMyClosedPositions for chainId: ${chainId}`);
if (!targetEndpoint) {
throw new Error('GraphQL endpoint not configured for this chain.');
}
const res = await axios.post( const res = await axios.post(
targetEndpoint, targetEndpoint,
{ {
@ -214,36 +206,41 @@ export async function loadMyClosedPositions(endpoint: string | undefined, accoun
})) as Position[]; })) as Position[];
} }
export async function loadPositions() { export async function loadPositions(chainId?: number) {
loading.value = true; loading.value = true;
const endpoint = chainData.value?.graphql?.trim(); const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
if (!endpoint) { activeChainId.value = targetChainId;
let endpoint: string;
try {
endpoint = resolveGraphqlEndpoint(targetChainId);
} catch (error) {
rawActivePositions.value = []; rawActivePositions.value = [];
rawClosedPositoins.value = []; rawClosedPositoins.value = [];
positionsError.value = 'GraphQL endpoint not configured for this chain.'; positionsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
clearPositionsRetryTimer(); retryManager.clear();
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY; retryManager.reset();
loading.value = false; loading.value = false;
return; return;
} }
try { try {
rawActivePositions.value = await loadActivePositions(endpoint); rawActivePositions.value = await loadActivePositions(targetChainId, endpoint);
const account = getAccount(config as Config); const account = getAccount(config as Config);
if (account.address) { if (account.address) {
rawClosedPositoins.value = await loadMyClosedPositions(endpoint, account); rawClosedPositoins.value = await loadMyClosedPositions(targetChainId, endpoint, account);
} else { } else {
rawClosedPositoins.value = []; rawClosedPositoins.value = [];
} }
positionsError.value = null; positionsError.value = null;
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY; retryManager.reset();
clearPositionsRetryTimer(); retryManager.clear();
} catch (error) { } catch (error) {
rawActivePositions.value = []; rawActivePositions.value = [];
rawClosedPositoins.value = []; rawClosedPositoins.value = [];
positionsError.value = formatGraphqlError(error); positionsError.value = formatGraphqlError(error);
schedulePositionsRetry(); retryManager.schedule();
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -253,48 +250,9 @@ let unwatch: WatchEventReturnType | null;
let unwatchPositionRemovedEvent: WatchEventReturnType | null; let unwatchPositionRemovedEvent: WatchEventReturnType | null;
let unwatchChainSwitch: WatchChainIdReturnType | null; let unwatchChainSwitch: WatchChainIdReturnType | null;
let unwatchAccountChanged: WatchAccountReturnType | 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() { function watchEvent() {
unwatch = watchContractEvent(config as Config, { unwatch = watchContractEvent(config as Config, {
address: HarbContract.contractAddress, address: HarbContract.contractAddress,
@ -325,7 +283,7 @@ export function usePositions() {
//initial loading positions //initial loading positions
if (activePositions.value.length < 1 && loading.value === false) { if (activePositions.value.length < 1 && loading.value === false) {
await loadPositions(); await loadPositions(activeChainId.value);
// await getMinStake(); // await getMinStake();
} }
@ -338,8 +296,10 @@ export function usePositions() {
if (!unwatchChainSwitch) { if (!unwatchChainSwitch) {
unwatchChainSwitch = watchChainId(config as Config, { unwatchChainSwitch = watchChainId(config as Config, {
async onChange(_chainId) { async onChange(nextChainId) {
await loadPositions(); const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
activeChainId.value = resolvedChainId;
await loadPositions(resolvedChainId);
}, },
}); });
} }
@ -347,7 +307,7 @@ export function usePositions() {
if (!unwatchAccountChanged) { if (!unwatchAccountChanged) {
unwatchAccountChanged = watchAccount(config as Config, { unwatchAccountChanged = watchAccount(config as Config, {
async onChange() { async onChange() {
await loadPositions(); await loadPositions(activeChainId.value);
}, },
}); });
} }
@ -370,7 +330,7 @@ export function usePositions() {
unwatchAccountChanged(); unwatchAccountChanged();
unwatchAccountChanged = null; unwatchAccountChanged = null;
} }
clearPositionsRetryTimer(); retryManager.clear();
}); });
function createRandomPosition(amount: number = 1) { function createRandomPosition(amount: number = 1) {

View file

@ -6,6 +6,7 @@ import { useStatCollection } from './useStatCollection';
import { useAdjustTaxRate } from './useAdjustTaxRates'; import { useAdjustTaxRate } from './useAdjustTaxRates';
import { calculateSnatchShortfall } from 'kraiken-lib/staking'; import { calculateSnatchShortfall } from 'kraiken-lib/staking';
import { selectSnatchPositions, minimumTaxRate, type SnatchablePosition } from 'kraiken-lib/snatch'; 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: * 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; return (assets * stakeTotalSupply) / kraikenTotalSupply;
} }
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) { export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>, chainId?: number) {
const { activePositions } = usePositions();
const stake = useStake();
const wallet = useWallet(); 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 adjustTaxRate = useAdjustTaxRate();
const snatchablePositions = ref<Position[]>([]); const snatchablePositions = ref<Position[]>([]);
@ -78,7 +80,7 @@ export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex; const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex;
const maxTaxRateDecimal = const maxTaxRateDecimal =
typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex) typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex)
? adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY ? (adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY)
: Number.POSITIVE_INFINITY; : Number.POSITIVE_INFINITY;
const includeOwned = demo; const includeOwned = demo;
const recipient = wallet.account.address ?? null; const recipient = wallet.account.address ?? null;

View file

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

View file

@ -1,12 +1,14 @@
import { ref, reactive, computed } from 'vue'; 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 { 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 { getAllowance, HarbContract, getNonce } from '@/contracts/harb';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { setHarbContract } from '@/contracts/harb'; import { setHarbContract } from '@/contracts/harb';
import { setStakeContract } from '@/contracts/stake'; import { setStakeContract } from '@/contracts/stake';
import { chainsData, DEFAULT_CHAIN_ID } from '@/config'; 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>({ const balance = ref<GetBalanceReturnType>({
value: 0n, value: 0n,
@ -25,24 +27,81 @@ export const chainData = computed(() => {
return chainsData.find(obj => obj.id === selectedChainId.value); 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 unwatch: WatchAccountReturnType | null = null;
let unwatchChain: WatchAccountReturnType | null = null; let unwatchChain: WatchAccountReturnType | null = null;
export function useWallet() { export function useWallet() {
async function loadBalance() { async function loadBalance() {
logger.contract('loadBalance'); logger.contract('loadBalance');
if (account.value.address) { const userAddress = account.value.address;
// console.log("HarbContract",HarbContract ); if (!userAddress) {
balance.value = await getBalance(config as Config, {
address: account.value.address,
token: HarbContract.contractAddress,
});
// console.log("balance.value", balance.value);
return balance.value;
} else {
return 0n; 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) { if (!unwatch) {
@ -54,6 +113,7 @@ export function useWallet() {
if (!data.address) { if (!data.address) {
logger.info(`disconnected`); logger.info(`disconnected`);
setWalletPublicClient(null);
balance.value = { balance.value = {
value: 0n, value: 0n,
decimals: 0, decimals: 0,
@ -63,6 +123,7 @@ export function useWallet() {
} else if (account.value.address !== data.address || account.value.chainId !== data.chainId) { } else if (account.value.address !== data.address || account.value.chainId !== data.chainId) {
logger.info(`Account changed!:`, data.address); logger.info(`Account changed!:`, data.address);
account.value = data; account.value = data;
await syncWalletPublicClient(data);
await loadBalance(); await loadBalance();
await getAllowance(); await getAllowance();
// await loadPositions(); // await loadPositions();
@ -75,11 +136,16 @@ export function useWallet() {
} }
//funzt nicht mehr-> library Änderung? //funzt nicht mehr-> library Änderung?
if (account.value.address) {
void syncWalletPublicClient(account.value);
}
if (!unwatchChain) { if (!unwatchChain) {
// console.log("unwatchChain"); // console.log("unwatchChain");
unwatchChain = watchChainId(config as Config, { unwatchChain = watchChainId(config as Config, {
async onChange(_chainId) { async onChange(_chainId) {
await syncWalletPublicClient(account.value);
await loadBalance(); await loadBalance();
await getAllowance(); await getAllowance();
await getNonce(); await getNonce();

View file

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

View file

@ -7,8 +7,7 @@ import { type Abi, type Address, type Hash } from 'viem';
import { StakeContract } from '@/contracts/stake'; import { StakeContract } from '@/contracts/stake';
import { getChain } from '@/config'; import { getChain } from '@/config';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
// const chain1 = useChain(); import { getWalletPublicClient } from '@/services/walletRpc';
// console.log("chain1", chain1);
interface Contract { interface Contract {
abi: Abi; abi: Abi;
@ -36,16 +35,9 @@ function getHarbJson() {
} }
export function setHarbContract() { export function setHarbContract() {
// console.log("setHarbContract");
HarbContract = getHarbJson(); HarbContract = getHarbJson();
} }
// watch(chainData, async (newQuestion, oldQuestion) => {
// console.log("log harb update");
// });
export async function getAllowance() { export async function getAllowance() {
logger.contract('getAllowance'); logger.contract('getAllowance');
@ -53,12 +45,18 @@ export async function getAllowance() {
if (!account.address) { if (!account.address) {
return 0n; 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, abi: HarbContract.abi,
address: HarbContract.contractAddress, address: HarbContract.contractAddress,
functionName: 'allowance', functionName: 'allowance',
args: [account.address, StakeContract.contractAddress], args: [account.address, StakeContract.contractAddress],
}); })) as bigint;
allowance.value = result; allowance.value = result;
return result; return result;
} }
@ -66,6 +64,18 @@ export async function getAllowance() {
export async function getMinStake() { export async function getMinStake() {
logger.contract('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, { const result: bigint = (await readContract(config as Config, {
abi: HarbContract.abi, abi: HarbContract.abi,
address: HarbContract.contractAddress, address: HarbContract.contractAddress,
@ -83,14 +93,19 @@ export async function getNonce() {
if (!account.address) { if (!account.address) {
return 0n; 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, abi: HarbContract.abi,
address: HarbContract.contractAddress, address: HarbContract.contractAddress,
functionName: 'nonces', functionName: 'nonces',
args: [account.address], args: [account.address],
}); })) as bigint;
nonce.value = result; nonce.value = result;
return result; return result;
@ -99,15 +114,19 @@ export async function getNonce() {
export async function getName() { export async function getName() {
logger.contract('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, abi: HarbContract.abi,
address: HarbContract.contractAddress, address: HarbContract.contractAddress,
functionName: 'name', functionName: 'name',
args: [], args: [],
}); })) as string;
name.value = result; name.value = result;
return result as string; return result;
} }
export async function approve(amount: bigint): Promise<Hash> { 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'); const showPanel = inject('showPanel');
import { InsertCommaNumber } from '@/utils/helper'; import { InsertCommaNumber } from '@/utils/helper';
const { myActivePositions, tresholdValue, activePositions } = usePositions();
const stats = useStatCollection();
const wallet = useWallet(); const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { myActivePositions, tresholdValue, activePositions } = usePositions(initialChainId);
const stats = useStatCollection(initialChainId);
const chains = useChains(); const chains = useChains();
function calculateAverageTaxRate(data: Array<{ taxRate: number | string }>): number { 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 { coinbaseWallet, walletConnect } from '@wagmi/vue/connectors';
import { defineChain } from 'viem'; 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, id: 31337,
name: 'Kraiken Local Fork', name: 'Kraiken Local Fork',
network: 'kraiken-local', network: 'kraiken-local',
@ -21,7 +21,7 @@ const kraikenLocalFork = defineChain({
}); });
export const config = createConfig({ export const config = createConfig({
chains: [kraikenLocalFork, baseSepolia], chains: [KRAIKEN_LOCAL_CHAIN, baseSepolia],
storage: createStorage({ storage: window.localStorage }), storage: createStorage({ storage: window.localStorage }),
connectors: [ connectors: [
@ -44,11 +44,11 @@ export const config = createConfig({
}), }),
], ],
transports: { transports: {
[kraikenLocalFork.id]: http(LOCAL_RPC_URL), [KRAIKEN_LOCAL_CHAIN.id]: http(LOCAL_RPC_URL),
[baseSepolia.id]: http(), [baseSepolia.id]: http(),
}, },
}); });
if (typeof window !== 'undefined' && config.state.chainId !== kraikenLocalFork.id) { if (typeof window !== 'undefined' && config.state.chainId !== KRAIKEN_LOCAL_CHAIN.id) {
config.setState(state => ({ ...state, chainId: kraikenLocalFork.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/ // https://vite.dev/config/
export default defineConfig(() => { export default defineConfig(() => {
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET 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 { return {
// base: "/HarbergPublic/", // base: "/HarbergPublic/",
@ -21,19 +23,47 @@ export default defineConfig(() => {
}, },
}, },
server: { server: {
proxy: localRpcProxyTarget proxy:
? { localRpcProxyTarget || localGraphqlProxyTarget || localTxnProxyTarget
'/rpc/anvil': { ? {
target: localRpcProxyTarget, ...(localRpcProxyTarget
changeOrigin: true, ? {
secure: false, '/api/rpc': {
rewrite: (path) => { target: localRpcProxyTarget,
const rewritten = path.replace(/^\/rpc\/anvil/, '') changeOrigin: true,
return rewritten.length === 0 ? '/' : rewritten secure: false,
}, rewrite: (path: string) => {
}, const rewritten = path.replace(/^\/api\/rpc/, '')
} return rewritten.length === 0 ? '/' : rewritten
: undefined, },
},
}
: {}),
...(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,
}, },
} }
}) })