Skip to main content

How Responses Work

Every method of IotaGrpcClient follows the same four conventions.

Body and metadata​

Every method resolves to { body, metadata }, typed as WithMetadata<T>. body is the answer itself. metadata is read from the x-iota-* headers the node sends along, and tells you what state the node was in when it answered:

const { body: objects, metadata } = await client.getObjects(['0x5']);

console.log(metadata.chain, metadata.epoch, metadata.checkpointHeight);
FieldHeaderMeaning
chainIdx-iota-chain-idBase58 digest identifying the chain
chainx-iota-chainChain name, such as mainnet
epochx-iota-epochCurrent epoch
checkpointHeightx-iota-checkpoint-heightLatest executed checkpoint
timestampMs, timestampx-iota-timestamp-ms, x-iota-timestampChain time, as milliseconds since the Unix epoch and as RFC 3339
lowestAvailableCheckpointx-iota-lowest-available-checkpointOldest checkpoint whose checkpoint data, transactions, effects and events the node still serves
lowestAvailableCheckpointObjectsx-iota-lowest-available-checkpoint-objectsOldest checkpoint whose input and output objects the node still serves
serverVersionx-iota-serverNode version, such as iota-node/1.32.1

Every field is optional. It is undefined when the header is missing or malformed. Epochs, checkpoints and timestamps are bigint.

The two lowestAvailable* fields show how much history a pruning node still keeps. Ask for anything older and the call fails with NOT_FOUND or FAILED_PRECONDITION.

Streams work a little differently: their metadata is a promise that settles once the first response arrives. See Stream Checkpoints.

Batch results​

getObjects, getTransactions, executeTransactions, simulateTransactions and viewFunctionCalls take a list, and return one ItemResult per item in the same order:

type ItemResult<T> = { ok: true; value: T } | { ok: false; error: IotaGrpcError };

If the node cannot serve one item, only that item's slot fails. The call as a whole still succeeds, so check ok on every result:

const { body } = await client.getObjects(['0x2', '0x5', `0x${'ab'.repeat(32)}`]);

for (const result of body) {
if (result.ok) {
console.log('found', result.value.reference?.version);
} else {
console.log('failed', result.error.message);
}
}

The single-item forms (executeTransaction, simulateTransaction and viewFunctionCall) return the value directly, and throw the error instead of returning it. Passing an empty list to a batch method throws EmptyRequestError.

The client also checks that the node sent back one result per item and, for objects and transactions, that each result belongs to the item at that position. If either check fails it throws a ProtocolError, so you never get results matched to the wrong request.

Read masks​

A read mask tells the node which fields of the response to fill in. Each method has a default, listed in DEFAULT_READ_MASKS. To get something else, pass readMask with the field constants for that method:

ConstantUsed by
ObjectFieldgetObjects
TransactionFieldgetTransactions, executeTransaction(s)
CheckpointResponseFieldgetCheckpoint, streamCheckpoints
EpochFieldgetEpoch
ServiceInfoFieldgetServiceInfo
OwnedObjectFieldlistOwnedObjects, listAllOwnedObjects
DynamicFieldFieldlistDynamicFields, listAllDynamicFields
SimulateFieldsimulateTransaction(s)
ViewFunctionCallFieldviewFunctionCall(s)
import { TransactionField } from '@iota/grpc';

const { body } = await client.getTransactions([digest], {
readMask: [TransactionField.EFFECTS, TransactionField.CHECKPOINT],
});
  • A single field works as well as a list: readMask: TransactionField.EFFECTS.
  • A field includes everything under it. TransactionField.EFFECTS covers effects.digest and effects.bcs, and ALL (*) selects every field.
  • Your mask replaces the default rather than adding to it.
  • An empty list selects nothing, and every field comes back unset.
  • If the node does not know a path, the whole call fails with INVALID_ARGUMENT.

Checkpoints are the case to watch: by default you get only the summary, and transactions and events have to be asked for.

BCS payloads​

Responses are protobuf messages, but the objects, transactions, effects, events and checkpoint data inside them are BCS bytes, the encoding the chain itself uses. The client leaves them encoded, so you only pay for decoding what you use. Each kind of payload has its own decoder:

FunctionTakesReturns
decodeObjectObject$IotaObject: data (Move struct or package), owner, previous transaction, storage rebate
objectIdOfIotaObjectThe object's ID
decodeCoinObject$Coin: { coinType, id, balance }
decodeTransactionExecutedTransaction['transaction']TransactionData
decodeTransactionEffectsExecutedTransaction['effects']TransactionEffects
decodeTransactionEventsExecutedTransaction['events']IotaEvent[]
decodeEventEventIotaEvent: package, module, sender, type and BCS contents
decodeCheckpointSummaryCheckpoint['summary']CheckpointSummary
decodeCheckpointContentsCheckpoint['contents']CheckpointContents
decodeValidatorAggregatedSignatureCheckpoint['signature']ValidatorAggregatedSignature
decodeUserSignatureA signature from ExecutedTransaction['signatures']The base64 serialized signature
decodeExecutionErrorAn execution errorExecutionError
decodeViewOutputsViewFunctionCallOutputs{ returnValues } or { executionError }
decodeCheckpointTransactionExecutedTransactionAll of the above parts at once
import { decodeObject, objectIdOf } from '@iota/grpc';

const [result] = (await client.getObjects(['0x5'])).body;

if (result.ok) {
const object = decodeObject(result.value);
console.log(objectIdOf(object), object.owner);
}

A decoder throws ProtoConversionError when the bytes are invalid or the field is missing. A missing field usually means the read mask left it out. decodeCheckpointTransaction needs the transaction, signatures, effects, and input and output objects, so include all of them in the mask.

Decoded values come from @iota/bcs, which returns u64 and larger integers as decimal strings. The protobuf messages use bigint for the same numbers, so convert with BigInt(...) before comparing:

import { decodeCheckpointSummary } from '@iota/grpc';

const { body } = await client.getCheckpoint();
const summary = decodeCheckpointSummary(body.checkpoint.summary!);

console.log(BigInt(summary.sequenceNumber) === body.sequenceNumber); // true

The one exception is listCoins and listAllCoins, which return coins already decoded, with balance as a bigint.