RPC: transaction lifecycle
This page is the canonical “how a tx moves through Catalyst” reference:
- fetch domain
- build + sign
- submit
- poll receipt until final
Canonical implementation: catalyst-node-rust/crates/catalyst-rpc/src/lib.rs (methods catalyst_getTxDomain, catalyst_sendRawTransaction, catalyst_getTransactionReceipt).
You will need
- An HTTP JSON-RPC URL (example:
https://testnet-eu-rpc.catalystnet.org) curlandjq
Steps
1) Fetch signing domain (required)
Always fetch the signing domain in one call:
RPC_URL="https://testnet-eu-rpc.catalystnet.org"
curl -sS -X POST "$RPC_URL" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"catalyst_getTxDomain","params":[]}' | jq
Expected response shape:
{
"result": {
"chain_id": "0x...",
"network_id": "catalyst-testnet",
"genesis_hash": "0x...",
"protocol_version": 0,
"tx_wire_version": 2
}
}
Why this matters:
- Tooling should not fetch
chainIdandgenesisHashvia separate calls. With load balancers, you can hit different backends and get mismatched identity, causing signature verification failures.
2) Build + sign a transaction (CTX2)
Catalyst uses CTX2 transactions submitted via:
catalyst_sendRawTransaction
This is not Ethereum eth_sendRawTransaction. The easiest way to build/sign is to use the SDK tooling.
Recommended paths:
- SDK library:
@catalyst/sdk - CLI deploy/call:
catalyst-sdkrepo (node packages/cli/dist/index.js ...) - Node CLI:
catalyst-cli(for operators)
3) Submit (catalyst_sendRawTransaction)
Submit a signed raw tx hex (string).
curl -sS -X POST "$RPC_URL" \
-H 'content-type: application/json' \
-d '{
"jsonrpc":"2.0",
"id":1,
"method":"catalyst_sendRawTransaction",
"params":["0x<signed_ctx2_bytes_hex>"]
}' | jq
Expected response:
{ "result": "0x<tx_id_32_bytes_hex>" }
Notes (implementation details that affect clients):
- The server rejects overly large tx payloads and validates a fee floor before accepting.
- The server verifies the tx signature using the domain (chain id + genesis hash).
- The server rejects nonce too low at the RPC boundary (replay protection).
4) Poll receipt (catalyst_getTransactionReceipt)
TX_ID="0x<tx_id>"
curl -sS -X POST "$RPC_URL" \
-H 'content-type: application/json' \
-d "{
\"jsonrpc\":\"2.0\",
\"id\":1,
\"method\":\"catalyst_getTransactionReceipt\",
\"params\":[\"$TX_ID\"]
}" | jq
Receipt fields (current shape, subject to additive changes):
status: one ofpending,selected,applied,droppedselected_cycle: cycle number once the tx is selected for inclusionapplied_cycle: cycle number once appliedapplied_state_root: authenticated state root after apply (when available)success/error: EVM/apply outcome (when available)
Verify
A tx is “final” for most clients when status == "applied" or status == "dropped".
For contract deployments, verify code exists:
CONTRACT="0x<address20>"
curl -sS -X POST "$RPC_URL" \
-H 'content-type: application/json' \
-d "{
\"jsonrpc\":\"2.0\",
\"id\":1,
\"method\":\"catalyst_getCode\",
\"params\":[\"$CONTRACT\"]
}" | jq -r '.result'
Expected:
0x...(non-empty) for deployed code0xfor “no code”
Troubleshooting
-32029 rate limited
Some endpoints will return JSON-RPC error code -32029 when rate limited.
Mitigation:
- add client-side backoff + jitter
- spread calls across time (don’t poll receipts at 50ms; use ~1–2s and increase)
- for indexers, prefer batch/range calls (see
catalyst_getBlocksByNumberRange)
“Invalid transaction signature”
Most common causes:
- tooling used the wrong signing domain (wrong network / mismatched
genesis_hash) - chain identity skew behind an LB (use
catalyst_getTxDomainsingle-call domain)
“Nonce too low”
You submitted a tx with nonce <= committed_nonce for that sender.
Mitigation:
- fetch nonce via
catalyst_getNonce(pubkey32)and increment correctly - ensure you don’t re-send already-applied txs after restart (track tx ids + nonces)