Skip to main content

Read Universal State

Overview

Universal Read lets your app request state from another blockchain or from an HTTPS endpoint and have the result delivered on-chain to Push Chain, agreed on by validators. Balances, token balances, contract calls, storage slots and JSON fields from web APIs can all be read this way.

A read is a paid, asynchronous request. It costs gas plus a protocol fee and a callback budget, and the result arrives after validators reach quorum. If only your frontend or backend needs the data, and nothing on-chain has to act on it, use an ordinary RPC call instead: see Reading Blockchain State.

The receiver is either the Universal Read Registry, a shared contract on Push Chain that stores results for you, or your own contract that inherits UniversalReadClient. Omit callback to use the registry.

Read Universal State

pushChainClient.universal.read(subject, {options}): Promise<UniversalReadResponse>

const result = await pushChainClient.universal.read(
'0xa54E96d3fB93BD9f6cCEf87c2170aEdB1D47E1cF', // account to read
{ chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, // native ETH balance on Sepolia
);

console.log(result.status); // READ.STATUS.FULFILLED once the read completes
console.log(result.value); // defined only when the read succeeded end to end

value is the success signal. The SDK sets it only when the read completed, the source returned data, your callback ran, and the bytes decoded, so if value is defined you can use it.

When value is undefined, the response says why.

How to debug the response
FieldWhat it tells you
statusLifecycle of the request: PENDING, VOTING, FULFILLED, EXPIRED, FAILED, ABORTED. FULFILLED means the read completed; it does not by itself mean your callback succeeded.
raw.statusWhat the source returned: READ.RESULT_STATUS.SUCCESS, or ERROR with raw.errorCode.
callbackDeliveredWhether the receiver contract ran. false when it reverted or ran out of gas; callbackFailReason carries the revert data.
decodeErrorWhy the result bytes could not be decoded into value, for example an ABI that does not match the query.

Read Parameters

These Arguments are mandatory

ArgumentsTypeDescription
subjectstringWhat to read: an account address for a balance, a contract address for a contract call or storage slot, or a URL for a Web2 read. Solana subjects are base58.
options.chainCHAINWhere to read from. Any supported EVM or Solana chain, or CHAIN.WEB2 for an HTTPS endpoint. Decides which query option applies; see the branches below. Pass at most one query option; with none, read returns the subject's native balance.
PushChain.CONSTANTS.CHAIN
PushChain.CONSTANTS.CHAIN.PUSH_TESTNETPushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUTPushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIAPushChain.CONSTANTS.CHAIN.BNB_TESTNETPushChain.CONSTANTS.CHAIN.BASE_SEPOLIAPushChain.CONSTANTS.CHAIN.ARBITRUM_SEPOLIAPushChain.CONSTANTS.CHAIN.SOLANA_DEVNET
:: Pass one of the following, based on options.chain
If options.chain is a Web3 chain [collapsed]
  options.tokenstringToken balance of subject. EVM: the ERC-20 contract address, read via balanceOf. Solana: the SPL mint address; the SDK derives the associated token account.
  options.abi | options.idlAbi | IdlEVM: the contract ABI, used to encode the call and decode value. Solana: the program's Anchor IDL; the subject account is decoded with the layout whose discriminator matches its data. Reads never execute an instruction.
  options.functionNamestringEVM: the function to call (required with abi). Solana: the account layout to decode; optional, since it is inferred from the discriminator. Both snake_case and camelCase are accepted and matched against the IDL.
  options.argsany[]EVM: positional function arguments. Solana: only when subject is the program id, the PDA seeds of the functionName account in IDL order; pubkeys accept base58 or 0x-hex 32-byte strings, integers take BigInt.
  options.storageSlotbigint | HexEVM only. One storage word from the subject contract. Not needed for Solana, as Solana has no storage slots.
If options.chain is a Web2 / HTTPS endpoint [collapsed]
  options.web2objectRequired. Describes the HTTPS request and which JSON fields to extract. Fields below.
    options.web2.extractArray<{ path, valueType, decimals? }>Required. 1 to 16 JSONPath entries. Results come back as an array in this order.
    options.web2.extract[].pathstringJSONPath into the response body, for example $.data.price.
    options.web2.extract[].valueType'uint256' | 'int256' | 'bool' | 'string' | 'bytes'How the extracted value is encoded on-chain.
    options.web2.extract[].decimalsnumberOptional, numeric types only. The value is multiplied by 10 to this power and truncated before encoding, so 1.2345 with decimals: 2 becomes 123.
    options.web2.method'GET' | 'POST'Optional. Defaults to GET.
    options.web2.headersRecord<string, string>Optional. Request headers. Written to a public event log forever, so never include secrets.
    options.web2.bodystring | Uint8ArrayOptional, POST only. A body on GET is rejected.
    options.web2.timeoutMsnumberOptional. Validator fetch timeout, default 5000, clamped by validators.
::
options.callback{ target, gasLimit, abi, functionName, args? }Optional. Routes the request through your own receiver contract instead of the registry; fields in Callback Arguments.
options.waitForCompletionbooleanDefault true. When false, resolves as soon as the request is confirmed on Push Chain, before validators vote.

Call wait() on the response to finish.
options.progressHook(progress: ProgressEvent) => voidCallback for progress updates through the read lifecycle.

Callback Arguments

Callback Arguments
ArgumentsTypeDefaultDescription
options.callback.targetstringUniversal Read RegistryAddress of your receiver contract on Push Chain (inherits UniversalReadClient). Omit to use the registry.
options.callback.gasLimitbigint500_000nGas reserved for the result callback (_onReadResult), not for the request entrypoint.
options.callback.abiAbi-ABI of your receiver, used to call its payable request entrypoint. Required with target.
options.callback.functionNamestring-The payable request entrypoint on your receiver, for example request. Required with target. It must emit exactly one ReadRequested per prepared read.
options.callback.args(spec, gasLimit) => unknown[](spec, gas) => [spec, gas]Optional. Only needed when your entrypoint does not take (spec, gasLimit) in that order.

The SDK builds the request for you and calls this to map it and the callback gas onto your entrypoint's argument list, for example (spec, gas) => [gas, spec, extra].
Advanced Arguments
ArgumentsTypeDefaultDescription
options.blockNumberbigintOracle height minus minConfirmationsEVM only. Pins the read to a specific destination block. The oracle-observed height can lag the real head.
options.minConfirmationsnumber1EVM only. Confirmations the destination block must have before validators read it.
options.expiryBlocksbigint300nRequest lifetime in Push Chain blocks. An unfulfilled request expires and refunds the unused callback budget.
options.maxFeebigintSDK estimatedCap on the upfront payment (protocol fee plus callback budget).
options.refundTostringYour Push accountWhere unused callback budget is sent. Must accept native Push transfers; a contract without a payable receive() forfeits the refund.
options.advanced.pollingIntervalMsnumber2000Milliseconds between status polls. Minimum 500.
options.advanced.timeoutnumberDerived from expiry, at most 180000Maximum milliseconds this caller waits. Timing out does not cancel the request; resume it with trackRead. An explicit value overrides the cap.
options.advanced.enforceGasCheckbooleanfalsefalse warns and proceeds when the pre-flight balance check finds a shortfall. true throws before broadcasting.

Read Progress Hook

ProgressHook Type and Response
FieldTypeDescription
progressObjectThe progress of the transaction.
progress.idstringUnique identifier for the progress event.
progress.titlestringBrief title of the progress event.
progress.messagestringDetailed message describing the event.
progress.levelINFO | SUCCESS | ERRORSeverity level of the event.
progress.responseobject | nullAdditional data object for the event, or null if not applicable.
progress.timestampstringISO-8601 timestamp when the event occurred (e.g. 2025-06-26T15:04:05.000Z).
IDTitleMessageLevelResponse
READ-TX-101<chain> Read RequestedPreparing a <namespace> read of <chain>INFO{ chain, namespace, queryType }
READ-TX-102-01Fetching Destination Height & FeeReading the oracle height and protocol fee for <chain>INFO{ chain, stage: 'preflight' }
READ-TX-102-02Read Spec AssembledPinned at block <n>, expires at Push height <n>; fee + budget = <total> UPCSUCCESS{ protocolFee, callbackBudget, totalValue, blockNumber, expiryPushChainHeight }
READ-TX-102-03Destination Height UnavailableThe oracle has no height for <chain>, so it is not readableERROR{ chain }
READ-TX-102-04Preflight Stale, RefetchingPreflight is <n>s old, refetching the Push heightWARNING{ fetchedAt, ageMs }
READ-TX-102-05Refund Target Is A Contract<refundTo> is a non-UEA contract; it needs a payable receive() or the unspent budget is forfeitedWARNING{ refundTo }
READ-TX-103-01Checking Balance RequirementsBalance <n> UPC covers / is short of the <n> UPC readINFO when sufficient / WARNING when short{ required, available, sufficient, shortfall, enforceGasCheck }
READ-TX-103-02Insufficient BalanceNeed <n> UPC, have <n> UPCERROR (only when enforceGasCheck is true){ required, available, shortfall }
READ-TX-103-03Sensitive Header DetectedHeaders are written to a public event log forever: <headers>WARNING{ matchedHeaders }
READ-TX-104-01Broadcasting Read RequestSending the read request to Push ChainINFO{ stage: 'broadcasting' }
READ-TX-104-02Request Confirmed, Read DetectedRead <requestId> requested in <txHash>SUCCESS{ txHash, requestId, logIndex }
READ-TX-105-01Awaiting QuorumValidators are observing the destination for <requestId>INFO{ requestId, status: 'PENDING' }
READ-TX-105-02Voting In ProgressValidators are voting on the result of <requestId>INFO{ requestId, status: 'VOTING' }
READ-TX-105-04Approaching Expiry<n> Push blocks until <requestId> expiresWARNING{ requestId, pushBlocksRemaining }
READ-TX-106-02Callback DeliveredReadFulfilled emitted for <requestId>SUCCESS{ requestId }
READ-TX-106-03Callback RevertedCallbackFailed for <requestId>; the read is still FULFILLED but your callback did not runWARNING{ requestId, reason }
READ-TX-106-04Callback Gas SettledBurned <n> UPC, refunding <n> UPCINFO{ requestId, burned, refunded }
READ-TX-106-05Refund Sent<amount> UPC pushed to <refundTo>INFO{ requestId, amount, refundTo }
READ-TX-106-06Refund Rejected<refundTo> rejected the refund; it sits in the admin rescue poolWARNING{ requestId, amount, refundTo }
READ-TX-199-01Read FulfilledRead <requestId> fulfilled and deliveredSUCCESS{ requestId, value, resultData, callbackDelivered }
READ-TX-199-02Read Failed / Expired / AbortedRead <requestId> ended <status>: <error>. A fulfilled read whose callback was not delivered also ends here, with status CALLBACK_FAILED (or SOURCE_ERROR, DECODE_FAILED)ERROR{ requestId, status, errorCode, errorMsg, refunded }
READ-TX-199-03Read TimeoutGave up waiting for <requestId> after <n>s; resume with trackReadERROR{ requestId, lastStatus, elapsedMs }

Returns UniversalReadResponse

Returns `UniversalReadResponse` <object>
{
requestId: '0x9c1f3c2b5c0d4a7e8f6b1a2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6',
txHash: '0xe2302bd21ab0902f37cb605d491ce5f95ee35ce4083405dddf3657d782acae35',
chain: 'eip155:11155111',
destination: {
chainNamespace: 'eip155',
chainId: '11155111',
caip2: 'eip155:11155111',
namespace: 'eip155',
},
status: 3, // READ.STATUS.FULFILLED
isTerminal: true,
callbackDelivered: true,
value: 1250000000000000000n,
decoded: { kind: 'uint256', value: 1250000000000000000n },
raw: { status: 1, resultData: '0x...', errorCode: 0 }, // READ.RESULT_STATUS.SUCCESS
errorMsg: '',
fees: {
paid: 1500000000000000n,
protocolFee: 0n,
callbackBudget: 1500000000000000n,
burned: 61200000000000n,
refunded: 1438800000000000n,
},
request: {
spec: { ... },
callbackTarget: '0x00000000000000000000000000000000000000b2',
originalFunder: '0x...',
refundTo: '0x...',
callbackGasLimit: 500000n,
logIndex: 2,
createdAtHeight: 3413247n,
},
pcTx: [{ txHash: '0x...', blockHeight: 3413260, status: 'SUCCESS', errorMsg: '' }],
explorerUrl: 'https://donut.push.network/tx/0xe2302bd2...',
wait: [Function: wait],
refresh: [Function: refresh],
}
PropertyTypeDescription
requestIdstringUnique identifier of this read. Use it to resume tracking in another session.
txHashstringPush Chain transaction that submitted the request.
chainCHAINDestination that was read.
statusREAD.STATUSLifecycle: PENDING, VOTING, FULFILLED, EXPIRED, FAILED, ABORTED.
isTerminalbooleantrue once the read can no longer change.
callbackDeliveredbooleanFULFILLED only. true when the receiver ran; false when it reverted or ran out of gas.
callbackFailReasonstringRevert data from a failed callback.
valueTDecoded result. Present only when fulfilled, delivered, successful and decodable. Typed from your query: bigint for balances, Hex for storage, the ABI return type for calls, an array for Web2.
decodeErrorstringWhy value is absent although the read succeeded.
rawobject | nullConsensus result: status (READ.RESULT_STATUS), resultData bytes and errorCode. null before validators have voted.
errorMsgstringNode-reported error, if any.
feesobjectpaid, protocolFee, callbackBudget, plus burned, refunded and refundFailed once settled.
requestobjectThe on-chain request: spec, callbackTarget, originalFunder, refundTo, callbackGasLimit, logIndex, createdAtHeight.
pcTxarrayPush Chain transactions the node sent for this read (fulfil, settle, expiry).
explorerUrlstringExplorer link for the request transaction.
waitfunctionPolls until the read is terminal and returns the final response. Only a timeout throws.
refreshfunctionReturns one fresh snapshot without polling.

Types of Universal Read

What you pass as subject, plus one query option, decides what is read. Every shape below has a runnable version in the Live Playground.

EVM Reads

const CHAIN = PushChain.CONSTANTS.CHAIN;

// Native balance (ETH)
await pushChainClient.universal.read(holder, { chain: CHAIN.ETHEREUM_SEPOLIA });

// Token balance: ERC-20 balanceOf(holder)
await pushChainClient.universal.read(holder, {
chain: CHAIN.ETHEREUM_SEPOLIA,
token: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
});

// Typed contract call: the ABI encodes the call and decodes the result
await pushChainClient.universal.read(tokenAddress, {
chain: CHAIN.ETHEREUM_SEPOLIA,
abi: [
{
type: 'function',
name: 'totalSupply',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'uint256' }],
},
] as const,
functionName: 'totalSupply',
});

