diff --git a/backend/consensus/base.py b/backend/consensus/base.py index f95d4cc1..424182df 100644 --- a/backend/consensus/base.py +++ b/backend/consensus/base.py @@ -85,15 +85,30 @@ def contract_snapshot_factory( session: Session, transaction: Transaction, ): + """ + Factory function to create a ContractSnapshot instance. + + Args: + contract_address (str): The address of the contract. + session (Session): The database session. + transaction (Transaction): The transaction related to the contract. + + Returns: + ContractSnapshot: A new ContractSnapshot instance. + """ + # Check if the transaction is a contract deployment and the contract address matches the transaction's to address if ( transaction.type == TransactionType.DEPLOY_CONTRACT and contract_address == transaction.to_address ): + # Create a new ContractSnapshot instance for the new contract ret = ContractSnapshot(None, session) ret.contract_address = transaction.to_address ret.contract_code = transaction.data["contract_code"] ret.encoded_state = {} return ret + + # Return a ContractSnapshot instance for an existing contract return ContractSnapshot(contract_address, session) @@ -247,7 +262,7 @@ async def exec_transaction( ) # Begin state transitions starting from PendingState - state = PendingState() + state = PendingState(called_from_pending_queue=True) while True: next_state = await state.handle(context) if next_state is None: @@ -371,20 +386,24 @@ async def _appeal_window(self): with self.get_session() as session: chain_snapshot = ChainSnapshot(session) - # Retrieve accepted transactions from the chain snapshot - accepted_transactions = ( - chain_snapshot.get_accepted_transactions() - ) # TODO: also get undetermined transactions - for transaction in accepted_transactions: + # Retrieve accepted and undetermined transactions from the chain snapshot + accepted_undetermined_transactions = ( + chain_snapshot.get_accepted_undetermined_transactions() + ) + for transaction in accepted_undetermined_transactions: transaction = Transaction.from_dict(transaction) # Check if the transaction is appealed if not transaction.appealed: - # Check if the transaction has exceeded the finality window - if ( - int(time.time()) - transaction.timestamp_accepted - ) > self.finality_window_time: + # Check if the transaction has exceeded the finality window or if it is a leader only transaction + if (transaction.leader_only) or ( + ( + int(time.time()) + - transaction.timestamp_awaiting_finalization + ) + > self.finality_window_time + ): # Create a transaction context for finalizing the transaction context = TransactionContext( @@ -407,58 +426,44 @@ async def _appeal_window(self): session.commit() else: - # Handle transactions that are appealed transactions_processor = TransactionsProcessor(session) - # Create a transaction context for the appeal process - context = TransactionContext( - transaction=transaction, - transactions_processor=transactions_processor, - snapshot=chain_snapshot, - accounts_manager=AccountsManager(session), - contract_snapshot_factory=lambda contract_address: contract_snapshot_factory( - contract_address, session, transaction - ), - node_factory=node_factory, - msg_handler=self.msg_handler, - ) - - # Set the leader receipt in the context - context.consensus_data.leader_receipt = ( - transaction.consensus_data.leader_receipt - ) - try: - # Attempt to get extra validators for the appeal process - context.remaining_validators = ( - ConsensusAlgorithm.get_extra_validators( - chain_snapshot, - transaction.consensus_data, - transaction.appeal_failed, - ) + if transaction.status == TransactionStatus.UNDETERMINED: + # Leader appeal + # Appeal data member is used in the frontend for both types of appeals + # Here the type is refined based on the status + transactions_processor.set_transaction_appeal_undetermined( + transaction.hash, True ) - except ValueError as e: - # When no validators are found, then the appeal failed - print(e, transaction) - context.transactions_processor.set_transaction_appeal( - context.transaction.hash, False + transactions_processor.set_transaction_appeal( + transaction.hash, False ) - context.transaction.appealed = False - session.commit() - else: - # Set up the context for the committing state - context.num_validators = len( - context.remaining_validators + transaction.appeal_undetermined = True + transaction.appealed = False + + ConsensusAlgorithm.dispatch_transaction_status_update( + transactions_processor, + transaction.hash, + TransactionStatus.PENDING, + self.msg_handler, ) - context.votes = {} - context.contract_snapshot_supplier = ( - lambda: context.contract_snapshot_factory( - context.transaction.to_address - ) + + # Create a transaction context for the appeal process + context = TransactionContext( + transaction=transaction, + transactions_processor=transactions_processor, + snapshot=chain_snapshot, + accounts_manager=AccountsManager(session), + contract_snapshot_factory=lambda contract_address: contract_snapshot_factory( + contract_address, session, transaction + ), + node_factory=node_factory, + msg_handler=self.msg_handler, ) - # Begin state transitions starting from CommittingState - state = CommittingState() + # Begin state transitions starting from PendingState + state = PendingState(called_from_pending_queue=False) while True: next_state = await state.handle(context) if next_state is None: @@ -466,6 +471,63 @@ async def _appeal_window(self): state = next_state session.commit() + else: + # Validator appeal + # Create a transaction context for the appeal process + context = TransactionContext( + transaction=transaction, + transactions_processor=transactions_processor, + snapshot=chain_snapshot, + accounts_manager=AccountsManager(session), + contract_snapshot_factory=lambda contract_address: contract_snapshot_factory( + contract_address, session, transaction + ), + node_factory=node_factory, + msg_handler=self.msg_handler, + ) + + # Set the leader receipt in the context + context.consensus_data.leader_receipt = ( + transaction.consensus_data.leader_receipt + ) + try: + # Attempt to get extra validators for the appeal process + context.remaining_validators = ( + ConsensusAlgorithm.get_extra_validators( + chain_snapshot, + transaction.consensus_data, + transaction.appeal_failed, + ) + ) + except ValueError as e: + # When no validators are found, then the appeal failed + print(e, transaction) + context.transactions_processor.set_transaction_appeal( + context.transaction.hash, False + ) + context.transaction.appealed = False + session.commit() + else: + # Set up the context for the committing state + context.num_validators = len( + context.remaining_validators + ) + context.votes = {} + context.contract_snapshot_supplier = ( + lambda: context.contract_snapshot_factory( + context.transaction.to_address + ) + ) + + # Begin state transitions starting from CommittingState + state = CommittingState() + while True: + next_state = await state.handle(context) + if next_state is None: + break + state = next_state + session.commit() + except Exception as e: print("Error running appeal window", e) print(traceback.format_exc()) @@ -684,6 +746,15 @@ class PendingState(TransactionState): Class representing the pending state of a transaction. """ + def __init__(self, called_from_pending_queue: bool): + """ + Initialize the PendingState. + + Args: + called_from_pending_queue (bool): Indicates if the PendingState was called from the pending queue. + """ + self.called_from_pending_queue = called_from_pending_queue + async def handle(self, context): """ Handle the pending state transition. @@ -694,12 +765,19 @@ async def handle(self, context): Returns: TransactionState | None: The ProposingState or None if the transaction is already in process, when it is a transaction or when there are no validators. """ - if ( + # Transactions that are put back to pending are processed again, so we need to get the latest data of the transaction + context.transaction = Transaction.from_dict( context.transactions_processor.get_transaction_by_hash( context.transaction.hash - )["status"] - != TransactionStatus.PENDING.value - ): + ) + ) + + # Transaction should not be processed from the pending queue if it is a leader appeal + # This is to filter out the transaction picked up by _crawl_snapshot + if self.called_from_pending_queue and context.transaction.appeal_undetermined: + return None + + if context.transaction.status != TransactionStatus.PENDING: # This is a patch for a TOCTOU problem we have https://github.com/yeagerai/genlayer-simulator/issues/387 # Problem: Pending transactions are checked by `_crawl_snapshot`, which appends them to queues. These queues are consumed by `_run_consensus`, which processes the transactions. This means that a transaction can be processed multiple times, since `_crawl_snapshot` can append the same transaction to the queue multiple times. # Partial solution: This patch checks if the transaction is still pending before processing it. This is not the best solution, but we'll probably refactor the whole consensus algorithm in the short term. @@ -736,11 +814,23 @@ async def handle(self, context): involved_validators = ConsensusAlgorithm.get_validators_from_consensus_data( all_validators, context.transaction.consensus_data ) + # Reset the transaction appeal status context.transactions_processor.set_transaction_appeal( context.transaction.hash, False ) context.transaction.appealed = False + + elif context.transaction.appeal_undetermined: + # Add n+2 validators, remove the old leader + current_validators = ConsensusAlgorithm.get_validators_from_consensus_data( + all_validators, context.transaction.consensus_data + ) + extra_validators = ConsensusAlgorithm.get_extra_validators( + context.snapshot, context.transaction.consensus_data, 0 + ) + involved_validators = current_validators + extra_validators + else: # If not appealed, get the default number of validators for the transaction involved_validators = get_validators_for_transaction( @@ -996,7 +1086,9 @@ async def handle(self, context): # Log the failure to reach consensus and transition to ProposingState print( "Consensus not reached for transaction, rotating leader: ", - context.transaction, + context.transactions_processor.get_transaction_by_hash( + context.transaction.hash + ), ) return ProposingState() @@ -1016,9 +1108,9 @@ async def handle(self, context): Returns: None: The transaction is accepted. """ + # When appeal fails, the appeal window is not reset if not context.transaction.appealed: - # When appeal fails, the appeal window is not reset - context.transactions_processor.set_transaction_timestamp_accepted( + context.transactions_processor.set_transaction_timestamp_awaiting_finalization( context.transaction.hash ) @@ -1028,6 +1120,17 @@ async def handle(self, context): ) context.transaction.appealed = False + # Set the transaction appeal undetermined status to false + context.transactions_processor.set_transaction_appeal_undetermined( + context.transaction.hash, False + ) + context.transaction.appeal_undetermined = False + + # Set the transaction result + context.transactions_processor.set_transaction_result( + context.transaction.hash, context.consensus_data.to_dict() + ) + # Update the transaction status to ACCEPTED ConsensusAlgorithm.dispatch_transaction_status_update( context.transactions_processor, @@ -1036,11 +1139,6 @@ async def handle(self, context): context.msg_handler, ) - # Set the transaction result - context.transactions_processor.set_transaction_result( - context.transaction.hash, context.consensus_data.to_dict() - ) - # Send a message indicating consensus was reached context.msg_handler.send_message( LogEvent( @@ -1060,7 +1158,11 @@ async def handle(self, context): # Get the contract snapshot for the transaction's target address leaders_contract_snapshot = context.contract_snapshot_supplier() + # Do not deploy the contract if the execution failed if leader_receipt.execution_result == ExecutionResultStatus.SUCCESS: + # Get the contract snapshot for the transaction's target address + leaders_contract_snapshot = context.contract_snapshot_supplier() + # Register contract if it is a new contract if context.transaction.type == TransactionType.DEPLOY_CONTRACT: new_contract = { @@ -1108,9 +1210,6 @@ async def handle(self, context): Returns: None: The transaction remains in an undetermined state. """ - # Log the failure to reach consensus for the transaction - print("Consensus not reached for transaction: ", context.transaction) - # Send a message indicating consensus failure context.msg_handler.send_message( LogEvent( @@ -1122,7 +1221,25 @@ async def handle(self, context): ) ) - # Update the transaction status to UNDETERMINED + # When appeal fails, the appeal window is not reset + if not context.transaction.appeal_undetermined: + context.transactions_processor.set_transaction_timestamp_awaiting_finalization( + context.transaction.hash + ) + + # Set the transaction appeal undetermined status to false + context.transactions_processor.set_transaction_appeal_undetermined( + context.transaction.hash, False + ) + context.transaction.appeal_undetermined = False + + # Set the transaction result with the current consensus data + context.transactions_processor.set_transaction_result( + context.transaction.hash, + context.consensus_data.to_dict(), + ) + + # Update the transaction status to undetermined ConsensusAlgorithm.dispatch_transaction_status_update( context.transactions_processor, context.transaction.hash, @@ -1130,10 +1247,12 @@ async def handle(self, context): context.msg_handler, ) - # Set the transaction result with the current consensus data - context.transactions_processor.set_transaction_result( - context.transaction.hash, - context.consensus_data.to_dict(), + # Log the failure to reach consensus for the transaction + print( + "Consensus not reached for transaction: ", + context.transactions_processor.get_transaction_by_hash( + context.transaction.hash + ), ) return None @@ -1161,61 +1280,64 @@ async def handle(self, context): context.msg_handler, ) - # Insert pending transactions generated by contract-to-contract calls - pending_transactions = ( - context.transaction.consensus_data.leader_receipt.pending_transactions - ) - for pending_transaction in pending_transactions: - nonce = context.transactions_processor.get_transaction_count( - context.transaction.to_address + if context.transaction.status != TransactionStatus.UNDETERMINED: + # Insert pending transactions generated by contract-to-contract calls + pending_transactions = ( + context.transaction.consensus_data.leader_receipt.pending_transactions ) - data: dict - transaction_type: TransactionType - if pending_transaction.is_deploy(): - transaction_type = TransactionType.DEPLOY_CONTRACT - new_contract_address: str - if pending_transaction.salt_nonce == 0: - # NOTE: this address is random, which doesn't 100% align with consensus spec - new_contract_address = ( - context.accounts_manager.create_new_account().address - ) + for pending_transaction in pending_transactions: + nonce = context.transactions_processor.get_transaction_count( + context.transaction.to_address + ) + data: dict + transaction_type: TransactionType + if pending_transaction.is_deploy(): + transaction_type = TransactionType.DEPLOY_CONTRACT + new_contract_address: str + if pending_transaction.salt_nonce == 0: + # NOTE: this address is random, which doesn't 100% align with consensus spec + new_contract_address = ( + context.accounts_manager.create_new_account().address + ) + else: + from eth_utils.crypto import keccak + from backend.node.types import Address + from backend.node.base import SIMULATOR_CHAIN_ID + + arr = bytearray() + arr.append(1) + arr.extend(Address(context.transaction.to_address).as_bytes) + arr.extend( + pending_transaction.salt_nonce.to_bytes( + 32, "big", signed=False + ) + ) + arr.extend(SIMULATOR_CHAIN_ID.to_bytes(32, "big", signed=False)) + new_contract_address = Address(keccak(arr)[:20]).as_hex + context.accounts_manager.create_new_account_with_address( + new_contract_address + ) + pending_transaction.address = new_contract_address + data = { + "contract_address": new_contract_address, + "contract_code": pending_transaction.code, + "calldata": pending_transaction.calldata, + } else: - from eth_utils.crypto import keccak - from backend.node.types import Address - from backend.node.base import SIMULATOR_CHAIN_ID - - arr = bytearray() - arr.append(1) - arr.extend(Address(context.transaction.to_address).as_bytes) - arr.extend( - pending_transaction.salt_nonce.to_bytes(32, "big", signed=False) - ) - arr.extend(SIMULATOR_CHAIN_ID.to_bytes(32, "big", signed=False)) - new_contract_address = Address(keccak(arr)[:20]).as_hex - context.accounts_manager.create_new_account_with_address( - new_contract_address - ) - pending_transaction.address = new_contract_address - data = { - "contract_address": new_contract_address, - "contract_code": pending_transaction.code, - "calldata": pending_transaction.calldata, - } - else: - transaction_type = TransactionType.RUN_CONTRACT - data = { - "calldata": pending_transaction.calldata, - } - context.transactions_processor.insert_transaction( - context.transaction.to_address, # new calls are done by the contract - pending_transaction.address, - data, - value=0, # we only handle EOA transfers at the moment, so no value gets transferred - type=transaction_type.value, - nonce=nonce, - leader_only=context.transaction.leader_only, # Cascade - triggered_by_hash=context.transaction.hash, - ) + transaction_type = TransactionType.RUN_CONTRACT + data = { + "calldata": pending_transaction.calldata, + } + context.transactions_processor.insert_transaction( + context.transaction.to_address, # new calls are done by the contract + pending_transaction.address, + data, + value=0, # we only handle EOA transfers at the moment, so no value gets transferred + type=transaction_type.value, + nonce=nonce, + leader_only=context.transaction.leader_only, # Cascade + triggered_by_hash=context.transaction.hash, + ) def rotate(nodes: list) -> Iterator[list]: diff --git a/backend/database_handler/chain_snapshot.py b/backend/database_handler/chain_snapshot.py index 9f401a94..62789846 100644 --- a/backend/database_handler/chain_snapshot.py +++ b/backend/database_handler/chain_snapshot.py @@ -18,7 +18,9 @@ def __init__(self, session: Session): self.all_validators = self.validators_registry.get_all_validators() self.pending_transactions = self._load_pending_transactions() self.num_validators = len(self.all_validators) - self.accepted_transactions = self._load_accepted_transactions() + self.accepted_undetermined_transactions = ( + self._load_accepted_undetermined_transactions() + ) def _load_pending_transactions(self) -> List[dict]: """Load and return the list of pending transactions from the database.""" @@ -41,12 +43,16 @@ def get_all_validators(self): """Return the list of all validators.""" return self.all_validators - def _load_accepted_transactions(self) -> List[dict]: - """Load and return the list of accepted transactions from the database.""" + def _load_accepted_undetermined_transactions(self) -> List[dict]: + """Load and return the list of accepted and undetermined transactions from the database.""" accepted_transactions = ( self.session.query(Transactions) - .filter(Transactions.status == TransactionStatus.ACCEPTED) + .filter( + (Transactions.status == TransactionStatus.ACCEPTED) + | (Transactions.status == TransactionStatus.UNDETERMINED) + ) + .order_by(Transactions.timestamp_awaiting_finalization) .all() ) return [ @@ -54,6 +60,6 @@ def _load_accepted_transactions(self) -> List[dict]: for transaction in accepted_transactions ] - def get_accepted_transactions(self): - """Return the list of accepted transactions.""" - return self.accepted_transactions + def get_accepted_undetermined_transactions(self): + """Return the list of accepted and undetermined transactions.""" + return self.accepted_undetermined_transactions diff --git a/backend/database_handler/migration/versions/a4a32d27dde2_appeal_undetermined.py b/backend/database_handler/migration/versions/a4a32d27dde2_appeal_undetermined.py new file mode 100644 index 00000000..fef2a55e --- /dev/null +++ b/backend/database_handler/migration/versions/a4a32d27dde2_appeal_undetermined.py @@ -0,0 +1,55 @@ +"""appeal_undetermined + +Revision ID: a4a32d27dde2 +Revises: 2a4ac5eb9455 +Create Date: 2024-11-25 14:49:46.916279 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "a4a32d27dde2" +down_revision: Union[str, None] = "2a4ac5eb9455" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "transactions", sa.Column("appeal_undetermined", sa.Boolean(), nullable=True) + ) + op.execute( + "UPDATE transactions SET appeal_undetermined = FALSE WHERE appeal_undetermined IS NULL" + ) + op.alter_column("transactions", "appeal_undetermined", nullable=False) + op.add_column( + "transactions", + sa.Column("timestamp_awaiting_finalization", sa.BigInteger(), nullable=True), + ) + op.execute( + "UPDATE transactions SET timestamp_awaiting_finalization = 0 WHERE timestamp_awaiting_finalization IS NULL" + ) + op.drop_column("transactions", "timestamp_accepted") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "transactions", + sa.Column( + "timestamp_accepted", sa.BIGINT(), autoincrement=False, nullable=True + ), + ) + op.execute( + "UPDATE transactions SET timestamp_accepted = 0 WHERE timestamp_accepted IS NULL" + ) + op.drop_column("transactions", "timestamp_awaiting_finalization") + op.drop_column("transactions", "appeal_undetermined") + # ### end Alembic commands ### diff --git a/backend/database_handler/models.py b/backend/database_handler/models.py index e94aced9..127f3bb7 100644 --- a/backend/database_handler/models.py +++ b/backend/database_handler/models.py @@ -116,7 +116,10 @@ class Transactions(Base): init=False, ) appealed: Mapped[bool] = mapped_column(Boolean, default=False) - timestamp_accepted: Mapped[Optional[int]] = mapped_column(BigInteger, default=None) + appeal_undetermined: Mapped[bool] = mapped_column(Boolean, default=False) + timestamp_awaiting_finalization: Mapped[Optional[int]] = mapped_column( + BigInteger, default=None + ) class Validators(Base): diff --git a/backend/database_handler/transactions_processor.py b/backend/database_handler/transactions_processor.py index 163c6f3d..eb40658f 100644 --- a/backend/database_handler/transactions_processor.py +++ b/backend/database_handler/transactions_processor.py @@ -61,8 +61,9 @@ def _parse_transaction_data(transaction_data: Transactions) -> dict: ], "ghost_contract_address": transaction_data.ghost_contract_address, "appealed": transaction_data.appealed, - "timestamp_accepted": transaction_data.timestamp_accepted, + "timestamp_awaiting_finalization": transaction_data.timestamp_awaiting_finalization, "appeal_failed": transaction_data.appeal_failed, + "appeal_undetermined": transaction_data.appeal_undetermined, } @staticmethod @@ -225,8 +226,9 @@ def insert_transaction( ), ghost_contract_address=ghost_contract_address, appealed=False, - timestamp_accepted=None, + timestamp_awaiting_finalization=None, appeal_failed=0, + appeal_undetermined=False, ) self.session.add(new_transaction) @@ -348,16 +350,18 @@ def set_transaction_appeal(self, transaction_hash: str, appeal: bool): ) transaction.appealed = appeal - def set_transaction_timestamp_accepted( - self, transaction_hash: str, timestamp_accepted: int = None + def set_transaction_timestamp_awaiting_finalization( + self, transaction_hash: str, timestamp_awaiting_finalization: int = None ): transaction = ( self.session.query(Transactions).filter_by(hash=transaction_hash).one() ) - if timestamp_accepted: - transaction.timestamp_accepted = timestamp_accepted + if timestamp_awaiting_finalization: + transaction.timestamp_awaiting_finalization = ( + timestamp_awaiting_finalization + ) else: - transaction.timestamp_accepted = int(time.time()) + transaction.timestamp_awaiting_finalization = int(time.time()) def set_transaction_appeal_failed(self, transaction_hash: str, appeal_failed: int): if appeal_failed < 0: @@ -366,3 +370,11 @@ def set_transaction_appeal_failed(self, transaction_hash: str, appeal_failed: in self.session.query(Transactions).filter_by(hash=transaction_hash).one() ) transaction.appeal_failed = appeal_failed + + def set_transaction_appeal_undetermined( + self, transaction_hash: str, appeal_undetermined: bool + ): + transaction = ( + self.session.query(Transactions).filter_by(hash=transaction_hash).one() + ) + transaction.appeal_undetermined = appeal_undetermined diff --git a/backend/domain/types.py b/backend/domain/types.py index 97477a23..cfcf387a 100644 --- a/backend/domain/types.py +++ b/backend/domain/types.py @@ -83,8 +83,9 @@ class Transaction: created_at: str | None = None ghost_contract_address: str | None = None appealed: bool = False - timestamp_accepted: int | None = None + timestamp_awaiting_finalization: int | None = None appeal_failed: int = 0 + appeal_undetermined: bool = False def to_dict(self): return { @@ -106,8 +107,9 @@ def to_dict(self): "created_at": self.created_at, "ghost_contract_address": self.ghost_contract_address, "appealed": self.appealed, - "timestamp_accepted": self.timestamp_accepted, + "timestamp_awaiting_finalization": self.timestamp_awaiting_finalization, "appeal_failed": self.appeal_failed, + "appeal_undetermined": self.appeal_undetermined, } @classmethod @@ -131,6 +133,9 @@ def from_dict(cls, input: dict) -> "Transaction": created_at=input.get("created_at"), ghost_contract_address=input.get("ghost_contract_address"), appealed=input.get("appealed"), - timestamp_accepted=input.get("timestamp_accepted"), + timestamp_awaiting_finalization=input.get( + "timestamp_awaiting_finalization" + ), appeal_failed=input.get("appeal_failed", 0), + appeal_undetermined=input.get("appeal_undetermined", False), ) diff --git a/frontend/src/components/Simulator/TransactionItem.vue b/frontend/src/components/Simulator/TransactionItem.vue index 3caa8d7e..a130c124 100644 --- a/frontend/src/components/Simulator/TransactionItem.vue +++ b/frontend/src/components/Simulator/TransactionItem.vue @@ -18,6 +18,7 @@ const transactionsStore = useTransactionsStore(); const props = defineProps<{ transaction: TransactionItem; + finalityWindow: number; }>(); const isDetailsModalOpen = ref(false); @@ -44,20 +45,17 @@ const shortHash = computed(() => { return props.transaction.hash?.slice(0, 6); }); -const isAppealed = ref(false); +const appealed = ref(props.transaction.data.appealed); const handleSetTransactionAppeal = () => { transactionsStore.setTransactionAppeal(props.transaction.hash); - - isAppealed.value = true; + appealed.value = true; }; watch( - () => props.transaction.status, - (newStatus) => { - if (newStatus !== 'ACCEPTED') { - isAppealed.value = false; - } + () => props.transaction.data.appealed, + (newVal) => { + appealed.value = newVal; }, ); @@ -155,8 +153,15 @@ function prettifyTxData(x: any): any {

diff --git a/frontend/src/components/Simulator/TransactionsList.vue b/frontend/src/components/Simulator/TransactionsList.vue index a12b5cb8..92ead343 100644 --- a/frontend/src/components/Simulator/TransactionsList.vue +++ b/frontend/src/components/Simulator/TransactionsList.vue @@ -1,5 +1,5 @@