Skip to content

feat: add sim_cancelTransaction RPC endpoint with auth and UI#1444

Merged
cristiam86 merged 4 commits intomainfrom
dxp-736-cancel-transactions
Feb 24, 2026
Merged

feat: add sim_cancelTransaction RPC endpoint with auth and UI#1444
cristiam86 merged 4 commits intomainfrom
dxp-736-cancel-transactions

Conversation

@cristiam86
Copy link
Member

@cristiam86 cristiam86 commented Feb 16, 2026

Closes DXP-736

What

  • Added sim_cancelTransaction JSON-RPC endpoint in the backend with the same auth pattern as sim_upgradeContractCode (admin API key or ECDSA signature from the transaction sender)
  • Cancellation is atomic: uses UPDATE ... WHERE status IN ('PENDING', 'ACTIVATED') AND blocked_at IS NULL to prevent race conditions with consensus workers
  • After cancellation, a WebSocket notification is emitted so the frontend updates in real-time
  • Frontend: added cancelTransaction() to the API service and transactions store (signs with viem for local accounts or MetaMask)
  • Frontend: added a CANCEL button in TransactionItem.vue for PENDING/ACTIVATED transactions; suppressed the loading spinner for CANCELED status

Why

  • Users need to be able to cancel pending transactions before they enter consensus processing
  • Mirrors the flow described in the Linear issue: same authorization mechanism as contract upgrade

Testing done

  • Python syntax check passes on all modified backend files
  • TypeScript type check (vue-tsc --noEmit) passes clean
  • Pre-commit hooks (black, eslint, prettier) all pass

Decisions made

  • Signature message is keccak256("cancel_transaction" + tx_hash_bytes) — tx hashes are globally unique so no extra nonce is needed for replay protection
  • Transactions with from_address = NULL (e.g., upgrade transactions) can only be cancelled via admin key, not signature
  • If blocked_at IS NOT NULL, the worker has already claimed the transaction and the cancel is rejected with a clear error

Checks

  • I have tested this code
  • I have reviewed my own PR
  • I have created an issue for this PR
  • I have set a descriptive PR title compliant with conventional commits

Reviewing tips

  • Start with backend/protocol_rpc/endpoints.py — the new cancel_transaction() function follows the exact same structure as admin_upgrade_contract_code() above it
  • The frontend signing logic in stores/transactions.ts mirrors useContractQueries.ts upgradeContract()

User facing release notes

  • Users can now cancel PENDING or ACTIVATED transactions directly from the transaction list in the Studio UI by clicking the new CANCEL button

Summary by CodeRabbit

  • New Features

    • Users can cancel PENDING/ACTIVATED transactions; cancellations return confirmation and status.
  • UI

    • Added a CANCEL button with loading/disabled states; canceled status excluded from loaders and badges.
  • API

    • Client and RPC surfaces extended to send cancellation requests with optional signature or admin key.
  • Backend

    • Cancellation is performed atomically and triggers real-time frontend notification; CANCELED treated as a terminal previous state.
  • Tests

    • Extensive unit tests covering validation, auth modes, state transitions, and notifications.

- Backend: add cancel_transaction() handler in endpoints.py with
  signature/admin_key auth (same pattern as sim_upgradeContractCode),
  atomic SQL UPDATE guarded by status IN ('PENDING','ACTIVATED') AND
  blocked_at IS NULL to avoid worker race conditions, and WebSocket
  notification via msg_handler.send_transaction_status_update()
- Backend: register sim_cancelTransaction in rpc_methods.py
- Frontend: add cancelTransaction() to IJsonRpcService, JsonRpcService,
  and transactions store (with ECDSA signing via viem for local/MetaMask)
- Frontend: add CANCEL button in TransactionItem.vue for PENDING/ACTIVATED
  transactions; suppress spinner for CANCELED status

Closes DXP-736

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Adds a new sim_cancelTransaction RPC and backend endpoint cancel_transaction to atomically cancel pending/activated transactions (with hosted/self-hosted access control via admin key or sender signature), frontend RPC/store/UI signing and cancel action, DB helper to perform conditional UPDATE, and tests. Duplicate endpoint insertion observed in the backend file.

Changes

