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:
johba 2025-10-07 19:26:08 +02:00
parent d8ca557eb6
commit 6cbb1781ce
40 changed files with 1243 additions and 213 deletions

9
.gitignore vendored
View file

@ -28,3 +28,12 @@ tmp
foundry.lock
services/ponder/.env.local
node_modules
# Test artifacts
test-results/
tests/.stack.log
playwright-report/
services/ponder/.ponder/
# Temporary files
/tmp/

View file

@ -1,7 +1,49 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
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
(cd onchain && npx lint-staged)
fi

View file

@ -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.
- **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
- `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.
@ -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.
- **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
- `foundryup` - update Foundry toolchain.
- `anvil --fork-url https://sepolia.base.org` - manual fork when diagnosing outside the helper script.

View 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
View 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.

View file

@ -0,0 +1,6 @@
{
"src/**/*.ts": [
"eslint --fix",
"prettier --write"
]
}

View file

@ -0,0 +1,5 @@
# Auto-generated files
src/taxRates.ts
# Generated TypeScript definitions
src/__generated__/

View file

@ -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/__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/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
- Schema source points to the Ponder GraphQL endpoint for the active environment.

View file

@ -4,7 +4,7 @@ import tsParser from "@typescript-eslint/parser";
export default [
{
files: ["src/**/*.ts"],
ignores: ["src/tests/**/*", "src/__generated__/**/*"],
ignores: ["src/tests/**/*", "src/__generated__/**/*", "src/taxRates.ts"],
languageOptions: {
parser: tsParser,
parserOptions: { project: "./tsconfig.json" }

View file

@ -45,6 +45,11 @@
"types": "./dist/abis.d.ts",
"require": "./dist/abis.js",
"import": "./dist/abis.js"
},
"./version": {
"types": "./dist/version.d.ts",
"require": "./dist/version.js",
"import": "./dist/version.js"
}
},
"files": [

View file

@ -22,3 +22,5 @@ export { KRAIKEN_ABI, STAKE_ABI, ABIS } from './abis.js';
// Backward compatible aliases
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
export { KRAIKEN_LIB_VERSION, COMPATIBLE_CONTRACT_VERSIONS, isCompatibleVersion, getVersionMismatchError } from './version.js';

View file

@ -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 {
index: number;
year: number;
@ -18,22 +25,42 @@ export const TAX_RATE_OPTIONS: TaxRateOption[] = [
{ index: 9, year: 50, daily: 0.13699, decimal: 0.5 },
{ index: 10, year: 60, daily: 0.16438, decimal: 0.6 },
{ 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: 14, year: 180, daily: 0.49315, decimal: 1.8 },
{ index: 15, year: 250, daily: 0.68493, decimal: 2.5 },
{ index: 16, year: 320, daily: 0.87671, decimal: 3.2 },
{ index: 17, year: 420, daily: 1.15068, decimal: 4.2 },
{ 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: 21, year: 1200, daily: 3.28767, decimal: 12.0 },
{ index: 22, year: 1600, daily: 4.38356, decimal: 16.0 },
{ index: 23, year: 2000, daily: 5.47945, decimal: 20.0 },
{ index: 24, year: 2600, daily: 7.12329, decimal: 26.0 },
{ index: 25, year: 3400, daily: 9.31507, decimal: 34.0 },
{ index: 26, year: 4400, daily: 12.05479, decimal: 44.0 },
{ index: 27, year: 5700, daily: 15.61644, decimal: 57.0 },
{ index: 28, year: 7500, daily: 20.54795, decimal: 75.0 },
{ index: 29, year: 9700, daily: 26.57534, decimal: 97.0 },
{ index: 21, year: 1200, daily: 3.28767, decimal: 12 },
{ index: 22, year: 1600, daily: 4.38356, decimal: 16 },
{ index: 23, year: 2000, daily: 5.47945, decimal: 20 },
{ index: 24, year: 2600, daily: 7.12329, decimal: 26 },
{ index: 25, year: 3400, daily: 9.31507, decimal: 34 },
{ index: 26, year: 4400, daily: 12.05479, decimal: 44 },
{ index: 27, year: 5700, daily: 15.61644, decimal: 57 },
{ index: 28, year: 7500, daily: 20.54795, decimal: 75 },
{ 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];

View file

@ -6,4 +6,20 @@ describe('taxRates', () => {
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
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);
}
});
});

View 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');
}

View file

@ -16,8 +16,18 @@ import { Math } from "@openzeppelin/utils/math/Math.sol";
*/
contract Kraiken is ERC20, ERC20Permit {
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;
// Address of the liquidity manager
address private liquidityManager;

View file

@ -45,11 +45,6 @@ services:
environment:
- ANVIL_RPC=http://anvil:8545
- GIT_BRANCH=${GIT_BRANCH:-}
depends_on:
anvil:
condition: service_healthy
postgres:
condition: service_healthy
restart: "no"
healthcheck:
test: ["CMD", "test", "-f", "/workspace/tmp/podman/contracts.env"]
@ -71,13 +66,6 @@ services:
environment:
- CHOKIDAR_USEPOLLING=1
- GIT_BRANCH=${GIT_BRANCH:-}
depends_on:
anvil:
condition: service_healthy
postgres:
condition: service_healthy
bootstrap:
condition: service_completed_successfully
expose:
- "42069"
ports:
@ -104,9 +92,6 @@ services:
environment:
- CHOKIDAR_USEPOLLING=1
- GIT_BRANCH=${GIT_BRANCH:-}
depends_on:
ponder:
condition: service_healthy
expose:
- "5173"
ports:
@ -132,9 +117,6 @@ services:
environment:
- CHOKIDAR_USEPOLLING=1
- GIT_BRANCH=${GIT_BRANCH:-}
depends_on:
ponder:
condition: service_healthy
expose:
- "5174"
restart: unless-stopped
@ -158,9 +140,6 @@ services:
working_dir: /workspace
environment:
- GIT_BRANCH=${GIT_BRANCH:-}
depends_on:
ponder:
condition: service_healthy
expose:
- "43069"
restart: unless-stopped
@ -176,13 +155,6 @@ services:
- ./containers/Caddyfile:/etc/caddy/Caddyfile:z
ports:
- "0.0.0.0:8081:80"
depends_on:
webapp:
condition: service_healthy
landing:
condition: service_healthy
txn-bot:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:80"]

View file

@ -1,7 +1,10 @@
#!/usr/bin/env bash
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 ! npm install --silent; then
@ -9,6 +12,9 @@ if [[ ! -d node_modules || ! -x node_modules/.bin/tsc ]]; then
fi
fi
# Ensure tax rate data mirrors onchain Stake.sol before compiling
node "$SCRIPT_DIR/sync-tax-rates.mjs"
./node_modules/.bin/tsc
echo "kraiken-lib built"

View file

@ -27,7 +27,64 @@ start_stack() {
./scripts/build-kraiken-lib.sh
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..."
./scripts/watch-kraiken-lib.sh &

106
scripts/sync-tax-rates.mjs Normal file
View 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');

View file

@ -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.
- 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.
- **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
- `ponder.config.ts` - Network selection and contract bindings (update addresses after deployments).

0
services/ponder/ponder-env.d.ts vendored Executable file → Normal file
View file

View file

@ -1,4 +1,5 @@
import { onchainTable, index } from 'ponder';
import { TAX_RATE_OPTIONS } from 'kraiken-lib/taxRates';
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
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
owner: t.hex().notNull(),
share: t.real().notNull(), // Share as decimal (0-1)
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%)
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(),
stakeDeposit: t.bigint().notNull(),
taxPaid: t
@ -142,14 +144,13 @@ export const positions = onchainTable(
table => ({
ownerIdx: index().on(table.owner),
statusIdx: index().on(table.status),
taxRateIndexIdx: index().on(table.taxRateIndex),
})
);
// Constants for tax rates (matches subgraph)
export const TAX_RATES = [
0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19, 0.21, 0.25, 0.29, 0.33, 0.37, 0.41, 0.45, 0.49, 0.53, 0.57, 0.61, 0.65, 0.69,
0.73, 0.77, 0.81, 0.85, 0.89, 0.93, 0.97,
];
// Export decimal values for backward compatibility in event handlers
// Maps index → decimal (e.g., TAX_RATES[0] = 0.01 for 1% yearly)
export const TAX_RATES = TAX_RATE_OPTIONS.map(opt => opt.decimal);
// Helper constants
export const STATS_ID = '0x01';

View 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);
}
}

View file

