min stake from backend (#78)

resolves #74

Co-authored-by: johba <johba@harb.eth>
Reviewed-on: https://codeberg.org/johba/harb/pulls/78
This commit is contained in:
johba 2025-10-11 15:30:08 +02:00
parent 0b3545091f
commit bd475c2271
11 changed files with 1176 additions and 1977 deletions

View file

@ -30,6 +30,7 @@ type stats {
stakeTotalSupply: BigInt!
outstandingStake: BigInt!
positionsUpdatedAt: BigInt!
minStake: BigInt!
totalMinted: BigInt!
totalBurned: BigInt!
totalTaxPaid: BigInt!
@ -102,6 +103,14 @@ input statsFilter {
positionsUpdatedAt_lt: BigInt
positionsUpdatedAt_gte: BigInt
positionsUpdatedAt_lte: BigInt
minStake: BigInt
minStake_not: BigInt
minStake_in: [BigInt]
minStake_not_in: [BigInt]
minStake_gt: BigInt
minStake_lt: BigInt
minStake_gte: BigInt
minStake_lte: BigInt
totalMinted: BigInt
totalMinted_not: BigInt
totalMinted_in: [BigInt]

View file

@ -23,6 +23,10 @@ export const stats = onchainTable('stats', t => ({
.bigint()
.notNull()
.$default(() => 0n),
minStake: t
.bigint()
.notNull()
.$default(() => 0n),
// Totals
totalMinted: t

View file

@ -132,7 +132,7 @@ export async function ensureStatsExists(context: StatsContext, timestamp?: bigin
}
};
const [kraikenTotalSupply, stakeTotalSupply, outstandingStake] = await Promise.all([
const [kraikenTotalSupply, stakeTotalSupply, outstandingStake, minStake] = await Promise.all([
readWithFallback(
() =>
client.readContract({
@ -163,6 +163,16 @@ export async function ensureStatsExists(context: StatsContext, timestamp?: bigin
0n,
'Stake.outstandingStake'
),
readWithFallback(
() =>
client.readContract({
abi: contracts.Kraiken.abi,
address: contracts.Kraiken.address,
functionName: 'minStake',
}),
0n,
'Kraiken.minStake'
),
]);
cachedStakeTotalSupply = stakeTotalSupply;
@ -178,6 +188,7 @@ export async function ensureStatsExists(context: StatsContext, timestamp?: bigin
ringBufferPointer: 0,
lastHourlyUpdateTimestamp: currentHour,
ringBuffer: serializeRingBuffer(makeEmptyRingBuffer()),
minStake,
});
statsData = await context.db.find(stats, { id: STATS_ID });
@ -295,3 +306,33 @@ export async function refreshOutstandingStake(context: StatsContext) {
outstandingStake,
});
}
export async function refreshMinStake(context: StatsContext, statsData?: Awaited<ReturnType<typeof ensureStatsExists>>) {
let currentStats = statsData;
if (!currentStats) {
currentStats = await context.db.find(stats, { id: STATS_ID });
}
if (!currentStats) {
currentStats = await ensureStatsExists(context);
}
if (!currentStats) return;
let minStake: bigint;
try {
minStake = await context.client.readContract({
abi: context.contracts.Kraiken.abi,
address: context.contracts.Kraiken.address,
functionName: 'minStake',
});
} catch (error) {
const logger = context.logger || console;
logger.warn('[stats.refreshMinStake] Failed to read Kraiken.minStake', error);
return;
}
if ((currentStats.minStake ?? 0n) === minStake) return;
await context.db.update(stats, { id: STATS_ID }).set({
minStake,
});
}

View file

@ -9,6 +9,8 @@ import type { Context } from 'ponder:registry';
* Fails hard (process.exit) on mismatch to prevent indexing wrong data.
*/
export async function validateContractVersion(context: Context): Promise<void> {
const logger = context.logger || console;
try {
const contractVersion = await context.client.readContract({
address: context.contracts.Kraiken.address,
@ -19,14 +21,14 @@ export async function validateContractVersion(context: Context): Promise<void> {
const versionNumber = Number(contractVersion);
if (!isCompatibleVersion(versionNumber)) {
console.error(getVersionMismatchError(versionNumber, 'ponder'));
logger.error(getVersionMismatchError(versionNumber, 'ponder'));
process.exit(1);
}
console.log(`✓ Contract version validated: v${versionNumber} (kraiken-lib v${KRAIKEN_LIB_VERSION})`);
logger.info(`✓ 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');
logger.error('Failed to read contract VERSION:', error);
logger.error('Ensure Kraiken contract has VERSION constant and is deployed correctly');
process.exit(1);
}
}

View file

@ -7,6 +7,7 @@ import {
updateHourlyData,
checkBlockHistorySufficient,
RING_BUFFER_SEGMENTS,
refreshMinStake,
} from './helpers/stats';
import { validateContractVersion } from './helpers/version';
@ -80,7 +81,9 @@ ponder.on('Kraiken:Transfer', async ({ event, context }) => {
});
ponder.on('StatsBlock:block', async ({ event, context }) => {
await ensureStatsExists(context, event.block.timestamp);
const statsData = await ensureStatsExists(context, event.block.timestamp);
await refreshMinStake(context, statsData);
// Only update hourly data if we have sufficient block history
if (checkBlockHistorySufficient(context, event)) {

3017
web-app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -155,7 +155,6 @@ import { useClaim } from '@/composables/useClaim';
import { useAdjustTaxRate } from '@/composables/useAdjustTaxRates';
import { useSnatchSelection } from '@/composables/useSnatchSelection';
import { assetsToShares } from '@/contracts/stake';
import { getMinStake } from '@/contracts/harb';
import { useWallet } from '@/composables/useWallet';
import { ref, onMounted, watch, computed, watchEffect, getCurrentInstance } from 'vue';
import { useStatCollection, loadStats } from '@/composables/useStatCollection';
@ -187,7 +186,7 @@ const snatchLabelId = `stake-snatch-${uid}`;
const stakeSummaryId = `stake-summary-${uid}`;
const formStatusId = `stake-status-${uid}`;
const minStake = ref<bigint>(0n);
const minStake = computed(() => statCollection.minStake ?? 0n);
const stakeSlots = ref<string>('0.00');
const supplyFreeze = ref<number>(0);
let debounceTimer: ReturnType<typeof setTimeout>;
@ -203,7 +202,6 @@ watchEffect(() => {
stake.stakingAmountShares = await assetsToShares(stake.stakingAmount);
const stakingAmountSharesNumber = bigInt2Number(stake.stakingAmountShares, 18);
const stakeableSupplyNumber = bigInt2Number(statCollection.stakeableSupply, 18);
minStake.value = await getMinStake();
if (stakeableSupplyNumber === 0) {
supplyFreeze.value = 0;
@ -502,7 +500,6 @@ async function handleSubmit() {
}
onMounted(async () => {
minStake.value = await getMinStake();
stake.stakingAmountNumber = minStakeAmount.value;
});
</script>

View file

@ -17,20 +17,15 @@ const { darkTheme } = useDark();
const wallet = useWallet();
const initialChainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { activePositions } = usePositions(initialChainId);
const ignoreOwner = ref(false);
const taxRate = ref<number>(1.0);
const stake = useStake();
const statCollection = useStatCollection(initialChainId);
const minStake = computed(() => statCollection.minStake ?? 0n);
const minStakeAmount = computed(() => {
return formatBigIntDivision(minStake.value, 10n ** 18n);
});
const ignoreOwner = ref(false);
const stakeAbleHarbAmount = computed(() => statCollection.kraikenTotalSupply / 5n);
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
const stake = useStake();
const statCollection = useStatCollection(initialChainId);
const taxRate = ref<number>(1.0);
const snatchPositions = computed(() => {
if (
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=

View file

@ -6,6 +6,7 @@ import { config } from '@/wagmi';
import logger from '@/utils/logger';
import type { WatchBlocksReturnType } from 'viem';
import { bigInt2Number } from '@/utils/helper';
import { minStake as stakeMinStake } from '@/contracts/stake';
import { DEFAULT_CHAIN_ID } from '@/config';
import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
const demo = sessionStorage.getItem('demo') === 'true';
@ -17,6 +18,7 @@ interface StatsRecord {
burnedLastDay: string;
burnedLastWeek: string;
id: string;
minStake: string;
mintNextHourProjected: string;
mintedLastDay: string;
mintedLastWeek: string;
@ -44,6 +46,7 @@ export async function loadStatsCollection(chainId: number, endpointOverride?: st
{
query: `query StatsQuery {
stats(id: "0x01") {
minStake
burnNextHourProjected
burnedLastDay
burnedLastWeek
@ -134,6 +137,14 @@ const stakeTotalSupply = computed(() => {
}
});
const minStake = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].minStake);
} else {
return 0n;
}
});
//Total Supply Change / 7d=mintedLastWeekburnedLastWeek
const totalSupplyChange7d = computed(() => {
if (rawStatsCollections.value?.length > 0) {
@ -207,9 +218,11 @@ export async function loadStats(chainId?: number) {
statsError.value = null;
retryManager.reset();
retryManager.clear();
stakeMinStake.value = minStake.value;
} catch (error) {
rawStatsCollections.value = [];
statsError.value = formatGraphqlError(error);
stakeMinStake.value = 0n;
retryManager.schedule();
} finally {
loading.value = false;
@ -269,5 +282,6 @@ export function useStatCollection(chainId: number = DEFAULT_CHAIN_ID) {
claimedSlots,
statsError,
loading,
minStake,
});
}

View file

@ -61,31 +61,6 @@ export async function getAllowance() {
return result;
}
export async function getMinStake() {
logger.contract('getMinStake');
const publicClient = getWalletPublicClient();
if (publicClient) {
const result = (await publicClient.readContract({
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'minStake',
args: [],
})) as bigint;
allowance.value = result;
return result;
}
const result: bigint = (await readContract(config as Config, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: 'minStake',
args: [],
})) as bigint;
allowance.value = result;
return result;
}
export async function getNonce() {
logger.contract('getNonce');

View file

@ -13,7 +13,7 @@ interface Contract {
contractAddress: Address;
}
export const minStake = ref();
export const minStake = ref<bigint>(0n);
export const totalSupply = ref(0n);
export const outstandingSupply = ref(0n);