Cohort / File(s) Summary
Backend RPC Endpoint(s)
backend/protocol_rpc/endpoints.py, backend/protocol_rpc/rpc_methods.py
Adds cancel_transaction(session, transaction_hash, msg_handler, signature=None, admin_key=None) in endpoints.py and RPC wrapper sim_cancelTransaction in rpc_methods.py. Validates hash, authorizes (admin_key or recovered sender signature under hosted mode), calls TransactionsProcessor to atomically set status to CANCELED, commits, and notifies frontend via WebSocket. Note: duplicate insertion of the same endpoint exists in endpoints.py.
Database transaction helper
backend/database_handler/transactions_processor.py
Adds cancel_transaction_if_available(session, transaction_hash) -> bool to atomically UPDATE transactions to CANCELED only when status IN (PENDING, ACTIVATED) and blocked_at IS NULL, returning whether a row was affected.
Consensus logic
backend/consensus/base.py, tests/unit/consensus/test_previous_canceled_transaction.py
Treats CANCELED as a previous transaction status that allows entering the pending-processing path; adds tests asserting exec_transaction behavior when previous status is CANCELED vs in-processing.
Frontend RPC Service
frontend/src/services/IJsonRpcService.ts, frontend/src/services/JsonRpcService.ts
Adds cancelTransaction(transactionHash: string, signature?: string) to the service interface and implements it to call RPC method sim_cancelTransaction.
Frontend Store & Signing
frontend/src/stores/transactions.ts
Adds cancelTransaction(txHash) action: constructs keccak256("cancel_transaction"+txHash) message, signs with local private key or MetaMask (personal_sign), and calls rpcClient.cancelTransaction(txHash, signature).
Frontend UI
frontend/src/components/Simulator/TransactionItem.vue
Adds canCancel computed prop, isCancelling state, handleCancelTransaction() action, and a CANCEL button (label toggles CANCELLING...), gated by hosted/admin rules; excludes CANCELED from certain loader/status logic.
Frontend Tests
frontend/test/unit/services/JsonRpcService.test.ts, frontend/test/unit/stores/transactions.test.ts
Adds unit tests for JsonRpcService.cancelTransaction (params, signature, errors) and useTransactionsStore.cancelTransaction (no-signature flow, MetaMask signing, and error propagation).
Backend Tests
tests/unit/test_cancel_transaction_endpoint.py
Adds comprehensive tests for the cancel_transaction endpoint covering hash validation, not-found, local vs hosted auth, admin key and signature scenarios, signature verification, atomic update semantics, commit behavior, WebSocket notification, and related edge cases.

Sequence Diagram

sequenceDiagram
    participant UI as UI (Frontend)
    participant Store as Store (Frontend)
    participant Service as RPC Service
    participant Backend as Backend Endpoint
    participant DB as Database
    participant WS as WebSocket

    UI->>Store: cancelTransaction(txHash)
    Store->>Store: Build message hash keccak256("cancel_transaction"+txHash)
    Store->>Store: Sign message (local key or MetaMask)
    Store->>Service: cancelTransaction(txHash, signature)
    Service->>Backend: RPC sim_cancelTransaction(txHash, signature)
    Backend->>Backend: Validate transaction_hash format
    Backend->>DB: SELECT transaction by hash
    alt not found
        Backend-->>Service: Error (not found)
    else found
        alt hosted & admin_key provided
            Backend->>DB: UPDATE set status=CANCELED where status IN (PENDING, ACTIVATED) AND blocked_at IS NULL
        else signature provided
            Backend->>Backend: Recover signer from signature
            alt signer == sender
                Backend->>DB: UPDATE set status=CANCELED where status IN (PENDING, ACTIVATED) AND blocked_at IS NULL
            else
                Backend-->>Service: Error (unauthorized)
            end
        else
            Backend-->>Service: Error (unauthorized)
        end
    end
    DB->>Backend: Rows updated / none
    alt rows updated
        Backend->>DB: COMMIT
        Backend->>WS: Notify status CANCELED (transaction_hash)
        Backend-->>Service: Return {transaction_hash, status:"CANCELED"}
    else no rows updated
        Backend-->>Service: Error (already processed / not cancellable)
    end
    Service-->>Store: Result / Error
    Store-->>UI: Update UI state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

run-tests, Studio

Suggested reviewers

  • kstroobants

Poem

