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

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"