Core API
The transport-agnostic client contract that SDKs and libraries build against
The Core API is the contract every Sui client implements. It exists so that libraries do not have to
care which transport their caller chose: an SDK written against the Core API works with
SuiGrpcClient, SuiGraphQLClient, and the deprecated SuiJsonRpcClient alike.
This page is for SDK and library authors. If you are writing application code, create a client and
call its top-level methods directly. You do not need client.core.
For packaging, extension, and code-generation patterns that go with this contract, see Building SDKs.
ClientWithCoreApi
ClientWithCoreApi is the type to accept in library code. It says "any Sui client", and gives you
client.core to work through:
import type { ClientWithCoreApi } from '@mysten/sui/client';
export class MySDK {
#client: ClientWithCoreApi;
constructor(client: ClientWithCoreApi) {
this.#client = client;
}
async getResource(objectId: string) {
const { object } = await this.#client.core.getObject({
objectId,
include: { content: true },
});
return object;
}
}Take the client as a parameter rather than constructing one: the caller has already configured a network, endpoint, and transport, and a second client would bypass all of it along with the request cache the caller's client holds.
Why go through client.core
Top-level methods on SuiGrpcClient and SuiGraphQLClient are a superset of the Core API: each
transport can add fields where it has extra native data, such as include: { protoJson: true } on
gRPC. Those additions are not portable. client.core is the subset that every transport guarantees,
so library code that stays inside it keeps working whichever client the caller supplies.
The contract
Every client implements these methods on client.core. Querying data
documents them in full, with options, include flags, and response shapes.
| Category | Methods |
|---|---|
| Objects | getObject, getObjects, listOwnedObjects |
| Coins | getBalance, listBalances, listCoins, getCoinMetadata |
| Dynamic fields | listDynamicFields, getDynamicField, getDynamicObjectField |
| Transactions | getTransaction, executeTransaction, signAndExecuteTransaction, waitForTransaction |
| Simulation | simulateTransaction |
| History | listTransactions, listEvents |
| Move | getMoveFunction |
| Names | resolveNameServiceAddress, defaultNameServiceName, mvr |
| Network | getReferenceGasPrice, getCurrentSystemState, getProtocolConfig, getChainIdentifier |
| Verification | verifyZkLoginSignature |
| Building | resolveTransactionPlugin |
SuiGrpcClient and SuiGraphQLClient also expose every one of these as a top-level method, so the
choice between client.core.getObject() and client.getObject() is about portability rather than
reach. The deprecated SuiJsonRpcClient is the exception: it implements the contract on
client.core, but its own top-level methods keep their legacy JSON-RPC names and shapes.
Typing library code
SuiClientTypes holds every option and response type in the contract. Import it to type your own
signatures rather than re-deriving shapes:
import type { ClientWithCoreApi, SuiClientTypes } from '@mysten/sui/client';
import { MyStruct } from './generated/my-module';
// Include generics flow through, so `object.content` is typed as present here
function parseResource(object: SuiClientTypes.Object<{ content: true }>) {
return MyStruct.parse(object.content);
}
async function fetchBalance(
client: ClientWithCoreApi,
owner: string,
): Promise<SuiClientTypes.Balance> {
const { balance } = await client.core.getBalance({ owner });
return balance;
}The Include generic is what makes optional data type-safe: a field that was not requested is typed
undefined, so forgetting to ask for content is a compile error rather than a runtime one.
Discriminated unions
Polymorphic types in the contract use a $kind discriminant rather than optional fields. Narrow on
$kind and the matching property is typed for you:
function describeOwner(owner: SuiClientTypes.ObjectOwner) {
switch (owner.$kind) {
case 'AddressOwner':
return `owned by ${owner.AddressOwner}`;
case 'ObjectOwner':
return `owned by object ${owner.ObjectOwner}`;
case 'Shared':
return `shared since ${owner.Shared.initialSharedVersion}`;
case 'ConsensusAddressOwner':
return `consensus-owned by ${owner.ConsensusAddressOwner.owner}`;
case 'Immutable':
return 'immutable';
default:
return 'unknown';
}
}TransactionResult, SimulateTransactionResult, DatatypeResponse, ExecutionError, and
OpenSignatureBody follow the same pattern.
Common types
| Type | Description |
|---|---|
Object<Include> | The unified object shape; Include controls which optional fields are present and typed |
Coin | Coin object with balance |
Balance | Balance summary for a coin type |
CoinMetadata | Metadata for a coin type |
Transaction<Include> | An executed transaction; Include controls which optional fields are present |
TransactionResult | Success or failure result from execution |
TransactionEffects | Detailed effects from transaction execution |
Event | Emitted event from a transaction |
EventEntry | Queried event with its ledger position |
TransactionFilter | Filter for transaction queries |
EventFilter | Filter for event queries |
ObjectOwner | Union of all owner types |
ExecutionStatus | Success/failure status with error details |
DynamicFieldName | Name identifier for dynamic fields |
FunctionResponse | Move function metadata |
Network | Network identifier type |
Option types follow a predictable naming scheme (GetObjectOptions, ListCoinsOptions,
SimulateTransactionOptions, and so on), along with the include shapes ObjectInclude,
TransactionInclude, and SimulateTransactionInclude.
Cross-transport differences
All transports produce the same shapes, but they read from different backends, and a few differences are visible to callers. Library code that runs against any client should account for them.
| Behavior | What differs |
|---|---|
include: { json: true } | Field shapes vary between implementations. Use content and parse BCS when the result must be stable |
include: { events: true } | GraphQL returns at most the first 50 events of a transaction; gRPC and JSON-RPC return all of them |
| JSON-RPC query gaps | EventEntry.checkpoint is null, and a bare generic eventType filter matches nothing where gRPC and GraphQL match any instantiation |
| Page-size caps | Over-large limit values are silently truncated on gRPC and rejected with an error on GraphQL and JSON-RPC |
| Short pages | gRPC bounds how much ledger a filtered query scans, so a page can be short or empty with hasNextPage: true |
| Read-after-write | GraphQL reads an index that trails execution. Use waitForTransaction before reading your own writes |
Two constraints are enforced by the client itself, so they fail the same way everywhere: a query
takes at most one of after or before, and paginating transactions filtered by function
requires a fully qualified package::module::function.
Client services
Beyond the methods, ClientWithCoreApi carries a few properties worth knowing about when writing
library code:
| Property | Purpose |
|---|---|
client.network | The network the client was created for ('mainnet', 'testnet', and so on) |
client.cache | A shared cache; call cache.scope('my-sdk') for a namespace of your own |
client.core.mvr | Move Registry resolution, with its own cache |
Every Core API method also accepts a signal, so a library method can pass its caller's
AbortSignal straight through:
async function getResource(
client: ClientWithCoreApi,
objectId: string,
options?: { signal?: AbortSignal },
) {
return client.core.getObject({
objectId,
include: { content: true },
signal: options?.signal,
});
}SDKs attach themselves to a client with $extend, so several SDKs can compose on one client
instance. See Building SDKs for how to author an extension, choose peer
dependencies, and structure your SDK's methods.