🐇 I nibbled code from front to back tonight,
A signed little note and a gentle byte;
Rows flipped to "CANCELED" — a soft little cheer,
A websocket wink and the UI drew near.
Hop hop — the rabbit closed the loop with delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a sim_cancelTransaction RPC endpoint with authentication and UI components.
Description check ✅ Passed The description covers all required template sections: What (detailed features), Why (user need), Testing done, Decisions made, and Checks. All major changes are documented with clear explanations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dxp-736-cancel-transactions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@backend/protocol_rpc/endpoints.py`:
- Around line 791-814: The current raw SQL UPDATE should be replaced with a call
to the canonical status updater: use
TransactionsProcessor.update_transaction_status(transaction,
TransactionStatus.CANCELED, update_current_status_changes=False) (or True if you
intentionally want bookkeeping) instead of executing raw SQL; call it with the
transaction object resolved from transaction_hash, commit the session after the
updater, and if the updater indicates no change (e.g., returns False or does not
transition because status/blocked_at prevented it) raise the same JSONRPCError
preserving transaction_hash and transaction.status.value; also ensure behavior
matches sim_updateTransactionStatus (the standard path) or document why you must
diverge.

In `@frontend/src/stores/transactions.ts`:
- Around line 130-135: The MetaMask branch is double-encoding the hash by
calling toHex(messageHash) before personal_sign; instead pass the raw hex hash
the same way as the local signer. Replace the personal_sign params so
personal_sign receives messageHash directly (mirror the local path that uses {
raw: messageHash }) — update the window.ethereum.request call (method
'personal_sign') to send messageHash (not toHex(messageHash)) with
account.address so MetaMask signs the actual hash bytes and backend verification
succeeds.
🧹 Nitpick comments (5)
backend/protocol_rpc/rpc_methods.py (1)

248-262: Use explicit | None for optional parameters.

Ruff (RUF013) flags signature: str = None and admin_key: str = None as implicit Optional, which PEP 484 prohibits. The existing upgrade_contract_code (lines 235-236) has the same issue, so this is a pre-existing pattern—but worth fixing in new code.

Proposed fix
 `@rpc.method`("sim_cancelTransaction")
 def cancel_transaction(
     transaction_hash: str,
-    signature: str = None,
-    admin_key: str = None,
+    signature: str | None = None,
+    admin_key: str | None = None,
     session: Session = Depends(get_db_session),
     msg_handler=Depends(get_message_handler),
 ) -> dict:
backend/protocol_rpc/endpoints.py (3)

712-717: Redundant local imports.

TransactionStatus is already imported at module level (line 17), and os is already imported at module level (line 53). These local re-imports are unnecessary. The existing admin_upgrade_contract_code has the same pattern, but new code should avoid it.

Proposed fix
     from backend.database_handler.models import Transactions, TransactionStatus
     from eth_account.messages import encode_defunct
     from eth_account import Account
     from web3 import Web3
     from sqlalchemy import text
-    import os

688-694: Add type hint for msg_handler parameter.

Other endpoint functions in this file use IMessageHandler as the type for message handler parameters (e.g., get_contract_schema at line 829). As per coding guidelines, type hints should be included in all Python code.

Proposed fix
 def cancel_transaction(
     session: Session,
     transaction_hash: str,
-    msg_handler,
+    msg_handler: IMessageHandler,
     signature: str | None = None,
     admin_key: str | None = None,
 ) -> dict:

As per coding guidelines: "Include type hints in all Python code".


761-783: Signature message lacks chain-id / domain separator — acceptable for studio but worth noting.

The cancel message is keccak256("cancel_transaction" + tx_hash_bytes). Since the tx_hash is unique per transaction, replay across transactions is prevented. However, there's no chain-id or domain separator, so the same signature could theoretically be replayed on a different GenLayer Studio instance sharing the same DB schema. This is likely acceptable for a studio/dev tool but worth documenting if the pattern is extended to production.

frontend/src/stores/transactions.ts (1)

107-139: No user feedback when account is missing.

If account is null (line 116), signature stays undefined and the RPC call proceeds without auth. In local dev mode this works, but in hosted mode the backend will reject with a generic error. Consider an early return or a more descriptive error.

Backend: 25 tests covering hash validation, auth (local/hosted/admin key/signature), atomic cancel race conditions, and success behavior.
Frontend: 4 JsonRpcService tests + 3 transactions store tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
frontend/test/unit/stores/transactions.test.ts (1)

206-227: Tests at lines 206–215 and 217–227 are functionally identical.

Both tests assert exactly the same thing: mockCancelTransaction was called with (hash, undefined). The second test ("should send no signature when no account is selected") doesn't set up any different conditions from the first, since the vi.doMock for accounts already defaults selectedAccount to null.

Collapse them into one, or differentiate the second by setting up a scenario with a selected account that has no signing capability.

tests/unit/test_cancel_transaction_endpoint.py (2)

167-178: Prefix unused mock_encode parameters with _ to signal intent.

The mock_encode argument is injected by the @patch decorator but never referenced in the test bodies (also flagged by Ruff ARG002 at lines 171, 195, 252). Renaming to _mock_encode across all three test methods makes the intent explicit.

Example fix (apply to all three methods)
     def test_hosted_mode_valid_signature_from_sender(
-        self, mock_web3_class, mock_encode, mock_recover
+        self, mock_web3_class, _mock_encode, mock_recover
     ):

409-421: Fragile param extraction — and test name overpromises.

The method is named test_session_execute_called_with_correct_sql but only asserts the bind parameter (hash), not the SQL statement text. The param extraction at line 420 also relies on the call convention (positional vs keyword) of session.execute, which can silently break if the implementation changes how it passes args.

Consider either:

  1. Renaming the test to test_session_execute_called_with_correct_params, or
  2. Also asserting something about the SQL text (e.g., checking "CANCELED" or "blocked_at" appears in the rendered statement).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/protocol_rpc/endpoints.py`:
- Around line 687-811: The cancel_transaction endpoint lacks a type hint for
msg_handler and its transaction_hash validation allows non-hex chars; update the
signature to type msg_handler as the IMessageHandler interface (or the concrete
handler class used elsewhere) and tighten the hash check in cancel_transaction
to verify the hex payload (e.g., ensure transaction_hash[2:] is 64 hex chars and
all characters are in [0-9a-fA-F]) before proceeding; keep the rest of the logic
(references: cancel_transaction, Transactions lookup,
TransactionsProcessor.cancel_transaction_if_available, and
msg_handler.send_transaction_status_update) unchanged so callers and tests pick
up the stronger validation and the declared handler type.