// Storage slot
await pushChainClient.universal.read(contractAddress, {
chain: CHAIN.ETHEREUM_SEPOLIA,
storageSlot: 0n,
});

blockNumber and minConfirmations pin an EVM read to a specific block; see Advanced Arguments.

Run each of these against a real address in the EVM Playground.

Solana Reads

const CHAIN = PushChain.CONSTANTS.CHAIN;

// Native balance (lamports)
await pushChainClient.universal.read(solanaHolder, { chain: CHAIN.SOLANA_DEVNET });

// Token balance: the holder's SPL token account for the mint
await pushChainClient.universal.read(solanaHolder, {
chain: CHAIN.SOLANA_DEVNET,
token: mintAddress,
});

// Program account, decoded with the program's Anchor IDL (the closest thing to a storage slot on Solana)
await pushChainClient.universal.read(accountAddress, {
chain: CHAIN.SOLANA_DEVNET,
idl: programIdl,
});

Solana reads use finalized state, so there is nothing to pin. Solana has no storage slots; a program account holds the state, and idl decodes it the way abi decodes a call result on EVM.

Run these against a real address in the Solana Playground.

Web2 Reads

const CHAIN = PushChain.CONSTANTS.CHAIN;

// Web2: JSON fields from an HTTPS endpoint
await pushChainClient.universal.read('https://jsonplaceholder.typicode.com/users/1', {
chain: CHAIN.WEB2,
web2: {
extract: [
{ path: '$.id', valueType: 'uint256' },
{ path: '$.name', valueType: 'string' },
],
},
});

