Skip to main content

RelayerProvider

The EIP-1193 provider a tz1-backed consumer exposes to EVM code. One class, two responsibilities: route standard request() calls (signing them Michelson-side through the wallet client), and manage the synthetic→real hash lifecycle of cross-runtime transactions.

Construction​

import { RelayerProvider, BeaconClient } from '@tezosx/relayer/tezos';
import type { PendingOpsStore } from '@tezosx/relayer/tezos';

// Temple/Beacon-backed, in-memory only — fine for a page that never reloads
// mid-transaction, and for tests.
const provider = new RelayerProvider(new BeaconClient());

// Any ITezosWalletClient + persistence — what the Tezos X Wallet does.
const persistent = new RelayerProvider(walletClient, pendingOpsStore);
  • walletClient — any ITezosWalletClient. The Tezos X Wallet passes its own Taquito-backed signer.
  • pendingOpsStore (optional, since 0.7.0) — persistence for the cross-runtime resolution state; see the contract.

On construction the provider also restores an existing session best-effort (getActiveAccount → alias → chain id) and emits accountsChanged + connect with no user interaction; offline, the session silently stays unset and re-establishes on the next request.

request() — the method surface​

provider.request({ method: string, params?: unknown[] }): Promise<unknown>

Session​

MethodReturnsNotes
eth_requestAccounts['0xAlias']Prompts the backing wallet (Temple via Beacon; the Tezos X Wallet runs its own approval UI). Re-running on an existing session returns it without re-prompting.
eth_accounts['0xAlias'] or []Never prompts.
tez_getAccounts['tz1…'] or []Non-standard, Tezos X-specific — the tz1 behind the alias. Other wallets answer -32601; tolerate that.
wallet_revokePermissions / wallet_disconnectnullEnds the session, clears pending-op state (including the persisted store), emits accountsChanged [] then disconnect.

Chain and reads​

MethodReturnsNotes
eth_chainId'0x1f440'Cached on the session.
net_version'128064'Decimal form.
eth_getBalance, eth_getTransactionCount, eth_callproxiedStandard shapes; malformed params → -32602.

Transactions​

MethodBehavior
eth_sendTransactionValidates with buildTezosToEvmCall before any signing prompt (-32602 on violation), signs the Michelson operation through the wallet client, returns a synthetic hash immediately. Reads only to, value, data — any gas field is ignored (why). No session → 4100 (Call eth_requestAccounts first).
eth_getTransactionByHashOn a tracked synthetic hash: a pending-shaped transaction object (blockNumber: null) until resolved — never null, so ethers/viem pollers keep polling. Then proxies with the real hash.
eth_getTransactionReceiptnull until the real transaction is found (the standard not-yet-mined answer), then the real receipt — real logs, gasUsed, blockNumber.

Fee model (short-circuited constants)​

Fees for tz1-routed transactions are paid in mutez on the Michelson runtime — there is no EVM gas market to sample, so the provider answers fee methods with fixed, well-formed constants that keep client libraries' fee math well-defined (rationale):

MethodReturns
eth_estimateGas0x1e8480 (2 000 000 — headroom, not consumption)
eth_gasPrice, eth_maxPriorityFeePerGas0x0
eth_feeHistorya minimal all-zero envelope

Everything else — the proxy​

Any other method is forwarded transparently to the EVM node (eth_blockNumber, eth_getBlockByNumber, eth_getLogs, eth_getCode, eth_sendRawTransaction, …). This is what keeps ethers.js tx.wait(), viem and wagmi functional. The passthrough is deadline-exempt (it may carry writes — aborting after a broadcast is worse than waiting).

Not supported​

Rejected with EIP-1193 4200, deliberately before any prompt — the 0x alias is not backed by a secp256k1 key, so recovery-based signature verification can never match (full rationale):

eth_sign · personal_sign · eth_signTypedData / _v3 / _v4

Wallet-host methods​

Beyond request(), three methods serve wallet UIs:

MethodReturnsPurpose
resolveSyntheticHash(hash)Promise<string | null>Awaits the kernel-synthesized real EVM hash. One call = up to 15 scan attempts, 2 s apart (≥ ~30 s wall clock in total; each attempt rescans from the send-time snapshot block to head). null on exhaustion — call again to keep trying.
getPendingL1Hash(hash)string | nullThe underlying Michelson operation hash (o…) — the TzKT fallback link when resolution exhausts.
listPendingOps()readonly PendingOpView[]The broadcast cross-runtime operations whose real hash is still unresolved — feeds a wallet's Activity view.
const real = await provider.resolveSyntheticHash(syntheticHash);
if (real == null) {
const opHash = provider.getPendingL1Hash(syntheticHash); // link the user to TzKT
}

The PendingOpsStore contract​

import type { PendingOpsStore, PendingOpsSnapshot } from '@tezosx/relayer/tezos';

interface PendingOpsSnapshot {
ops: Record<string, PendingOp>; // keyed by synthetic hash
claimed: string[]; // real hashes already claimed (dedup)
}

interface PendingOpsStore {
load(): Promise<PendingOpsSnapshot | undefined>;
save(snapshot: PendingOpsSnapshot): Promise<void>;
clear(): Promise<void>;
}

load() runs once at construction (rehydration), save() after every mutation (best-effort), clear() on disconnect. The data is non-secret; scope one store per account. Production implementations in the repo: packages/wallet/src/adapters/chrome/chrome-pending-ops-store.ts (chrome.storage) and packages/mobile/src/adapters/mmkv-pending-ops-store.ts (MMKV) — both ~25 lines.

Without a store, resolution state lives only in memory: a reload, account switch or service-worker eviction mid-resolution leaves the synthetic hash permanently unresolvable.

Events​

Three events are actually emitted:

EventPayloadWhen
accountsChangedstring[]Session established (connect or the constructor's silent restore), account switched in the backing wallet, or session ended ([])
connect{ chainId }Session established
disconnectProviderRpcError (code 4100)Backing wallet disconnected, or wallet_revokePermissions

chainChanged exists on the EIP1193Provider type for completeness but is never emitted — the provider is pinned to one chain.

Timeouts and transport errors​

Every read against the EVM node runs under a 15-second deadline (RPC_TIMEOUT_MS, AbortController-enforced). Three failure shapes:

  • Timeout — a plain Error, message Request timed out after 15000ms calling <method>, no EIP-1193 code. Deliberate: a timeout is not a transport loss; route it to retry logic, not disconnect handling.
  • Transport loss — failed fetch or non-2xx → ProviderRpcError code 4900.
  • Node error — rethrown with the node's own code/message/data (the only path where err.data is populated).

The unknown-method proxy is deadline-exempt (see above). The JSON-RPC helper implementing this is internal — not part of the exports map.

Error codes​

CodeMeaningRaised when
4001User rejectedBeacon prompt dismissed; Tezos X Wallet approval rejected
4100UnauthorizedNo session (Call eth_requestAccounts first); wallet locked; also the disconnect event payload
4200Unsupported methodThe five signature methods — rationale
4900DisconnectedTransport loss (failed fetch / non-2xx)
-32601Method not foundUnknown to the node too (via the proxy); also trackCrossRuntimeStatus on a wrong direction
-32602Invalid paramsMalformed shapes + the three typed builder errors
-32603InternalNon-abort wallet-backend failure (e.g. a Beacon failure)
-32005Limit exceededTezos X Wallet's per-origin approval cap (wallet-side, not the SDK)
(no code)TimeoutSee above