improve web-app config
This commit is contained in:
parent
280e2973cd
commit
371a8557b7
28 changed files with 831 additions and 344 deletions
|
|
@ -28,7 +28,7 @@
|
|||
## Testing & Tooling
|
||||
- Contracts: run `forge build`, `forge test`, and `forge snapshot` inside `onchain/`.
|
||||
- Fuzzing: scripts under `onchain/analysis/` (e.g., `./analysis/run-fuzzing.sh [optimizer] debugCSV`) generate replayable scenarios.
|
||||
- Integration: after the stack boots, inspect Anvil logs, hit `http://localhost:42069/graphql` for Ponder, and poll `http://127.0.0.1:43069/status` for txnBot health.
|
||||
- Integration: after the stack boots, inspect Anvil logs, hit `http://localhost:8081/api/graphql` for Ponder, and poll `http://localhost:8081/api/txn/status` for txnBot health.
|
||||
- **E2E Tests**: Playwright-based full-stack tests in `tests/e2e/` verify complete user journeys (mint ETH → swap KRK → stake). Run with `npm run test:e2e` from repo root. Tests use mocked wallet provider with Anvil accounts and automatically start/stop the stack. See `INTEGRATION_TEST_STATUS.md` and `SWAP_VERIFICATION.md` for details.
|
||||
|
||||
## Version Validation System
|
||||
|
|
@ -65,8 +65,8 @@
|
|||
- `anvil --fork-url https://sepolia.base.org` - manual fork when diagnosing outside the helper script.
|
||||
- `cast call <POOL> "slot0()"` - inspect pool state.
|
||||
- `PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK npm run dev` (inside `services/ponder/`) - focused indexer debugging when the full stack is already running.
|
||||
- `curl -X POST http://localhost:42069/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
|
||||
- `curl http://127.0.0.1:43069/status`
|
||||
- `curl -X POST http://localhost:8081/api/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
|
||||
- `curl http://localhost:8081/api/txn/status`
|
||||
|
||||
## References
|
||||
- Deployment history: `onchain/deployments-local.json`, `onchain/broadcast/`.
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
route /app* {
|
||||
reverse_proxy webapp:5173
|
||||
}
|
||||
route /graphql* {
|
||||
route /api/graphql* {
|
||||
uri strip_prefix /api
|
||||
reverse_proxy ponder:42069
|
||||
}
|
||||
route /health* {
|
||||
reverse_proxy ponder:42069
|
||||
}
|
||||
route /rpc/anvil* {
|
||||
uri strip_prefix /rpc/anvil
|
||||
route /api/rpc* {
|
||||
uri strip_prefix /api/rpc
|
||||
reverse_proxy anvil:8545
|
||||
}
|
||||
route /txn* {
|
||||
uri strip_prefix /txn
|
||||
route /api/txn* {
|
||||
uri strip_prefix /api/txn
|
||||
reverse_proxy txn-bot:43069
|
||||
}
|
||||
reverse_proxy landing:5174
|
||||
|
|
|
|||
|
|
@ -218,13 +218,15 @@ EOPONDER
|
|||
}
|
||||
|
||||
write_txn_bot_env() {
|
||||
local provider_url=${TXNBOT_PROVIDER_URL:-$ANVIL_RPC}
|
||||
local graphql_endpoint=${TXNBOT_GRAPHQL_ENDPOINT:-http://ponder:42069/graphql}
|
||||
cat >"$TXNBOT_ENV" <<EOTXNBOT
|
||||
ENVIRONMENT=BASE_SEPOLIA_LOCAL_FORK
|
||||
PROVIDER_URL=$ANVIL_RPC
|
||||
PROVIDER_URL=$provider_url
|
||||
PRIVATE_KEY=$TXNBOT_PRIVATE_KEY
|
||||
LM_CONTRACT_ADDRESS=$LIQUIDITY_MANAGER
|
||||
STAKE_CONTRACT_ADDRESS=$STAKE
|
||||
GRAPHQL_ENDPOINT=http://ponder:42069/graphql
|
||||
GRAPHQL_ENDPOINT=$graphql_endpoint
|
||||
WALLET_ADDRESS=$TXNBOT_ADDRESS
|
||||
PORT=43069
|
||||
EOTXNBOT
|
||||
|
|
|
|||
|
|
@ -54,13 +54,15 @@ else
|
|||
fi
|
||||
|
||||
export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337}
|
||||
export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/rpc/anvil}
|
||||
export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/api/rpc}
|
||||
export VITE_LOCAL_RPC_PROXY_TARGET=${VITE_LOCAL_RPC_PROXY_TARGET:-http://anvil:8545}
|
||||
export VITE_LOCAL_GRAPHQL_PROXY_TARGET=${VITE_LOCAL_GRAPHQL_PROXY_TARGET:-http://ponder:42069}
|
||||
export VITE_LOCAL_TXN_PROXY_TARGET=${VITE_LOCAL_TXN_PROXY_TARGET:-http://txn-bot:43069}
|
||||
export VITE_KRAIKEN_ADDRESS=$KRAIKEN
|
||||
export VITE_STAKE_ADDRESS=$STAKE
|
||||
export VITE_SWAP_ROUTER=$SWAP_ROUTER
|
||||
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/app/graphql}
|
||||
export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/app/txn}
|
||||
export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/api/graphql}
|
||||
export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn}
|
||||
export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1}
|
||||
|
||||
exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ The Podman stack mirrors `scripts/dev.sh` using long-lived containers. Every boo
|
|||
- `ponder` – `npm run dev` for the indexer (port 42069 inside the pod)
|
||||
- `frontend` – Vite dev server for `web-app` (port 5173 inside the pod)
|
||||
- `txn-bot` – automation loop plus Express status API (port 43069 inside the pod)
|
||||
- `caddy` – front door at `http://<host>:80`, routing `/graphql`, `/health`, `/rpc/anvil`, and `/txn` to the internal services
|
||||
- `caddy` – front door at `http://<host>:80`, routing `/api/graphql`, `/health`, `/api/rpc`, and `/api/txn` to the internal services
|
||||
|
||||
All containers mount the repository so code edits hot-reload exactly as the local script. Named volumes keep `node_modules` caches between restarts.
|
||||
|
||||
|
|
@ -26,9 +26,9 @@ podman-compose -f podman-compose.yml up
|
|||
|
||||
### Access Points (via Caddy)
|
||||
- Frontend: `http://<host>/`
|
||||
- GraphQL: `http://<host>/graphql`
|
||||
- RPC passthrough: `http://<host>/rpc/anvil`
|
||||
- Txn bot status: `http://<host>/txn/status`
|
||||
- GraphQL: `http://<host>/api/graphql`
|
||||
- RPC passthrough: `http://<host>/api/rpc`
|
||||
- Txn bot status: `http://<host>/api/txn/status`
|
||||
|
||||
## Configuration Knobs
|
||||
Set environment variables before `podman-compose up`:
|
||||
|
|
|
|||
11
kraiken-lib/package-lock.json
generated
11
kraiken-lib/package-lock.json
generated
|
|
@ -219,7 +219,6 @@
|
|||
"integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.2",
|
||||
|
|
@ -3107,7 +3106,6 @@
|
|||
"integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.13.0"
|
||||
}
|
||||
|
|
@ -3192,7 +3190,6 @@
|
|||
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.45.0",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
|
|
@ -3520,7 +3517,6 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3943,7 +3939,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
|
|
@ -4780,7 +4775,6 @@
|
|||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -5521,7 +5515,6 @@
|
|||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
|
||||
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
|
|
@ -5606,7 +5599,6 @@
|
|||
"integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
"website"
|
||||
],
|
||||
|
|
@ -6266,7 +6258,6 @@
|
|||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
|
|
@ -9249,7 +9240,6 @@
|
|||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -9572,7 +9562,6 @@
|
|||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2803,11 +2803,6 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@^2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
|
|
|
|||
124
package-lock.json
generated
124
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "harb",
|
||||
"name": "harb-wa-conf",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -64,6 +66,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -81,6 +84,7 @@
|
|||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -98,6 +102,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -115,6 +120,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -132,6 +138,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -149,6 +156,7 @@
|
|||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -166,6 +174,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -183,6 +192,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -200,6 +210,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -217,6 +228,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -234,6 +246,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -251,6 +264,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -268,6 +282,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -285,6 +300,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -302,6 +318,7 @@
|
|||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -319,6 +336,7 @@
|
|||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -336,6 +354,7 @@
|
|||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -353,6 +372,7 @@
|
|||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -370,6 +390,7 @@
|
|||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -387,6 +408,7 @@
|
|||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -404,6 +426,7 @@
|
|||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -421,6 +444,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -438,6 +462,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -455,6 +480,7 @@
|
|||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -1269,7 +1295,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1283,7 +1310,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1297,7 +1325,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1311,7 +1340,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1325,7 +1355,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1339,7 +1370,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1353,7 +1385,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1367,7 +1400,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1381,7 +1415,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1395,7 +1430,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1409,7 +1445,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1423,7 +1460,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1437,7 +1475,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1451,7 +1490,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1465,7 +1505,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1479,7 +1520,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1493,7 +1535,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1507,7 +1550,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1521,7 +1565,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1535,7 +1580,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1549,7 +1595,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.2",
|
||||
|
|
@ -1563,7 +1610,8 @@
|
|||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@skorotkiewicz/snowflake-id": {
|
||||
"version": "1.0.1",
|
||||
|
|
@ -1858,7 +1906,8 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -2208,6 +2257,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -2338,7 +2388,6 @@
|
|||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
|
|
@ -2412,6 +2461,7 @@
|
|||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
|
|
@ -2688,7 +2738,6 @@
|
|||
"integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
|
|
@ -2713,7 +2762,6 @@
|
|||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
|
|
@ -3085,6 +3133,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
|
@ -3184,7 +3233,8 @@
|
|||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
|
|
@ -3296,6 +3346,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -3416,7 +3467,6 @@
|
|||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -3441,7 +3491,6 @@
|
|||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
|
|
@ -3555,6 +3604,7 @@
|
|||
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
|
|
@ -3824,8 +3874,7 @@
|
|||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
|
|
@ -3874,6 +3923,7 @@
|
|||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
|
|
@ -3917,13 +3967,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
|
||||
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
@ -4005,6 +4048,7 @@
|
|||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -4085,6 +4129,7 @@
|
|||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
|
|
@ -4169,7 +4214,6 @@
|
|||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ readonly POLL_INTERVAL=2 # Check health every N seconds
|
|||
PID_FILE=/tmp/kraiken-watcher.pid
|
||||
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}
|
||||
|
||||
container_name() {
|
||||
local service="$1"
|
||||
echo "${PROJECT_NAME}_${service}_1"
|
||||
}
|
||||
|
||||
cleanup_existing() {
|
||||
# Kill any existing watch scripts
|
||||
pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true
|
||||
|
|
@ -24,11 +29,11 @@ cleanup_existing() {
|
|||
rm -f "$PID_FILE"
|
||||
|
||||
# Kill zombie podman processes
|
||||
pkill -9 -f "podman wait.*harb_" 2>/dev/null || true
|
||||
pkill -9 -f "podman wait.*${PROJECT_NAME}_" 2>/dev/null || true
|
||||
|
||||
# Remove any existing containers (suppress errors if they don't exist)
|
||||
echo " Cleaning up existing containers..."
|
||||
podman ps -a --filter "name=harb_" --format "{{.Names}}" 2>/dev/null | \
|
||||
podman ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" 2>/dev/null | \
|
||||
xargs -r podman rm -f 2>&1 | grep -v "Error.*no container" || true
|
||||
}
|
||||
|
||||
|
|
@ -94,32 +99,32 @@ start_stack() {
|
|||
echo " Starting anvil & postgres..."
|
||||
podman-compose up -d anvil postgres 2>&1 | grep -v "STEP\|Copying\|Writing\|Getting\|fetch\|Installing\|Executing" || true
|
||||
|
||||
wait_for_healthy harb_anvil_1 "$ANVIL_TIMEOUT" || exit 1
|
||||
wait_for_healthy harb_postgres_1 "$POSTGRES_TIMEOUT" || exit 1
|
||||
wait_for_healthy "$(container_name anvil)" "$ANVIL_TIMEOUT" || exit 1
|
||||
wait_for_healthy "$(container_name postgres)" "$POSTGRES_TIMEOUT" || exit 1
|
||||
|
||||
# Phase 2: Start bootstrap (depends on anvil & postgres healthy)
|
||||
echo " Starting bootstrap..."
|
||||
podman-compose up -d bootstrap >/dev/null 2>&1
|
||||
|
||||
wait_for_exited harb_bootstrap_1 "$BOOTSTRAP_TIMEOUT" || exit 1
|
||||
wait_for_exited "$(container_name bootstrap)" "$BOOTSTRAP_TIMEOUT" || exit 1
|
||||
|
||||
# Phase 3: Start ponder (depends on bootstrap completed)
|
||||
echo " Starting ponder..."
|
||||
podman-compose up -d ponder >/dev/null 2>&1
|
||||
|
||||
wait_for_healthy harb_ponder_1 "$PONDER_TIMEOUT" || exit 1
|
||||
wait_for_healthy "$(container_name ponder)" "$PONDER_TIMEOUT" || exit 1
|
||||
|
||||
# Phase 4: Start frontend services (depend on ponder healthy)
|
||||
echo " Starting webapp, landing, txn-bot..."
|
||||
podman-compose up -d webapp landing txn-bot >/dev/null 2>&1
|
||||
|
||||
wait_for_healthy harb_webapp_1 "$WEBAPP_TIMEOUT" || exit 1
|
||||
wait_for_healthy "$(container_name webapp)" "$WEBAPP_TIMEOUT" || exit 1
|
||||
|
||||
# Phase 5: Start caddy (depends on frontend services)
|
||||
echo " Starting caddy..."
|
||||
podman-compose up -d caddy >/dev/null 2>&1
|
||||
|
||||
wait_for_healthy harb_caddy_1 "$CADDY_TIMEOUT" || exit 1
|
||||
wait_for_healthy "$(container_name caddy)" "$CADDY_TIMEOUT" || exit 1
|
||||
|
||||
if [[ -z "${SKIP_WATCH:-}" ]]; then
|
||||
echo "Watching for kraiken-lib changes..."
|
||||
|
|
@ -131,7 +136,8 @@ start_stack() {
|
|||
echo ""
|
||||
echo "[ok] Stack started in ${total_time}s"
|
||||
echo " Web App: http://localhost:8081/app/"
|
||||
echo " GraphQL: http://localhost:8081/graphql"
|
||||
echo " RPC Proxy: http://localhost:8081/api/rpc"
|
||||
echo " GraphQL: http://localhost:8081/api/graphql"
|
||||
}
|
||||
|
||||
stop_stack() {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import {
|
|||
formatHealthCheckError,
|
||||
} from './health-checks.js';
|
||||
|
||||
const DEFAULT_RPC_URL = 'http://127.0.0.1:8545';
|
||||
const DEFAULT_RPC_URL = 'http://localhost:8081/api/rpc';
|
||||
const DEFAULT_WEBAPP_URL = 'http://localhost:8081';
|
||||
const DEFAULT_GRAPHQL_URL = 'http://localhost:8081/graphql';
|
||||
const DEFAULT_GRAPHQL_URL = 'http://localhost:8081/api/graphql';
|
||||
|
||||
export interface StackConfig {
|
||||
rpcUrl: string;
|
||||
|
|
|
|||
278
web-app/AGENTS.md
Normal file
278
web-app/AGENTS.md
Normal 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
|
||||
|
|
@ -159,6 +159,7 @@ import { getMinStake } from '@/contracts/harb';
|
|||
import { useWallet } from '@/composables/useWallet';
|
||||
import { ref, onMounted, watch, computed, watchEffect, getCurrentInstance } from 'vue';
|
||||
import { useStatCollection, loadStats } from '@/composables/useStatCollection';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
|
||||
const demo = sessionStorage.getItem('demo') === 'true';
|
||||
|
||||
|
|
@ -168,8 +169,10 @@ const taxRateIndex = ref<number>(defaultTaxRateIndex);
|
|||
const stake = useStake();
|
||||
const _claim = useClaim();
|
||||
const wallet = useWallet();
|
||||
const statCollection = useStatCollection();
|
||||
const { activePositions: _activePositions } = usePositions();
|
||||
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
|
||||
const statCollection = useStatCollection(initialChainId);
|
||||
const { activePositions: _activePositions } = usePositions(initialChainId);
|
||||
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const uid = instance?.uid ?? Math.floor(Math.random() * 10000);
|
||||
|
|
@ -330,7 +333,7 @@ const taxRateAnnouncement = computed(() => {
|
|||
return `Selected tax rate index ${selectedTaxOption.value.index}. You will pay ${formatNumber(selectedTaxOption.value.year, 2)} percent yearly.`;
|
||||
});
|
||||
|
||||
const snatchSelection = useSnatchSelection(demo, taxRateIndex);
|
||||
const snatchSelection = useSnatchSelection(demo, taxRateIndex, initialChainId);
|
||||
|
||||
const floorTaxDisplay = computed(() => `${formatNumber(snatchSelection.floorTax.value ?? 0, 2)} %`);
|
||||
const floorTaxHelpText = 'Your tax needs to exceed this value to displace an existing position.';
|
||||
|
|
@ -487,8 +490,8 @@ async function stakeSnatch() {
|
|||
const snatchAblePositionsIds = snatchSelection.snatchablePositions.value.map((p: Position) => p.positionId);
|
||||
await stake.snatch(stake.stakingAmount, taxRateIndex.value, snatchAblePositionsIds);
|
||||
}
|
||||
await loadPositions();
|
||||
await loadStats();
|
||||
await loadPositions(currentChainId.value);
|
||||
await loadStats(currentChainId.value);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ import { useStatCollection } from '@/composables/useStatCollection';
|
|||
import { useStake } from '@/composables/useStake';
|
||||
import { usePositions, type Position } from '@/composables/usePositions';
|
||||
import { useDark } from '@/composables/useDark';
|
||||
import { useWallet } from '@/composables/useWallet';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
const { darkTheme } = useDark();
|
||||
|
||||
const { activePositions } = usePositions();
|
||||
const wallet = useWallet();
|
||||
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
|
||||
const { activePositions } = usePositions(initialChainId);
|
||||
const ignoreOwner = ref(false);
|
||||
|
||||
const taxRate = ref<number>(1.0);
|
||||
|
|
@ -26,7 +30,7 @@ const stakeAbleHarbAmount = computed(() => statCollection.kraikenTotalSupply / 5
|
|||
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
|
||||
|
||||
const stake = useStake();
|
||||
const statCollection = useStatCollection();
|
||||
const statCollection = useStatCollection(initialChainId);
|
||||
const snatchPositions = computed(() => {
|
||||
if (
|
||||
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=
|
||||
|
|
|
|||
|
|
@ -83,12 +83,17 @@ import { computed, ref, onMounted } from 'vue';
|
|||
import { getTaxDue, payTax } from '@/contracts/stake';
|
||||
import { type Position, loadPositions } from '@/composables/usePositions';
|
||||
import { useStatCollection } from '@/composables/useStatCollection';
|
||||
import { useWallet } from '@/composables/useWallet';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
const unstake = useUnstake();
|
||||
const adjustTaxRate = useAdjustTaxRate();
|
||||
const statCollection = useStatCollection();
|
||||
const wallet = useWallet();
|
||||
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
|
||||
const statCollection = useStatCollection(initialChainId);
|
||||
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
|
||||
|
||||
const props = defineProps<{
|
||||
taxRate: number;
|
||||
|
|
@ -134,7 +139,7 @@ async function unstakePosition() {
|
|||
await unstake.exitPosition(props.id);
|
||||
loading.value = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
await loadPositions();
|
||||
await loadPositions(currentChainId.value);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="connected-tokens">
|
||||
<FOutput name="$KRK" :price="harbAmount" :variant="3">
|
||||
<template #end>
|
||||
<FButton size="small" dense><a :href="chain.chainData?.uniswap" target="_blank">Buy</a></FButton>
|
||||
<FButton size="small" dense><a :href="chain?.uniswap" target="_blank">Buy</a></FButton>
|
||||
</template>
|
||||
</FOutput>
|
||||
<FOutput name="Staked $KRK" :price="compactNumber(stakedAmount)" :variant="3">
|
||||
|
|
@ -64,8 +64,8 @@ import FOutput from '@/components/fcomponents/FOutput.vue';
|
|||
// import { usePositions } from "@/composables/usePositions";
|
||||
// import { useWallet } from "@/composables/useWallet";
|
||||
import { usePositions } from '@/composables/usePositions';
|
||||
import { useWallet } from '@/composables/useWallet';
|
||||
import { useChain } from '@/composables/useChain';
|
||||
import { useWallet, chainData as walletChainData } from '@/composables/useWallet';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
|
||||
import { useAccount, useDisconnect, useConnect, useChainId, type CreateConnectorFn, type Connector } from '@wagmi/vue';
|
||||
|
||||
|
|
@ -74,9 +74,10 @@ const { disconnect } = useDisconnect();
|
|||
const { connectors, connect } = useConnect();
|
||||
const router = useRouter();
|
||||
const chainId = useChainId();
|
||||
const { myActivePositions } = usePositions();
|
||||
const initialChainId = chainId.value ?? DEFAULT_CHAIN_ID;
|
||||
const { myActivePositions } = usePositions(initialChainId);
|
||||
const wallet = useWallet();
|
||||
const chain = useChain();
|
||||
const chain = walletChainData;
|
||||
function loadConnectorImage(connector: Connector) {
|
||||
if (connector.icon) {
|
||||
return connector.icon;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -9,17 +9,17 @@ import { HarbContract } from '@/contracts/harb';
|
|||
import { bytesToUint256 } from 'kraiken-lib';
|
||||
import { bigInt2Number } from '@/utils/helper';
|
||||
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
|
||||
import { chainData } from '@/composables/useWallet';
|
||||
import logger from '@/utils/logger';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
|
||||
const rawActivePositions = ref<Array<Position>>([]);
|
||||
const rawClosedPositoins = ref<Array<Position>>([]);
|
||||
const loading = ref(false);
|
||||
const positionsError = ref<string | null>(null);
|
||||
const POSITIONS_RETRY_BASE_DELAY = 1_500;
|
||||
const POSITIONS_RETRY_MAX_DELAY = 60_000;
|
||||
const positionsRetryDelayMs = ref(POSITIONS_RETRY_BASE_DELAY);
|
||||
const GRAPHQL_TIMEOUT_MS = 15_000;
|
||||
let positionsRetryTimer: number | null = null;
|
||||
const activeChainId = ref<number>(DEFAULT_CHAIN_ID);
|
||||
|
||||
const retryManager = createRetryManager(loadPositions, activeChainId);
|
||||
const activePositions = computed(() => {
|
||||
const account = getAccount(config as Config);
|
||||
|
||||
|
|
@ -108,9 +108,7 @@ const myActivePositions: ComputedRef<Position[]> = computed(() =>
|
|||
|
||||
const tresholdValue = computed(() => {
|
||||
// Compute average tax rate index instead of percentage to avoid floating-point issues
|
||||
const validIndices = activePositions.value
|
||||
.map(obj => obj.taxRateIndex)
|
||||
.filter((idx): idx is number => typeof idx === 'number');
|
||||
const validIndices = activePositions.value.map(obj => obj.taxRateIndex).filter((idx): idx is number => typeof idx === 'number');
|
||||
|
||||
if (validIndices.length === 0) return 0;
|
||||
|
||||
|
|
@ -121,14 +119,11 @@ const tresholdValue = computed(() => {
|
|||
return Math.floor(avgIndex / 2);
|
||||
});
|
||||
|
||||
export async function loadActivePositions(endpoint?: string) {
|
||||
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`);
|
||||
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
|
||||
if (!targetEndpoint) {
|
||||
throw new Error('GraphQL endpoint not configured for this chain.');
|
||||
}
|
||||
export async function loadActivePositions(chainId: number, endpointOverride?: string) {
|
||||
const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
|
||||
logger.info(`loadActivePositions for chainId: ${chainId}`);
|
||||
|
||||
// console.log("chainData.value?.graphql", targetEndpoint);
|
||||
// console.log("graphql endpoint", targetEndpoint);
|
||||
|
||||
const res = await axios.post(
|
||||
targetEndpoint,
|
||||
|
|
@ -173,12 +168,9 @@ function formatId(id: Hex) {
|
|||
return bigIntId;
|
||||
}
|
||||
|
||||
export async function loadMyClosedPositions(endpoint: string | undefined, account: GetAccountReturnType) {
|
||||
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`);
|
||||
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
|
||||
if (!targetEndpoint) {
|
||||
throw new Error('GraphQL endpoint not configured for this chain.');
|
||||
}
|
||||
export async function loadMyClosedPositions(chainId: number, endpointOverride: string | undefined, account: GetAccountReturnType) {
|
||||
const targetEndpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
|
||||
logger.info(`loadMyClosedPositions for chainId: ${chainId}`);
|
||||
const res = await axios.post(
|
||||
targetEndpoint,
|
||||
{
|
||||
|
|
@ -214,36 +206,41 @@ export async function loadMyClosedPositions(endpoint: string | undefined, accoun
|
|||
})) as Position[];
|
||||
}
|
||||
|
||||
export async function loadPositions() {
|
||||
export async function loadPositions(chainId?: number) {
|
||||
loading.value = true;
|
||||
|
||||
const endpoint = chainData.value?.graphql?.trim();
|
||||
if (!endpoint) {
|
||||
const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
|
||||
activeChainId.value = targetChainId;
|
||||
|
||||
let endpoint: string;
|
||||
try {
|
||||
endpoint = resolveGraphqlEndpoint(targetChainId);
|
||||
} catch (error) {
|
||||
rawActivePositions.value = [];
|
||||
rawClosedPositoins.value = [];
|
||||
positionsError.value = 'GraphQL endpoint not configured for this chain.';
|
||||
clearPositionsRetryTimer();
|
||||
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
|
||||
positionsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
|
||||
retryManager.clear();
|
||||
retryManager.reset();
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
rawActivePositions.value = await loadActivePositions(endpoint);
|
||||
rawActivePositions.value = await loadActivePositions(targetChainId, endpoint);
|
||||
const account = getAccount(config as Config);
|
||||
if (account.address) {
|
||||
rawClosedPositoins.value = await loadMyClosedPositions(endpoint, account);
|
||||
rawClosedPositoins.value = await loadMyClosedPositions(targetChainId, endpoint, account);
|
||||
} else {
|
||||
rawClosedPositoins.value = [];
|
||||
}
|
||||
positionsError.value = null;
|
||||
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
|
||||
clearPositionsRetryTimer();
|
||||
retryManager.reset();
|
||||
retryManager.clear();
|
||||
} catch (error) {
|
||||
rawActivePositions.value = [];
|
||||
rawClosedPositoins.value = [];
|
||||
positionsError.value = formatGraphqlError(error);
|
||||
schedulePositionsRetry();
|
||||
retryManager.schedule();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
@ -253,48 +250,9 @@ let unwatch: WatchEventReturnType | null;
|
|||
let unwatchPositionRemovedEvent: WatchEventReturnType | null;
|
||||
let unwatchChainSwitch: WatchChainIdReturnType | null;
|
||||
let unwatchAccountChanged: WatchAccountReturnType | null;
|
||||
export function usePositions(chainId: number = DEFAULT_CHAIN_ID) {
|
||||
activeChainId.value = chainId;
|
||||
|
||||
function formatGraphqlError(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
|
||||
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
|
||||
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
|
||||
}
|
||||
if (error.response?.status) {
|
||||
return `GraphQL request failed with status ${error.response.status}`;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown GraphQL error';
|
||||
}
|
||||
|
||||
function clearPositionsRetryTimer() {
|
||||
if (positionsRetryTimer !== null) {
|
||||
clearTimeout(positionsRetryTimer);
|
||||
positionsRetryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePositionsRetry() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (positionsRetryTimer !== null) {
|
||||
return;
|
||||
}
|
||||
const delay = positionsRetryDelayMs.value;
|
||||
positionsRetryTimer = window.setTimeout(async () => {
|
||||
positionsRetryTimer = null;
|
||||
await loadPositions();
|
||||
}, delay);
|
||||
positionsRetryDelayMs.value = Math.min(positionsRetryDelayMs.value * 2, POSITIONS_RETRY_MAX_DELAY);
|
||||
}
|
||||
export function usePositions() {
|
||||
function watchEvent() {
|
||||
unwatch = watchContractEvent(config as Config, {
|
||||
address: HarbContract.contractAddress,
|
||||
|
|
@ -325,7 +283,7 @@ export function usePositions() {
|
|||
//initial loading positions
|
||||
|
||||
if (activePositions.value.length < 1 && loading.value === false) {
|
||||
await loadPositions();
|
||||
await loadPositions(activeChainId.value);
|
||||
// await getMinStake();
|
||||
}
|
||||
|
||||
|
|
@ -338,8 +296,10 @@ export function usePositions() {
|
|||
|
||||
if (!unwatchChainSwitch) {
|
||||
unwatchChainSwitch = watchChainId(config as Config, {
|
||||
async onChange(_chainId) {
|
||||
await loadPositions();
|
||||
async onChange(nextChainId) {
|
||||
const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
|
||||
activeChainId.value = resolvedChainId;
|
||||
await loadPositions(resolvedChainId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -347,7 +307,7 @@ export function usePositions() {
|
|||
if (!unwatchAccountChanged) {
|
||||
unwatchAccountChanged = watchAccount(config as Config, {
|
||||
async onChange() {
|
||||
await loadPositions();
|
||||
await loadPositions(activeChainId.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -370,7 +330,7 @@ export function usePositions() {
|
|||
unwatchAccountChanged();
|
||||
unwatchAccountChanged = null;
|
||||
}
|
||||
clearPositionsRetryTimer();
|
||||
retryManager.clear();
|
||||
});
|
||||
|
||||
function createRandomPosition(amount: number = 1) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useStatCollection } from './useStatCollection';
|
|||
import { useAdjustTaxRate } from './useAdjustTaxRates';
|
||||
import { calculateSnatchShortfall } from 'kraiken-lib/staking';
|
||||
import { selectSnatchPositions, minimumTaxRate, type SnatchablePosition } from 'kraiken-lib/snatch';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
|
||||
/**
|
||||
* Converts Kraiken token assets to shares using the same formula as Stake.sol:
|
||||
|
|
@ -24,11 +25,12 @@ function assetsToSharesLocal(assets: bigint, kraikenTotalSupply: bigint, stakeTo
|
|||
return (assets * stakeTotalSupply) / kraikenTotalSupply;
|
||||
}
|
||||
|
||||
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
|
||||
const { activePositions } = usePositions();
|
||||
const stake = useStake();
|
||||
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>, chainId?: number) {
|
||||
const wallet = useWallet();
|
||||
const statCollection = useStatCollection();
|
||||
const resolvedChainId = chainId ?? wallet.account.chainId ?? DEFAULT_CHAIN_ID;
|
||||
const { activePositions } = usePositions(resolvedChainId);
|
||||
const stake = useStake();
|
||||
const statCollection = useStatCollection(resolvedChainId);
|
||||
const adjustTaxRate = useAdjustTaxRate();
|
||||
|
||||
const snatchablePositions = ref<Position[]>([]);
|
||||
|
|
@ -78,7 +80,7 @@ export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
|
|||
const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex;
|
||||
const maxTaxRateDecimal =
|
||||
typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex)
|
||||
? adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY
|
||||
? (adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const includeOwned = demo;
|
||||
const recipient = wallet.account.address ?? null;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { ref, reactive, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { chainData } from './useWallet';
|
||||
import { watchChainId } from '@wagmi/core';
|
||||
import type { Config } from '@wagmi/core';
|
||||
import { config } from '@/wagmi';
|
||||
import logger from '@/utils/logger';
|
||||
import type { WatchBlocksReturnType } from 'viem';
|
||||
import { bigInt2Number } from '@/utils/helper';
|
||||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||||
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
|
||||
const demo = sessionStorage.getItem('demo') === 'true';
|
||||
|
||||
const GRAPHQL_TIMEOUT_MS = 15_000;
|
||||
const RETRY_BASE_DELAY_MS = 1_500;
|
||||
const RETRY_MAX_DELAY_MS = 60_000;
|
||||
|
||||
interface StatsRecord {
|
||||
burnNextHourProjected: string;
|
||||
|
|
@ -33,52 +32,13 @@ const rawStatsCollections = ref<Array<StatsRecord>>([]);
|
|||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const statsError = ref<string | null>(null);
|
||||
const statsRetryDelayMs = ref(RETRY_BASE_DELAY_MS);
|
||||
let statsRetryTimer: number | null = null;
|
||||
const activeChainId = ref<number>(DEFAULT_CHAIN_ID);
|
||||
|
||||
function formatGraphqlError(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
|
||||
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
|
||||
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
|
||||
}
|
||||
if (error.response?.status) {
|
||||
return `GraphQL request failed with status ${error.response.status}`;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown GraphQL error';
|
||||
}
|
||||
const retryManager = createRetryManager(loadStats, activeChainId);
|
||||
|
||||
function clearStatsRetryTimer() {
|
||||
if (statsRetryTimer !== null) {
|
||||
clearTimeout(statsRetryTimer);
|
||||
statsRetryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleStatsRetry() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (statsRetryTimer !== null) {
|
||||
return;
|
||||
}
|
||||
const delay = statsRetryDelayMs.value;
|
||||
statsRetryTimer = window.setTimeout(async () => {
|
||||
statsRetryTimer = null;
|
||||
await loadStats();
|
||||
}, delay);
|
||||
statsRetryDelayMs.value = Math.min(statsRetryDelayMs.value * 2, RETRY_MAX_DELAY_MS);
|
||||
}
|
||||
|
||||
export async function loadStatsCollection(endpoint: string) {
|
||||
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
|
||||
export async function loadStatsCollection(chainId: number, endpointOverride?: string) {
|
||||
const endpoint = resolveGraphqlEndpoint(chainId, endpointOverride);
|
||||
logger.info(`loadStatsCollection for chainId: ${chainId}`);
|
||||
const res = await axios.post(
|
||||
endpoint,
|
||||
{
|
||||
|
|
@ -223,30 +183,34 @@ const claimedSlots = computed(() => {
|
|||
}
|
||||
});
|
||||
|
||||
export async function loadStats() {
|
||||
export async function loadStats(chainId?: number) {
|
||||
loading.value = true;
|
||||
|
||||
const endpoint = chainData.value?.graphql?.trim();
|
||||
if (!endpoint) {
|
||||
const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID);
|
||||
activeChainId.value = targetChainId;
|
||||
|
||||
let endpoint: string;
|
||||
try {
|
||||
endpoint = resolveGraphqlEndpoint(targetChainId);
|
||||
} catch (error) {
|
||||
rawStatsCollections.value = [];
|
||||
statsError.value = 'GraphQL endpoint not configured for this chain.';
|
||||
clearStatsRetryTimer();
|
||||
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
|
||||
statsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.';
|
||||
retryManager.clear();
|
||||
retryManager.reset();
|
||||
loading.value = false;
|
||||
initialized.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
rawStatsCollections.value = await loadStatsCollection(endpoint);
|
||||
rawStatsCollections.value = await loadStatsCollection(targetChainId, endpoint);
|
||||
statsError.value = null;
|
||||
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
|
||||
clearStatsRetryTimer();
|
||||
retryManager.reset();
|
||||
retryManager.clear();
|
||||
} catch (error) {
|
||||
// console.warn('[stats] loadStats() failed', error);
|
||||
rawStatsCollections.value = [];
|
||||
statsError.value = formatGraphqlError(error);
|
||||
scheduleStatsRetry();
|
||||
retryManager.schedule();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
initialized.value = true;
|
||||
|
|
@ -256,11 +220,13 @@ export async function loadStats() {
|
|||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
let unwatch: WatchBlocksReturnType | null = null;
|
||||
export function useStatCollection() {
|
||||
export function useStatCollection(chainId: number = DEFAULT_CHAIN_ID) {
|
||||
activeChainId.value = chainId;
|
||||
|
||||
onMounted(async () => {
|
||||
//initial loading stats
|
||||
if (rawStatsCollections.value?.length === 0 && !loading.value) {
|
||||
await loadStats();
|
||||
await loadStats(activeChainId.value);
|
||||
}
|
||||
});
|
||||
if (!unwatch) {
|
||||
|
|
@ -268,8 +234,10 @@ export function useStatCollection() {
|
|||
|
||||
//chain Switch reload stats for other chain
|
||||
unwatch = watchChainId(config as Config, {
|
||||
async onChange(_chainId) {
|
||||
await loadStats();
|
||||
async onChange(nextChainId) {
|
||||
const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID;
|
||||
activeChainId.value = resolvedChainId;
|
||||
await loadStats(resolvedChainId);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -281,7 +249,7 @@ export function useStatCollection() {
|
|||
// })
|
||||
}
|
||||
onUnmounted(() => {
|
||||
clearStatsRetryTimer();
|
||||
retryManager.clear();
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { ref, reactive, computed } from 'vue';
|
||||
import { getAccount, getBalance, watchAccount, watchChainId, type Config } from '@wagmi/core';
|
||||
import { getAccount, watchAccount, watchChainId, type Config } from '@wagmi/core';
|
||||
import { type WatchAccountReturnType, type GetAccountReturnType, type GetBalanceReturnType } from '@wagmi/core';
|
||||
import { config } from '@/wagmi';
|
||||
import { config, KRAIKEN_LOCAL_CHAIN } from '@/wagmi';
|
||||
import { getAllowance, HarbContract, getNonce } from '@/contracts/harb';
|
||||
import logger from '@/utils/logger';
|
||||
import { setHarbContract } from '@/contracts/harb';
|
||||
import { setStakeContract } from '@/contracts/stake';
|
||||
import { chainsData, DEFAULT_CHAIN_ID } from '@/config';
|
||||
import { createPublicClient, custom, formatUnits, type EIP1193Provider, type PublicClient, type Transport, type WalletClient } from 'viem';
|
||||
import { getWalletPublicClient, setWalletPublicClient } from '@/services/walletRpc';
|
||||
|
||||
const balance = ref<GetBalanceReturnType>({
|
||||
value: 0n,
|
||||
|
|
@ -25,24 +27,81 @@ export const chainData = computed(() => {
|
|||
return chainsData.find(obj => obj.id === selectedChainId.value);
|
||||
});
|
||||
|
||||
const TOKEN_DECIMALS = 18;
|
||||
const TOKEN_SYMBOL = 'KRK';
|
||||
|
||||
async function syncWalletPublicClient(data: GetAccountReturnType | undefined = account.value): Promise<PublicClient | null> {
|
||||
if (!data?.address || !data.connector) {
|
||||
setWalletPublicClient(null);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const connector = data.connector;
|
||||
if (!connector || typeof connector.getProvider !== 'function') {
|
||||
setWalletPublicClient(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectorAny = connector as {
|
||||
getProvider?: () => Promise<unknown>;
|
||||
getWalletClient?: (args: { chainId: number }) => Promise<WalletClient | null>;
|
||||
};
|
||||
const provider = connectorAny.getProvider ? await connectorAny.getProvider() : null;
|
||||
const walletClient = connectorAny.getWalletClient
|
||||
? await connectorAny.getWalletClient({ chainId: data.chainId ?? selectedChainId.value })
|
||||
: null;
|
||||
|
||||
if (!provider || typeof (provider as { request?: unknown }).request !== 'function') {
|
||||
setWalletPublicClient(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const chain = (walletClient as WalletClient | null)?.chain ?? KRAIKEN_LOCAL_CHAIN;
|
||||
const transport = custom(provider as EIP1193Provider) as Transport;
|
||||
const publicClient = createPublicClient({
|
||||
chain,
|
||||
transport,
|
||||
}) as PublicClient;
|
||||
setWalletPublicClient(publicClient);
|
||||
return publicClient;
|
||||
} catch (_error) {
|
||||
logger.contract('Failed to create wallet public client');
|
||||
setWalletPublicClient(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let unwatch: WatchAccountReturnType | null = null;
|
||||
let unwatchChain: WatchAccountReturnType | null = null;
|
||||
export function useWallet() {
|
||||
async function loadBalance() {
|
||||
logger.contract('loadBalance');
|
||||
if (account.value.address) {
|
||||
// console.log("HarbContract",HarbContract );
|
||||
|
||||
balance.value = await getBalance(config as Config, {
|
||||
address: account.value.address,
|
||||
token: HarbContract.contractAddress,
|
||||
});
|
||||
// console.log("balance.value", balance.value);
|
||||
|
||||
return balance.value;
|
||||
} else {
|
||||
const userAddress = account.value.address;
|
||||
if (!userAddress) {
|
||||
return 0n;
|
||||
}
|
||||
let publicClient = getWalletPublicClient();
|
||||
if (!publicClient) {
|
||||
publicClient = await syncWalletPublicClient();
|
||||
}
|
||||
if (!publicClient) {
|
||||
return 0n;
|
||||
}
|
||||
const value = (await publicClient.readContract({
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: 'balanceOf',
|
||||
args: [userAddress],
|
||||
})) as bigint;
|
||||
|
||||
balance.value = {
|
||||
value,
|
||||
decimals: TOKEN_DECIMALS,
|
||||
symbol: TOKEN_SYMBOL,
|
||||
formatted: formatUnits(value, TOKEN_DECIMALS),
|
||||
} as GetBalanceReturnType;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
if (!unwatch) {
|
||||
|
|
@ -54,6 +113,7 @@ export function useWallet() {
|
|||
|
||||
if (!data.address) {
|
||||
logger.info(`disconnected`);
|
||||
setWalletPublicClient(null);
|
||||
balance.value = {
|
||||
value: 0n,
|
||||
decimals: 0,
|
||||
|
|
@ -63,6 +123,7 @@ export function useWallet() {
|
|||
} else if (account.value.address !== data.address || account.value.chainId !== data.chainId) {
|
||||
logger.info(`Account changed!:`, data.address);
|
||||
account.value = data;
|
||||
await syncWalletPublicClient(data);
|
||||
await loadBalance();
|
||||
await getAllowance();
|
||||
// await loadPositions();
|
||||
|
|
@ -75,11 +136,16 @@ export function useWallet() {
|
|||
}
|
||||
|
||||
//funzt nicht mehr-> library Änderung?
|
||||
if (account.value.address) {
|
||||
void syncWalletPublicClient(account.value);
|
||||
}
|
||||
|
||||
if (!unwatchChain) {
|
||||
// console.log("unwatchChain");
|
||||
|
||||
unwatchChain = watchChainId(config as Config, {
|
||||
async onChange(_chainId) {
|
||||
await syncWalletPublicClient(account.value);
|
||||
await loadBalance();
|
||||
await getAllowance();
|
||||
await getNonce();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import deploymentsLocal from '../../onchain/deployments-local.json';
|
|||
|
||||
const env = import.meta.env;
|
||||
|
||||
const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? 'http://127.0.0.1:42069/graphql';
|
||||
const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? 'http://127.0.0.1:43069';
|
||||
const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? '/rpc/anvil';
|
||||
const LOCAL_PONDER_URL = env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? '/api/graphql';
|
||||
const LOCAL_TXNBOT_URL = env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? '/api/txn';
|
||||
const LOCAL_RPC_URL = env.VITE_LOCAL_RPC_URL ?? '/api/rpc';
|
||||
|
||||
interface DeploymentContracts {
|
||||
Kraiken?: string;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import { type Abi, type Address, type Hash } from 'viem';
|
|||
import { StakeContract } from '@/contracts/stake';
|
||||
import { getChain } from '@/config';
|
||||
import logger from '@/utils/logger';
|
||||
// const chain1 = useChain();
|
||||
// console.log("chain1", chain1);
|
||||
import { getWalletPublicClient } from '@/services/walletRpc';
|
||||
|
||||
interface Contract {
|
||||
abi: Abi;
|
||||
|
|
@ -36,16 +35,9 @@ function getHarbJson() {
|
|||
}
|
||||
|
||||
export function setHarbContract() {
|
||||
// console.log("setHarbContract");
|
||||
|
||||
HarbContract = getHarbJson();
|
||||
}
|
||||
|
||||
// watch(chainData, async (newQuestion, oldQuestion) => {
|
||||
// console.log("log harb update");
|
||||
|
||||
// });
|
||||
|
||||
export async function getAllowance() {
|
||||
logger.contract('getAllowance');
|
||||
|
||||
|
|
@ -53,12 +45,18 @@ export async function getAllowance() {
|
|||
if (!account.address) {
|
||||
return 0n;
|
||||
}
|
||||
const result = await readContract(config as Config, {
|
||||
|
||||
const publicClient = getWalletPublicClient();
|
||||
if (!publicClient) {
|
||||
throw new Error('Wallet public client unavailable');
|
||||
}
|
||||
|
||||
const result = (await publicClient.readContract({
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: 'allowance',
|
||||
args: [account.address, StakeContract.contractAddress],
|
||||
});
|
||||
})) as bigint;
|
||||
allowance.value = result;
|
||||
return result;
|
||||
}
|
||||
|
|
@ -66,6 +64,18 @@ export async function getAllowance() {
|
|||
export async function getMinStake() {
|
||||
logger.contract('getMinStake');
|
||||
|
||||
const publicClient = getWalletPublicClient();
|
||||
if (publicClient) {
|
||||
const result = (await publicClient.readContract({
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: 'minStake',
|
||||
args: [],
|
||||
})) as bigint;
|
||||
allowance.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
const result: bigint = (await readContract(config as Config, {
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
|
|
@ -83,14 +93,19 @@ export async function getNonce() {
|
|||
if (!account.address) {
|
||||
return 0n;
|
||||
}
|
||||
// console.log("HarbContract.contractAddress", HarbContract.contractAddress);
|
||||
// console.log('HarbContract.contractAddress', HarbContract.contractAddress);
|
||||
|
||||
const result = await readContract(config as Config, {
|
||||
const publicClient = getWalletPublicClient();
|
||||
if (!publicClient) {
|
||||
throw new Error('Wallet public client unavailable');
|
||||
}
|
||||
|
||||
const result = (await publicClient.readContract({
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: 'nonces',
|
||||
args: [account.address],
|
||||
});
|
||||
})) as bigint;
|
||||
nonce.value = result;
|
||||
|
||||
return result;
|
||||
|
|
@ -99,15 +114,19 @@ export async function getNonce() {
|
|||
export async function getName() {
|
||||
logger.contract('getName');
|
||||
|
||||
const result = await readContract(config as Config, {
|
||||
const publicClient = getWalletPublicClient();
|
||||
if (!publicClient) {
|
||||
throw new Error('Wallet public client unavailable');
|
||||
}
|
||||
const result = (await publicClient.readContract({
|
||||
abi: HarbContract.abi,
|
||||
address: HarbContract.contractAddress,
|
||||
functionName: 'name',
|
||||
args: [],
|
||||
});
|
||||
})) as string;
|
||||
name.value = result;
|
||||
|
||||
return result as string;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function approve(amount: bigint): Promise<Hash> {
|
||||
|
|
|
|||
38
web-app/src/services/chainConfig.ts
Normal file
38
web-app/src/services/chainConfig.ts
Normal 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();
|
||||
11
web-app/src/services/walletRpc.ts
Normal file
11
web-app/src/services/walletRpc.ts
Normal 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;
|
||||
}
|
||||
107
web-app/src/utils/graphqlRetry.ts
Normal file
107
web-app/src/utils/graphqlRetry.ts
Normal 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');
|
||||
}
|
||||
|
|
@ -81,10 +81,11 @@ const { status } = useAccount();
|
|||
const showPanel = inject('showPanel');
|
||||
|
||||
import { InsertCommaNumber } from '@/utils/helper';
|
||||
const { myActivePositions, tresholdValue, activePositions } = usePositions();
|
||||
|
||||
const stats = useStatCollection();
|
||||
const wallet = useWallet();
|
||||
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
|
||||
const { myActivePositions, tresholdValue, activePositions } = usePositions(initialChainId);
|
||||
|
||||
const stats = useStatCollection(initialChainId);
|
||||
const chains = useChains();
|
||||
|
||||
function calculateAverageTaxRate(data: Array<{ taxRate: number | string }>): number {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { baseSepolia } from '@wagmi/vue/chains';
|
|||
import { coinbaseWallet, walletConnect } from '@wagmi/vue/connectors';
|
||||
import { defineChain } from 'viem';
|
||||
|
||||
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? '/rpc/anvil';
|
||||
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? '/api/rpc';
|
||||
|
||||
const kraikenLocalFork = defineChain({
|
||||
export const KRAIKEN_LOCAL_CHAIN = defineChain({
|
||||
id: 31337,
|
||||
name: 'Kraiken Local Fork',
|
||||
network: 'kraiken-local',
|
||||
|
|
@ -21,7 +21,7 @@ const kraikenLocalFork = defineChain({
|
|||
});
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [kraikenLocalFork, baseSepolia],
|
||||
chains: [KRAIKEN_LOCAL_CHAIN, baseSepolia],
|
||||
storage: createStorage({ storage: window.localStorage }),
|
||||
|
||||
connectors: [
|
||||
|
|
@ -44,11 +44,11 @@ export const config = createConfig({
|
|||
}),
|
||||
],
|
||||
transports: {
|
||||
[kraikenLocalFork.id]: http(LOCAL_RPC_URL),
|
||||
[KRAIKEN_LOCAL_CHAIN.id]: http(LOCAL_RPC_URL),
|
||||
[baseSepolia.id]: http(),
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined' && config.state.chainId !== kraikenLocalFork.id) {
|
||||
config.setState(state => ({ ...state, chainId: kraikenLocalFork.id }));
|
||||
if (typeof window !== 'undefined' && config.state.chainId !== KRAIKEN_LOCAL_CHAIN.id) {
|
||||
config.setState(state => ({ ...state, chainId: KRAIKEN_LOCAL_CHAIN.id }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
|||
// https://vite.dev/config/
|
||||
export default defineConfig(() => {
|
||||
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET
|
||||
const localGraphqlProxyTarget = process.env.VITE_LOCAL_GRAPHQL_PROXY_TARGET ?? 'http://127.0.0.1:42069'
|
||||
const localTxnProxyTarget = process.env.VITE_LOCAL_TXN_PROXY_TARGET ?? 'http://127.0.0.1:43069'
|
||||
|
||||
return {
|
||||
// base: "/HarbergPublic/",
|
||||
|
|
@ -21,19 +23,47 @@ export default defineConfig(() => {
|
|||
},
|
||||
},
|
||||
server: {
|
||||
proxy: localRpcProxyTarget
|
||||
? {
|
||||
'/rpc/anvil': {
|
||||
target: localRpcProxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => {
|
||||
const rewritten = path.replace(/^\/rpc\/anvil/, '')
|
||||
return rewritten.length === 0 ? '/' : rewritten
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
proxy:
|
||||
localRpcProxyTarget || localGraphqlProxyTarget || localTxnProxyTarget
|
||||
? {
|
||||
...(localRpcProxyTarget
|
||||
? {
|
||||
'/api/rpc': {
|
||||
target: localRpcProxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path: string) => {
|
||||
const rewritten = path.replace(/^\/api\/rpc/, '')
|
||||
return rewritten.length === 0 ? '/' : rewritten
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(localGraphqlProxyTarget
|
||||
? {
|
||||
'/api/graphql': {
|
||||
target: localGraphqlProxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path: string) => path.replace(/^\/api\/graphql/, '/graphql'),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(localTxnProxyTarget
|
||||
? {
|
||||
'/api/txn': {
|
||||
target: localTxnProxyTarget,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path: string) => {
|
||||
const rewritten = path.replace(/^\/api\/txn/, '')
|
||||
return rewritten.length === 0 ? '/' : rewritten
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue