diff --git a/.env.example b/.env.example index 04e08c820..ecccd0b46 100644 --- a/.env.example +++ b/.env.example @@ -101,7 +101,10 @@ VALIDATORS_CONFIG_JSON = '' # Consensus mechanism VITE_FINALITY_WINDOW = 1800 # in seconds VITE_FINALITY_WINDOW_APPEAL_FAILED_REDUCTION = 0.2 # 20% reduction per appeal failed -VITE_MAX_ROTATIONS = 3 +VITE_LEADER_TIMEOUT_FEE = 300 +VITE_VALIDATORS_TIMEOUT_FEE = 300 +VITE_APPEAL_ROUNDS_FEE = 3 +VITE_ROTATIONS_FEE = 2 # Set the compose profile to 'hardhat' to use the hardhat network COMPOSE_PROFILES = 'hardhat' diff --git a/frontend/src/components/Simulator/ConstructorParameters.vue b/frontend/src/components/Simulator/ConstructorParameters.vue index d849dfe82..9fb0e5603 100644 --- a/frontend/src/components/Simulator/ConstructorParameters.vue +++ b/frontend/src/components/Simulator/ConstructorParameters.vue @@ -5,10 +5,11 @@ import PageSection from '@/components/Simulator/PageSection.vue'; import { ArrowUpTrayIcon } from '@heroicons/vue/16/solid'; import ContractParams from './ContractParams.vue'; import { type ArgData, unfoldArgsData } from './ContractParams'; +import type { FeesDistribution } from '@/types'; const props = defineProps<{ leaderOnly: boolean; - consensusMaxRotations: number; + feesDistribution: FeesDistribution; }>(); const { contract, contractSchemaQuery, deployContract, isDeploying } = @@ -25,7 +26,7 @@ const emit = defineEmits(['deployed-contract']); const handleDeployContract = async () => { const args = calldataArguments.value; const newArgs = unfoldArgsData(args); - await deployContract(newArgs, props.leaderOnly, props.consensusMaxRotations); + await deployContract(newArgs, props.leaderOnly, props.feesDistribution); emit('deployed-contract'); }; diff --git a/frontend/src/components/Simulator/ContractMethodItem.vue b/frontend/src/components/Simulator/ContractMethodItem.vue index d9ba1f5c6..5e04b1c41 100644 --- a/frontend/src/components/Simulator/ContractMethodItem.vue +++ b/frontend/src/components/Simulator/ContractMethodItem.vue @@ -9,6 +9,7 @@ import { ChevronDownIcon } from '@heroicons/vue/16/solid'; import { useEventTracking, useContractQueries } from '@/hooks'; import { unfoldArgsData, type ArgData } from './ContractParams'; import ContractParams from './ContractParams.vue'; +import type { FeesDistribution } from '@/types'; const { callWriteMethod, callReadMethod, contract } = useContractQueries(); const { trackEvent } = useEventTracking(); @@ -18,7 +19,7 @@ const props = defineProps<{ method: ContractMethod; methodType: 'read' | 'write'; leaderOnly: boolean; - consensusMaxRotations?: number; + feesDistribution: FeesDistribution; }>(); const isExpanded = ref(false); @@ -101,7 +102,7 @@ const handleCallWriteMethod = async () => { await callWriteMethod({ method: props.name, leaderOnly: props.leaderOnly, - consensusMaxRotations: props.consensusMaxRotations, + feesDistribution: props.feesDistribution, args: unfoldArgsData({ args: calldataArguments.value.args, kwargs: calldataArguments.value.kwargs, diff --git a/frontend/src/components/Simulator/ContractWriteMethods.vue b/frontend/src/components/Simulator/ContractWriteMethods.vue index 2be7444ef..59a4933bd 100644 --- a/frontend/src/components/Simulator/ContractWriteMethods.vue +++ b/frontend/src/components/Simulator/ContractWriteMethods.vue @@ -5,9 +5,11 @@ import PageSection from '@/components/Simulator/PageSection.vue'; import ContractMethodItem from '@/components/Simulator/ContractMethodItem.vue'; import EmptyListPlaceholder from '@/components/Simulator/EmptyListPlaceholder.vue'; import type { ContractSchema } from 'genlayer-js/types'; +import type { FeesDistribution } from '@/types'; + const props = defineProps<{ leaderOnly: boolean; - consensusMaxRotations: number; + feesDistribution: FeesDistribution; }>(); const { contractAbiQuery } = useContractQueries(); @@ -42,7 +44,7 @@ const writeMethods = computed(() => { :method="method[1]" methodType="write" :leaderOnly="props.leaderOnly" - :consensusMaxRotations="consensusMaxRotations" + :feesDistribution="props.feesDistribution" /> diff --git a/frontend/src/components/Simulator/settings/ConsensusInputSection.vue b/frontend/src/components/Simulator/settings/ConsensusInputSection.vue new file mode 100644 index 000000000..9f9a349af --- /dev/null +++ b/frontend/src/components/Simulator/settings/ConsensusInputSection.vue @@ -0,0 +1,54 @@ + + + diff --git a/frontend/src/components/Simulator/settings/ConsensusSection.vue b/frontend/src/components/Simulator/settings/ConsensusSection.vue index be5819450..dd74bdf0b 100644 --- a/frontend/src/components/Simulator/settings/ConsensusSection.vue +++ b/frontend/src/components/Simulator/settings/ConsensusSection.vue @@ -1,49 +1,89 @@ diff --git a/frontend/src/hooks/useContractQueries.ts b/frontend/src/hooks/useContractQueries.ts index c9ef55344..4543561b4 100644 --- a/frontend/src/hooks/useContractQueries.ts +++ b/frontend/src/hooks/useContractQueries.ts @@ -1,6 +1,6 @@ import { watch, ref, computed } from 'vue'; import { useQuery, useQueryClient } from '@tanstack/vue-query'; -import type { TransactionItem } from '@/types'; +import type { TransactionItem, FeesDistribution } from '@/types'; import { useContractsStore, useTransactionsStore, @@ -100,7 +100,7 @@ export function useContractQueries() { kwargs: { [key: string]: CalldataEncodable }; }, leaderOnly: boolean, - consensusMaxRotations: number, + feesDistribution: FeesDistribution, ) { isDeploying.value = true; @@ -116,7 +116,7 @@ export function useContractQueries() { code: code_bytes as any as string, // FIXME: code should accept both bytes and string in genlayer-js args: args.args, leaderOnly, - consensusMaxRotations, + feesDistribution: feesDistribution, }); const tx: TransactionItem = { @@ -209,7 +209,7 @@ export function useContractQueries() { method, args, leaderOnly, - consensusMaxRotations, + feesDistribution, }: { method: string; args: { @@ -217,7 +217,7 @@ export function useContractQueries() { kwargs: { [key: string]: CalldataEncodable }; }; leaderOnly: boolean; - consensusMaxRotations?: number; + feesDistribution: FeesDistribution; }) { try { if (!accountsStore.selectedAccount) { @@ -230,7 +230,7 @@ export function useContractQueries() { args: args.args, value: BigInt(0), leaderOnly, - consensusMaxRotations, + feesDistribution: feesDistribution, }); transactionsStore.addTransaction({ diff --git a/frontend/src/stores/consensus.ts b/frontend/src/stores/consensus.ts index 6ba38a84c..53262dfc8 100644 --- a/frontend/src/stores/consensus.ts +++ b/frontend/src/stores/consensus.ts @@ -2,12 +2,54 @@ import { defineStore } from 'pinia'; import { ref } from 'vue'; import { useRpcClient, useWebSocketClient } from '@/hooks'; +// Helper function to get stored value or default +const getStoredValue = (key: string, defaultValue: number): number => { + const stored = localStorage.getItem(`consensusStore.${key}`); + return stored ? Number(stored) : defaultValue; +}; + export const useConsensusStore = defineStore('consensusStore', () => { const rpcClient = useRpcClient(); const webSocketClient = useWebSocketClient(); const finalityWindow = ref(Number(import.meta.env.VITE_FINALITY_WINDOW)); const isLoading = ref(true); // Needed for the delay between creating the variable and fetching the initial value - const maxRotations = ref(Number(import.meta.env.VITE_MAX_ROTATIONS)); + const leaderTimeoutFee = ref( + getStoredValue( + 'leaderTimeoutFee', + Number(import.meta.env.VITE_LEADER_TIMEOUT_FEE), + ), + ); + const validatorsTimeoutFee = ref( + getStoredValue( + 'validatorsTimeoutFee', + Number(import.meta.env.VITE_VALIDATORS_TIMEOUT_FEE), + ), + ); + const appealRoundFee = ref( + getStoredValue( + 'appealRoundFee', + Number(import.meta.env.VITE_APPEAL_ROUNDS_FEE), + ), + ); + const defaultRotationFee = Number(import.meta.env.VITE_ROTATIONS_FEE); + const rotationsFee = ref( + ((): number[] => { + try { + const stored = localStorage.getItem('consensusStore.rotationsFee'); + if (stored) { + return JSON.parse(stored); + } + } catch (error) { + console.warn( + 'Failed to parse stored rotationsFee, using default:', + error, + ); + } + // Initialize based on current appealRoundFee + const rounds = appealRoundFee.value + 1; + return rounds > 0 ? Array(rounds).fill(defaultRotationFee) : []; + })(), + ); if (!webSocketClient.connected) webSocketClient.connect(); @@ -35,8 +77,52 @@ export const useConsensusStore = defineStore('consensusStore', () => { finalityWindow.value = time; } - function setMaxRotations(rotations: number) { - maxRotations.value = rotations; + function setLeaderTimeoutFee(fee: number) { + leaderTimeoutFee.value = fee; + localStorage.setItem('consensusStore.leaderTimeoutFee', fee.toString()); + } + + function setValidatorsTimeoutFee(fee: number) { + validatorsTimeoutFee.value = fee; + localStorage.setItem('consensusStore.validatorsTimeoutFee', fee.toString()); + } + + function setAppealRoundFee(fee: number) { + appealRoundFee.value = fee; + localStorage.setItem('consensusStore.appealRoundFee', fee.toString()); + + // Increment fee for rotation calculations + const rotationCount = fee + 1; + const currentRotations = rotationsFee.value; + + // Adjust rotations fee array based on new count + if (rotationCount > currentRotations.length) { + // Add new rounds with default fee + rotationsFee.value = [ + ...currentRotations, + ...Array(rotationCount - currentRotations.length).fill( + defaultRotationFee, + ), + ]; + } else if (rotationCount < currentRotations.length) { + // Remove excess rounds + rotationsFee.value = currentRotations.slice(0, rotationCount); + } + + localStorage.setItem( + 'consensusStore.rotationsFee', + JSON.stringify(rotationsFee.value), + ); + } + + function setRotationsFee(roundIndex: number, fee: number) { + const newRotationsFee = [...rotationsFee.value]; + newRotationsFee[roundIndex] = fee; + rotationsFee.value = newRotationsFee; + localStorage.setItem( + 'consensusStore.rotationsFee', + JSON.stringify(rotationsFee.value), + ); } return { @@ -44,7 +130,13 @@ export const useConsensusStore = defineStore('consensusStore', () => { setFinalityWindowTime, fetchFinalityWindowTime, isLoading, - maxRotations, - setMaxRotations, + leaderTimeoutFee, + setLeaderTimeoutFee, + validatorsTimeoutFee, + setValidatorsTimeoutFee, + appealRoundFee, + setAppealRoundFee, + rotationsFee, + setRotationsFee, }; }); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 2fad3375d..8acb94229 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -45,6 +45,13 @@ export interface NewProviderDataModel { plugin_config: Record; } +export interface FeesDistribution { + leaderTimeoutFee: number; + validatorsTimeoutFee: number; + appealRounds: number; + rotations: number[]; +} + export type Address = `0x${string}`; export interface SchemaProperty { diff --git a/frontend/src/types/store.ts b/frontend/src/types/store.ts index 5de8132d0..75645fa31 100644 --- a/frontend/src/types/store.ts +++ b/frontend/src/types/store.ts @@ -46,3 +46,11 @@ export interface UIState { mode: UIMode; showTutorial: boolean; } + +export interface ConsensusInput { + value: number; + label: string; + id: string; + testId: string; + setter: (value: number) => void; +} diff --git a/frontend/src/views/Simulator/RunDebugView.vue b/frontend/src/views/Simulator/RunDebugView.vue index 9adabe8b0..0a446a6f6 100644 --- a/frontend/src/views/Simulator/RunDebugView.vue +++ b/frontend/src/views/Simulator/RunDebugView.vue @@ -12,6 +12,7 @@ import { useConsensusStore, useUIStore, } from '@/stores'; +import type { FeesDistribution } from '@/types'; import ContractInfo from '@/components/Simulator/ContractInfo.vue'; import BooleanField from '@/components/global/fields/BooleanField.vue'; @@ -51,7 +52,12 @@ function isFinalityWindowValid(value: number) { return Number.isInteger(value) && value >= 0; } -const consensusMaxRotations = computed(() => consensusStore.maxRotations); +const feesDistribution = computed(() => ({ + leaderTimeoutFee: consensusStore.leaderTimeoutFee, + validatorsTimeoutFee: consensusStore.validatorsTimeoutFee, + appealRounds: consensusStore.appealRoundFee, + rotations: consensusStore.rotationsFee, +}));