Note: extract can take up to 16 entries that need to be extracted. Results come back as an array in the same order.

Requests are GET by default. Set method to POST to send a body, and timeoutMs to change the 5 second fetch timeout.

Note: Validators only agree when every one of them extracted identical bytes, so a value that changes between fetches (a live price at full precision) may never reach quorum. Pick stable fields, or lower the precision with decimals.

Request data is public

URLs, headers and bodies are written to a public event log. Never include API keys, bearer tokens or other secrets. The SDK warns on headers that look sensitive, but it cannot catch everything.

Run this against a real endpoint in the Web2 Playground.

Live Playground

The playground creates a temporary Donut wallet. Fund only the address it prints, with test PC from the faucet. The key lives in the browser session and is lost on reload, so use test funds only.

Every example reads real state on a real address. Each one says where the address was taken from.

EVM Playground

EVM reads target Ethereum Sepolia and are pinned to a block, so every validator sees the same state. See Types of Universal Read for the query shapes.

Live Playground: Native Balance (ETH of vitalik.eth)
VIRTUAL NODE IDE
Copy playground link
Copy code
Live Playground: ERC-20 Balance (USDC held by the Push Vault)
VIRTUAL NODE IDE
Copy playground link
Copy code
Live Playground: Contract Call (Chainlink ETH / USD price)
VIRTUAL NODE IDE
Copy playground link
Copy code
Live Playground: Storage Slot (WETH decimals)
VIRTUAL NODE IDE
Copy playground link
Copy code