In `@tests/unit/consensus/test_previous_canceled_transaction.py`:
- Around line 12-74: Add explicit type hints to the two new async test functions
test_exec_transaction_continues_after_previous_canceled and
test_exec_transaction_waits_when_previous_in_processing: annotate the
consensus_algorithm parameter with its fixture/type (e.g., ConsensusAlgorithm or
appropriate test fixture type) and add the async function return type -> None;
ensure any other parameters (like transaction, transactions_processor) created
inside the test have local type hints where helpful. Update the function
signatures for both tests to include these parameter and return type annotations
so they conform to the project's typing guidelines.
- Around line 5-9: Remove the direct import of consensus_algorithm from the
module and register the fixture via pytest_plugins (add pytest_plugins =
["tests.unit.consensus.test_helpers"]) so pytest discovers the fixture without
redefinition; then add explicit type hints by importing typing.Callable and
typing.Any and annotate the consensus_algorithm fixture parameter as
Callable[..., Any] and all test functions with a return type of -> None (also
remove the now-unnecessary noqa on the consensus_algorithm import).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99c1373 and c621bec.

📒 Files selected for processing (7)
  • backend/consensus/base.py
  • backend/database_handler/transactions_processor.py
  • backend/protocol_rpc/endpoints.py
  • frontend/src/components/Simulator/TransactionItem.vue
  • frontend/src/stores/transactions.ts
  • frontend/test/unit/stores/transactions.test.ts
  • tests/unit/consensus/test_previous_canceled_transaction.py

