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
|
||||
services/ponder/.env.local
|
||||
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
|
||||
. "$(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
|
||||
|
|
|
|||
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.
|
||||
- **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.
|
||||
|
|
|
|||
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/__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.
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
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 {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
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.
|
||||
- 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
0
services/ponder/ponder-env.d.ts
vendored
Executable file → Normal 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';
|
||||
|
|
|
|||
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,
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
web-app/env.d.ts
vendored
|
|
@ -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>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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
|
||||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue