1. Read Core state
In this tutorial, a contract reads balances, positions and prices on HyperCore through the read precompiles, and moves nothing. You compare each value with the Hyperliquid info API.
You'll learn:
- How to read a spot balance, the perp account summary,
withdrawable, mark and oracle prices and perp metadata from a contract. - How each value's integer form maps to the human number that the info API shows.
- How
core_user_existstells you whether an address has a Core account.
Prerequisites
| Item | Amount |
|---|---|
| HYPE on HyperEVM for the deployment | less than 0.001 HYPE |
| Core funds | none |
HYPER_VYPER_TESTNET_KEY |
for the deploy step only |
See testnet setup.
The contract
tutorials/read_state/read_state.vy has no storage and no write functions:
# pragma version 0.4.3
# pragma evm-version cancun
# pragma optimize gas
# SPDX-License-Identifier: MIT
"""
@title Tutorial: read HyperCore state
@notice Reads the Core balances and the perp account of any address, and the
prices of one perp market. It has no storage and no write functions.
@dev Tutorial code. Nobody has audited it.
"""
from hyper_vyper import constants
from hyper_vyper import conversions
from hyper_vyper import precompiles
struct UserView:
evm_block: uint256
l1_block: uint64
core_exists: bool
spot_usdc: precompiles.SpotBalance # Core USDC, 8 decimals
spot_hype: precompiles.SpotBalance # Core HYPE, 8 decimals
perp: precompiles.AccountMarginSummary # perp USD, 6 decimals
withdrawable: uint64 # perp USD, 6 decimals
struct PerpView:
evm_block: uint256
coin: String[64]
sz_decimals: uint8
mark_px: uint64 # divide by 10 ** (6 - sz_decimals)
oracle_px: uint64 # divide by 10 ** (6 - sz_decimals)
@external
@view
def read_user(user: address) -> UserView:
"""All Core reads for one address, from one call. They share one EVM block."""
return UserView(
evm_block=block.number,
l1_block=precompiles.l1_block_number(),
core_exists=precompiles.core_user_exists(user),
spot_usdc=precompiles.spot_balance(user, constants.USDC_TOKEN_INDEX),
spot_hype=precompiles.spot_balance(user, conversions.hype_token_index()),
perp=precompiles.account_margin_summary(constants.DEFAULT_PERP_DEX, user),
withdrawable=precompiles.withdrawable(user),
)
@external
@view
def read_perp(perp: uint32) -> PerpView:
"""The name, size decimals, mark price and oracle price of one perp market."""
info: precompiles.PerpAssetInfo = precompiles.perp_asset_info(perp)
return PerpView(
evm_block=block.number,
coin=info.coin,
sz_decimals=info.sz_decimals,
mark_px=precompiles.mark_px(perp),
oracle_px=precompiles.oracle_px(perp),
)
read_usermakes all reads for one address in one call, so all values come from the same EVM block.spot_usdcandspot_hypeareSpotBalance(total, hold, entry_ntl)in Core units with 8 decimals.perpisAccountMarginSummary(account_value, margin_used, ntl_pos, raw_usd)in perp USD with 6 decimals.mark_pxandoracle_pxare raw integers that you divide by10**(6 - sz_decimals). For BTC,sz_decimalsis 5, so you divide by 10.
Steps
-
Show the deployment plan. This step runs the deployment on a local copy of the chain and sends nothing:
-
Deploy the contract:
chain 998 (HyperEVM testnet) plan: deploy read_state.vy sent deploy: 0x... in block N contract 0x...The deploy step saves the contract address in
tutorials/.state/read_state.json. -
Read your own address and perp market 3 (BTC on testnet). Give the address with
--user:This step only reads. Each line shows the precompile value with its units, then the info API value:
chain 998 (HyperEVM testnet) EVM block N, Core block M, user 0xYOUR_ADDRESS [ok ] core_user_exists precompile True | info API user [ok ] spot USDC precompile 100000000000 (1000 USDC, 8 decimals) | info API 1000.0 [ok ] spot HYPE precompile 25000000 (0.25 HYPE, 8 decimals) | info API 0.25 [ok ] perp account value precompile 0 (0 USD, 6 decimals) | info API 0.0 [ok ] perp withdrawable precompile 0 (0 USD, 6 decimals) | info API 0.0 [ok ] perp 3 name precompile BTC | info API BTC [ok ] perp 3 sz_decimals precompile 5 | info API 5 [ok ] mark price precompile 831540 / 10**1 = 83154 | info API 83150.0 [ok ] oracle price precompile 836580 / 10**1 = 83658 | info API 83658.0 All values match.The mark price moved between the two reads, which is within the tolerance for prices (see What can go wrong).
-
Read a new random address, which has no Core account, and perp 0:
uv run --locked tutorials/read_state/run.py read --perp 0 \ --user 0x8f3a2b9c1d4e5f60718293a4b5c6d7e8f9012345[ok ] core_user_exists precompile False | info API missing [ok ] spot USDC precompile 0 (0 USDC, 8 decimals) | info API 0 ... [ok ] perp 0 name precompile SOL | info API SOL [ok ] perp 0 sz_decimals precompile 2 | info API 2 [ok ] mark price precompile 1074500 / 10**4 = 107.45 | info API 107.45core_user_existsisFalseand all balances are 0. Perp 0 is SOL, withsz_decimals2, so the script divides the prices by10**4.
Read without a deployment
Add --no-deploy to the read step. The script then deploys the contract
on the local copy of the chain only, reads through it, and sends nothing.
What can go wrong
- A price shows
DIFF. The script reads the precompiles and the info API one after the other, and the price can change between the two reads. The script accepts a difference of 0.5%, so run the step again. - A balance shows
DIFFin unified account mode. A new user wallet is in unified account mode, where balance reads can show perp funds through spot, so the two sources can differ. See account mode. - Funds in escrow. The precompiles do not show an escrow. If the info API
shows
evmEscrows, the script prints a note. See tutorial 2. - A past block. An
eth_callto a precompile always returns the current Core state, even for an old block tag. Record each reading when you make it. - No contract in the state file. Run the deploy step first, or use
--no-deploy.
Clean up
The contract holds no funds, so you can leave it. To deploy again, delete
tutorials/.state/read_state.json.