@ -8,10 +8,20 @@ import {
checkBlockHistorySufficient,
RING_BUFFER_SEGMENTS,
} from './helpers/stats';
import { validateContractVersion } from './helpers/version';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
// Track if version has been validated
let versionValidated = false;
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;
await ensureStatsExists(context, event.block.timestamp);

View file

@ -34,11 +34,13 @@ ponder.on('Stake:PositionCreated', async ({ event, context }) => {
const shareRatio = toShareRatio(event.args.share, stakeTotalSupply);
const totalSupplyInit = await getKraikenTotalSupply(context);
const taxRateIndex = Number(event.args.taxRate);
await context.db.insert(positions).values({
id: event.args.positionId.toString(),
owner: event.args.owner as `0x${string}`,
share: shareRatio,
taxRate: TAX_RATES[Number(event.args.taxRate)] || 0,
taxRate: TAX_RATES[taxRateIndex] || 0,
taxRateIndex,
kraikenDeposit: event.args.kraikenDeposit,
stakeDeposit: event.args.kraikenDeposit,
taxPaid: ZERO,
@ -115,10 +117,12 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
const stakeTotalSupply = await getStakeTotalSupply(context);
const shareRatio = toShareRatio(event.args.newShares, stakeTotalSupply);
const taxRateIndex = Number(event.args.taxRate);
await context.db.update(positions, { id: positionId }).set({
taxPaid: BigInt(position.taxPaid ?? ZERO) + event.args.taxPaid,
share: shareRatio,
taxRate: TAX_RATES[Number(event.args.taxRate)] || position.taxRate,
taxRate: TAX_RATES[taxRateIndex] || position.taxRate,
taxRateIndex,
lastTaxTime: event.block.timestamp,
});
@ -154,7 +158,9 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
ponder.on('Stake:PositionRateHiked', async ({ event, context }) => {
const positionId = event.args.positionId.toString();
const taxRateIndex = Number(event.args.newTaxRate);
await context.db.update(positions, { id: positionId }).set({
taxRate: TAX_RATES[Number(event.args.newTaxRate)] || 0,
taxRate: TAX_RATES[taxRateIndex] || 0,
taxRateIndex,
});
});

View file

@ -163,7 +163,7 @@ test.describe('Acquire & Stake', () => {
}
await window.__testHelpers.fillStakeForm({
amount: 100, // Stake 100 KRK
taxRate: 5.0, // 5% tax rate
taxRateIndex: 2, // 5% tax rate option
});
});

View file

@ -46,7 +46,7 @@ Programmatically fills the staking form without requiring fragile UI selectors.
**Parameters:**
- `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:**
```typescript
@ -54,7 +54,7 @@ Programmatically fills the staking form without requiring fragile UI selectors.
await page.evaluate(async () => {
await window.__testHelpers.fillStakeForm({
amount: 100,
taxRate: 5.0,
taxRateIndex: 2,
});
});
@ -66,14 +66,14 @@ await stakeButton.click();
**Validation:**
- Throws if amount is below minimum stake
- 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:**
Type declarations are available in `env.d.ts`:
```typescript
interface Window {
__testHelpers?: {
fillStakeForm: (params: { amount: number; taxRate: number }) => Promise<void>;
fillStakeForm: (params: { amount: number; taxRateIndex: number }) => Promise<void>;
};
}
```

2
web-app/env.d.ts vendored
View file

@ -6,7 +6,7 @@ declare global {
interface Window {
ethereum?: EIP1193Provider;
__testHelpers?: {
fillStakeForm: (params: { amount: number; taxRate: number }) => Promise<void>;
fillStakeForm: (params: { amount: number; taxRateIndex: number }) => Promise<void>;
};
}
}

View file

@ -30,7 +30,7 @@
</FInput>
</div>
<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>
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.
@ -105,7 +105,8 @@ const route = useRoute();
const adjustTaxRate = useAdjustTaxRate();
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 stakeSnatchLoading = ref<boolean>(false);
const stake = useStake();
@ -150,10 +151,10 @@ const _tokenIssuance = computed(() => {
async function stakeSnatch() {
if (snatchSelection.snatchablePositions.value.length === 0) {
await stake.snatch(stake.stakingAmount, taxRate.value);
await stake.snatch(stake.stakingAmount, taxRateIndex.value);
} else {
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;
await new Promise(resolve => setTimeout(resolve, 10000));
@ -207,13 +208,13 @@ function setMaxAmount() {
stake.stakingAmountNumber = maxStakeAmount.value;
}
const snatchSelection = useSnatchSelection(demo, taxRate);
const snatchSelection = useSnatchSelection(demo, taxRateIndex);
// Test helper - only available in dev mode
if (import.meta.env.DEV) {
if (typeof window !== 'undefined') {
window.__testHelpers = {
fillStakeForm: async (params: { amount: number; taxRate: number }) => {
fillStakeForm: async (params: { amount: number; taxRateIndex: number }) => {
// Validate inputs
const minStakeNum = bigInt2Number(minStake.value, 18);
if (params.amount < minStakeNum) {
@ -225,13 +226,15 @@ if (import.meta.env.DEV) {
throw new Error(`Stake amount ${params.amount} exceeds balance ${maxStakeNum}`);
}
if (params.taxRate <= 0 || params.taxRate > 100) {
throw new Error(`Tax rate ${params.taxRate} must be between 0 and 100`);
const options = adjustTaxRate.taxRates;
const selectedOption = options[params.taxRateIndex];
if (!selectedOption) {
throw new Error(`Tax rate index ${params.taxRateIndex} is invalid`);
}
// Fill the form
stake.stakingAmountNumber = params.amount;
taxRate.value = params.taxRate;
taxRateIndex.value = params.taxRateIndex;
// Wait for reactive updates
await new Promise(resolve => setTimeout(resolve, 100));

View file

@ -53,10 +53,10 @@
>
<template v-else>
<div class="collapse-menu-input">
<FSelect :items="filteredTaxRates" v-model="newTaxRate"> </FSelect>
<FSelect :items="filteredTaxRates" v-model="newTaxRateIndex"> </FSelect>
</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>
</div>
</template>
@ -92,21 +92,24 @@ const statCollection = useStatCollection();
const props = defineProps<{
taxRate: number;
treshold: number;
taxRateIndex?: number;
tresholdIndex: number;
id: bigint;
amount: number;
position: Position;
}>();
const showTaxMenu = ref(false);
const newTaxRate = ref<number>(1);
const newTaxRateIndex = ref<number | null>(null);
const taxDue = ref<bigint>();
const taxPaidGes = ref<string>();
const profit = ref<number>();
const loading = ref<boolean>(false);
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 '';
@ -114,8 +117,16 @@ const tag = computed(() => {
const total = computed(() => props.amount + profit.value! + -taxPaidGes.value!);
async function changeTax(id: bigint, newTaxRate: number) {
await adjustTaxRate.changeTax(id, newTaxRate);
const filteredTaxRates = computed(() => {
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;
}
@ -141,16 +152,15 @@ async function loadActivePositionData() {
}
onMounted(() => {
if (props.position.taxRateIndex !== undefined) {
const taxRate = adjustTaxRate.taxRates.find(obj => obj.index === props.position.taxRateIndex! + 1);
if (taxRate) {
newTaxRate.value = taxRate.year;
}
const availableRates = filteredTaxRates.value;
if (availableRates.length > 0) {
newTaxRateIndex.value = availableRates[0]?.index ?? null;
} else if (typeof props.position.taxRateIndex === 'number') {
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>
<style lang="sass">

View file

@ -6,7 +6,7 @@
:label="props.label ?? undefined"
:selectedable="false"
:focus="showList"
:modelValue="`${year} % yearly`"
:modelValue="`${selectedYear} % yearly`"
readonly
>
<template #info v-if="slots.info">
@ -22,14 +22,14 @@
<div
class="select-list-item"
v-for="(item, index) in props.items"
:key="item.year"
:class="{ active: year === item.year, hovered: activeIndex === index }"
:key="item.index"
:class="{ active: selectedIndex === item.index, hovered: activeIndex === index }"
@click.stop="clickItem(item)"
@mouseenter="mouseEnter($event, index)"
@mouseleave="mouseLeave($event, index)"
>
<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>
<div class="yearly">
@ -54,6 +54,7 @@ import useClickOutside from '@/composables/useClickOutside';
import { Icon } from '@iconify/vue';
interface Item {
index: number;
year: number;
daily: number;
}
@ -97,17 +98,21 @@ useClickOutside(componentRef, () => {
showList.value = false;
});
const year = computed({
// getter
const selectedIndex = computed({
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) {
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) {
const target = event.target as HTMLElement;
activeIndex.value = index;
@ -127,9 +132,9 @@ function clickSelect(_event: unknown) {
showList.value = !showList.value;
}
function clickItem(item: { year: number }) {
function clickItem(item: { index: number }) {
// console.log("item", item);
year.value = item.year;
selectedIndex.value = item.index;
showList.value = false;
// console.log("showList.value", showList.value);
// emit('input', item)

View file

@ -13,7 +13,7 @@ interface MockPositionsReturn {
interface MockStakeReturn {
stakingAmountShares: bigint;
taxRate: number;
taxRateIndex: number;
}
interface MockWalletReturn {
@ -27,7 +27,7 @@ interface MockStatCollectionReturn {
}
interface MockAdjustTaxRateReturn {
taxRates: Array<{ year: number }>;
taxRates: Array<{ index: number; year: number; daily: number; decimal: number }>;
}
// Mock all composables
@ -83,7 +83,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 0n,
taxRate: 1.0,
taxRateIndex: 0,
} as MockStakeReturn);
vi.mocked(useWallet).mockReturnValue({
@ -100,7 +100,11 @@ describe('useSnatchSelection', () => {
} as MockStatCollectionReturn);
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);
});
@ -123,7 +127,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 900n,
taxRate: 1.0,
taxRateIndex: 0,
});
const { snatchablePositions, openPositionsAvailable } = useSnatchSelection();
@ -131,14 +135,14 @@ describe('useSnatchSelection', () => {
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({
activePositions: ref([
{
positionId: 1n,
owner: '0x456',
harbDeposit: 100n,
taxRate: 2.0,
taxRate: 0.02,
taxRateIndex: 1,
iAmOwner: false,
},
@ -147,7 +151,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 100n,
taxRate: 1.0,
taxRateIndex: 0,
} as MockStakeReturn);
const { snatchablePositions } = useSnatchSelection();
@ -155,6 +159,7 @@ describe('useSnatchSelection', () => {
// Wait for watchEffect to run
await new Promise(resolve => setTimeout(resolve, 0));
// Position with taxRateIndex 1 should be filtered out when selecting taxRateIndex 0
expect(snatchablePositions.value).toEqual([]);
});
@ -182,8 +187,8 @@ describe('useSnatchSelection', () => {
positionId: 1n,
owner: '0x123',
harbDeposit: 100n,
taxRate: 0.005, // 0.5% tax rate (less than maxTaxRate)
taxRateIndex: 1,
taxRate: 0.01, // 1% tax rate (index 0)
taxRateIndex: 0,
iAmOwner: true,
};
@ -193,7 +198,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 100n,
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
taxRateIndex: 1, // Corresponds to 2% decimal (index 1)
} as MockStakeReturn);
// Need outstandingStake > stakingAmountShares to create shortfall
@ -216,8 +221,8 @@ describe('useSnatchSelection', () => {
positionId: 1n,
owner: '0x456',
harbDeposit: 100n,
taxRate: 0.005, // 0.5% tax rate
taxRateIndex: 1,
taxRate: 0.01, // 1% tax rate (index 0)
taxRateIndex: 0,
iAmOwner: false,
};
@ -225,8 +230,8 @@ describe('useSnatchSelection', () => {
positionId: 2n,
owner: '0x789',
harbDeposit: 200n,
taxRate: 0.006, // 0.6% tax rate
taxRateIndex: 2,
taxRate: 0.02, // 2% tax rate (index 1)
taxRateIndex: 1,
iAmOwner: false,
};
@ -236,7 +241,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 150n,
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
taxRateIndex: 2, // Corresponds to 3% decimal (index 2)
} as MockStakeReturn);
// Need outstandingStake > stakingAmountShares to create shortfall
@ -259,7 +264,7 @@ describe('useSnatchSelection', () => {
positionId: 1n,
owner: '0x456',
harbDeposit: 100n,
taxRate: 0.005, // 0.5% tax rate
taxRate: 0.02, // 2% tax rate (index 1)
taxRateIndex: 1,
iAmOwner: false,
};
@ -270,7 +275,7 @@ describe('useSnatchSelection', () => {
vi.mocked(useStake).mockReturnValue({
stakingAmountShares: 100n,
taxRate: 1.0, // Will be converted to 0.01 (1%) decimal
taxRateIndex: 2, // Corresponds to 3% decimal (index 2)
} as MockStakeReturn);
// Need outstandingStake > stakingAmountShares to create shortfall
@ -281,7 +286,11 @@ describe('useSnatchSelection', () => {
} as MockStatCollectionReturn);
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);
const { floorTax } = useSnatchSelection();

View file

@ -14,12 +14,27 @@ enum State {
Action = 'Action',
}
export const taxRates = TAX_RATE_OPTIONS.map(({ index, year, daily }) => ({
export const taxRates = TAX_RATE_OPTIONS.map(({ index, year, daily, decimal }) => ({
index,
year,
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() {
const loading = 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 {
// console.log("changeTax", { positionId, taxRate });
loading.value = true;
const index = taxRates.findIndex(obj => obj.year === taxRate);
const hash = await StakeContract.changeTax(positionId, index);
const option = getTaxRateOptionByIndex(taxRateIndex);
if (!option) {
throw new Error(`Invalid tax rate index: ${taxRateIndex}`);
}
const hash = await StakeContract.changeTax(positionId, option.index);
// console.log("hash", hash);
loading.value = false;
waiting.value = true;
@ -48,7 +67,7 @@ export function useAdjustTaxRate() {
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;
} catch (error: unknown) {
// console.error("error", error);

View file

@ -8,7 +8,7 @@ import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnTy
import { HarbContract } from '@/contracts/harb';
import { bytesToUint256 } from 'kraiken-lib';
import { bigInt2Number } from '@/utils/helper';
import { taxRates } from '@/composables/useAdjustTaxRates';
import { getTaxRateIndexByDecimal } from '@/composables/useAdjustTaxRates';
import { chainData } from '@/composables/useWallet';
import logger from '@/utils/logger';
const rawActivePositions = ref<Array<Position>>([]);
@ -25,13 +25,17 @@ const activePositions = computed(() => {
return rawActivePositions.value
.map(obj => {
const taxRateDecimal = Number(obj.taxRate);
const taxRateIndex =
Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;
return {
...obj,
positionId: formatId(obj.id as Hex),
amount: bigInt2Number(obj.harbDeposit, 18),
taxRatePercentage: Number(obj.taxRate) * 100,
taxRate: Number(obj.taxRate),
taxRateIndex: taxRates.find(taxRate => taxRate.year === Number(obj.taxRate) * 100)?.index,
taxRatePercentage: taxRateDecimal * 100,
taxRate: taxRateDecimal,
taxRateIndex,
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
totalSupplyInit: BigInt(obj.totalSupplyInit),
@ -40,13 +44,11 @@ const activePositions = computed(() => {
};
})
.sort((a, b) => {
if (a.taxRate > b.taxRate) {
return 1;
} else if (a.taxRate < b.taxRate) {
return -1;
} else {
return 0;
}
// Sort by tax rate index instead of decimal to avoid floating-point issues
// Positions without an index are pushed to the end
if (typeof a.taxRateIndex !== 'number') return 1;
if (typeof b.taxRateIndex !== 'number') return -1;
return a.taxRateIndex - b.taxRateIndex;
});
});
@ -77,14 +79,18 @@ const myClosedPositions: ComputedRef<Position[]> = computed(() => {
// console.log("taxRates[taxRatePosition]", taxRates[taxRatePosition]);
const taxRateDecimal = Number(obj.taxRate);
const taxRateIndex =
Number.isFinite(taxRateDecimal) && !Number.isNaN(taxRateDecimal) ? getTaxRateIndexByDecimal(taxRateDecimal) : undefined;
return {
...obj,
positionId: formatId(obj.id as Hex),
amount: obj.share * 1000000,
// amount: bigInt2Number(obj.harbDeposit, 18),
taxRatePercentage: Number(obj.taxRate) * 100,
taxRate: Number(obj.taxRate),
taxRateIndex: taxRates.find(taxRate => taxRate.year === Number(obj.taxRate) * 100)?.index,
taxRatePercentage: taxRateDecimal * 100,
taxRate: taxRateDecimal,
taxRateIndex,
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
totalSupplyEnd: obj.totalSupplyEnd !== undefined ? BigInt(obj.totalSupplyEnd) : undefined,
totalSupplyInit: BigInt(obj.totalSupplyInit),
@ -101,12 +107,18 @@ const myActivePositions: ComputedRef<Position[]> = computed(() =>
);
const tresholdValue = computed(() => {
const arrayTaxRatePositions = activePositions.value.map(obj => obj.taxRatePercentage);
const sortedPositions = arrayTaxRatePositions.sort((a, b) => (a > b ? 1 : -1));
const sumq = sortedPositions.reduce((partialSum, a) => partialSum + a, 0);
const avg = sumq / sortedPositions.length;
// Compute average tax rate index instead of percentage to avoid floating-point issues
const validIndices = activePositions.value
.map(obj => obj.taxRateIndex)
.filter((idx): idx is number => typeof idx === 'number');
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) {

View file

@ -24,7 +24,7 @@ function assetsToSharesLocal(assets: bigint, kraikenTotalSupply: bigint, stakeTo
return (assets * stakeTotalSupply) / kraikenTotalSupply;
}
export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
export function useSnatchSelection(demo = false, taxRateIndex?: Ref<number>) {
const { activePositions } = usePositions();
const stake = useStake();
const wallet = useWallet();
@ -33,7 +33,7 @@ export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
const snatchablePositions = ref<Position[]>([]);
const shortfallShares = ref<bigint>(0n);
const floorTax = ref(1);
const floorTax = ref(adjustTaxRate.taxRates[0]?.year ?? 1);
let selectionRun = 0;
const openPositionsAvailable = computed(() => shortfallShares.value <= 0n);
@ -74,14 +74,19 @@ export function useSnatchSelection(demo = false, taxRate?: Ref<number>) {
return;
}
const stakeTaxRate = (stake as { taxRate?: number }).taxRate;
const taxRatePercent = taxRate?.value ?? stakeTaxRate ?? Number.POSITIVE_INFINITY;
const maxTaxRateDecimal = Number.isFinite(taxRatePercent) ? taxRatePercent / 100 : Number.POSITIVE_INFINITY;
const stakeTaxRateIndex = (stake as { taxRateIndex?: number }).taxRateIndex;
const selectedTaxRateIndex = taxRateIndex?.value ?? stakeTaxRateIndex;
const maxTaxRateDecimal =
typeof selectedTaxRateIndex === 'number' && Number.isInteger(selectedTaxRateIndex)
? adjustTaxRate.taxRates[selectedTaxRateIndex]?.decimal ?? Number.POSITIVE_INFINITY
: Number.POSITIVE_INFINITY;
const includeOwned = demo;
const recipient = wallet.account.address ?? null;
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;
}
if (!includeOwned && position.iAmOwner) {

View file

@ -9,7 +9,7 @@ import { getNonce, nonce, getName } from '@/contracts/harb';
import { useWallet } from '@/composables/useWallet';
import { createPermitObject, getSignatureRSV } from '@/utils/blockchain';
import { formatBigIntDivision, compactNumber } from '@/utils/helper';
import { taxRates } from '@/composables/useAdjustTaxRates';
import { getTaxRateOptionByIndex } from '@/composables/useAdjustTaxRates';
import { useContractToast } from './useContractToast';
const wallet = useWallet();
const contractToast = useContractToast();
@ -82,10 +82,13 @@ export function useStake() {
// const stakingAmountNumber = computed(() => return staking)
async function snatch(stakingAmount: bigint, taxRate: number, positions: Array<bigint> = []) {
// console.log("snatch", { stakingAmount, taxRate, positions });
async function snatch(stakingAmount: bigint, taxRateIndex: number, positions: Array<bigint> = []) {
// console.log("snatch", { stakingAmount, taxRateIndex, positions });
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 {
const assets: bigint = stakingAmount;
@ -127,10 +130,9 @@ export function useStake() {
const { r, s, v } = getSignatureRSV(signature);
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!, taxRateIndex, positions, deadline, v, r, s);
const hash = await permitAndSnatch(assets, account.address!, taxRateOption.index, positions, deadline, v, r, s);
// console.log("hash", hash);
loading.value = false;
waiting.value = true;

View 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,
};
}

View file

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

View file

@ -45,8 +45,9 @@
<CollapseActive
v-for="position in myActivePositions"
:taxRate="position.taxRatePercentage"
:taxRateIndex="position.taxRateIndex"
:amount="position.amount"
:treshold="tresholdValue"
:tresholdIndex="tresholdValue"
:id="position.positionId"
:position="position"
:key="position.id"