llms.txt
@mysten/sui v2.0 and a new dApp Kit are here! Check out the migration guide
Mysten Labs SDKs
Clients

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.

CategoryMethods
ObjectsgetObject, getObjects, listOwnedObjects
CoinsgetBalance, listBalances, listCoins, getCoinMetadata
Dynamic fieldslistDynamicFields, getDynamicField, getDynamicObjectField
TransactionsgetTransaction, executeTransaction, signAndExecuteTransaction, waitForTransaction
SimulationsimulateTransaction
HistorylistTransactions, listEvents
MovegetMoveFunction
NamesresolveNameServiceAddress, defaultNameServiceName, mvr
NetworkgetReferenceGasPrice, getCurrentSystemState, getProtocolConfig, getChainIdentifier
VerificationverifyZkLoginSignature
BuildingresolveTransactionPlugin

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

TypeDescription
Object<Include>The unified object shape; Include controls which optional fields are present and typed
CoinCoin object with balance
BalanceBalance summary for a coin type
CoinMetadataMetadata for a coin type
Transaction<Include>An executed transaction; Include controls which optional fields are present
TransactionResultSuccess or failure result from execution
TransactionEffectsDetailed effects from transaction execution
EventEmitted event from a transaction
EventEntryQueried event with its ledger position
TransactionFilterFilter for transaction queries
EventFilterFilter for event queries
ObjectOwnerUnion of all owner types
ExecutionStatusSuccess/failure status with error details
DynamicFieldNameName identifier for dynamic fields
FunctionResponseMove function metadata
NetworkNetwork 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.

BehaviorWhat 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 gapsEventEntry.checkpoint is null, and a bare generic eventType filter matches nothing where gRPC and GraphQL match any instantiation
Page-size capsOver-large limit values are silently truncated on gRPC and rejected with an error on GraphQL and JSON-RPC
Short pagesgRPC bounds how much ledger a filtered query scans, so a page can be short or empty with hasNextPage: true
Read-after-writeGraphQL 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:

PropertyPurpose
client.networkThe network the client was created for ('mainnet', 'testnet', and so on)
client.cacheA shared cache; call cache.scope('my-sdk') for a namespace of your own
client.core.mvrMove 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.

On this page