ZENANET DocsBeta
RPC & Contracts

Query Guide

How the JSON-RPC and REST APIs fit together, browser and library integration, and troubleshooting

This guide covers what the reference pages do not: how the two API families fit together, how to call them from browsers and libraries, and what to do when a call fails. Every method and endpoint, with parameters and request/response examples, is documented in the JSON-RPC Reference and the REST API Reference.

TaskWhere to look
Transaction, receipt, logs by 0x hashJSON-RPC Reference → Transactions
Transfer history of one tokenJSON-RPC Reference → Token Transfers and Logs (eth_getLogs)
All transactions of one addressREST API Reference → Transactions (/cosmos/tx/v1beta1/txs?query=)
Balances, blocks, feesJSON-RPC Reference → Accounts and State, Blocks, Chain and Fees
Cosmos tx by HEX hash, token pairs, multi-denom balancesREST API Reference
Real-time subscriptionsJSON-RPC Reference → Real-time and Filters

Explorer-style routes such as /transactions, /token-transfers, or /search belong to the Blockscout API v2, not to the node. They only exist once a Blockscout instance is indexing the node.


Address Conversion (hex ↔ bech32)

The same key is shown as 0x… on the EVM side and zenanet1… on the Cosmos side. The underlying 20 bytes are identical.

zenad debug addr 0x<address>          # → bech32
zenad debug addr zenanet1...          # → hex

Inside a contract, call hexToBech32 / bech32ToHex on the bech32 precompile at 0x0000000000000000000000000000000000000400.


Calling from a Web Frontend

Request and response formats are identical to the curl examples. What changes in a browser is CORS, origin checks on WebSocket, and whether you go through a wallet.

CORS behaviour per server

ServerDirect browser callsNode setting
JSON-RPC HTTP :8545allowed by defaultPOST with Content-Type: application/json from any origin. Custom headers need --api.enabled-unsafe-cors
JSON-RPC WS :8546origin whitelist[json-rpc] ws-origins defaults to ["127.0.0.1", "localhost"]. Add your frontend host, or "*" for all
Cosmos REST :1317blocked by default[api] enabled-unsafe-cors = true
CometBFT RPC :26657blocked by defaultconfig.toml [rpc] cors_allowed_origins = ["*"]

On the public mainnet hosts, an nginx reverse proxy terminates TLS and handles CORS, so https://evm.zena-ai.io and https://api.zena-ai.io are callable from browsers as-is. If you run your own proxy:

  • Do not duplicate CORS headers. The JSON-RPC server already sets Access-Control-Allow-Origin; adding it again in nginx makes browsers reject the response. The REST server sets none, so nginx should add it there.
  • Handle OPTIONS preflight. JSON POST triggers a preflight. The JSON-RPC server answers it, but if you POST to REST (for example /cosmos/tx/v1beta1/simulate), nginx must return 204 for OPTIONS. GET needs no preflight.
  • WebSocket path /ws. Set proxy_set_header Upgrade $http_upgrade and proxy_set_header Connection "upgrade". The browser's Origin header reaches the node, so the frontend hostname must be in ws-origins.
  • No mixed content. An HTTPS page cannot call an http:// RPC. Expose the node over HTTPS through the proxy.

fetch

async function rpc<T>(method: string, params: unknown[] = []): Promise<T> {
  const res = await fetch("https://evm.zena-ai.io", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const json = await res.json();
  if (json.error) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json.result;
}

const tx      = await rpc("eth_getTransactionByHash", ["0x<txhash>"]);
const receipt = await rpc("eth_getTransactionReceipt", ["0x<txhash>"]);
const logs    = await rpc("eth_getLogs", [{
  address: "0x<token_contract>",
  fromBlock: "0x1000", toBlock: "0x3000",
  topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
}]);

// Cosmos REST is a plain GET
const balances = await fetch("https://api.zena-ai.io/cosmos/bank/v1beta1/balances/zenanet1...")
  .then(r => r.json());

Through a wallet provider (MetaMask)

Requests made through window.ethereum go out via the wallet's configured RPC URL, so CORS never applies. Read-only queries work through this path too.

import { BrowserProvider } from "ethers";

const provider = new BrowserProvider(window.ethereum);
await provider.send("wallet_switchEthereumChain", [{ chainId: "0x3b2e" }]); // 15150

const receipt = await provider.getTransactionReceipt("0x<txhash>");
const logs    = await provider.getLogs({ address: "0x<token_contract>", fromBlock: 0x1000, toBlock: 0x3000 });

Network values for the wallet are listed in Add to MetaMask.

WebSocket from the browser

const ws = new WebSocket("wss://evm.zena-ai.io/ws");
ws.onopen = () => ws.send(JSON.stringify({
  jsonrpc: "2.0", id: 1, method: "eth_subscribe",
  params: ["logs", { address: "0x<token_contract>",
    topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] }],
}));
ws.onmessage = (e) => console.log(JSON.parse(e.data));

Browsers attach an Origin header to the upgrade request and the node checks it against ws-origins. If the host is missing, the connection is closed silently. curl and server-side clients send no Origin, so they pass.


Library Examples

ethers.js v6

import { JsonRpcProvider, Contract, id, zeroPadValue } from "ethers";

const provider = new JsonRpcProvider("https://evm.zena-ai.io", 15150);

// tx / receipt by hash
const tx = await provider.getTransaction("0x<txhash>");
const receipt = await provider.getTransactionReceipt("0x<txhash>");

// Transfer logs of one token, received by one wallet
const TRANSFER = id("Transfer(address,address,uint256)");
const logs = await provider.getLogs({
  address: "0x<token_contract>",
  fromBlock: 0x1000,
  toBlock: 0x3000,             // within 10,000 blocks
  topics: [TRANSFER, null, zeroPadValue("0x<wallet>", 32)],
});

// ERC-20 balance
const erc20 = new Contract("0x<token_contract>",
  ["function balanceOf(address) view returns (uint256)"], provider);
const bal = await erc20.balanceOf("0x<wallet>");

Foundry cast

export ETH_RPC_URL=https://evm.zena-ai.io
cast tx 0x<txhash>
cast receipt 0x<txhash>
cast logs --from-block 4096 --to-block 12288 \
  --address 0x<token_contract> \
  'Transfer(address indexed from, address indexed to, uint256 value)'
cast call 0x<token_contract> 'balanceOf(address)(uint256)' 0x<wallet>

Troubleshooting

SymptomCause / fix
eth_getLogs returns block range exceeds capRange over 10,000 blocks. Split the call
eth_getTransactionByHash is slowSet enable-indexer = true on the node and restart
Cosmos REST :1317 does not respondCheck [api] enable = true in app.toml
txpool_* / debug_* return Method not foundAdd the namespace to json-rpc.api
A MsgSend transfer is missing from eth_getLogsNative transfers emit no logs. Use the bank event search
/cosmos/tx/v1beta1/txs?events=… returns an errorSDK v0.53 expects query=. Paginate with limit and page
ethereum_tx.recipient search returns nothingThe value is case-sensitive. Use the EIP-55 checksum form of the address
eth_call fails on a heavy view functionGas cap 25,000,000 and 5 s timeout per call by default
Browser call to REST :1317 fails with CORS[api] enabled-unsafe-cors = true or handle CORS in the reverse proxy
Browser WebSocket disconnects immediatelyAdd the frontend hostname to ws-origins
RPC call fails from an HTTPS pageMixed content. Expose RPC over HTTPS via the proxy
Balance looks wrongEVM uses 18 decimals (aznnt). Divide by 10^18 for znnt

On this page