tax rate, version and compose (#70)
resolves #67 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/70
This commit is contained in:
parent
d8ca557eb6
commit
6cbb1781ce
40 changed files with 1243 additions and 213 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -28,3 +28,12 @@ tmp
|
||||||
foundry.lock
|
foundry.lock
|
||||||
services/ponder/.env.local
|
services/ponder/.env.local
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
test-results/
|
||||||
|
tests/.stack.log
|
||||||
|
playwright-report/
|
||||||
|
services/ponder/.ponder/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
/tmp/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,49 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Version validation - check if contract VERSION is in COMPATIBLE_CONTRACT_VERSIONS
|
||||||
|
# Only run if both version files exist (system may not be implemented yet)
|
||||||
|
if [ -f "onchain/src/Kraiken.sol" ] && [ -f "kraiken-lib/src/version.ts" ]; then
|
||||||
|
STAGED_FILES=$(git diff --staged --name-only)
|
||||||
|
if echo "$STAGED_FILES" | grep -qE "(onchain/src/Kraiken.sol|kraiken-lib/src/version.ts)"; then
|
||||||
|
echo "Validating version sync between contract and kraiken-lib..."
|
||||||
|
|
||||||
|
# Extract contract VERSION constant
|
||||||
|
CONTRACT_VERSION=$(grep -oP 'VERSION\s*=\s*\K\d+' onchain/src/Kraiken.sol | head -1)
|
||||||
|
|
||||||
|
# Only validate if VERSION constant exists in contract
|
||||||
|
if [ -n "$CONTRACT_VERSION" ]; then
|
||||||
|
# Extract library version and compatible versions
|
||||||
|
LIB_VERSION=$(grep -oP 'KRAIKEN_LIB_VERSION\s*=\s*\K\d+' kraiken-lib/src/version.ts)
|
||||||
|
COMPATIBLE=$(grep -oP 'COMPATIBLE_CONTRACT_VERSIONS\s*=\s*\[\K[^\]]+' kraiken-lib/src/version.ts)
|
||||||
|
|
||||||
|
echo " Contract VERSION: $CONTRACT_VERSION"
|
||||||
|
echo " Library VERSION: $LIB_VERSION"
|
||||||
|
echo " Compatible versions: $COMPATIBLE"
|
||||||
|
|
||||||
|
# Check if contract version is in compatible list
|
||||||
|
if echo ",$COMPATIBLE," | grep -q ",$CONTRACT_VERSION,"; then
|
||||||
|
echo " ✓ Version sync validated"
|
||||||
|
else
|
||||||
|
echo " ❌ Version validation failed!"
|
||||||
|
echo ""
|
||||||
|
echo "Contract VERSION ($CONTRACT_VERSION) not in COMPATIBLE_CONTRACT_VERSIONS"
|
||||||
|
echo ""
|
||||||
|
echo "To fix:"
|
||||||
|
echo "1. Update COMPATIBLE_CONTRACT_VERSIONS in kraiken-lib/src/version.ts to include $CONTRACT_VERSION:"
|
||||||
|
echo " export const COMPATIBLE_CONTRACT_VERSIONS = [$COMPATIBLE, $CONTRACT_VERSION];"
|
||||||
|
echo ""
|
||||||
|
echo "2. Or update KRAIKEN_LIB_VERSION if this is a breaking change:"
|
||||||
|
echo " export const KRAIKEN_LIB_VERSION = $CONTRACT_VERSION;"
|
||||||
|
echo " export const COMPATIBLE_CONTRACT_VERSIONS = [$CONTRACT_VERSION];"
|
||||||
|
echo ""
|
||||||
|
echo "See VERSION_VALIDATION.md for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -d "onchain" ]; then
|
if [ -d "onchain" ]; then
|
||||||
(cd onchain && npx lint-staged)
|
(cd onchain && npx lint-staged)
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
19
AGENTS.md
19
AGENTS.md
|
|
@ -31,6 +31,19 @@
|
||||||
- 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:42069/graphql` for Ponder, and poll `http://127.0.0.1:43069/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
|
||||||
|
- **Contract VERSION**: `Kraiken.sol` exposes a `VERSION` constant (currently v1) that must be incremented for breaking changes to TAX_RATES, events, or core data structures.
|
||||||
|
- **Ponder Validation**: On startup, Ponder reads the contract VERSION and validates against `COMPATIBLE_CONTRACT_VERSIONS` in `kraiken-lib/src/version.ts`. Fails hard (exit 1) on mismatch to prevent indexing wrong data.
|
||||||
|
- **Frontend Check**: Web-app validates `KRAIKEN_LIB_VERSION` at runtime (currently placeholder; future: query Ponder GraphQL for full 3-way validation).
|
||||||
|
- **CI Enforcement**: GitHub workflow validates that contract VERSION is in `COMPATIBLE_CONTRACT_VERSIONS` before merging PRs.
|
||||||
|
- See `VERSION_VALIDATION.md` for complete architecture, workflows, and troubleshooting.
|
||||||
|
|
||||||
|
## Podman Orchestration
|
||||||
|
- **Dependency Management**: `podman-compose.yml` has NO `depends_on` declarations. All service ordering is handled in `scripts/dev.sh` via phased startup with explicit health checks.
|
||||||
|
- **Why**: Podman's dependency graph validator fails when containers have compose metadata dependencies, causing "container not found in input list" errors even when containers exist.
|
||||||
|
- **Startup Phases**: (1) Create all containers, (2) Start anvil+postgres and wait for healthy, (3) Start bootstrap and wait for completion, (4) Start ponder and wait for healthy, (5) Start webapp/landing/txn-bot, (6) Start caddy.
|
||||||
|
- If you see dependency graph errors, verify `depends_on` was not re-added to `podman-compose.yml`.
|
||||||
|
|
||||||
## Guardrails & Tips
|
## Guardrails & Tips
|
||||||
- `token0isWeth` flips amount semantics; confirm ordering before seeding or interpreting liquidity.
|
- `token0isWeth` flips amount semantics; confirm ordering before seeding or interpreting liquidity.
|
||||||
- VWAP, `ethScarcity`, and Optimizer outputs operate on price^2 (X96). Avoid "normalising" to sqrt inadvertently.
|
- VWAP, `ethScarcity`, and Optimizer outputs operate on price^2 (X96). Avoid "normalising" to sqrt inadvertently.
|
||||||
|
|
@ -41,6 +54,12 @@
|
||||||
- **kraiken-lib Build**: Run `./scripts/build-kraiken-lib.sh` before `podman-compose up` so containers mount a fresh `kraiken-lib/dist` from the host.
|
- **kraiken-lib Build**: Run `./scripts/build-kraiken-lib.sh` before `podman-compose up` so containers mount a fresh `kraiken-lib/dist` from the host.
|
||||||
- **Live Reload**: `scripts/watch-kraiken-lib.sh` rebuilds on file changes (requires inotify-tools) and restarts dependent containers automatically.
|
- **Live Reload**: `scripts/watch-kraiken-lib.sh` rebuilds on file changes (requires inotify-tools) and restarts dependent containers automatically.
|
||||||
|
|
||||||
|
## Code Quality & Git Hooks
|
||||||
|
- **Pre-commit Hooks**: Husky runs lint-staged on all staged files before commits. Each component (onchain, kraiken-lib, ponder, txnBot, web-app, landing) has `.lintstagedrc.json` configured for ESLint + Prettier.
|
||||||
|
- **Version Validation (Future)**: Pre-commit hook includes validation logic that will enforce version sync between `onchain/src/Kraiken.sol` (contract VERSION constant) and `kraiken-lib/src/version.ts` (COMPATIBLE_CONTRACT_VERSIONS array). This validation only runs if both files exist and contain version information.
|
||||||
|
- **Husky Setup**: `.husky/pre-commit` orchestrates all pre-commit checks. Modify this file to add new validation steps.
|
||||||
|
- To test hooks manually: `git add <files> && .husky/pre-commit`
|
||||||
|
|
||||||
## Handy Commands
|
## Handy Commands
|
||||||
- `foundryup` - update Foundry toolchain.
|
- `foundryup` - update Foundry toolchain.
|
||||||
- `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.
|
||||||
|
|
|
||||||
264
CHANGELOG_VERSION_VALIDATION.md
Normal file
264
CHANGELOG_VERSION_VALIDATION.md
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
# Changelog: Version Validation System & Tax Rate Index Refactoring
|
||||||
|
|
||||||
|
## Date: 2025-10-07
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
This release implements a comprehensive version validation system to ensure contract-indexer-frontend compatibility and completes the tax rate index refactoring to eliminate fragile decimal lookups.
|
||||||
|
|
||||||
|
## Major Features
|
||||||
|
|
||||||
|
### 1. Version Validation System
|
||||||
|
|
||||||
|
**Contract Changes:**
|
||||||
|
- `onchain/src/Kraiken.sol`: Added `VERSION = 1` constant (line 28)
|
||||||
|
- Public constant for runtime validation
|
||||||
|
- Must be incremented for breaking changes to TAX_RATES, events, or data structures
|
||||||
|
|
||||||
|
**kraiken-lib:**
|
||||||
|
- `kraiken-lib/src/version.ts` (NEW): Central version tracking
|
||||||
|
- `KRAIKEN_LIB_VERSION = 1`
|
||||||
|
- `COMPATIBLE_CONTRACT_VERSIONS = [1]`
|
||||||
|
- `isCompatibleVersion()` validation function
|
||||||
|
- `getVersionMismatchError()` for detailed error reporting
|
||||||
|
- `kraiken-lib/package.json`: Added `./version` export
|
||||||
|
|
||||||
|
**Ponder Indexer:**
|
||||||
|
- `services/ponder/src/helpers/version.ts` (NEW): Contract version validation
|
||||||
|
- Reads `VERSION` from deployed contract at startup
|
||||||
|
- Validates against `COMPATIBLE_CONTRACT_VERSIONS`
|
||||||
|
- **Fails hard (exit 1)** on mismatch to prevent indexing wrong data
|
||||||
|
- `services/ponder/src/kraiken.ts`: Integrated version check on first Transfer event
|
||||||
|
- `services/ponder/ponder-env.d.ts`: Fixed permissions (chmod 666)
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `web-app/src/composables/useVersionCheck.ts` (NEW): Version validation composable
|
||||||
|
- Validates `KRAIKEN_LIB_VERSION` loads correctly
|
||||||
|
- Placeholder for future GraphQL-based 3-way validation
|
||||||
|
- Warns (doesn't fail) on mismatch
|
||||||
|
|
||||||
|
**CI/CD:**
|
||||||
|
- `.github/workflows/validate-version.yml` (NEW): Automated version validation
|
||||||
|
- Validates contract VERSION is in COMPATIBLE_CONTRACT_VERSIONS
|
||||||
|
- Runs on PRs and pushes to master/main
|
||||||
|
- Prevents merging incompatible versions
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- `VERSION_VALIDATION.md` (NEW): Complete architecture and workflows
|
||||||
|
- System architecture diagram
|
||||||
|
- Version bump workflow
|
||||||
|
- Troubleshooting guide
|
||||||
|
- Maintenance guidelines
|
||||||
|
|
||||||
|
### 2. Podman Orchestration Fix
|
||||||
|
|
||||||
|
**Problem:** Podman's dependency graph validator fails with "container not found in input list" errors when containers have `depends_on` metadata.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- `podman-compose.yml`: Removed ALL `depends_on` declarations from:
|
||||||
|
- bootstrap
|
||||||
|
- ponder
|
||||||
|
- webapp
|
||||||
|
- landing
|
||||||
|
- txn-bot
|
||||||
|
- caddy
|
||||||
|
|
||||||
|
- `scripts/dev.sh`: Implemented phased startup with explicit health checks:
|
||||||
|
1. Create all containers (`podman-compose up --no-start`)
|
||||||
|
2. Start anvil & postgres, wait for healthy
|
||||||
|
3. Start bootstrap, wait for completion
|
||||||
|
4. Start ponder, wait for healthy
|
||||||
|
5. Start webapp/landing/txn-bot
|
||||||
|
6. Start caddy
|
||||||
|
|
||||||
|
**Result:** Stack starts reliably without dependency graph errors.
|
||||||
|
|
||||||
|
### 3. Tax Rate Index Refactoring (Completion)
|
||||||
|
|
||||||
|
**Web App:**
|
||||||
|
- `web-app/src/composables/useSnatchSelection.ts`:
|
||||||
|
- Replaced `position.taxRate >= maxTaxRateDecimal` with `posIndex >= selectedTaxRateIndex`
|
||||||
|
- Fixed test data to match index-based logic
|
||||||
|
|
||||||
|
- `web-app/src/composables/usePositions.ts`:
|
||||||
|
- Replaced decimal-based sorting with index-based sorting
|
||||||
|
- Changed threshold calculation from average percentage to average index
|
||||||
|
|
||||||
|
- `web-app/src/components/collapse/CollapseActive.vue`:
|
||||||
|
- Changed low tax detection from decimal to index comparison
|
||||||
|
|
||||||
|
- `web-app/src/views/GraphView.vue`: **DELETED** (dead code, 63 lines)
|
||||||
|
|
||||||
|
**Ponder:**
|
||||||
|
- `services/ponder/ponder.schema.ts`:
|
||||||
|
- **CRITICAL FIX**: Import `TAX_RATE_OPTIONS` from kraiken-lib instead of hardcoded array
|
||||||
|
- Added `taxRateIndex` column to positions table
|
||||||
|
- Added index on `taxRateIndex` column
|
||||||
|
|
||||||
|
- `services/ponder/src/stake.ts`:
|
||||||
|
- Extract and store `taxRateIndex` from contract events
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- `kraiken-lib/src/tests/taxRates.test.ts`: Fixed Jest ES module compatibility
|
||||||
|
- `kraiken-lib/jest.config.js` → `kraiken-lib/jest.config.cjs`: Renamed for CommonJS
|
||||||
|
- `web-app/src/composables/__tests__/useSnatchSelection.spec.ts`: Fixed test data inconsistencies
|
||||||
|
|
||||||
|
## File Changes
|
||||||
|
|
||||||
|
### Added Files (7)
|
||||||
|
1. `.github/workflows/validate-version.yml` - CI/CD validation
|
||||||
|
2. `VERSION_VALIDATION.md` - Documentation
|
||||||
|
3. `kraiken-lib/src/version.ts` - Version tracking
|
||||||
|
4. `kraiken-lib/jest.config.cjs` - Jest config
|
||||||
|
5. `services/ponder/src/helpers/version.ts` - Ponder validation
|
||||||
|
6. `web-app/src/composables/useVersionCheck.ts` - Frontend validation
|
||||||
|
7. `scripts/sync-tax-rates.mjs` - Tax rate sync script
|
||||||
|
|
||||||
|
### Deleted Files (2)
|
||||||
|
1. `web-app/src/views/GraphView.vue` - Dead code
|
||||||
|
2. `kraiken-lib/jest.config.js` - Replaced with .cjs
|
||||||
|
|
||||||
|
### Modified Files (29)
|
||||||
|
1. `.gitignore` - Added test artifacts, logs, ponder state
|
||||||
|
2. `CLAUDE.md` - Added Version Validation and Podman Orchestration sections
|
||||||
|
3. `kraiken-lib/AGENTS.md` - Added version.ts to Key Modules
|
||||||
|
4. `kraiken-lib/package.json` - Added ./version export
|
||||||
|
5. `kraiken-lib/src/index.ts` - Export version validation functions
|
||||||
|
6. `kraiken-lib/src/taxRates.ts` - Generated tax rates with checksums
|
||||||
|
7. `kraiken-lib/src/tests/taxRates.test.ts` - Fixed Jest compatibility
|
||||||
|
8. `onchain/src/Kraiken.sol` - Added VERSION constant
|
||||||
|
9. `podman-compose.yml` - Removed all depends_on declarations
|
||||||
|
10. `scripts/build-kraiken-lib.sh` - Updated build process
|
||||||
|
11. `scripts/dev.sh` - Implemented phased startup
|
||||||
|
12. `services/ponder/AGENTS.md` - Updated documentation
|
||||||
|
13. `services/ponder/ponder-env.d.ts` - Fixed permissions
|
||||||
|
14. `services/ponder/ponder.schema.ts` - Import from kraiken-lib, add taxRateIndex
|
||||||
|
15. `services/ponder/src/kraiken.ts` - Added version validation
|
||||||
|
16. `services/ponder/src/stake.ts` - Store taxRateIndex
|
||||||
|
17. `tests/e2e/01-acquire-and-stake.spec.ts` - Test updates
|
||||||
|
18. `web-app/README.md` - Documentation updates
|
||||||
|
19. `web-app/env.d.ts` - Type updates
|
||||||
|
20. `web-app/src/components/StakeHolder.vue` - Index-based logic
|
||||||
|
21. `web-app/src/components/collapse/CollapseActive.vue` - Index comparison
|
||||||
|
22. `web-app/src/components/fcomponents/FSelect.vue` - Index handling
|
||||||
|
23. `web-app/src/composables/__tests__/useSnatchSelection.spec.ts` - Fixed tests
|
||||||
|
24. `web-app/src/composables/useAdjustTaxRates.ts` - Index-based adjustments
|
||||||
|
25. `web-app/src/composables/usePositions.ts` - Index-based sorting and threshold
|
||||||
|
26. `web-app/src/composables/useSnatchSelection.ts` - Index-based filtering
|
||||||
|
27. `web-app/src/composables/useStake.ts` - Index handling
|
||||||
|
28-29. Various documentation and configuration updates
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
### For Contract Deployments
|
||||||
|
- **New VERSION constant must be present** in Kraiken.sol
|
||||||
|
- Ponder will fail to start if VERSION is missing or incompatible
|
||||||
|
|
||||||
|
### For Ponder
|
||||||
|
- **Schema migration required**: Add `taxRateIndex` column to positions table
|
||||||
|
- **Database reset recommended**: Delete `.ponder/` directory before starting
|
||||||
|
- **New import required**: Import TAX_RATE_OPTIONS from kraiken-lib
|
||||||
|
|
||||||
|
### For kraiken-lib Consumers
|
||||||
|
- **New export**: `kraiken-lib/version` must be built
|
||||||
|
- Run `./scripts/build-kraiken-lib.sh` to regenerate dist/
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### Updating to This Version
|
||||||
|
|
||||||
|
1. **Stop the stack:**
|
||||||
|
```bash
|
||||||
|
./scripts/dev.sh stop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clean Ponder state:**
|
||||||
|
```bash
|
||||||
|
rm -rf services/ponder/.ponder/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rebuild kraiken-lib:**
|
||||||
|
```bash
|
||||||
|
./scripts/build-kraiken-lib.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Rebuild contracts (if needed):**
|
||||||
|
```bash
|
||||||
|
cd onchain && forge build
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start the stack:**
|
||||||
|
```bash
|
||||||
|
./scripts/dev.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verify version validation:**
|
||||||
|
```bash
|
||||||
|
podman logs harb_ponder_1 | grep "version validated"
|
||||||
|
```
|
||||||
|
Should output: `✓ Contract version validated: v1 (kraiken-lib v1)`
|
||||||
|
|
||||||
|
### Future Version Bumps
|
||||||
|
|
||||||
|
When making breaking changes to TAX_RATES, events, or data structures:
|
||||||
|
|
||||||
|
1. **Increment VERSION in Kraiken.sol:**
|
||||||
|
```solidity
|
||||||
|
uint256 public constant VERSION = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update COMPATIBLE_CONTRACT_VERSIONS in kraiken-lib/src/version.ts:**
|
||||||
|
```typescript
|
||||||
|
export const KRAIKEN_LIB_VERSION = 2;
|
||||||
|
export const COMPATIBLE_CONTRACT_VERSIONS = [2]; // Or [1, 2] for backward compat
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rebuild and redeploy:**
|
||||||
|
```bash
|
||||||
|
./scripts/build-kraiken-lib.sh
|
||||||
|
rm -rf services/ponder/.ponder/
|
||||||
|
cd onchain && forge script script/Deploy.s.sol
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- ✅ kraiken-lib tests pass
|
||||||
|
- ✅ web-app tests pass
|
||||||
|
- ✅ Ponder codegen succeeds
|
||||||
|
- ✅ onchain tests pass
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- ✅ Stack starts without dependency errors
|
||||||
|
- ✅ Ponder validates contract version successfully
|
||||||
|
- ✅ Ponder indexes events with taxRateIndex
|
||||||
|
- ✅ GraphQL endpoint responds
|
||||||
|
- ✅ Version validation logs appear in Ponder output
|
||||||
|
|
||||||
|
### Manual Verification
|
||||||
|
```bash
|
||||||
|
# Check Ponder logs for version validation
|
||||||
|
podman logs harb_ponder_1 | grep "version validated"
|
||||||
|
# Output: ✓ Contract version validated: v1 (kraiken-lib v1)
|
||||||
|
|
||||||
|
# Check contract VERSION
|
||||||
|
cast call $KRAIKEN_ADDRESS "VERSION()" --rpc-url http://localhost:8545
|
||||||
|
# Output: 1
|
||||||
|
|
||||||
|
# Query positions with taxRateIndex
|
||||||
|
curl -X POST http://localhost:42069/graphql \
|
||||||
|
-d '{"query":"{ positions { id taxRateIndex taxRate } }"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None. All blocking issues resolved.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
- Claude Code (Anthropic)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Full architecture: `VERSION_VALIDATION.md`
|
||||||
|
- Podman orchestration: `CLAUDE.md` § Podman Orchestration
|
||||||
|
- Tax rate system: `kraiken-lib/src/taxRates.ts`
|
||||||
279
VERSION_VALIDATION.md
Normal file
279
VERSION_VALIDATION.md
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
# Version Validation System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Kraiken protocol now includes a **version validation system** that ensures all stack components (contract, indexer, frontend) are synchronized and compatible. This prevents subtle data corruption bugs that arise from version mismatches.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Kraiken.sol │
|
||||||
|
│ uint256 public constant VERSION=1 │ ← Source of Truth
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│ read at startup
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Ponder Indexer │
|
||||||
|
│ • Reads contract.VERSION() │
|
||||||
|
│ • Validates against │
|
||||||
|
│ COMPATIBLE_CONTRACT_VERSIONS │
|
||||||
|
│ • FAILS HARD if mismatch │
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│ GraphQL
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Frontend (web-app) │
|
||||||
|
│ • Checks KRAIKEN_LIB_VERSION │
|
||||||
|
│ • Shows warning if issues │
|
||||||
|
│ • Continues operation │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Contract Version Constant
|
||||||
|
|
||||||
|
**File:** `onchain/src/Kraiken.sol`
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
contract Kraiken is ERC20, ERC20Permit {
|
||||||
|
/**
|
||||||
|
* @notice Protocol version for data structure compatibility.
|
||||||
|
* Increment when making breaking changes to TAX_RATES, events, or core data structures.
|
||||||
|
*/
|
||||||
|
uint256 public constant VERSION = 1;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key properties:**
|
||||||
|
- `public constant` = free to read, immutable
|
||||||
|
- Forces contract redeployment on breaking changes
|
||||||
|
- Single source of truth for the entire stack
|
||||||
|
|
||||||
|
### 2. kraiken-lib Version Tracking
|
||||||
|
|
||||||
|
**File:** `kraiken-lib/src/version.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const KRAIKEN_LIB_VERSION = 1;
|
||||||
|
|
||||||
|
export const COMPATIBLE_CONTRACT_VERSIONS = [1];
|
||||||
|
|
||||||
|
export function isCompatibleVersion(contractVersion: number): boolean {
|
||||||
|
return COMPATIBLE_CONTRACT_VERSIONS.includes(contractVersion);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key features:**
|
||||||
|
- Manually maintained list (not parsed from contract)
|
||||||
|
- Allows backward compatibility (lib can support multiple contract versions)
|
||||||
|
- Exported by `kraiken-lib/src/index.ts` for all consumers
|
||||||
|
|
||||||
|
### 3. Ponder Validation (FAIL HARD)
|
||||||
|
|
||||||
|
**File:** `services/ponder/src/helpers/version.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function validateContractVersion(context: Context): Promise<void> {
|
||||||
|
const contractVersion = await context.client.readContract({
|
||||||
|
address: context.contracts.Kraiken.address,
|
||||||
|
abi: context.contracts.Kraiken.abi,
|
||||||
|
functionName: 'VERSION',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isCompatibleVersion(Number(contractVersion))) {
|
||||||
|
console.error(getVersionMismatchError(contractVersion, 'ponder'));
|
||||||
|
process.exit(1); // FAIL HARD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Called in:** `services/ponder/src/kraiken.ts` (first Transfer event)
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Reads `VERSION` from deployed Kraiken contract
|
||||||
|
- Compares against `COMPATIBLE_CONTRACT_VERSIONS`
|
||||||
|
- **Exits with error** if incompatible (prevents indexing wrong data)
|
||||||
|
- Logs success if compatible
|
||||||
|
|
||||||
|
### 4. Frontend Validation (WARN)
|
||||||
|
|
||||||
|
**File:** `web-app/src/composables/useVersionCheck.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useVersionCheck() {
|
||||||
|
async function checkVersions(graphqlUrl: string) {
|
||||||
|
// Currently placeholder - validates lib loaded correctly
|
||||||
|
// Future: Query Ponder for stored contract version
|
||||||
|
versionStatus.value = {
|
||||||
|
isValid: true,
|
||||||
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Validates `KRAIKEN_LIB_VERSION` is loaded
|
||||||
|
- Future enhancement: Query Ponder GraphQL for contract version
|
||||||
|
- Shows warning banner if mismatch (doesn't break app)
|
||||||
|
|
||||||
|
### 5. CI/CD Validation
|
||||||
|
|
||||||
|
**File:** `.github/workflows/validate-version.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Extract versions and validate
|
||||||
|
run: |
|
||||||
|
CONTRACT_VERSION=$(grep -oP 'VERSION\s*=\s*\K\d+' onchain/src/Kraiken.sol)
|
||||||
|
LIB_VERSION=$(grep -oP 'KRAIKEN_LIB_VERSION\s*=\s*\K\d+' kraiken-lib/src/version.ts)
|
||||||
|
COMPATIBLE=$(grep -oP 'COMPATIBLE_CONTRACT_VERSIONS\s*=\s*\[\K[^\]]+' kraiken-lib/src/version.ts)
|
||||||
|
|
||||||
|
if echo ",$COMPATIBLE," | grep -q ",$CONTRACT_VERSION,"; then
|
||||||
|
echo "✓ Version sync validated"
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Triggered on:**
|
||||||
|
- PRs touching `Kraiken.sol` or `version.ts`
|
||||||
|
- Pushes to `master`/`main`
|
||||||
|
|
||||||
|
**Prevents:**
|
||||||
|
- Merging incompatible versions
|
||||||
|
- Deploying with stale kraiken-lib
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Version Bump (Breaking Change)
|
||||||
|
|
||||||
|
When modifying TAX_RATES or other critical data structures:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
1. Edit onchain/src/Kraiken.sol
|
||||||
|
uint256 public constant VERSION = 2; // Increment
|
||||||
|
|
||||||
|
2. Edit kraiken-lib/src/version.ts
|
||||||
|
export const KRAIKEN_LIB_VERSION = 2;
|
||||||
|
export const COMPATIBLE_CONTRACT_VERSIONS = [2]; // Or [1, 2] for backward compat
|
||||||
|
|
||||||
|
3. Rebuild kraiken-lib
|
||||||
|
./scripts/build-kraiken-lib.sh
|
||||||
|
|
||||||
|
4. Clear Ponder state
|
||||||
|
rm -rf services/ponder/.ponder/
|
||||||
|
|
||||||
|
5. Redeploy contracts (if needed)
|
||||||
|
cd onchain && forge script ...
|
||||||
|
|
||||||
|
6. Restart stack
|
||||||
|
./scripts/dev.sh restart --full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Backward Compatibility
|
||||||
|
|
||||||
|
If lib can work with old AND new contract versions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// kraiken-lib/src/version.ts
|
||||||
|
export const KRAIKEN_LIB_VERSION = 2;
|
||||||
|
export const COMPATIBLE_CONTRACT_VERSIONS = [1, 2]; // Supports both
|
||||||
|
```
|
||||||
|
|
||||||
|
Ponder will accept either v1 or v2 contracts.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Ponder fails with "VERSION MISMATCH"
|
||||||
|
|
||||||
|
```
|
||||||
|
╔════════════════════════════════════════════════════════════╗
|
||||||
|
║ ❌ CRITICAL: VERSION MISMATCH (ponder)
|
||||||
|
╠════════════════════════════════════════════════════════════╣
|
||||||
|
║ Contract VERSION: 2
|
||||||
|
║ Library VERSION: 1
|
||||||
|
║ Compatible versions: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Check if contract was upgraded
|
||||||
|
2. Update `COMPATIBLE_CONTRACT_VERSIONS` in `kraiken-lib/src/version.ts`
|
||||||
|
3. Run `./scripts/build-kraiken-lib.sh`
|
||||||
|
4. Run `rm -rf services/ponder/.ponder/`
|
||||||
|
5. Restart Ponder
|
||||||
|
|
||||||
|
#### CI/CD check fails
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Contract VERSION (2) not in COMPATIBLE_CONTRACT_VERSIONS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Update `kraiken-lib/src/version.ts` to include new version
|
||||||
|
- Or revert contract changes if unintentional
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **Prevents data corruption** - Ponder won't index with wrong schema
|
||||||
|
✅ **Early detection** - Fails at startup, not after hours of indexing
|
||||||
|
✅ **Clear errors** - Tells you exactly what to do
|
||||||
|
✅ **Backward compat** - Can support multiple versions if needed
|
||||||
|
✅ **CI enforcement** - Prevents merging incompatible changes
|
||||||
|
✅ **Future-proof** - Easy to extend to other contracts (Stake, LiquidityManager)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Store version in Ponder stats table**
|
||||||
|
- Add `contractVersion` column to `stats`
|
||||||
|
- Frontend can query via GraphQL
|
||||||
|
- Enables full 3-way validation (contract → Ponder → frontend)
|
||||||
|
|
||||||
|
2. **Multi-contract versioning**
|
||||||
|
- Add `VERSION` to `Stake.sol`, `LiquidityManager.sol`
|
||||||
|
- Track all contract versions in kraiken-lib
|
||||||
|
- Validate full deployment is compatible
|
||||||
|
|
||||||
|
3. **Version migration helpers**
|
||||||
|
- Scripts to handle data migrations
|
||||||
|
- Backward-compatible event handling
|
||||||
|
- Gradual rollout support
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### When to increment VERSION
|
||||||
|
|
||||||
|
Increment when changes affect **indexed data or frontend interpretation**:
|
||||||
|
|
||||||
|
- ✅ Modifying `TAX_RATES` array
|
||||||
|
- ✅ Adding/removing event parameters
|
||||||
|
- ✅ Changing event names
|
||||||
|
- ✅ Modifying struct definitions that frontends consume
|
||||||
|
- ✅ Breaking changes to staking logic
|
||||||
|
|
||||||
|
**Don't** increment for:
|
||||||
|
|
||||||
|
- ❌ Gas optimizations (no data changes)
|
||||||
|
- ❌ Internal logic changes (if events unchanged)
|
||||||
|
- ❌ Comment updates
|
||||||
|
- ❌ Pure view function changes
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Update `VERSION` comment in contract with each change:
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
/**
|
||||||
|
* Version History:
|
||||||
|
* - v1: Initial deployment (30-tier TAX_RATES)
|
||||||
|
* - v2: Added new tax tier at 150% (31 tiers total)
|
||||||
|
* - v3: Modified PositionCreated event (added taxRateIndex field)
|
||||||
|
*/
|
||||||
|
uint256 public constant VERSION = 3;
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an audit trail for version changes.
|
||||||
6
kraiken-lib/.lintstagedrc.json
Normal file
6
kraiken-lib/.lintstagedrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"src/**/*.ts": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
5
kraiken-lib/.prettierignore
Normal file
5
kraiken-lib/.prettierignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Auto-generated files
|
||||||
|
src/taxRates.ts
|
||||||
|
|
||||||
|
# Generated TypeScript definitions
|
||||||
|
src/__generated__/
|
||||||
|
|
@ -14,6 +14,8 @@ Shared TypeScript helpers used by the landing app, txnBot, and other services to
|
||||||
- `src/queries/` - GraphQL operations that target the Ponder schema.
|
- `src/queries/` - GraphQL operations that target the Ponder schema.
|
||||||
- `src/__generated__/graphql.ts` - Codegen output consumed throughout the stack.
|
- `src/__generated__/graphql.ts` - Codegen output consumed throughout the stack.
|
||||||
- `src/abis.ts` - Contract ABIs imported directly from `onchain/out/` forge artifacts. Single source of truth for all ABI consumers.
|
- `src/abis.ts` - Contract ABIs imported directly from `onchain/out/` forge artifacts. Single source of truth for all ABI consumers.
|
||||||
|
- `src/taxRates.ts` - Generated from `onchain/src/Stake.sol` by `scripts/sync-tax-rates.mjs`; never edit by hand.
|
||||||
|
- `src/version.ts` - Version validation system tracking `KRAIKEN_LIB_VERSION` and `COMPATIBLE_CONTRACT_VERSIONS` for runtime dependency checking.
|
||||||
|
|
||||||
## GraphQL Code Generation
|
## GraphQL Code Generation
|
||||||
- Schema source points to the Ponder GraphQL endpoint for the active environment.
|
- Schema source points to the Ponder GraphQL endpoint for the active environment.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import tsParser from "@typescript-eslint/parser";
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
files: ["src/**/*.ts"],
|
files: ["src/**/*.ts"],
|
||||||
ignores: ["src/tests/**/*", "src/__generated__/**/*"],
|
ignores: ["src/tests/**/*", "src/__generated__/**/*", "src/taxRates.ts"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
parserOptions: { project: "./tsconfig.json" }
|
parserOptions: { project: "./tsconfig.json" }
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@
|
||||||
"types": "./dist/abis.d.ts",
|
"types": "./dist/abis.d.ts",
|
||||||
"require": "./dist/abis.js",
|
"require": "./dist/abis.js",
|
||||||
"import": "./dist/abis.js"
|
"import": "./dist/abis.js"
|
||||||
|
},
|
||||||
|
"./version": {
|
||||||
|
"types": "./dist/version.d.ts",
|
||||||
|
"require": "./dist/version.js",
|
||||||
|
"import": "./dist/version.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,5 @@ export { KRAIKEN_ABI, STAKE_ABI, ABIS } from './abis.js';
|
||||||
|
|
||||||
// Backward compatible aliases
|
// Backward compatible aliases
|
||||||
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
|
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
|
||||||
|
|
||||||
|
export { KRAIKEN_LIB_VERSION, COMPATIBLE_CONTRACT_VERSIONS, isCompatibleVersion, getVersionMismatchError } from './version.js';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
/**
|
||||||
|
* AUTO-GENERATED FILE — DO NOT EDIT
|
||||||
|
*
|
||||||
|
* Generated by scripts/sync-tax-rates.mjs from onchain/src/Stake.sol.
|
||||||
|
* Run `node scripts/sync-tax-rates.mjs` after modifying the contract TAX_RATES array.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface TaxRateOption {
|
export interface TaxRateOption {
|
||||||
index: number;
|
index: number;
|
||||||
year: number;
|
year: number;
|
||||||
|
|
@ -18,22 +25,42 @@ export const TAX_RATE_OPTIONS: TaxRateOption[] = [
|
||||||
{ index: 9, year: 50, daily: 0.13699, decimal: 0.5 },
|
{ index: 9, year: 50, daily: 0.13699, decimal: 0.5 },
|
||||||
{ index: 10, year: 60, daily: 0.16438, decimal: 0.6 },
|
{ index: 10, year: 60, daily: 0.16438, decimal: 0.6 },
|
||||||
{ index: 11, year: 80, daily: 0.21918, decimal: 0.8 },
|
{ index: 11, year: 80, daily: 0.21918, decimal: 0.8 },
|
||||||
{ index: 12, year: 100, daily: 0.27397, decimal: 1.0 },
|
{ index: 12, year: 100, daily: 0.27397, decimal: 1 },
|
||||||
{ index: 13, year: 130, daily: 0.35616, decimal: 1.3 },
|
{ index: 13, year: 130, daily: 0.35616, decimal: 1.3 },
|
||||||
{ index: 14, year: 180, daily: 0.49315, decimal: 1.8 },
|
{ index: 14, year: 180, daily: 0.49315, decimal: 1.8 },
|
||||||
{ index: 15, year: 250, daily: 0.68493, decimal: 2.5 },
|
{ index: 15, year: 250, daily: 0.68493, decimal: 2.5 },
|
||||||
{ index: 16, year: 320, daily: 0.87671, decimal: 3.2 },
|
{ index: 16, year: 320, daily: 0.87671, decimal: 3.2 },
|
||||||
{ index: 17, year: 420, daily: 1.15068, decimal: 4.2 },
|
{ index: 17, year: 420, daily: 1.15068, decimal: 4.2 },
|
||||||
{ index: 18, year: 540, daily: 1.47945, decimal: 5.4 },
|
{ index: 18, year: 540, daily: 1.47945, decimal: 5.4 },
|
||||||
{ index: 19, year: 700, daily: 1.91781, decimal: 7.0 },
|
{ index: 19, year: 700, daily: 1.91781, decimal: 7 },
|
||||||
{ index: 20, year: 920, daily: 2.52055, decimal: 9.2 },
|
{ index: 20, year: 920, daily: 2.52055, decimal: 9.2 },
|
||||||
{ index: 21, year: 1200, daily: 3.28767, decimal: 12.0 },
|
{ index: 21, year: 1200, daily: 3.28767, decimal: 12 },
|
||||||
{ index: 22, year: 1600, daily: 4.38356, decimal: 16.0 },
|
{ index: 22, year: 1600, daily: 4.38356, decimal: 16 },
|
||||||
{ index: 23, year: 2000, daily: 5.47945, decimal: 20.0 },
|
{ index: 23, year: 2000, daily: 5.47945, decimal: 20 },
|
||||||
{ index: 24, year: 2600, daily: 7.12329, decimal: 26.0 },
|
{ index: 24, year: 2600, daily: 7.12329, decimal: 26 },
|
||||||
{ index: 25, year: 3400, daily: 9.31507, decimal: 34.0 },
|
{ index: 25, year: 3400, daily: 9.31507, decimal: 34 },
|
||||||
{ index: 26, year: 4400, daily: 12.05479, decimal: 44.0 },
|
{ index: 26, year: 4400, daily: 12.05479, decimal: 44 },
|
||||||
{ index: 27, year: 5700, daily: 15.61644, decimal: 57.0 },
|
{ index: 27, year: 5700, daily: 15.61644, decimal: 57 },
|
||||||
{ index: 28, year: 7500, daily: 20.54795, decimal: 75.0 },
|
{ index: 28, year: 7500, daily: 20.54795, decimal: 75 },
|
||||||
{ index: 29, year: 9700, daily: 26.57534, decimal: 97.0 },
|
{ index: 29, year: 9700, daily: 26.57534, decimal: 97 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checksum of the contract TAX_RATES array (first 16 chars of SHA-256).
|
||||||
|
* Used for runtime validation to ensure kraiken-lib is in sync with deployed contracts.
|
||||||
|
*
|
||||||
|
* To validate at runtime:
|
||||||
|
* 1. Read TAX_RATES array from the Stake contract
|
||||||
|
* 2. Compute checksum: sha256(values.join(',')).slice(0, 16)
|
||||||
|
* 3. Compare with TAX_RATES_CHECKSUM
|
||||||
|
*
|
||||||
|
* If mismatch: Run `node scripts/sync-tax-rates.mjs` and rebuild kraiken-lib.
|
||||||
|
*/
|
||||||
|
export const TAX_RATES_CHECKSUM = '1e37f2312ef082e9';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of raw year values (matches contract uint256[] TAX_RATES exactly).
|
||||||
|
* Used for runtime validation without needing to parse options.
|
||||||
|
*/
|
||||||
|
export const TAX_RATES_RAW = [1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,20 @@ describe('taxRates', () => {
|
||||||
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
|
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
|
||||||
expect(TAX_RATE_OPTIONS[0]).toEqual(expect.objectContaining({ index: 0, year: 1, decimal: 0.01 }));
|
expect(TAX_RATE_OPTIONS[0]).toEqual(expect.objectContaining({ index: 0, year: 1, decimal: 0.01 }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tax rate options have required fields', () => {
|
||||||
|
TAX_RATE_OPTIONS.forEach((option, idx) => {
|
||||||
|
expect(option.index).toBe(idx);
|
||||||
|
expect(option.year).toBeGreaterThan(0);
|
||||||
|
expect(option.daily).toBeGreaterThan(0);
|
||||||
|
expect(option.decimal).toBeGreaterThan(0);
|
||||||
|
expect(option.decimal).toBe(option.year / 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tax rates are in ascending order', () => {
|
||||||
|
for (let i = 1; i < TAX_RATE_OPTIONS.length; i++) {
|
||||||
|
expect(TAX_RATE_OPTIONS[i].year).toBeGreaterThan(TAX_RATE_OPTIONS[i - 1].year);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
66
kraiken-lib/src/version.ts
Normal file
66
kraiken-lib/src/version.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Protocol version compatibility tracking.
|
||||||
|
*
|
||||||
|
* The Kraiken contract exposes a VERSION constant that must be checked
|
||||||
|
* at runtime by indexers and frontends to ensure data compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current version this library is built for.
|
||||||
|
* Should match the deployed Kraiken contract VERSION.
|
||||||
|
*/
|
||||||
|
export const KRAIKEN_LIB_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of Kraiken contract versions this library is compatible with.
|
||||||
|
*
|
||||||
|
* - Indexers MUST validate contract.VERSION is in this list at startup
|
||||||
|
* - Frontends SHOULD validate indexer version matches KRAIKEN_LIB_VERSION
|
||||||
|
*
|
||||||
|
* Version History:
|
||||||
|
* - v1: Initial deployment (30-tier TAX_RATES, index-based staking)
|
||||||
|
*/
|
||||||
|
export const COMPATIBLE_CONTRACT_VERSIONS = [1];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a contract version is compatible with this library.
|
||||||
|
*/
|
||||||
|
export function isCompatibleVersion(contractVersion: number): boolean {
|
||||||
|
return COMPATIBLE_CONTRACT_VERSIONS.includes(contractVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error message generator for version mismatches.
|
||||||
|
*/
|
||||||
|
export function getVersionMismatchError(contractVersion: number, context: 'ponder' | 'frontend'): string {
|
||||||
|
const compatibleVersions = COMPATIBLE_CONTRACT_VERSIONS.join(', ');
|
||||||
|
const instructions =
|
||||||
|
context === 'ponder'
|
||||||
|
? [
|
||||||
|
'1. Check if contract was upgraded',
|
||||||
|
'2. Update COMPATIBLE_CONTRACT_VERSIONS in kraiken-lib/src/version.ts',
|
||||||
|
'3. Run: ./scripts/build-kraiken-lib.sh',
|
||||||
|
'4. Run: rm -rf services/ponder/.ponder/',
|
||||||
|
'5. Restart Ponder for full re-index',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'1. Contact administrator - indexer may need updating',
|
||||||
|
'2. Try refreshing the page',
|
||||||
|
'3. Check if contract was recently upgraded',
|
||||||
|
];
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'╔════════════════════════════════════════════════════════════╗',
|
||||||
|
`║ ❌ CRITICAL: VERSION MISMATCH (${context})`.padEnd(61) + '║',
|
||||||
|
'╠════════════════════════════════════════════════════════════╣',
|
||||||
|
`║ Contract VERSION: ${contractVersion}`.padEnd(61) + '║',
|
||||||
|
`║ Library VERSION: ${KRAIKEN_LIB_VERSION}`.padEnd(61) + '║',
|
||||||
|
`║ Compatible versions: ${compatibleVersions}`.padEnd(61) + '║',
|
||||||
|
'║'.padEnd(61) + '║',
|
||||||
|
'║ 📋 Required Actions:'.padEnd(61) + '║',
|
||||||
|
...instructions.map(line => `║ ${line}`.padEnd(61) + '║'),
|
||||||
|
'╚════════════════════════════════════════════════════════════╝',
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
@ -16,8 +16,18 @@ import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||||
*/
|
*/
|
||||||
contract Kraiken is ERC20, ERC20Permit {
|
contract Kraiken is ERC20, ERC20Permit {
|
||||||
using Math for uint256;
|
using Math for uint256;
|
||||||
// Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Protocol version for data structure compatibility.
|
||||||
|
* Increment when making breaking changes to TAX_RATES, events, or core data structures.
|
||||||
|
* Indexers and frontends validate against this to ensure sync.
|
||||||
|
*
|
||||||
|
* Version History:
|
||||||
|
* - v1: Initial deployment with 30-tier TAX_RATES
|
||||||
|
*/
|
||||||
|
uint256 public constant VERSION = 1;
|
||||||
|
|
||||||
|
// Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
|
||||||
uint256 private constant MIN_STAKE_FRACTION = 3000;
|
uint256 private constant MIN_STAKE_FRACTION = 3000;
|
||||||
// Address of the liquidity manager
|
// Address of the liquidity manager
|
||||||
address private liquidityManager;
|
address private liquidityManager;
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,6 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- ANVIL_RPC=http://anvil:8545
|
- ANVIL_RPC=http://anvil:8545
|
||||||
- GIT_BRANCH=${GIT_BRANCH:-}
|
- GIT_BRANCH=${GIT_BRANCH:-}
|
||||||
depends_on:
|
|
||||||
anvil:
|
|
||||||
condition: service_healthy
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: "no"
|
restart: "no"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "test", "-f", "/workspace/tmp/podman/contracts.env"]
|
test: ["CMD", "test", "-f", "/workspace/tmp/podman/contracts.env"]
|
||||||
|
|
@ -71,13 +66,6 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- CHOKIDAR_USEPOLLING=1
|
- CHOKIDAR_USEPOLLING=1
|
||||||
- GIT_BRANCH=${GIT_BRANCH:-}
|
- GIT_BRANCH=${GIT_BRANCH:-}
|
||||||
depends_on:
|
|
||||||
anvil:
|
|
||||||
condition: service_healthy
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
bootstrap:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
expose:
|
expose:
|
||||||
- "42069"
|
- "42069"
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -104,9 +92,6 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- CHOKIDAR_USEPOLLING=1
|
- CHOKIDAR_USEPOLLING=1
|
||||||
- GIT_BRANCH=${GIT_BRANCH:-}
|
- GIT_BRANCH=${GIT_BRANCH:-}
|
||||||
depends_on:
|
|
||||||
ponder:
|
|
||||||
condition: service_healthy
|
|
||||||
expose:
|
expose:
|
||||||
- "5173"
|
- "5173"
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -132,9 +117,6 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- CHOKIDAR_USEPOLLING=1
|
- CHOKIDAR_USEPOLLING=1
|
||||||
- GIT_BRANCH=${GIT_BRANCH:-}
|
- GIT_BRANCH=${GIT_BRANCH:-}
|
||||||
depends_on:
|
|
||||||
ponder:
|
|
||||||
condition: service_healthy
|
|
||||||
expose:
|
expose:
|
||||||
- "5174"
|
- "5174"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
@ -158,9 +140,6 @@ services:
|
||||||
working_dir: /workspace
|
working_dir: /workspace
|
||||||
environment:
|
environment:
|
||||||
- GIT_BRANCH=${GIT_BRANCH:-}
|
- GIT_BRANCH=${GIT_BRANCH:-}
|
||||||
depends_on:
|
|
||||||
ponder:
|
|
||||||
condition: service_healthy
|
|
||||||
expose:
|
expose:
|
||||||
- "43069"
|
- "43069"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
@ -176,13 +155,6 @@ services:
|
||||||
- ./containers/Caddyfile:/etc/caddy/Caddyfile:z
|
- ./containers/Caddyfile:/etc/caddy/Caddyfile:z
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:8081:80"
|
- "0.0.0.0:8081:80"
|
||||||
depends_on:
|
|
||||||
webapp:
|
|
||||||
condition: service_healthy
|
|
||||||
landing:
|
|
||||||
condition: service_healthy
|
|
||||||
txn-bot:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:80"]
|
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:80"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/../kraiken-lib"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$REPO_ROOT/kraiken-lib"
|
||||||
|
|
||||||
if [[ ! -d node_modules || ! -x node_modules/.bin/tsc ]]; then
|
if [[ ! -d node_modules || ! -x node_modules/.bin/tsc ]]; then
|
||||||
if ! npm install --silent; then
|
if ! npm install --silent; then
|
||||||
|
|
@ -9,6 +12,9 @@ if [[ ! -d node_modules || ! -x node_modules/.bin/tsc ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Ensure tax rate data mirrors onchain Stake.sol before compiling
|
||||||
|
node "$SCRIPT_DIR/sync-tax-rates.mjs"
|
||||||
|
|
||||||
./node_modules/.bin/tsc
|
./node_modules/.bin/tsc
|
||||||
|
|
||||||
echo "kraiken-lib built"
|
echo "kraiken-lib built"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,64 @@ start_stack() {
|
||||||
./scripts/build-kraiken-lib.sh
|
./scripts/build-kraiken-lib.sh
|
||||||
|
|
||||||
echo "Starting stack..."
|
echo "Starting stack..."
|
||||||
podman-compose up -d
|
# Start services in strict dependency order with explicit create+start
|
||||||
|
# This avoids podman dependency graph issues
|
||||||
|
|
||||||
|
# Create all containers first (without starting)
|
||||||
|
echo " Creating containers..."
|
||||||
|
podman-compose up --no-start 2>&1 | grep -v "STEP\|Copying\|Writing\|Getting\|fetch\|Installing\|Executing" || true
|
||||||
|
|
||||||
|
# Phase 1: Start base services (no dependencies)
|
||||||
|
echo " Starting anvil & postgres..."
|
||||||
|
podman-compose start anvil postgres >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Wait for base services to be healthy
|
||||||
|
echo " Waiting for anvil & postgres..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
anvil_healthy=$(podman healthcheck run harb_anvil_1 >/dev/null 2>&1 && echo "yes" || echo "no")
|
||||||
|
postgres_healthy=$(podman healthcheck run harb_postgres_1 >/dev/null 2>&1 && echo "yes" || echo "no")
|
||||||
|
if [[ "$anvil_healthy" == "yes" ]] && [[ "$postgres_healthy" == "yes" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Phase 2: Start bootstrap (depends on anvil & postgres healthy)
|
||||||
|
echo " Starting bootstrap..."
|
||||||
|
podman-compose start bootstrap >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Wait for bootstrap to complete
|
||||||
|
echo " Waiting for bootstrap..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
bootstrap_status=$(podman inspect harb_bootstrap_1 --format='{{.State.Status}}')
|
||||||
|
if [[ "$bootstrap_status" == "exited" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Phase 3: Start ponder (depends on bootstrap completed)
|
||||||
|
echo " Starting ponder..."
|
||||||
|
podman-compose start ponder >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Wait for ponder to be healthy
|
||||||
|
echo " Waiting for ponder..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
ponder_healthy=$(podman healthcheck run harb_ponder_1 >/dev/null 2>&1 && echo "yes" || echo "no")
|
||||||
|
if [[ "$ponder_healthy" == "yes" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Phase 4: Start frontend services (depend on ponder healthy)
|
||||||
|
echo " Starting webapp, landing, txn-bot..."
|
||||||
|
podman-compose start webapp landing txn-bot >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Phase 5: Start caddy (depends on frontend services)
|
||||||
|
sleep 5
|
||||||
|
echo " Starting caddy..."
|
||||||
|
podman-compose start caddy >/dev/null 2>&1
|
||||||
|
|
||||||
echo "Watching for kraiken-lib changes..."
|
echo "Watching for kraiken-lib changes..."
|
||||||
./scripts/watch-kraiken-lib.sh &
|
./scripts/watch-kraiken-lib.sh &
|
||||||
|
|
|
||||||
106
scripts/sync-tax-rates.mjs
Normal file
106
scripts/sync-tax-rates.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync TAX_RATE options from the on-chain Stake contract to kraiken-lib.
|
||||||
|
*
|
||||||
|
* The Stake.sol contract defines the canonical TAX_RATES array in basis points (yearly percentage * 100).
|
||||||
|
* This script parses that array and regenerates kraiken-lib/src/taxRates.ts so that all front-end consumers
|
||||||
|
* share a single source of truth that mirrors the deployed contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const stakeSourcePath = path.resolve(__dirname, '../onchain/src/Stake.sol');
|
||||||
|
const taxRatesDestPath = path.resolve(__dirname, '../kraiken-lib/src/taxRates.ts');
|
||||||
|
|
||||||
|
const source = readFileSync(stakeSourcePath, 'utf8');
|
||||||
|
|
||||||
|
const match = source.match(/uint256\[]\s+public\s+TAX_RATES\s*=\s*\[([^\]]+)\]/m);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Unable to locate TAX_RATES array in ${stakeSourcePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawValues = match[1]
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (rawValues.length === 0) {
|
||||||
|
throw new Error('TAX_RATES array is empty or failed to parse');
|
||||||
|
}
|
||||||
|
|
||||||
|
const taxRateOptions = rawValues.map((value, index) => {
|
||||||
|
const basisPoints = Number(value.replace(/_/g, ''));
|
||||||
|
if (!Number.isFinite(basisPoints)) {
|
||||||
|
throw new Error(`Invalid TAX_RATES entry "${value}" at index ${index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearlyPercent = basisPoints;
|
||||||
|
const decimalRate = yearlyPercent / 100;
|
||||||
|
const dailyPercent = yearlyPercent / 365;
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
year: yearlyPercent,
|
||||||
|
daily: Number(dailyPercent.toFixed(5)),
|
||||||
|
decimal: Number(decimalRate.toFixed(2)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate checksum for runtime validation
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
const taxRatesString = rawValues.join(',');
|
||||||
|
const checksum = createHash('sha256').update(taxRatesString).digest('hex').slice(0, 16);
|
||||||
|
|
||||||
|
const fileHeader = `/**
|
||||||
|
* AUTO-GENERATED FILE — DO NOT EDIT
|
||||||
|
*
|
||||||
|
* Generated by scripts/sync-tax-rates.mjs from onchain/src/Stake.sol.
|
||||||
|
* Run \`node scripts/sync-tax-rates.mjs\` after modifying the contract TAX_RATES array.
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const interfaceDef = `export interface TaxRateOption {
|
||||||
|
index: number;
|
||||||
|
year: number;
|
||||||
|
daily: number;
|
||||||
|
decimal: number;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const optionLines = taxRateOptions
|
||||||
|
.map(option => ` { index: ${option.index}, year: ${option.year}, daily: ${option.daily}, decimal: ${option.decimal} }`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const content = `${fileHeader}
|
||||||
|
${interfaceDef}
|
||||||
|
export const TAX_RATE_OPTIONS: TaxRateOption[] = [
|
||||||
|
${optionLines}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checksum of the contract TAX_RATES array (first 16 chars of SHA-256).
|
||||||
|
* Used for runtime validation to ensure kraiken-lib is in sync with deployed contracts.
|
||||||
|
*
|
||||||
|
* To validate at runtime:
|
||||||
|
* 1. Read TAX_RATES array from the Stake contract
|
||||||
|
* 2. Compute checksum: sha256(values.join(',')).slice(0, 16)
|
||||||
|
* 3. Compare with TAX_RATES_CHECKSUM
|
||||||
|
*
|
||||||
|
* If mismatch: Run \`node scripts/sync-tax-rates.mjs\` and rebuild kraiken-lib.
|
||||||
|
*/
|
||||||
|
export const TAX_RATES_CHECKSUM = '${checksum}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of raw year values (matches contract uint256[] TAX_RATES exactly).
|
||||||
|
* Used for runtime validation without needing to parse options.
|
||||||
|
*/
|
||||||
|
export const TAX_RATES_RAW = [${rawValues.join(', ')}];
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(taxRatesDestPath, content.trimStart() + '\n');
|
||||||
|
|
@ -6,6 +6,7 @@ Ponder-based indexer that records Kraiken protocol activity and exposes the Grap
|
||||||
- Track Kraiken token transfers and staking events to maintain protocol stats, hourly ring buffers, and position state.
|
- Track Kraiken token transfers and staking events to maintain protocol stats, hourly ring buffers, and position state.
|
||||||
- Serve a GraphQL endpoint at `http://localhost:42069/graphql` for downstream consumers.
|
- Serve a GraphQL endpoint at `http://localhost:42069/graphql` for downstream consumers.
|
||||||
- Support `BASE_SEPOLIA_LOCAL_FORK`, `BASE_SEPOLIA`, and `BASE` environments through a single TypeScript codebase.
|
- Support `BASE_SEPOLIA_LOCAL_FORK`, `BASE_SEPOLIA`, and `BASE` environments through a single TypeScript codebase.
|
||||||
|
- **Tax Rate Handling**: Import `TAX_RATE_OPTIONS` from `kraiken-lib` (synced with Stake.sol via `scripts/sync-tax-rates.mjs`). The schema stores both `taxRateIndex` (source of truth) and `taxRate` decimal (for display). Event handlers extract the index from contract events and look up the decimal value.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
- `ponder.config.ts` - Network selection and contract bindings (update addresses after deployments).
|
- `ponder.config.ts` - Network selection and contract bindings (update addresses after deployments).
|
||||||
|
|
|
||||||
0
services/ponder/ponder-env.d.ts
vendored
Executable file → Normal file
0
services/ponder/ponder-env.d.ts
vendored
Executable file → Normal file
|
|
@ -1,4 +1,5 @@
|
||||||
import { onchainTable, index } from 'ponder';
|
import { onchainTable, index } from 'ponder';
|
||||||
|
import { TAX_RATE_OPTIONS } from 'kraiken-lib/taxRates';
|
||||||
|
|
||||||
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
|
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
|
||||||
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
|
||||||
|
|
@ -113,7 +114,8 @@ export const positions = onchainTable(
|
||||||
id: t.text().primaryKey(), // Position ID from contract
|
id: t.text().primaryKey(), // Position ID from contract
|
||||||
owner: t.hex().notNull(),
|
owner: t.hex().notNull(),
|
||||||
share: t.real().notNull(), // Share as decimal (0-1)
|
share: t.real().notNull(), // Share as decimal (0-1)
|
||||||
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%)
|
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%) - for display
|
||||||
|
taxRateIndex: t.integer().notNull(), // Tax rate index from contract - source of truth
|
||||||
kraikenDeposit: t.bigint().notNull(),
|
kraikenDeposit: t.bigint().notNull(),
|
||||||
stakeDeposit: t.bigint().notNull(),
|
stakeDeposit: t.bigint().notNull(),
|
||||||
taxPaid: t
|
taxPaid: t
|
||||||
|
|
@ -142,14 +144,13 @@ export const positions = onchainTable(
|
||||||
table => ({
|
table => ({
|
||||||
ownerIdx: index().on(table.owner),
|
ownerIdx: index().on(table.owner),
|
||||||
statusIdx: index().on(table.status),
|
statusIdx: index().on(table.status),
|
||||||
|
taxRateIndexIdx: index().on(table.taxRateIndex),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Constants for tax rates (matches subgraph)
|
// Export decimal values for backward compatibility in event handlers
|
||||||
export const TAX_RATES = [
|
// Maps index → decimal (e.g., TAX_RATES[0] = 0.01 for 1% yearly)
|
||||||
0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19, 0.21, 0.25, 0.29, 0.33, 0.37, 0.41, 0.45, 0.49, 0.53, 0.57, 0.61, 0.65, 0.69,
|
export const TAX_RATES = TAX_RATE_OPTIONS.map(opt => opt.decimal);
|
||||||
0.73, 0.77, 0.81, 0.85, 0.89, 0.93, 0.97,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Helper constants
|
// Helper constants
|
||||||
export const STATS_ID = '0x01';
|
export const STATS_ID = '0x01';
|
||||||
|
|
|
||||||
32
services/ponder/src/helpers/version.ts
Normal file
32
services/ponder/src/helpers/version.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { isCompatibleVersion, getVersionMismatchError, KRAIKEN_LIB_VERSION } from 'kraiken-lib/version';
|
||||||
|
import type { Context } from 'ponder:registry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the deployed Kraiken contract version is compatible
|
||||||
|
* with this indexer's kraiken-lib version.
|
||||||
|
*
|
||||||
|
* MUST be called at Ponder startup before processing any events.
|
||||||
|
* Fails hard (process.exit) on mismatch to prevent indexing wrong data.
|
||||||
|
*/
|
||||||
|
export async function validateContractVersion(context: Context): Promise<void> {
|
||||||
|
try {
|
||||||
|
const contractVersion = await context.client.readContract({
|
||||||
|
address: context.contracts.Kraiken.address,
|
||||||
|
abi: context.contracts.Kraiken.abi,
|
||||||
|
functionName: 'VERSION',
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionNumber = Number(contractVersion);
|
||||||
|
|
||||||
|
if (!isCompatibleVersion(versionNumber)) {
|
||||||
|
console.error(getVersionMismatchError(versionNumber, 'ponder'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Contract version validated: v${versionNumber} (kraiken-lib v${KRAIKEN_LIB_VERSION})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read contract VERSION:', error);
|
||||||
|
console.error('Ensure Kraiken contract has VERSION constant and is deployed correctly');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,20 @@ import {
|
||||||
checkBlockHistorySufficient,
|
checkBlockHistorySufficient,
|
||||||
RING_BUFFER_SEGMENTS,
|
RING_BUFFER_SEGMENTS,
|
||||||
} from './helpers/stats';
|
} from './helpers/stats';
|
||||||
|
import { validateContractVersion } from './helpers/version';
|
||||||
|
|
||||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
|
||||||
|
|
||||||
|
// Track if version has been validated
|
||||||
|
let versionValidated = false;
|
||||||
|
|
||||||
ponder.on('Kraiken:Transfer', async ({ event, context }) => {
|
ponder.on('Kraiken:Transfer', async ({ event, context }) => {
|
||||||
|
// Validate version once at first event
|
||||||
|
if (!versionValidated) {
|
||||||
|
await validateContractVersion(context);
|
||||||
|
versionValidated = true;
|
||||||
|
}
|
||||||
|
|
||||||
const { from, to, value } = event.args;
|
const { from, to, value } = event.args;
|
||||||
|
|
||||||
await ensureStatsExists(context, event.block.timestamp);
|
await ensureStatsExists(context, event.block.timestamp);
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,13 @@ ponder.on('Stake:PositionCreated', async ({ event, context }) => {
|
||||||
const shareRatio = toShareRatio(event.args.share, stakeTotalSupply);
|
const shareRatio = toShareRatio(event.args.share, stakeTotalSupply);
|
||||||
const totalSupplyInit = await getKraikenTotalSupply(context);
|
const totalSupplyInit = await getKraikenTotalSupply(context);
|
||||||
|
|
||||||
|
const taxRateIndex = Number(event.args.taxRate);
|
||||||
await context.db.insert(positions).values({
|
await context.db.insert(positions).values({
|
||||||
id: event.args.positionId.toString(),
|
id: event.args.positionId.toString(),
|
||||||
owner: event.args.owner as `0x${string}`,
|
owner: event.args.owner as `0x${string}`,
|
||||||
share: shareRatio,
|
share: shareRatio,
|
||||||
taxRate: TAX_RATES[Number(event.args.taxRate)] || 0,
|
taxRate: TAX_RATES[taxRateIndex] || 0,
|
||||||
|
taxRateIndex,
|
||||||
kraikenDeposit: event.args.kraikenDeposit,
|
kraikenDeposit: event.args.kraikenDeposit,
|
||||||
stakeDeposit: event.args.kraikenDeposit,
|
stakeDeposit: event.args.kraikenDeposit,
|
||||||
taxPaid: ZERO,
|
taxPaid: ZERO,
|
||||||
|
|
@ -115,10 +117,12 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
|
||||||
const stakeTotalSupply = await getStakeTotalSupply(context);
|
const stakeTotalSupply = await getStakeTotalSupply(context);
|
||||||
const shareRatio = toShareRatio(event.args.newShares, stakeTotalSupply);
|
const shareRatio = toShareRatio(event.args.newShares, stakeTotalSupply);
|
||||||
|
|
||||||
|
const taxRateIndex = Number(event.args.taxRate);
|
||||||
await context.db.update(positions, { id: positionId }).set({
|
await context.db.update(positions, { id: positionId }).set({
|
||||||
taxPaid: BigInt(position.taxPaid ?? ZERO) + event.args.taxPaid,
|
taxPaid: BigInt(position.taxPaid ?? ZERO) + event.args.taxPaid,
|
||||||
share: shareRatio,
|
share: shareRatio,
|
||||||
taxRate: TAX_RATES[Number(event.args.taxRate)] || position.taxRate,
|
taxRate: TAX_RATES[taxRateIndex] || position.taxRate,
|
||||||
|
taxRateIndex,
|
||||||
lastTaxTime: event.block.timestamp,
|
lastTaxTime: event.block.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -154,7 +158,9 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
|
||||||
|
|
||||||
ponder.on('Stake:PositionRateHiked', async ({ event, context }) => {
|
ponder.on('Stake:PositionRateHiked', async ({ event, context }) => {
|
||||||
const positionId = event.args.positionId.toString();
|
const positionId = event.args.positionId.toString();
|
||||||
|
const taxRateIndex = Number(event.args.newTaxRate);
|
||||||
await context.db.update(positions, { id: positionId }).set({
|
await context.db.update(positions, { id: positionId }).set({
|
||||||
taxRate: TAX_RATES[Number(event.args.newTaxRate)] || 0,
|
taxRate: TAX_RATES[taxRateIndex] || 0,
|
||||||
|
taxRateIndex,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ test.describe('Acquire & Stake', () => {
|
||||||
}
|
}
|
||||||
await window.__testHelpers.fillStakeForm({
|
await window.__testHelpers.fillStakeForm({
|
||||||
amount: 100, // Stake 100 KRK
|
amount: 100, // Stake 100 KRK
|
||||||
taxRate: 5.0, // 5% tax rate
|
taxRateIndex: 2, // 5% tax rate option
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Programmatically fills the staking form without requiring fragile UI selectors.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `amount` (number): Amount of KRK tokens to stake (must be >= minimum stake)
|
- `amount` (number): Amount of KRK tokens to stake (must be >= minimum stake)
|
||||||
- `taxRate` (number): Tax rate percentage (must be between 0 and 100)
|
- `taxRateIndex` (number): Index of the tax rate option (must match one of the configured options)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -54,7 +54,7 @@ Programmatically fills the staking form without requiring fragile UI selectors.
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
await window.__testHelpers.fillStakeForm({
|
await window.__testHelpers.fillStakeForm({
|
||||||
amount: 100,
|
amount: 100,
|
||||||
taxRate: 5.0,
|
taxRateIndex: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,14 +66,14 @@ await stakeButton.click();
|
||||||
**Validation:**
|
**Validation:**
|
||||||
- Throws if amount is below minimum stake
|
- Throws if amount is below minimum stake
|
||||||
- Throws if amount exceeds wallet balance
|
- Throws if amount exceeds wallet balance
|
||||||
- Throws if tax rate is outside valid range (0-100)
|
- Throws if `taxRateIndex` does not match an available option
|
||||||
|
|
||||||
**TypeScript Support:**
|
**TypeScript Support:**
|
||||||
Type declarations are available in `env.d.ts`:
|
Type declarations are available in `env.d.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
interface Window {
|
interface Window {
|
||||||
__testHelpers?: {
|
__testHelpers?: {
|
||||||
fillStakeForm: (params: { amount: number; taxRate: number }) => Promise<void>;
|
fillStakeForm: (params: { amount: number; taxRateIndex: number }) => Promise<void>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
2
web-app/env.d.ts
vendored
2
web-app/env.d.ts
vendored
|
|
@ -6,7 +6,7 @@ declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
ethereum?: EIP1193Provider;
|
ethereum?: EIP1193Provider;
|
||||||
__testHelpers?: {
|
__testHelpers?: {
|
||||||
fillStakeForm: (params: { amount: number; taxRate: number }) => Promise<void>;
|
fillStakeForm: (params: { amount: number; taxRateIndex: number }) => Promise<void>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
</FInput>
|
</FInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-2">
|
<div class="row row-2">
|
||||||
<FSelect :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRate">
|
<FSelect :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRateIndex">
|
||||||
<template v-slot:info>
|
<template v-slot:info>
|
||||||
The yearly tax you have to pay to keep your slots open. The tax is paid when unstaking or manually in the dashboard. If
|
The yearly tax you have to pay to keep your slots open. The tax is paid when unstaking or manually in the dashboard. If
|
||||||
someone pays a higher tax they can buy you out.
|
someone pays a higher tax they can buy you out.
|
||||||
|
|
@ -105,7 +105,8 @@ const route = useRoute();
|
||||||
const adjustTaxRate = useAdjustTaxRate();
|
const adjustTaxRate = useAdjustTaxRate();
|
||||||
|
|
||||||
const StakeMenuOpen = ref(false);
|
const StakeMenuOpen = ref(false);
|
||||||
const taxRate = ref<number>(1.0);
|
const defaultTaxRateIndex = adjustTaxRate.taxRates[0]?.index ?? 0;
|
||||||
|
const taxRateIndex = ref<number>(defaultTaxRateIndex);
|
||||||
const loading = ref<boolean>(true);
|
const loading = ref<boolean>(true);
|
||||||
const stakeSnatchLoading = ref<boolean>(false);
|
const stakeSnatchLoading = ref<boolean>(false);
|
||||||
const stake = useStake();
|
const stake = useStake();
|
||||||
|
|
@ -150,10 +151,10 @@ const _tokenIssuance = computed(() => {
|
||||||
|
|
||||||
async function stakeSnatch() {
|
async function stakeSnatch() {
|
||||||
if (snatchSelection.snatchablePositions.value.length === 0) {
|
if (snatchSelection.snatchablePositions.value.length === 0) {
|
||||||
await stake.snatch(stake.stakingAmount, taxRate.value);
|
await stake.snatch(stake.stakingAmount, taxRateIndex.value);
|
||||||
} else {
|
} else {
|
||||||
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, taxRate.value, snatchAblePositionsIds);
|
await stake.snatch(stake.stakingAmount, taxRateIndex.value, snatchAblePositionsIds);
|
||||||
}
|
}
|
||||||
stakeSnatchLoading.value = true;
|
stakeSnatchLoading.value = true;
|
||||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||||
|
|
@ -207,13 +208,13 @@ function setMaxAmount() {
|
||||||
stake.stakingAmountNumber = maxStakeAmount.value;
|
stake.stakingAmountNumber = maxStakeAmount.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snatchSelection = useSnatchSelection(demo, taxRate);
|
const snatchSelection = useSnatchSelection(demo, taxRateIndex);
|
||||||
|
|
||||||
// Test helper - only available in dev mode
|
// Test helper - only available in dev mode
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.__testHelpers = {
|
window.__testHelpers = {
|
||||||
fillStakeForm: async (params: { amount: number; taxRate: number }) => {
|
fillStakeForm: async (params: { amount: number; taxRateIndex: number }) => {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
const minStakeNum = bigInt2Number(minStake.value, 18);
|
const minStakeNum = bigInt2Number(minStake.value, 18);
|
||||||
if (params.amount < minStakeNum) {
|
if (params.amount < minStakeNum) {
|
||||||
|
|
@ -225,13 +226,15 @@ if (import.meta.env.DEV) {
|
||||||
throw new Error(`Stake amount ${params.amount} exceeds balance ${maxStakeNum}`);
|
throw new Error(`Stake amount ${params.amount} exceeds balance ${maxStakeNum}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.taxRate <= 0 || params.taxRate > 100) {
|
const options = adjustTaxRate.taxRates;
|
||||||
throw new Error(`Tax rate ${params.taxRate} must be between 0 and 100`);
|
const selectedOption = options[params.taxRateIndex];
|
||||||
|
if (!selectedOption) {
|
||||||
|
throw new Error(`Tax rate index ${params.taxRateIndex} is invalid`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the form
|
// Fill the form
|
||||||
stake.stakingAmountNumber = params.amount;
|
stake.stakingAmountNumber = params.amount;
|
||||||
taxRate.value = params.taxRate;
|
taxRateIndex.value = params.taxRateIndex;
|
||||||
|
|
||||||
// Wait for reactive updates
|
// Wait for reactive updates
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,10 @@
|
||||||
>
|
>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="collapse-menu-input">
|
<div class="collapse-menu-input">
|
||||||
<FSelect :items="filteredTaxRates" v-model="newTaxRate"> </FSelect>
|
<FSelect :items="filteredTaxRates" v-model="newTaxRateIndex"> </FSelect>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FButton size="small" dense @click="changeTax(props.id, newTaxRate)">Confirm</FButton>
|
<FButton size="small" dense @click="changeTax(props.id, newTaxRateIndex)">Confirm</FButton>
|
||||||
<FButton size="small" dense outlined @click="showTaxMenu = false">Cancel</FButton>
|
<FButton size="small" dense outlined @click="showTaxMenu = false">Cancel</FButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -92,21 +92,24 @@ const statCollection = useStatCollection();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
taxRate: number;
|
taxRate: number;
|
||||||
treshold: number;
|
taxRateIndex?: number;
|
||||||
|
tresholdIndex: number;
|
||||||
id: bigint;
|
id: bigint;
|
||||||
amount: number;
|
amount: number;
|
||||||
position: Position;
|
position: Position;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showTaxMenu = ref(false);
|
const showTaxMenu = ref(false);
|
||||||
const newTaxRate = ref<number>(1);
|
const newTaxRateIndex = ref<number | null>(null);
|
||||||
const taxDue = ref<bigint>();
|
const taxDue = ref<bigint>();
|
||||||
const taxPaidGes = ref<string>();
|
const taxPaidGes = ref<string>();
|
||||||
const profit = ref<number>();
|
const profit = ref<number>();
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
const tag = computed(() => {
|
const tag = computed(() => {
|
||||||
if (props.taxRate < props.treshold) {
|
// Compare by index instead of decimal to avoid floating-point issues
|
||||||
|
const idx = props.taxRateIndex ?? props.position.taxRateIndex;
|
||||||
|
if (typeof idx === 'number' && idx < props.tresholdIndex) {
|
||||||
return 'Low Tax!';
|
return 'Low Tax!';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -114,8 +117,16 @@ const tag = computed(() => {
|
||||||
|
|
||||||
const total = computed(() => props.amount + profit.value! + -taxPaidGes.value!);
|
const total = computed(() => props.amount + profit.value! + -taxPaidGes.value!);
|
||||||
|
|
||||||
async function changeTax(id: bigint, newTaxRate: number) {
|
const filteredTaxRates = computed(() => {
|
||||||
await adjustTaxRate.changeTax(id, newTaxRate);
|
const currentIndex = props.position.taxRateIndex ?? -1;
|
||||||
|
return adjustTaxRate.taxRates.filter(option => option.index > currentIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function changeTax(id: bigint, nextTaxRateIndex: number | null) {
|
||||||
|
if (typeof nextTaxRateIndex !== 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adjustTaxRate.changeTax(id, nextTaxRateIndex);
|
||||||
showTaxMenu.value = false;
|
showTaxMenu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,16 +152,15 @@ async function loadActivePositionData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.position.taxRateIndex !== undefined) {
|
const availableRates = filteredTaxRates.value;
|
||||||
const taxRate = adjustTaxRate.taxRates.find(obj => obj.index === props.position.taxRateIndex! + 1);
|
if (availableRates.length > 0) {
|
||||||
|
newTaxRateIndex.value = availableRates[0]?.index ?? null;
|
||||||
if (taxRate) {
|
} else if (typeof props.position.taxRateIndex === 'number') {
|
||||||
newTaxRate.value = taxRate.year;
|
newTaxRateIndex.value = props.position.taxRateIndex;
|
||||||
}
|
} else {
|
||||||
|
newTaxRateIndex.value = adjustTaxRate.taxRates[0]?.index ?? null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredTaxRates = computed(() => adjustTaxRate.taxRates.filter(obj => obj.year > props.taxRate));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
:label="props.label ?? undefined"
|
:label="props.label ?? undefined"
|
||||||
:selectedable="false"
|
:selectedable="false"
|
||||||
:focus="showList"
|
:focus="showList"
|
||||||
:modelValue="`${year} % yearly`"
|
:modelValue="`${selectedYear} % yearly`"
|
||||||
readonly
|
readonly
|
||||||
>
|
>
|
||||||
<template #info v-if="slots.info">
|
<template #info v-if="slots.info">
|
||||||
|
|
@ -22,14 +22,14 @@
|
||||||
<div
|
<div
|
||||||
class="select-list-item"
|
class="select-list-item"
|
||||||
v-for="(item, index) in props.items"
|
v-for="(item, index) in props.items"
|
||||||
:key="item.year"
|
:key="item.index"
|
||||||
:class="{ active: year === item.year, hovered: activeIndex === index }"
|
:class="{ active: selectedIndex === item.index, hovered: activeIndex === index }"
|
||||||
@click.stop="clickItem(item)"
|
@click.stop="clickItem(item)"
|
||||||
@mouseenter="mouseEnter($event, index)"
|
@mouseenter="mouseEnter($event, index)"
|
||||||
@mouseleave="mouseLeave($event, index)"
|
@mouseleave="mouseLeave($event, index)"
|
||||||
>
|
>
|
||||||
<div class="circle">
|
<div class="circle">
|
||||||
<div class="active" v-if="year === item.year"></div>
|
<div class="active" v-if="selectedIndex === item.index"></div>
|
||||||
<div class="hovered" v-else-if="activeIndex === index"></div>
|
<div class="hovered" v-else-if="activeIndex === index"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="yearly">
|
<div class="yearly">
|
||||||
|
|
@ -54,6 +54,7 @@ import useClickOutside from '@/composables/useClickOutside';
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
|
index: number;
|
||||||
year: number;
|
year: number;
|
||||||
daily: number;
|
daily: number;
|
||||||
}
|
}
|
||||||
|
|
@ -97,17 +98,21 @@ useClickOutside(componentRef, () => {
|
||||||
showList.value = false;
|
showList.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const year = computed({
|
const selectedIndex = computed({
|
||||||
// getter
|
|
||||||
get() {
|
get() {
|
||||||
return props.modelValue || props.items[0].year;
|
if (typeof props.modelValue === 'number') {
|
||||||
|
return props.modelValue;
|
||||||
|
}
|
||||||
|
return props.items[0]?.index ?? 0;
|
||||||
},
|
},
|
||||||
// setter
|
|
||||||
set(newValue: number) {
|
set(newValue: number) {
|
||||||
emit('update:modelValue', newValue);
|
emit('update:modelValue', newValue);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedOption = computed(() => props.items.find(item => item.index === selectedIndex.value) ?? props.items[0]);
|
||||||
|
const selectedYear = computed(() => selectedOption.value?.year ?? 0);
|
||||||
|
|
||||||
function mouseEnter(event: MouseEvent, index: number) {
|
function mouseEnter(event: MouseEvent, index: number) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
activeIndex.value = index;
|
activeIndex.value = index;
|
||||||
|
|
@ -127,9 +132,9 @@ function clickSelect(_event: unknown) {
|
||||||
showList.value = !showList.value;
|
showList.value = !showList.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickItem(item: { year: number }) {
|
function clickItem(item: { index: number }) {
|
||||||
// console.log("item", item);
|
// console.log("item", item);
|
||||||
year.value = item.year;
|
selectedIndex.value = item.index;
|
||||||
showList.value = false;
|
showList.value = false;
|
||||||
// console.log("showList.value", showList.value);
|
// console.log("showList.value", showList.value);
|
||||||
// emit('input', item)
|
// emit('input', item)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ interface MockPositionsReturn {
|
||||||
|
|
||||||
interface MockStakeReturn {
|
interface MockStakeReturn {
|
||||||
stakingAmountShares: bigint;
|
stakingAmountShares: bigint;
|
||||||
taxRate: number;
|
taxRateIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MockWalletReturn {
|
interface MockWalletReturn {
|
||||||
|
|
@ -27,7 +27,7 @@ interface MockStatCollectionReturn {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MockAdjustTaxRateReturn {
|
interface MockAdjustTaxRateReturn {
|
||||||
taxRates: Array<{ year: number }>;
|
taxRates: Array<{ index: number; year: number; daily: number; decimal: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock all composables
|
// Mock all composables
|
||||||
|
|
@ -83,7 +83,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 0n,
|
stakingAmountShares: 0n,
|
||||||
taxRate: 1.0,
|
taxRateIndex: 0,
|
||||||
} as MockStakeReturn);
|
} as MockStakeReturn);
|
||||||
|
|
||||||
vi.mocked(useWallet).mockReturnValue({
|
vi.mocked(useWallet).mockReturnValue({
|
||||||
|
|
@ -100,7 +100,11 @@ describe('useSnatchSelection', () => {
|
||||||
} as MockStatCollectionReturn);
|
} as MockStatCollectionReturn);
|
||||||
|
|
||||||
vi.mocked(useAdjustTaxRate).mockReturnValue({
|
vi.mocked(useAdjustTaxRate).mockReturnValue({
|
||||||
taxRates: [{ year: 1 }],
|
taxRates: [
|
||||||
|
{ index: 0, year: 1, daily: 0.00274, decimal: 0.01 },
|
||||||
|
{ index: 1, year: 2, daily: 0.00548, decimal: 0.02 },
|
||||||
|
{ index: 2, year: 3, daily: 0.00822, decimal: 0.03 },
|
||||||
|
],
|
||||||
} as MockAdjustTaxRateReturn);
|
} as MockAdjustTaxRateReturn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -123,7 +127,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 900n,
|
stakingAmountShares: 900n,
|
||||||
taxRate: 1.0,
|
taxRateIndex: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { snatchablePositions, openPositionsAvailable } = useSnatchSelection();
|
const { snatchablePositions, openPositionsAvailable } = useSnatchSelection();
|
||||||
|
|
@ -131,14 +135,14 @@ describe('useSnatchSelection', () => {
|
||||||
expect(openPositionsAvailable.value).toBe(true);
|
expect(openPositionsAvailable.value).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out positions with higher tax rate', async () => {
|
it('should filter out positions with higher or equal tax rate index', async () => {
|
||||||
vi.mocked(usePositions).mockReturnValue({
|
vi.mocked(usePositions).mockReturnValue({
|
||||||
activePositions: ref([
|
activePositions: ref([
|
||||||
{
|
{
|
||||||
positionId: 1n,
|
positionId: 1n,
|
||||||
owner: '0x456',
|
owner: '0x456',
|
||||||
harbDeposit: 100n,
|
harbDeposit: 100n,
|
||||||
taxRate: 2.0,
|
taxRate: 0.02,
|
||||||
taxRateIndex: 1,
|
taxRateIndex: 1,
|
||||||
iAmOwner: false,
|
iAmOwner: false,
|
||||||
},
|
},
|
||||||
|
|
@ -147,7 +151,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 100n,
|
stakingAmountShares: 100n,
|
||||||
taxRate: 1.0,
|
taxRateIndex: 0,
|
||||||
} as MockStakeReturn);
|
} as MockStakeReturn);
|
||||||
|
|
||||||
const { snatchablePositions } = useSnatchSelection();
|
const { snatchablePositions } = useSnatchSelection();
|
||||||
|
|
@ -155,6 +159,7 @@ describe('useSnatchSelection', () => {
|
||||||
// Wait for watchEffect to run
|
// Wait for watchEffect to run
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
// Position with taxRateIndex 1 should be filtered out when selecting taxRateIndex 0
|
||||||
expect(snatchablePositions.value).toEqual([]);
|
expect(snatchablePositions.value).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -182,8 +187,8 @@ describe('useSnatchSelection', () => {
|
||||||
positionId: 1n,
|
positionId: 1n,
|
||||||
owner: '0x123',
|
owner: '0x123',
|
||||||
harbDeposit: 100n,
|
harbDeposit: 100n,
|
||||||
taxRate: 0.005, // 0.5% tax rate (less than maxTaxRate)
|
taxRate: 0.01, // 1% tax rate (index 0)
|
||||||
taxRateIndex: 1,
|
taxRateIndex: 0,
|
||||||
iAmOwner: true,
|
iAmOwner: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -193,7 +198,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 100n,
|
stakingAmountShares: 100n,
|
||||||
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
|
taxRateIndex: 1, // Corresponds to 2% decimal (index 1)
|
||||||
} as MockStakeReturn);
|
} as MockStakeReturn);
|
||||||
|
|
||||||
// Need outstandingStake > stakingAmountShares to create shortfall
|
// Need outstandingStake > stakingAmountShares to create shortfall
|
||||||
|
|
@ -216,8 +221,8 @@ describe('useSnatchSelection', () => {
|
||||||
positionId: 1n,
|
positionId: 1n,
|
||||||
owner: '0x456',
|
owner: '0x456',
|
||||||
harbDeposit: 100n,
|
harbDeposit: 100n,
|
||||||
taxRate: 0.005, // 0.5% tax rate
|
taxRate: 0.01, // 1% tax rate (index 0)
|
||||||
taxRateIndex: 1,
|
taxRateIndex: 0,
|
||||||
iAmOwner: false,
|
iAmOwner: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -225,8 +230,8 @@ describe('useSnatchSelection', () => {
|
||||||
positionId: 2n,
|
positionId: 2n,
|
||||||
owner: '0x789',
|
owner: '0x789',
|
||||||
harbDeposit: 200n,
|
harbDeposit: 200n,
|
||||||
taxRate: 0.006, // 0.6% tax rate
|
taxRate: 0.02, // 2% tax rate (index 1)
|
||||||
taxRateIndex: 2,
|
taxRateIndex: 1,
|
||||||
iAmOwner: false,
|
iAmOwner: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -236,7 +241,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 150n,
|
stakingAmountShares: 150n,
|
||||||
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
|
taxRateIndex: 2, // Corresponds to 3% decimal (index 2)
|
||||||
} as MockStakeReturn);
|
} as MockStakeReturn);
|
||||||
|
|
||||||
// Need outstandingStake > stakingAmountShares to create shortfall
|
// Need outstandingStake > stakingAmountShares to create shortfall
|
||||||
|
|
@ -259,7 +264,7 @@ describe('useSnatchSelection', () => {
|
||||||
positionId: 1n,
|
positionId: 1n,
|
||||||
owner: '0x456',
|
owner: '0x456',
|
||||||
harbDeposit: 100n,
|
harbDeposit: 100n,
|
||||||
taxRate: 0.005, // 0.5% tax rate
|
taxRate: 0.02, // 2% tax rate (index 1)
|
||||||
taxRateIndex: 1,
|
taxRateIndex: 1,
|
||||||
iAmOwner: false,
|
iAmOwner: false,
|
||||||
};
|
};
|
||||||
|
|
@ -270,7 +275,7 @@ describe('useSnatchSelection', () => {
|
||||||
|
|
||||||
vi.mocked(useStake).mockReturnValue({
|
vi.mocked(useStake).mockReturnValue({
|
||||||
stakingAmountShares: 100n,
|
stakingAmountShares: 100n,
|
||||||
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
|
taxRateIndex: 2, // Corresponds to 3% decimal (index 2)
|
||||||
} as MockStakeReturn);
|
} as MockStakeReturn);
|
||||||
|
|
||||||
// Need outstandingStake > stakingAmountShares to create shortfall
|
// Need outstandingStake > stakingAmountShares to create shortfall
|
||||||
|
|
@ -281,7 +286,11 @@ describe('useSnatchSelection', () => {
|
||||||
} as MockStatCollectionReturn);
|
} as MockStatCollectionReturn);
|
||||||
|
|
||||||
vi.mocked(useAdjustTaxRate).mockReturnValue({
|
vi.mocked(useAdjustTaxRate).mockReturnValue({
|
||||||
taxRates: [{ year: 1 }, { year: 2 }, { year: 3 }],
|
taxRates: [
|
||||||
|
{ index: 0, year: 1, daily: 0.00274, decimal: 0.01 },
|
||||||
|
{ index: 1, year: 2, daily: 0.00548, decimal: 0.02 },
|
||||||
|
{ index: 2, year: 3, daily: 0.00822, decimal: 0.03 },
|
||||||
|
],
|
||||||
} as MockAdjustTaxRateReturn);
|
} as MockAdjustTaxRateReturn);
|
||||||
|
|
||||||
const { floorTax } = useSnatchSelection();
|
const { floorTax } = useSnatchSelection();
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,27 @@ enum State {
|
||||||
Action = 'Action',
|
Action = 'Action',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const taxRates = TAX_RATE_OPTIONS.map(({ index, year, daily }) => ({
|
export const taxRates = TAX_RATE_OPTIONS.map(({ index, year, daily, decimal }) => ({
|
||||||
index,
|
index,
|
||||||
year,
|
year,
|
||||||
daily,
|
daily,
|
||||||
|
decimal,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const taxRateIndexByDecimal = new Map<number, number>();
|
||||||
|
|
||||||
|
for (const option of taxRates) {
|
||||||
|
taxRateIndexByDecimal.set(option.decimal, option.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaxRateOptionByIndex(index: number) {
|
||||||
|
return taxRates[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaxRateIndexByDecimal(decimal: number) {
|
||||||
|
return taxRateIndexByDecimal.get(decimal);
|
||||||
|
}
|
||||||
|
|
||||||
export function useAdjustTaxRate() {
|
export function useAdjustTaxRate() {
|
||||||
const loading = ref();
|
const loading = ref();
|
||||||
const waiting = ref();
|
const waiting = ref();
|
||||||
|
|
@ -34,13 +49,17 @@ export function useAdjustTaxRate() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function changeTax(positionId: bigint, taxRate: number) {
|
async function changeTax(positionId: bigint, taxRateIndex: number) {
|
||||||
try {
|
try {
|
||||||
// console.log("changeTax", { positionId, taxRate });
|
// console.log("changeTax", { positionId, taxRate });
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const index = taxRates.findIndex(obj => obj.year === taxRate);
|
const option = getTaxRateOptionByIndex(taxRateIndex);
|
||||||
const hash = await StakeContract.changeTax(positionId, index);
|
if (!option) {
|
||||||
|
throw new Error(`Invalid tax rate index: ${taxRateIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await StakeContract.changeTax(positionId, option.index);
|
||||||
// console.log("hash", hash);
|
// console.log("hash", hash);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
waiting.value = true;
|
waiting.value = true;
|
||||||
|
|
@ -48,7 +67,7 @@ export function useAdjustTaxRate() {
|
||||||
hash: hash,
|
hash: hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
contractToast.showSuccessToast(taxRate.toString(), 'Success!', 'You adjusted your position tax to', '', '%');
|
contractToast.showSuccessToast(option.year.toString(), 'Success!', 'You adjusted your position tax to', '', '%');
|
||||||
waiting.value = false;
|
waiting.value = false;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// console.error("error", error);
|
// console.error("error", error);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnTy
|
||||||
import { HarbContract } from '@/contracts/harb';
|
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 { taxRates } from '@/composables/useAdjustTaxRates';
|
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
|
||||||
import { chainData } from '@/composables/useWallet';
|
import { chainData } from '@/composables/useWallet';
|
||||||
import logger from '@/utils/logger';
|
import logger from '@/utils/logger';
|
||||||
const rawActivePositions = ref<Array<Position>>([]);
|
const rawActivePositions = ref<Array<Position>>([]);
|
||||||
|
|
@ -25,13 +25,17 @@ const activePositions = computed(() => {
|
||||||
|
|
||||||
return rawActivePositions.value
|
return rawActivePositions.value
|
||||||
.map(obj => {
|
.map(obj => {
|
||||||
|
const taxRateDecimal = Number(obj.taxRate);
|
||||||
|
const taxRateIndex =
|
||||||
|
Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
positionId: formatId(obj.id as Hex),
|
positionId: formatId(obj.id as Hex),
|
||||||
amount: bigInt2Number(obj.harbDeposit, 18),
|
amount: bigInt2Number(obj.harbDeposit, 18),
|
||||||
taxRatePercentage: Number(obj.taxRate) * 100,
|
taxRatePercentage: taxRateDecimal * 100,
|
||||||
taxRate: Number(obj.taxRate),
|
taxRate: taxRateDecimal,
|
||||||
taxRateIndex: taxRates.find(taxRate => taxRate.year === Number(obj.taxRate) * 100)?.index,
|
taxRateIndex,
|
||||||
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
||||||
totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
|
totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
|
||||||
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
||||||
|
|
@ -40,13 +44,11 @@ const activePositions = computed(() => {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.taxRate > b.taxRate) {
|
// Sort by tax rate index instead of decimal to avoid floating-point issues
|
||||||
return 1;
|
// Positions without an index are pushed to the end
|
||||||
} else if (a.taxRate < b.taxRate) {
|
if (typeof a.taxRateIndex !== 'number') return 1;
|
||||||
return -1;
|
if (typeof b.taxRateIndex !== 'number') return -1;
|
||||||
} else {
|
return a.taxRateIndex - b.taxRateIndex;
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,14 +79,18 @@ const myClosedPositions: ComputedRef<Position[]> = computed(() => {
|
||||||
|
|
||||||
// console.log("taxRates[taxRatePosition]", taxRates[taxRatePosition]);
|
// console.log("taxRates[taxRatePosition]", taxRates[taxRatePosition]);
|
||||||
|
|
||||||
|
const taxRateDecimal = Number(obj.taxRate);
|
||||||
|
const taxRateIndex =
|
||||||
|
Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
positionId: formatId(obj.id as Hex),
|
positionId: formatId(obj.id as Hex),
|
||||||
amount: obj.share * 1000000,
|
amount: obj.share * 1000000,
|
||||||
// amount: bigInt2Number(obj.harbDeposit, 18),
|
// amount: bigInt2Number(obj.harbDeposit, 18),
|
||||||
taxRatePercentage: Number(obj.taxRate) * 100,
|
taxRatePercentage: taxRateDecimal * 100,
|
||||||
taxRate: Number(obj.taxRate),
|
taxRate: taxRateDecimal,
|
||||||
taxRateIndex: taxRates.find(taxRate => taxRate.year === Number(obj.taxRate) * 100)?.index,
|
taxRateIndex,
|
||||||
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
|
||||||
totalSupplyEnd: obj.totalSupplyEnd !== undefined ? BigInt(obj.totalSupplyEnd) : undefined,
|
totalSupplyEnd: obj.totalSupplyEnd !== undefined ? BigInt(obj.totalSupplyEnd) : undefined,
|
||||||
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
totalSupplyInit: BigInt(obj.totalSupplyInit),
|
||||||
|
|
@ -101,12 +107,18 @@ const myActivePositions: ComputedRef<Position[]> = computed(() =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const tresholdValue = computed(() => {
|
const tresholdValue = computed(() => {
|
||||||
const arrayTaxRatePositions = activePositions.value.map(obj => obj.taxRatePercentage);
|
// Compute average tax rate index instead of percentage to avoid floating-point issues
|
||||||
const sortedPositions = arrayTaxRatePositions.sort((a, b) => (a > b ? 1 : -1));
|
const validIndices = activePositions.value
|
||||||
const sumq = sortedPositions.reduce((partialSum, a) => partialSum + a, 0);
|
.map(obj => obj.taxRateIndex)
|
||||||
const avg = sumq / sortedPositions.length;
|
.filter((idx): idx is number => typeof idx === 'number');
|
||||||
|
|
||||||
return avg / 2;
|
if (validIndices.length === 0) return 0;
|
||||||
|
|
||||||
|
const sum = validIndices.reduce((partialSum, idx) => partialSum + idx, 0);
|
||||||
|
const avgIndex = sum / validIndices.length;
|
||||||
|
|
||||||
|
// Return half the average index (rounded down)
|
||||||
|
return Math.floor(avgIndex / 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function loadActivePositions(endpoint?: string) {
|
export async function loadActivePositions(endpoint?: string) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function assetsToSharesLocal(assets: bigint, kraikenTotalSupply: bigint, stakeTo
|
||||||
return (assets * stakeTotalSupply) / kraikenTotalSupply;
|
return (assets * stakeTotalSupply) / kraikenTotalSupply;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
|
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
|
||||||
const { activePositions } = usePositions();
|
const { activePositions } = usePositions();
|
||||||
const stake = useStake();
|
const stake = useStake();
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
|
|
@ -33,7 +33,7 @@ export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
|
||||||
|
|
||||||
const snatchablePositions = ref<Position[]>([]);
|
const snatchablePositions = ref<Position[]>([]);
|
||||||
const shortfallShares = ref<bigint>(0n);
|
const shortfallShares = ref<bigint>(0n);
|
||||||
const floorTax = ref(1);
|
const floorTax = ref(adjustTaxRate.taxRates[0]?.year ?? 1);
|
||||||
let selectionRun = 0;
|
let selectionRun = 0;
|
||||||
|
|
||||||
const openPositionsAvailable = computed(() => shortfallShares.value <= 0n);
|
const openPositionsAvailable = computed(() => shortfallShares.value <= 0n);
|
||||||
|
|
@ -74,14 +74,19 @@ export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stakeTaxRate = (stake as { taxRate?: number }).taxRate;
|
const stakeTaxRateIndex = (stake as { taxRateIndex?: number }).taxRateIndex;
|
||||||
const taxRatePercent = taxRate?.value ?? stakeTaxRate ?? Number.POSITIVE_INFINITY;
|
const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex;
|
||||||
const maxTaxRateDecimal = Number.isFinite(taxRatePercent) ? taxRatePercent / 100 : Number.POSITIVE_INFINITY;
|
const maxTaxRateDecimal =
|
||||||
|
typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex)
|
||||||
|
? adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
const includeOwned = demo;
|
const includeOwned = demo;
|
||||||
const recipient = wallet.account.address ?? null;
|
const recipient = wallet.account.address ?? null;
|
||||||
|
|
||||||
const eligiblePositions = activePositions.value.filter((position: Position) => {
|
const eligiblePositions = activePositions.value.filter((position: Position) => {
|
||||||
if (position.taxRate >= maxTaxRateDecimal) {
|
// Filter by tax rate index instead of decimal to avoid floating-point issues
|
||||||
|
const posIndex = position.taxRateIndex;
|
||||||
|
if (typeof posIndex !== 'number' || (typeof selectedTaxRateIndex === 'number' && posIndex >= selectedTaxRateIndex)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!includeOwned && position.iAmOwner) {
|
if (!includeOwned && position.iAmOwner) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { getNonce, nonce, getName } from '@/contracts/harb';
|
||||||
import { useWallet } from '@/composables/useWallet';
|
import { useWallet } from '@/composables/useWallet';
|
||||||
import { createPermitObject, getSignatureRSV } from '@/utils/blockchain';
|
import { createPermitObject, getSignatureRSV } from '@/utils/blockchain';
|
||||||
import { formatBigIntDivision, compactNumber } from '@/utils/helper';
|
import { formatBigIntDivision, compactNumber } from '@/utils/helper';
|
||||||
import { taxRates } from '@/composables/useAdjustTaxRates';
|
import { getTaxRateOptionByIndex } from '@/composables/useAdjustTaxRates';
|
||||||
import { useContractToast } from './useContractToast';
|
import { useContractToast } from './useContractToast';
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const contractToast = useContractToast();
|
const contractToast = useContractToast();
|
||||||
|
|
@ -82,10 +82,13 @@ export function useStake() {
|
||||||
|
|
||||||
// const stakingAmountNumber = computed(() => return staking)
|
// const stakingAmountNumber = computed(() => return staking)
|
||||||
|
|
||||||
async function snatch(stakingAmount: bigint, taxRate: number, positions: Array<bigint> = []) {
|
async function snatch(stakingAmount: bigint, taxRateIndex: number, positions: Array<bigint> = []) {
|
||||||
// console.log("snatch", { stakingAmount, taxRate, positions });
|
// console.log("snatch", { stakingAmount, taxRateIndex, positions });
|
||||||
const account = getAccount(wagmiConfig);
|
const account = getAccount(wagmiConfig);
|
||||||
const taxRateObj = taxRates.find(obj => obj.year === taxRate);
|
const taxRateOption = getTaxRateOptionByIndex(taxRateIndex);
|
||||||
|
if (!taxRateOption) {
|
||||||
|
throw new Error(`Invalid tax rate index: ${taxRateIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assets: bigint = stakingAmount;
|
const assets: bigint = stakingAmount;
|
||||||
|
|
@ -127,10 +130,9 @@ export function useStake() {
|
||||||
|
|
||||||
const { r, s, v } = getSignatureRSV(signature);
|
const { r, s, v } = getSignatureRSV(signature);
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
// console.log("permitAndSnatch", assets, account.address!, taxRateObj?.index!, positions, deadline, v, r, s);
|
// console.log("permitAndSnatch", assets, account.address!, taxRateOption.index, positions, deadline, v, r, s);
|
||||||
|
|
||||||
const taxRateIndex = taxRateObj?.index ?? 0;
|
const hash = await permitAndSnatch(assets, account.address!, taxRateOption.index, positions, deadline, v, r, s);
|
||||||
const hash = await permitAndSnatch(assets, account.address!, taxRateIndex, positions, deadline, v, r, s);
|
|
||||||
// console.log("hash", hash);
|
// console.log("hash", hash);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
waiting.value = true;
|
waiting.value = true;
|
||||||
|
|
|
||||||
84
web-app/src/composables/useVersionCheck.ts
Normal file
84
web-app/src/composables/useVersionCheck.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { KRAIKEN_LIB_VERSION } from 'kraiken-lib/version';
|
||||||
|
|
||||||
|
export interface VersionStatus {
|
||||||
|
isValid: boolean;
|
||||||
|
error?: string;
|
||||||
|
contractVersion?: number;
|
||||||
|
indexerVersion?: number;
|
||||||
|
libVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionStatus = ref<VersionStatus>({
|
||||||
|
isValid: true,
|
||||||
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isChecking = ref(false);
|
||||||
|
const hasChecked = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates version compatibility between contract, indexer (Ponder), and frontend (kraiken-lib).
|
||||||
|
*
|
||||||
|
* Queries Ponder GraphQL for the contract version it indexed, then compares:
|
||||||
|
* 1. Frontend lib version vs Ponder's lib version (should match exactly)
|
||||||
|
* 2. Contract version vs compatible versions list (should be in COMPATIBLE_CONTRACT_VERSIONS)
|
||||||
|
*/
|
||||||
|
export function useVersionCheck() {
|
||||||
|
async function checkVersions(_graphqlUrl: string) {
|
||||||
|
if (isChecking.value || hasChecked.value) return versionStatus.value;
|
||||||
|
|
||||||
|
isChecking.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, we don't have contract version in Ponder stats yet
|
||||||
|
// This is a placeholder for when we add it to the schema
|
||||||
|
// Just validate that kraiken-lib is loaded correctly
|
||||||
|
|
||||||
|
versionStatus.value = {
|
||||||
|
isValid: true,
|
||||||
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Implement actual version check against Ponder GraphQL
|
||||||
|
// console.log(`✓ Frontend version check passed: v${KRAIKEN_LIB_VERSION}`);
|
||||||
|
hasChecked.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: Add proper error logging
|
||||||
|
// console.error('Version check failed:', error);
|
||||||
|
versionStatus.value = {
|
||||||
|
isValid: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to check version compatibility',
|
||||||
|
libVersion: KRAIKEN_LIB_VERSION,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isChecking.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionStatus.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
versionStatus,
|
||||||
|
checkVersions,
|
||||||
|
isChecking,
|
||||||
|
hasChecked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue composable that automatically checks versions on mount.
|
||||||
|
* Shows a warning banner if versions are incompatible.
|
||||||
|
*/
|
||||||
|
export function useVersionCheckOnMount(graphqlUrl: string) {
|
||||||
|
const { versionStatus, checkVersions, isChecking } = useVersionCheck();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await checkVersions(graphqlUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
versionStatus,
|
||||||
|
isChecking,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<ChartJs :snatchedPositions="snatchPositions.map(obj => obj.id)" :positions="activePositions" :dark="darkTheme"></ChartJs>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ChartJs from '@/components/chart/ChartJs.vue';
|
|
||||||
import { bigInt2Number, formatBigIntDivision } from '@/utils/helper';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useStatCollection } from '@/composables/useStatCollection';
|
|
||||||
import { useStake } from '@/composables/useStake';
|
|
||||||
import { usePositions, type Position } from '@/composables/usePositions';
|
|
||||||
import { useDark } from '@/composables/useDark';
|
|
||||||
const { darkTheme } = useDark();
|
|
||||||
|
|
||||||
const { activePositions } = usePositions();
|
|
||||||
const ignoreOwner = ref(false);
|
|
||||||
|
|
||||||
const taxRate = ref<number>(1.0);
|
|
||||||
|
|
||||||
const minStakeAmount = computed(() => {
|
|
||||||
// console.log("minStake", minStake.value);
|
|
||||||
|
|
||||||
return formatBigIntDivision(minStake.value, 10n ** 18n);
|
|
||||||
});
|
|
||||||
|
|
||||||
const stakeAbleHarbAmount = computed(() => statCollection.kraikenTotalSupply / 5n);
|
|
||||||
|
|
||||||
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
|
|
||||||
|
|
||||||
const stake = useStake();
|
|
||||||
const statCollection = useStatCollection();
|
|
||||||
const snatchPositions = computed(() => {
|
|
||||||
if (
|
|
||||||
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=
|
|
||||||
bigInt2Number(statCollection.kraikenTotalSupply, 18) * 0.2
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
//Differenz aus outstandingSupply und totalSupply bestimmen, wie viel HARB kann zum Snatch verwendet werden
|
|
||||||
const difference =
|
|
||||||
bigInt2Number(statCollection.outstandingStake, 18) +
|
|
||||||
stake.stakingAmountNumber -
|
|
||||||
bigInt2Number(statCollection.kraikenTotalSupply, 18) * 0.2;
|
|
||||||
// console.log("difference", difference);
|
|
||||||
|
|
||||||
//Division ohne Rest, um zu schauen wie viele Positionen gesnatched werden könnten
|
|
||||||
const snatchAblePositionsCount = Math.floor(difference / minStakeAmount.value);
|
|
||||||
|
|
||||||
//wenn mehr als 0 Positionen gesnatched werden könnten, wird geschaut wie viele Positionen in Frage kommen
|
|
||||||
if (snatchAblePositionsCount > 0) {
|
|
||||||
const snatchAblePositions = activePositions.value.filter((obj: Position) => {
|
|
||||||
if (ignoreOwner.value) {
|
|
||||||
return obj.taxRatePercentage < taxRate.value;
|
|
||||||
}
|
|
||||||
return obj.taxRatePercentage < taxRate.value && !obj.iAmOwner;
|
|
||||||
});
|
|
||||||
const slicedArray = snatchAblePositions.slice(0, snatchAblePositionsCount);
|
|
||||||
return slicedArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -45,8 +45,9 @@
|
||||||
<CollapseActive
|
<CollapseActive
|
||||||
v-for="position in myActivePositions"
|
v-for="position in myActivePositions"
|
||||||
:taxRate="position.taxRatePercentage"
|
:taxRate="position.taxRatePercentage"
|
||||||
|
:taxRateIndex="position.taxRateIndex"
|
||||||
:amount="position.amount"
|
:amount="position.amount"
|
||||||
:treshold="tresholdValue"
|
:tresholdIndex="tresholdValue"
|
||||||
:id="position.positionId"
|
:id="position.positionId"
|
||||||
:position="position"
|
:position="position"
|
||||||
:key="position.id"
|
:key="position.id"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue