MirrorNodeTokenBalanceQuery - Test specification
Description:
This test specification covers MirrorNodeTokenBalanceQuery, the mirror-node REST query that reads one account's balance of one token. Together with MirrorNodeAccountBalanceQuery (HBAR only), it replaces the token balances the deprecated consensus-node AccountBalanceQuery returned. The query sends a single GET /api/v1/accounts/{id}/tokens?token.id={tokenId} request to the mirror node. It is pure HTTP, with no query payment and no consensus node involvement.
The contract tested here is the single-token query the SDKs ship today: the account and the token are both required, and one request returns one balance, so there is no pagination. The source of truth for how the query reaches the mirror node is the shared HTTP transport proposal (sdk-collaboration-hub#286), which changes no public signature of this query.
Behaviours that shape the tests:
- Not-found is the SDK's mapping. The endpoint returns HTTP 404 for an account the mirror node does not know. The SDK maps it to the
INVALID_ACCOUNT_IDstatus, the same statusMirrorNodeAccountBalanceQueryreports. Because the mirror node lags on an account's existence, a freshly created account can transiently fail withINVALID_ACCOUNT_IDuntil ingested. - Associated but never funded. The mirror node lists an associated relationship even at a zero balance, so the query returns a balance of
"0"with the token's real decimals. - No relationship (PROVISIONAL). For an existing account with no relationship to the token, the mirror node returns an empty list. The SDKs currently return a balance of
"0"with decimals0, because the token's decimals cannot be known from that response. This is documented current behaviour, not a settled contract. A follow-up TCK issue (hiero-sdk-tck#716) will align it with the cross-SDK proposal, and test 5 will change with it. - NFTs. For a non-fungible token, the balance is the number of NFTs (serials) the account holds, and the mirror node reports decimals as
0. - Eventual consistency. The mirror node ingests consensus state asynchronously and typically lags the network by a few seconds. The test driver polls the query for about 30 seconds (60 attempts, 500 ms apart) until it reflects the setup transactions, and retries a transient
INVALID_ACCOUNT_IDwithin that bound. A test still unmatched when the budget runs out fails with the last response it saw.
Out of scope: pagination and token filters other than a single token ID (not part of this contract); retry of transient mirror-node failures (HTTP 5xx, network errors), for the same reason as in the MirrorNodeAccountBalanceQuery specification; token IDs given as EVM addresses.
Transport proposal:
https://github.com/hiero-ledger/sdk-collaboration-hub/pull/286
Mirror node account tokens endpoint:
https://docs.hedera.com/hedera/sdks-and-apis/rest-api/accounts
JSON-RPC API Endpoint Documentation
Method Name
getMirrorNodeTokenBalance
Input Parameters
| Parameter Name | Type | Required/Optional | Description/Notes |
|---|---|---|---|
| accountId | string | required | The account whose token balance to read, in any form the SDK's account ID parsing accepts: an account ID (shard.realm.num), an EVM address, or a public key alias. |
| tokenId | string | required | The token to read the balance of (shard.realm.num). |
Output Parameters
| Parameter Name | Type | Description/Notes |
|---|---|---|
| tokenId | string | The token ID the balance belongs to. |
| balance | string | The balance in the token's smallest unit (raw units, not divided by 10^decimals); for an NFT, the number of NFTs held. A decimal string, so int64 values are not rounded by JSON number handling. |
| decimals | number | The token's decimals, as a JSON number. 0 for an NFT. |
JSON Request/Response Examples
Balance of a fungible token held by an account
{
"jsonrpc": "2.0",
"id": 9130,
"method": "getMirrorNodeTokenBalance",
"params": {
"accountId": "0.0.1544",
"tokenId": "0.0.1545"
}
}
{
"jsonrpc": "2.0",
"id": 9130,
"result": {
"tokenId": "0.0.1545",
"balance": "250",
"decimals": 2
}
}
Account the mirror node does not know
{
"jsonrpc": "2.0",
"id": 9131,
"error": {
"code": -32001,
"message": "Hiero error",
"data": {
"status": "INVALID_ACCOUNT_ID",
"message": "account 0.0.999999999 was not found on the mirror node"
}
}
}
Malformed token ID
{
"jsonrpc": "2.0",
"id": 9132,
"error": {
"code": -32603,
"message": "Internal error",
"data": {
"message": "failed to parse entity id: not-a-token-id"
}
}
}
Error Mapping
- An account the mirror node does not know fails with code
-32001(Hiero error) anddata.statusINVALID_ACCOUNT_ID. - A missing or malformed
accountIdortokenIdfails with code-32603(INTERNAL_ERROR) before any network call.
Function Tests
GetMirrorNodeTokenBalance:
- Reads one account's balance of one token from the mirror node
| Test no | Name | Input | Expected response | Implemented (Y/N) |
|---|---|---|---|---|
| 1 | Query the balance of a fungible token held by an account | accountId=<CREATED_ACCOUNT_ID>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 2 decimals, after a transfer of 250 units to the account | The query returns balance "250" and decimals 2 | Y |
| 2 | Query the balance of a token's treasury | accountId=<TREASURY_ACCOUNT_ID>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 3 decimals and an initial supply of 1000000 | The query returns balance "1000000" and decimals 3 | Y |
| 3 | Query the balance of an NFT | accountId=<TREASURY_ACCOUNT_ID>, tokenId=<CREATED_NFT_ID>, after minting 3 serials | The query returns balance "3" and decimals 0 | Y |
| 4 | Query the balance of an associated token the account never received | accountId=<CREATED_ACCOUNT_ID> associated with tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 3 decimals | The query returns balance "0" and decimals 3 | Y |
| 5 | Query the balance of a token the account has no relationship with | accountId=<CREATED_ACCOUNT_ID>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 3 decimals, never associated | The query returns balance "0" and decimals 0 (PROVISIONAL, see the description) | Y |
| 6 | Query the balance of an account that doesn't exist | accountId=0.0.999999999, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> | The query fails with a JSON-RPC error response of code -32001 (Hiero error) with data.status INVALID_ACCOUNT_ID | Y |
| 7 | Query the balance with a malformed account ID | accountId="not-an-id", tokenId=<CREATED_FUNGIBLE_TOKEN_ID> | The query fails with a JSON-RPC error response of code -32603 (INTERNAL_ERROR) before any network call | Y |
| 8 | Query the balance with no account ID | tokenId=<CREATED_FUNGIBLE_TOKEN_ID> | The query fails with a JSON-RPC error response of code -32603 (INTERNAL_ERROR) before any network call | Y |
| 9 | Query the balance with no token ID | accountId=<OPERATOR_ACCOUNT_ID> | The query fails with a JSON-RPC error response of code -32603 (INTERNAL_ERROR) before any network call | Y |
| 10 | Query the balance with a malformed token ID | accountId=<OPERATOR_ACCOUNT_ID>, tokenId="not-a-token-id" | The query fails with a JSON-RPC error response of code -32603 (INTERNAL_ERROR) before any network call | Y |
| 11 | Query the balance by EVM address | accountId=<EVM_ADDRESS_OF_CREATED_ACCOUNT>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 2 decimals, after a transfer of 40 units to the account | The query resolves the EVM address and returns balance "40" and decimals 2 | Y |
| 12 | Query the balances of two fungible tokens held by one account | accountId=<CREATED_ACCOUNT_ID>, token A with 2 decimals (30 units transferred) and token B with 4 decimals (70 units transferred), one query per token | The query for token A returns balance "30" and decimals 2; the query for token B returns balance "70" and decimals 4 | Y |
| 13 | Query a balance above 2^53 | accountId=<TREASURY_ACCOUNT_ID>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with infinite supply, 0 decimals and an initial supply of 9007199254740993 (2^53 + 1) | The query returns balance "9007199254740993" and decimals 0 | Y |
| 14 | Query the balance by public key alias | accountId=<ALIAS_OF_AUTO_CREATED_ACCOUNT>, tokenId=<CREATED_FUNGIBLE_TOKEN_ID> with 2 decimals, after a transfer of 60 units to the account | The query resolves the alias and returns balance "60" and decimals 2 | Y |
Every successful result also carries tokenId equal to the requested token. Expected balances come from the amounts the driver transferred or minted, never from the query under test. Test 4 uses a token with non-zero decimals so that an associated relationship ("0", 3) is told apart from no relationship ("0", 0). For test 6 the mirror node returns a 404; the SDK under test is expected to map it to INVALID_ACCOUNT_ID, matching MirrorNodeAccountBalanceQuery. Test 11 creates the account with an EVM address alias, the same way test 3 of the MirrorNodeAccountBalanceQuery specification does. Test 13 requires the balance to be exact beyond 2^53: an SDK that parses the mirror node's JSON number into a double returns a rounded value (the known JS SDK issue hiero-sdk-js#4336). Test 14 auto-creates the account the same way test 4 of the MirrorNodeAccountBalanceQuery specification does and passes the alias account ID in the DER-hex form (0.0.<hex-encoded DER public key>); the SDK is responsible for querying the mirror node with the base32 alias form it accepts.