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