Solana Playground

Solana reads target Solana Devnet and use finalized state, so there is nothing to pin. See Types of Universal Read.

Live Playground: Native Balance (SOL held by the Push gateway vault)
VIRTUAL NODE IDE
Copy playground link
Copy code
Live Playground: SPL Token Balance (USDC held by the same vault)
VIRTUAL NODE IDE
Copy playground link
Copy code

Web2 Playground

Web2 reads fetch an HTTPS endpoint and extract JSON fields; validators only agree when every one of them got identical bytes. See Types of Universal Read.

Live Playground: JSON Fields (id and name from a public API)
VIRTUAL NODE IDE
Copy playground link
Copy code

Troubleshooting

SymptomAction
value is undefined (the playground fails inside formatEther or formatUnits)The read did not produce a usable result. Open the "How to debug the response" dropdown above and check status, raw.status, callbackDelivered and decodeError.
Callback failed on a large resultSubmit a new request with more callback gas, up to 1_000_000n. Failed requests are not retried automatically.
Client timed outResume with trackRead using the saved request ID or transaction hash.
Only some batch calls were submittedRecover committed hashes; check the pending hash before resubmitting.
Web2 read never reaches quorumExtract stable fields, or lower numeric precision with decimals.
ReadRegistryUnavailableErrorThe registry exists on Donut only. On other networks, provide your own receiver and request ABI.
Prepared read fails revalidationPrepare a fresh request; the old pin, expiry or budget is no longer valid.

Next Steps