Comment on lines +5 to +9
from backend.database_handler.models import TransactionStatus
from tests.unit.consensus.test_helpers import (
consensus_algorithm, # noqa: F401 - imported so pytest can discover fixture
init_dummy_transaction,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n tests/unit/consensus/test_previous_canceled_transaction.py | head -80

Repository: genlayerlabs/genlayer-studio

Length of output: 3182


🏁 Script executed:

find tests/common -type f -name "*.py" | head -20

Repository: genlayerlabs/genlayer-studio

Length of output: 174


🏁 Script executed:

rg "def consensus_algorithm" tests/ -A 5

Repository: genlayerlabs/genlayer-studio

Length of output: 521


🏁 Script executed:

rg "pytest_plugins" tests/unit/consensus/ -l

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

python -m ruff check tests/unit/consensus/test_previous_canceled_transaction.py --extend-select=F811

Repository: genlayerlabs/genlayer-studio

Length of output: 2589


🏁 Script executed:

cat -n tests/unit/consensus/test_helpers.py | head -50

Repository: genlayerlabs/genlayer-studio

Length of output: 2129


🏁 Script executed:

python -m ruff check tests/unit/consensus/test_previous_canceled_transaction.py --extend-select=ANN

Repository: genlayerlabs/genlayer-studio

Length of output: 5236


🏁 Script executed:

rg "async def test_" tests/unit/consensus/test_helpers.py -A 2

Repository: genlayerlabs/genlayer-studio

Length of output: 54


Fix F811 redefinition and add missing type hints.

Remove the direct consensus_algorithm import and register it via pytest_plugins to avoid the redefinition error. Add explicit type hints for both the test function return types (-> None) and the consensus_algorithm parameter.

🔧 Suggested changes
+from typing import TYPE_CHECKING
 from unittest.mock import AsyncMock, MagicMock, patch
 
 import pytest
 
 from backend.database_handler.models import TransactionStatus
 from tests.unit.consensus.test_helpers import (
-    consensus_algorithm,  # noqa: F401 - imported so pytest can discover fixture
     init_dummy_transaction,
 )
+
+if TYPE_CHECKING:
+    from tests.unit.consensus.test_helpers import ConsensusAlgorithm
+
+pytest_plugins = ["tests.unit.consensus.test_helpers"]

Then update test signatures:

-async def test_exec_transaction_continues_after_previous_canceled(consensus_algorithm):
+async def test_exec_transaction_continues_after_previous_canceled(consensus_algorithm: "ConsensusAlgorithm") -> None:
-async def test_exec_transaction_waits_when_previous_in_processing(consensus_algorithm):
+async def test_exec_transaction_waits_when_previous_in_processing(consensus_algorithm: "ConsensusAlgorithm") -> None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from backend.database_handler.models import TransactionStatus
from tests.unit.consensus.test_helpers import (
consensus_algorithm, # noqa: F401 - imported so pytest can discover fixture
init_dummy_transaction,
)
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from backend.database_handler.models import TransactionStatus
from tests.unit.consensus.test_helpers import (
init_dummy_transaction,
)
if TYPE_CHECKING:
from tests.unit.consensus.test_helpers import ConsensusAlgorithm
pytest_plugins = ["tests.unit.consensus.test_helpers"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/consensus/test_previous_canceled_transaction.py` around lines 5 -
9, Remove the direct import of consensus_algorithm from the module and register
the fixture via pytest_plugins (add pytest_plugins =
["tests.unit.consensus.test_helpers"]) so pytest discovers the fixture without
redefinition; then add explicit type hints by importing typing.Callable and
typing.Any and annotate the consensus_algorithm fixture parameter as
Callable[..., Any] and all test functions with a return type of -> None (also
remove the now-unnecessary noqa on the consensus_algorithm import).

Comment on lines +12 to +74
@pytest.mark.asyncio
async def test_exec_transaction_continues_after_previous_canceled(consensus_algorithm):
transaction = init_dummy_transaction()
transactions_processor = MagicMock()
transactions_processor.get_previous_transaction.return_value = {
"appealed": False,
"appeal_undetermined": False,
"appeal_leader_timeout": False,
"appeal_validators_timeout": False,
"status": TransactionStatus.CANCELED.value,
}

with patch(
"backend.consensus.base.PendingState.handle",
new_callable=AsyncMock,
return_value=None,
) as handle_mock:
await consensus_algorithm.exec_transaction(
transaction=transaction,
transactions_processor=transactions_processor,
chain_snapshot=MagicMock(),
accounts_manager=MagicMock(),
contract_snapshot_factory=lambda _address: MagicMock(),
contract_processor=MagicMock(),
node_factory=MagicMock(),
validators_snapshot=MagicMock(),
)

transactions_processor.get_previous_transaction.assert_called_once_with(
transaction.hash
)
handle_mock.assert_awaited_once()


@pytest.mark.asyncio
async def test_exec_transaction_waits_when_previous_in_processing(consensus_algorithm):
transaction = init_dummy_transaction()
transactions_processor = MagicMock()
transactions_processor.get_previous_transaction.return_value = {
"appealed": False,
"appeal_undetermined": False,
"appeal_leader_timeout": False,
"appeal_validators_timeout": False,
"status": TransactionStatus.PROPOSING.value,
}

with patch(
"backend.consensus.base.PendingState.handle",
new_callable=AsyncMock,
return_value=None,
) as handle_mock:
await consensus_algorithm.exec_transaction(
transaction=transaction,
transactions_processor=transactions_processor,
chain_snapshot=MagicMock(),
accounts_manager=MagicMock(),
contract_snapshot_factory=lambda _address: MagicMock(),
contract_processor=MagicMock(),
node_factory=MagicMock(),
validators_snapshot=MagicMock(),
)

handle_mock.assert_not_awaited()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n tests/unit/consensus/test_previous_canceled_transaction.py

Repository: genlayerlabs/genlayer-studio

Length of output: 3182


🏁 Script executed:

cat -n tests/unit/consensus/test_helpers.py

Repository: genlayerlabs/genlayer-studio

Length of output: 44734


🏁 Script executed:

cd tests/unit/consensus && python -c "from backend.consensus.base import ConsensusAlgorithm; print('Import successful')"

Repository: genlayerlabs/genlayer-studio

Length of output: 192


🏁 Script executed:

find . -name "base.py" -path "*/consensus/*" -type f | head -5

Repository: genlayerlabs/genlayer-studio

Length of output: 99


🏁 Script executed:

grep -n "^class ConsensusAlgorithm" backend/consensus/base.py

Repository: genlayerlabs/genlayer-studio

Length of output: 101


Add explicit type hints to the new async tests.
Both test functions are missing parameter and return type annotations. Per the coding guidelines, include type hints in all Python code.

🧩 Suggested change
+from backend.consensus.base import ConsensusAlgorithm
+
 `@pytest.mark.asyncio`
-async def test_exec_transaction_continues_after_previous_canceled(consensus_algorithm):
+async def test_exec_transaction_continues_after_previous_canceled(
+    consensus_algorithm: ConsensusAlgorithm,
+) -> None:
@@
 `@pytest.mark.asyncio`
-async def test_exec_transaction_waits_when_previous_in_processing(consensus_algorithm):
+async def test_exec_transaction_waits_when_previous_in_processing(
+    consensus_algorithm: ConsensusAlgorithm,
+) -> None:
🧰 Tools
🪛 Ruff (0.15.2)

[error] 13-13: Redefinition of unused consensus_algorithm from line 7: consensus_algorithm redefined here

(F811)


[error] 47-47: Redefinition of unused consensus_algorithm from line 7: consensus_algorithm redefined here

(F811)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/consensus/test_previous_canceled_transaction.py` around lines 12 -
74, Add explicit type hints to the two new async test functions
test_exec_transaction_continues_after_previous_canceled and
test_exec_transaction_waits_when_previous_in_processing: annotate the
consensus_algorithm parameter with its fixture/type (e.g., ConsensusAlgorithm or
appropriate test fixture type) and add the async function return type -> None;
ensure any other parameters (like transaction, transactions_processor) created
inside the test have local type hints where helpful. Update the function
signatures for both tests to include these parameter and return type annotations
so they conform to the project's typing guidelines.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
backend/protocol_rpc/endpoints.py (2)

691-691: msg_handler still missing its type annotation.

IMessageHandler is already imported at the top of the file (line 24) and used as the annotated type on every other endpoint that takes a message handler. Leaving msg_handler untyped here is inconsistent.

 def cancel_transaction(
     session: Session,
     transaction_hash: str,
-    msg_handler,
+    msg_handler: IMessageHandler,
     signature: str | None = None,
     admin_key: str | None = None,
 ) -> dict:

As per coding guidelines: "Include type hints in all Python code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` at line 691, The parameter msg_handler is
missing its type annotation; update the function signature where msg_handler is
declared to annotate it as IMessageHandler (import already present) so it
matches other endpoints that accept a message handler—ensure any references to
msg_handler within that function remain unchanged and the annotation uses the
existing IMessageHandler symbol.

719-728: Hash validation still missing hex-character check.

The existing update_transaction_status endpoint (line 1806) validates non-hex characters with int(transaction_hash, 16). This endpoint doesn't, so inputs like "0x" + "ZZ" * 32 would pass validation and produce a confusing DB miss rather than a clear 400 error.

🛡️ Proposed fix
     if (
         not transaction_hash
         or not transaction_hash.startswith("0x")
         or len(transaction_hash) != 66
     ):
         raise JSONRPCError(
             code=-32602,
             message="Invalid transaction hash format",
             data={},
         )
+    try:
+        int(transaction_hash, 16)
+    except ValueError as e:
+        raise JSONRPCError(
+            code=-32602,
+            message="Invalid transaction hash format: contains non-hexadecimal characters",
+            data={},
+        ) from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` around lines 719 - 728, The current
validation allows non-hex chars (e.g. "0xZZ...") because it only checks
prefix/length; add a hex-character check by attempting to parse the hex payload
with int(transaction_hash[2:], 16) inside a try/except and raise the same
JSONRPCError on ValueError; specifically update the block that inspects
transaction_hash (use the transaction_hash variable and JSONRPCError) to catch
non-hex input and return the "Invalid transaction hash format" error instead of
letting it fall through to a DB miss (mirror the approach used in
update_transaction_status).
🧹 Nitpick comments (2)
backend/protocol_rpc/endpoints.py (2)

806-811: Use TransactionStatus.CANCELED.value instead of the hardcoded string.

Hardcoding "CANCELED" in both the WebSocket call and the return dict creates a silent divergence risk if the enum value is ever renamed.

♻️ Proposed fix
+    canceled_status = TransactionStatus.CANCELED.value
     # Notify frontend via WebSocket
-    msg_handler.send_transaction_status_update(transaction_hash, "CANCELED")
+    msg_handler.send_transaction_status_update(transaction_hash, canceled_status)

     return {
         "transaction_hash": transaction_hash,
-        "status": "CANCELED",
+        "status": canceled_status,
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` around lines 806 - 811, Replace the
hardcoded "CANCELED" string with the enum value to avoid divergence: call
msg_handler.send_transaction_status_update(transaction_hash,
TransactionStatus.CANCELED.value) and return {"transaction_hash":
transaction_hash, "status": TransactionStatus.CANCELED.value}; update references
in this endpoint where send_transaction_status_update and the response dict are
set so both use TransactionStatus.CANCELED.value consistently.

712-716: Remove redundant import os inside function body.

os is already imported at module level (line 53). The in-function import os is a no-op but misleads readers into thinking os is unavailable in the outer scope.

♻️ Proposed fix
     from backend.database_handler.models import Transactions
     from eth_account.messages import encode_defunct
     from eth_account import Account
     from web3 import Web3
-    import os
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` around lines 712 - 716, Remove the
redundant "import os" that appears inside the function body in
backend/protocol_rpc/endpoints.py (the local import near the
Transactions/encode_defunct/Account/Web3 imports); rely on the module-level os
already imported at top of the file and delete the in-function import to avoid
confusion and potential shadowing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/protocol_rpc/endpoints.py`:
- Around line 795-803: The error payload uses a stale transaction.status value
because status was read before calling cancel_transaction_if_available; after
detecting was_cancelled is False you should re-query the latest transaction
state and use that current status in the JSONRPCError data. Locate the cancel
flow around cancel_transaction_if_available and replace the pre-read
transaction.status value with a fresh fetch (using the same transaction lookup
used earlier keyed by transaction_hash) so the JSONRPCError includes the actual
terminal status in its data field (transaction_hash and the re-fetched
transaction.status.value).
- Around line 687-811: Add a type annotation for the msg_handler parameter
(e.g., msg_handler: MessageHandler) and import that MessageHandler type where
the module-level imports live; validate the transaction_hash hex payload by
wrapping int(transaction_hash[2:], 16) in a try/except and raise JSONRPCError on
ValueError to reject non-hex characters; remove the local "import os" and rely
on the module-level os import; replace hardcoded "CANCELED" string usages with
TransactionStatus.CANCELED.value (import TransactionStatus from
backend.database_handler.models or the appropriate enum) and when constructing
the error response on failed cancel, refresh or re-query the Transactions row
(or fetch its status after
TransactionsProcessor.cancel_transaction_if_available) before reading
transaction.status.value so the returned status is current.

---

Duplicate comments:
In `@backend/protocol_rpc/endpoints.py`:
- Line 691: The parameter msg_handler is missing its type annotation; update the
function signature where msg_handler is declared to annotate it as
IMessageHandler (import already present) so it matches other endpoints that
accept a message handler—ensure any references to msg_handler within that
function remain unchanged and the annotation uses the existing IMessageHandler
symbol.
- Around line 719-728: The current validation allows non-hex chars (e.g.
"0xZZ...") because it only checks prefix/length; add a hex-character check by
attempting to parse the hex payload with int(transaction_hash[2:], 16) inside a
try/except and raise the same JSONRPCError on ValueError; specifically update
the block that inspects transaction_hash (use the transaction_hash variable and
JSONRPCError) to catch non-hex input and return the "Invalid transaction hash
format" error instead of letting it fall through to a DB miss (mirror the
approach used in update_transaction_status).

---

Nitpick comments:
In `@backend/protocol_rpc/endpoints.py`:
- Around line 806-811: Replace the hardcoded "CANCELED" string with the enum
value to avoid divergence: call
msg_handler.send_transaction_status_update(transaction_hash,
TransactionStatus.CANCELED.value) and return {"transaction_hash":
transaction_hash, "status": TransactionStatus.CANCELED.value}; update references
in this endpoint where send_transaction_status_update and the response dict are
set so both use TransactionStatus.CANCELED.value consistently.
- Around line 712-716: Remove the redundant "import os" that appears inside the
function body in backend/protocol_rpc/endpoints.py (the local import near the
Transactions/encode_defunct/Account/Web3 imports); rely on the module-level os
already imported at top of the file and delete the in-function import to avoid
confusion and potential shadowing.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c621bec and ff66278.

📒 Files selected for processing (1)
  • backend/protocol_rpc/endpoints.py

Comment on lines +687 to +811
####### CANCEL TRANSACTION ENDPOINTS #######
def cancel_transaction(
session: Session,
transaction_hash: str,
msg_handler,
signature: str | None = None,
admin_key: str | None = None,
) -> dict:
"""
Cancel a pending or activated transaction. Returns immediately with status.

Access control:
- Local (no env vars): open access
- Hosted/Self-hosted: admin_key allows ANY transaction, signature allows own transactions

Args:
session: Database session
transaction_hash: Hash of the transaction to cancel
msg_handler: Message handler for WebSocket notifications
signature: Hex-encoded signature from tx sender (required in hosted mode unless admin_key)
admin_key: Admin API key for full access to any transaction

Returns:
dict with transaction_hash and status
"""
from backend.database_handler.models import Transactions
from eth_account.messages import encode_defunct
from eth_account import Account
from web3 import Web3
import os

# Validate transaction hash format
if (
not transaction_hash
or not transaction_hash.startswith("0x")
or len(transaction_hash) != 66
):
raise JSONRPCError(
code=-32602,
message="Invalid transaction hash format",
data={},
)

# Look up the transaction
transaction = (
session.query(Transactions).filter_by(hash=transaction_hash).one_or_none()
)
if not transaction:
raise NotFoundError(
message="Transaction not found",
data={"transaction_hash": transaction_hash},
)

is_hosted = os.getenv("VITE_IS_HOSTED") == "true"
admin_api_key = os.getenv("ADMIN_API_KEY")

# Check if authorization is needed (hosted or self-hosted with key configured)
needs_auth = is_hosted or admin_api_key

if needs_auth:
# Option 1: Admin key grants full access to ANY transaction
if admin_api_key and admin_key == admin_api_key:
pass # Authorized - proceed with cancel

# Option 2: Signature from tx sender grants access to own transactions
elif signature:
if not transaction.from_address:
raise JSONRPCError(
code=-32000,
message="Transaction has no sender - only admin key can cancel",
data={},
)

try:
# Message: keccak256("cancel_transaction" + tx_hash_bytes)
# tx_hash is unique, so no nonce needed for replay protection
message_hash = Web3.keccak(
b"cancel_transaction" + Web3.to_bytes(hexstr=transaction_hash)
)
message = encode_defunct(primitive=message_hash)
signer = Account.recover_message(message, signature=signature)

if signer.lower() != transaction.from_address.lower():
raise JSONRPCError(
code=-32000,
message="Only transaction sender can cancel",
data={"signer": signer, "sender": transaction.from_address},
)
except JSONRPCError:
raise
except Exception as e:
raise JSONRPCError(
code=-32000,
message=f"Invalid signature: {e!s}",
data={},
) from e
else:
raise JSONRPCError(
code=-32000,
message="Cancel requires admin key or sender signature",
data={},
)

# Atomic cancel - only succeeds if tx is still pending/activated and not claimed by worker
was_cancelled = TransactionsProcessor.cancel_transaction_if_available(
session, transaction_hash
)

if not was_cancelled:
raise JSONRPCError(
code=-32000,
message="Transaction cannot be cancelled: already being processed or in a terminal state",
data={
"transaction_hash": transaction_hash,
"status": transaction.status.value,
},
)

# Notify frontend via WebSocket
msg_handler.send_transaction_status_update(transaction_hash, "CANCELED")

return {
"transaction_hash": transaction_hash,
"status": "CANCELED",
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the number of `cancel_transaction` function definitions in the endpoints file
echo "=== Checking for duplicate cancel_transaction definitions ==="
rg -n "^def cancel_transaction" backend/protocol_rpc/endpoints.py

echo ""
echo "=== Line count and context around lines 687-811 ==="
wc -l backend/protocol_rpc/endpoints.py

Repository: genlayerlabs/genlayer-studio

Length of output: 253


🏁 Script executed:

#!/bin/bash
# Get the full cancel_transaction function and surrounding context
sed -n '685,815p' backend/protocol_rpc/endpoints.py | cat -n

Repository: genlayerlabs/genlayer-studio

Length of output: 5495


🏁 Script executed:

#!/bin/bash
# Check module-level imports of os in the file
head -80 backend/protocol_rpc/endpoints.py | grep -n "^import os\|^from os"

Repository: genlayerlabs/genlayer-studio

Length of output: 84


Add type hint to msg_handler parameter and validate hex characters in transaction hash.

Line 691: msg_handler is missing a type hint. This violates the coding guideline requiring type hints on all Python code. Provide the correct type annotation.

Lines 719–728: Transaction hash validation checks only the format (0x prefix, 66 characters), but does not validate that the remaining 64 characters are valid hexadecimal. Add validation using int(transaction_hash, 16) inside a try-except to reject malformed hashes early.

Additional minor improvements:

  • Line 716: import os is redundant; os is already imported at line 53. Remove the local import.
  • Lines 806, 810: Replace hardcoded "CANCELED" strings with TransactionStatus.CANCELED.value for consistency and maintainability.
  • Line 801: The transaction.status.value in the error response is fetched before the cancel attempt and may be stale if the transaction state changes concurrently. Consider deferring or refreshing.
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 770-774: Abstract raise to an inner function

(TRY301)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` around lines 687 - 811, Add a type
annotation for the msg_handler parameter (e.g., msg_handler: MessageHandler) and
import that MessageHandler type where the module-level imports live; validate
the transaction_hash hex payload by wrapping int(transaction_hash[2:], 16) in a
try/except and raise JSONRPCError on ValueError to reject non-hex characters;
remove the local "import os" and rely on the module-level os import; replace
hardcoded "CANCELED" string usages with TransactionStatus.CANCELED.value (import
TransactionStatus from backend.database_handler.models or the appropriate enum)
and when constructing the error response on failed cancel, refresh or re-query
the Transactions row (or fetch its status after
TransactionsProcessor.cancel_transaction_if_available) before reading
transaction.status.value so the returned status is current.

Comment on lines +795 to +803
if not was_cancelled:
raise JSONRPCError(
code=-32000,
message="Transaction cannot be cancelled: already being processed or in a terminal state",
data={
"transaction_hash": transaction_hash,
"status": transaction.status.value,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error data may show a stale status after a failed cancel.

transaction.status is read before cancel_transaction_if_available executes (line 732). If a consensus worker claimed and advanced the transaction in the interim, the error data will display the pre-race status (e.g., still PENDING) rather than the actual terminal state that caused the rejection, which can confuse callers.

🛠️ Proposed fix — re-fetch status on failure
     if not was_cancelled:
+        # Re-query to get the current (post-race) status for an accurate error payload
+        session.refresh(transaction)
         raise JSONRPCError(
             code=-32000,
             message="Transaction cannot be cancelled: already being processed or in a terminal state",
             data={
                 "transaction_hash": transaction_hash,
                 "status": transaction.status.value,
             },
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/protocol_rpc/endpoints.py` around lines 795 - 803, The error payload
uses a stale transaction.status value because status was read before calling
cancel_transaction_if_available; after detecting was_cancelled is False you
should re-query the latest transaction state and use that current status in the
JSONRPCError data. Locate the cancel flow around cancel_transaction_if_available
and replace the pre-read transaction.status value with a fresh fetch (using the
same transaction lookup used earlier keyed by transaction_hash) so the
JSONRPCError includes the actual terminal status in its data field
(transaction_hash and the re-fetched transaction.status.value).

@cristiam86 cristiam86 merged commit 9f3fe15 into main Feb 24, 2026
26 checks passed
@github-actions
Copy link
Contributor

🎉 This PR is included in version 0.103.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant