Skip to main content

View Functions and Signed Queries

eth_call on a public EVM is trivial: anyone can call any view function, and the result is a public fact. On OXN, view functions can operate on encrypted state — which raises a question: who is authorized to see the result?

OXN answers this with three call modes for reads. This page explains all three, when to use each, and how the SDK helps.

Three modes of reading

ModeAuthenticationResult confidentialityTypical use case
Plain eth_callNonePublic (any caller sees the same result)Anyone can read this without restriction.
Encrypted call (unauth)NoneEncrypted to the calling session onlyConfidential public data (rare — usually you want authentication too).
Signed queryCaller signatureEncrypted to the caller's session keyReading data intended only for the caller.

Plain eth_call

Bare ethers.js sends eth_call requests in plain. The contract executes the view function, and the return value is passed back to the caller unencrypted.

Because there is no signature and no encryption:

  • msg.sender inside the contract is the zero address (0x0).
  • The result travels in the clear to whoever asked — the client, the RPC provider, network observers, everyone.

Use plain eth_call only for functions that intentionally return public data.

Encrypted view call

A wrapped provider automatically encrypts eth_call requests and receives an encrypted response. This protects the result from RPC providers and network observers, but does not authenticate the caller:

  • msg.sender inside the contract is still the zero address.
  • The result is only decryptable by the client that made the call (specifically: the session key).

Use encrypted view calls when the result is sensitive but the caller does not need to prove identity. This is a narrow use case in practice.

Signed queries

A signed query is eth_call with a caller signature attached. This is what you use to read balanceOf(msg.sender) in a confidential ERC-20, or myBid() in a sealed-bid auction.

The mechanism:

  1. Client crafts the eth_call payload as usual.
  2. Client signs the payload with the private key of the account whose "view" they want.
  3. Client sends the payload plus signature to the enclave, encrypted.
  4. Enclave verifies the signature.
  5. Enclave sets msg.sender to the signer's address inside the view function.
  6. Enclave executes the view function.
  7. Enclave encrypts the result back to the client's session key.

Now the contract can use msg.sender inside a view function to enforce "you can only read your own balance":

mapping(address => uint256) private balances;

function myBalance() external view returns (uint256) {
return balances[msg.sender]; // works because signed query populates msg.sender
}

The SDK handles the signing:

const provider = wrapEthersProvider(raw);
const signer = wrapEthersSigner(new ethers.Wallet(PRIVATE_KEY, raw));
const contract = new ethers.Contract(addr, abi, signer); // signer, not provider

const balance = await contract.myBalance(); // signed query under the hood

Note the receiver of contract is the signer, not the provider. That is how the SDK knows to sign the query with the caller's key.

Gas semantics

Signed queries do not consume gas from the caller. They are eth_call requests — off-chain, non-state-changing. The signature is used purely to populate msg.sender. If your view function is expensive, the RPC provider may still throttle or bill you, but there is no on-chain gas.

Common patterns

"Read my own private state." The most common pattern. Contract exposes a view function like function myBalance() view returns (uint256) that reads balances[msg.sender]. Caller signs the query. Nobody else can read their balance.

"Read someone else's state, if they gave you permission." Contract maintains a mapping(address => mapping(address => bool)) approvals. The view function checks that approvals[owner][msg.sender] is true before returning balances[owner]. Requires signed query.

"Read admin-only data." Contract has an owner. View function is require(msg.sender == owner). Only the owner's signed query returns anything useful.

Pitfalls

"My view function returns 0 for everyone." You probably sent a plain eth_call. msg.sender was the zero address, and the address-keyed mapping returned its default value. Use a wrapped signer (not a wrapped provider) as the contract's runner.

"The signature failed." The client's chain ID or the enclave's expected verification parameters might be misconfigured. Check that your provider is connected to Chain ID 186.

"I want to sign as address A but read using address B's session." This is not what signed queries do. The signer and the session are the same account. If you want cross-account access, model it explicitly with approvals inside the contract.

Next steps