@mysten/sui
Version:
Sui TypeScript API
1,360 lines (1,258 loc) • 42.1 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import type { CoreClientOptions, SuiClientTypes } from '../client/index.js';
import { CoreClient, formatMoveAbortMessage, SimulationError } from '../client/index.js';
import type { SuiGrpcClient } from './client.js';
import type { Owner } from './proto/sui/rpc/v2/owner.js';
import { Owner_OwnerKind } from './proto/sui/rpc/v2/owner.js';
import { chunk, fromBase64, toBase64 } from '@mysten/utils';
import type { ExecutedTransaction } from './proto/sui/rpc/v2/executed_transaction.js';
import type { TransactionEffects } from './proto/sui/rpc/v2/effects.js';
import {
UnchangedConsensusObject_UnchangedConsensusObjectKind,
ChangedObject_IdOperation,
ChangedObject_InputObjectState,
ChangedObject_OutputObjectState,
} from './proto/sui/rpc/v2/effects.js';
import type {
ExecutionError as GrpcExecutionError,
MoveAbort as GrpcMoveAbort,
} from './proto/sui/rpc/v2/execution_status.js';
import {
ExecutionError_ExecutionErrorKind,
CommandArgumentError_CommandArgumentErrorKind,
TypeArgumentError_TypeArgumentErrorKind,
PackageUpgradeError_PackageUpgradeErrorKind,
} from './proto/sui/rpc/v2/execution_status.js';
import type { BuildTransactionOptions } from '../transactions/index.js';
import { TransactionDataBuilder } from '../transactions/index.js';
import { bcs } from '../bcs/index.js';
import { normalizeStructTag, normalizeSuiAddress } from '../utils/sui-types.js';
import { SUI_TYPE_ARG } from '../utils/constants.js';
import type { OpenSignature, OpenSignatureBody } from './proto/sui/rpc/v2/move_package.js';
import {
Ability,
FunctionDescriptor_Visibility,
OpenSignature_Reference,
OpenSignatureBody_Type,
} from './proto/sui/rpc/v2/move_package.js';
import {
applyGrpcResolvedTransaction,
transactionDataToGrpcTransaction,
transactionToGrpcTransaction,
grpcTransactionToTransactionData,
} from '../client/transaction-resolver.js';
import { Value } from './proto/google/protobuf/struct.js';
import { SimulateTransactionRequest_TransactionChecks } from './proto/sui/rpc/v2/transaction_execution_service.js';
export interface GrpcCoreClientOptions extends CoreClientOptions {
client: SuiGrpcClient;
}
export class GrpcCoreClient extends CoreClient {
#client: SuiGrpcClient;
constructor({ client, ...options }: GrpcCoreClientOptions) {
super(options);
this.#client = client;
}
async getObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
options: SuiClientTypes.GetObjectsOptions<Include>,
): Promise<SuiClientTypes.GetObjectsResponse<Include>> {
const batches = chunk(options.objectIds, 50);
const results: SuiClientTypes.GetObjectsResponse<Include>['objects'] = [];
const paths = ['owner', 'object_type', 'digest', 'version', 'object_id'];
if (options.include?.content) {
paths.push('contents');
}
if (options.include?.previousTransaction) {
paths.push('previous_transaction');
}
if (options.include?.objectBcs) {
paths.push('bcs');
}
if (options.include?.json) {
paths.push('json');
}
if (options.include?.display) {
paths.push('display');
}
for (const batch of batches) {
const response = await this.#client.ledgerService.batchGetObjects({
requests: batch.map((id) => ({ objectId: id })),
readMask: {
paths,
},
});
results.push(
...response.response.objects.map((object): SuiClientTypes.Object<Include> | Error => {
if (object.result.oneofKind === 'error') {
// TODO: improve error handling
return new Error(object.result.error.message);
}
if (object.result.oneofKind !== 'object') {
return new Error('Unexpected result type');
}
const bcsContent = object.result.object.contents?.value ?? undefined;
const objectBcs = object.result.object.bcs?.value ?? undefined;
// Package objects have type "package" which is not a struct tag, so don't normalize it
const objectType = object.result.object.objectType;
const type =
objectType && objectType.includes('::')
? normalizeStructTag(objectType)
: (objectType ?? '');
const jsonContent = options.include?.json
? object.result.object.json
? (Value.toJson(object.result.object.json) as Record<string, unknown>)
: null
: undefined;
const displayData = mapDisplayProto(
options.include?.display,
object.result.object.display,
);
return {
objectId: object.result.object.objectId!,
version: object.result.object.version?.toString()!,
digest: object.result.object.digest!,
content: bcsContent as SuiClientTypes.Object<Include>['content'],
owner: mapOwner(object.result.object.owner)!,
type,
previousTransaction: (object.result.object.previousTransaction ??
undefined) as SuiClientTypes.Object<Include>['previousTransaction'],
objectBcs: objectBcs as SuiClientTypes.Object<Include>['objectBcs'],
json: jsonContent as SuiClientTypes.Object<Include>['json'],
display: displayData as SuiClientTypes.Object<Include>['display'],
};
}),
);
}
return {
objects: results,
};
}
async listOwnedObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
options: SuiClientTypes.ListOwnedObjectsOptions<Include>,
): Promise<SuiClientTypes.ListOwnedObjectsResponse<Include>> {
const paths = ['owner', 'object_type', 'digest', 'version', 'object_id'];
if (options.include?.content) {
paths.push('contents');
}
if (options.include?.previousTransaction) {
paths.push('previous_transaction');
}
if (options.include?.objectBcs) {
paths.push('bcs');
}
if (options.include?.json) {
paths.push('json');
}
if (options.include?.display) {
paths.push('display');
}
const response = await this.#client.stateService.listOwnedObjects({
owner: options.owner,
objectType: options.type
? (await this.mvr.resolveType({ type: options.type })).type
: undefined,
pageToken: options.cursor ? fromBase64(options.cursor) : undefined,
pageSize: options.limit,
readMask: {
paths,
},
});
const objects = response.response.objects.map(
(object): SuiClientTypes.Object<Include> => ({
objectId: object.objectId!,
version: object.version?.toString()!,
digest: object.digest!,
content: object.contents?.value as SuiClientTypes.Object<Include>['content'],
owner: mapOwner(object.owner)!,
type: object.objectType!,
previousTransaction: (object.previousTransaction ??
undefined) as SuiClientTypes.Object<Include>['previousTransaction'],
objectBcs: object.bcs?.value as SuiClientTypes.Object<Include>['objectBcs'],
json: (options.include?.json
? object.json
? (Value.toJson(object.json) as Record<string, unknown>)
: null
: undefined) as SuiClientTypes.Object<Include>['json'],
display: mapDisplayProto(
options.include?.display,
object.display,
) as SuiClientTypes.Object<Include>['display'],
}),
);
return {
objects,
cursor: response.response.nextPageToken ? toBase64(response.response.nextPageToken) : null,
hasNextPage: response.response.nextPageToken !== undefined,
};
}
async listCoins(
options: SuiClientTypes.ListCoinsOptions,
): Promise<SuiClientTypes.ListCoinsResponse> {
const paths = ['owner', 'object_type', 'digest', 'version', 'object_id', 'balance'];
const coinType = options.coinType ?? SUI_TYPE_ARG;
const response = await this.#client.stateService.listOwnedObjects({
owner: options.owner,
objectType: `0x2::coin::Coin<${(await this.mvr.resolveType({ type: coinType })).type}>`,
pageToken: options.cursor ? fromBase64(options.cursor) : undefined,
readMask: {
paths,
},
});
return {
objects: response.response.objects.map(
(object): SuiClientTypes.Coin => ({
objectId: object.objectId!,
version: object.version?.toString()!,
digest: object.digest!,
owner: mapOwner(object.owner)!,
type: object.objectType!,
balance: object.balance?.toString()!,
}),
),
cursor: response.response.nextPageToken ? toBase64(response.response.nextPageToken) : null,
hasNextPage: response.response.nextPageToken !== undefined,
};
}
async getBalance(
options: SuiClientTypes.GetBalanceOptions,
): Promise<SuiClientTypes.GetBalanceResponse> {
const coinType = options.coinType ?? SUI_TYPE_ARG;
const result = await this.#client.stateService.getBalance({
owner: options.owner,
coinType: (await this.mvr.resolveType({ type: coinType })).type,
});
return {
balance: {
balance: result.response.balance?.balance?.toString() ?? '0',
coinType: result.response.balance?.coinType ?? coinType,
coinBalance: result.response.balance?.coinBalance?.toString() ?? '0',
addressBalance: result.response.balance?.addressBalance?.toString() ?? '0',
},
};
}
async getCoinMetadata(
options: SuiClientTypes.GetCoinMetadataOptions,
): Promise<SuiClientTypes.GetCoinMetadataResponse> {
const coinType = (await this.mvr.resolveType({ type: options.coinType })).type;
let response;
try {
({ response } = await this.#client.stateService.getCoinInfo({
coinType,
}));
} catch {
return { coinMetadata: null };
}
if (!response.metadata) {
return { coinMetadata: null };
}
return {
coinMetadata: {
id: response.metadata.id ?? null,
decimals: response.metadata.decimals ?? 0,
name: response.metadata.name ?? '',
symbol: response.metadata.symbol ?? '',
description: response.metadata.description ?? '',
iconUrl: response.metadata.iconUrl ?? null,
},
};
}
async listBalances(
options: SuiClientTypes.ListBalancesOptions,
): Promise<SuiClientTypes.ListBalancesResponse> {
const result = await this.#client.stateService.listBalances({
owner: options.owner,
pageToken: options.cursor ? fromBase64(options.cursor) : undefined,
pageSize: options.limit,
});
return {
hasNextPage: !!result.response.nextPageToken,
cursor: result.response.nextPageToken ? toBase64(result.response.nextPageToken) : null,
balances: result.response.balances.map((balance) => ({
balance: balance.balance?.toString() ?? '0',
coinType: balance.coinType!,
coinBalance: balance.coinBalance?.toString() ?? '0',
addressBalance: balance.addressBalance?.toString() ?? '0',
})),
};
}
async getTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
options: SuiClientTypes.GetTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
const paths = ['digest', 'transaction.digest', 'signatures', 'effects.status'];
if (options.include?.transaction) {
paths.push(
'transaction.sender',
'transaction.gas_payment',
'transaction.expiration',
'transaction.kind',
);
}
if (options.include?.bcs) {
paths.push('transaction.bcs');
}
if (options.include?.balanceChanges) {
paths.push('balance_changes');
}
if (options.include?.effects) {
paths.push('effects');
}
if (options.include?.events) {
paths.push('events');
}
if (options.include?.objectTypes) {
paths.push('effects.changed_objects.object_type');
paths.push('effects.changed_objects.object_id');
}
const { response } = await this.#client.ledgerService.getTransaction({
digest: options.digest,
readMask: {
paths,
},
});
if (!response.transaction) {
throw new Error(`Transaction ${options.digest} not found`);
}
return parseTransaction(response.transaction, options.include);
}
async executeTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
options: SuiClientTypes.ExecuteTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
const paths = ['digest', 'transaction.digest', 'signatures', 'effects.status'];
if (options.include?.transaction) {
paths.push(
'transaction.sender',
'transaction.gas_payment',
'transaction.expiration',
'transaction.kind',
);
}
if (options.include?.bcs) {
paths.push('transaction.bcs');
}
if (options.include?.balanceChanges) {
paths.push('balance_changes');
}
if (options.include?.effects) {
paths.push('effects');
}
if (options.include?.events) {
paths.push('events');
}
if (options.include?.objectTypes) {
paths.push('effects.changed_objects.object_type');
paths.push('effects.changed_objects.object_id');
}
const { response } = await this.#client.transactionExecutionService.executeTransaction({
transaction: {
bcs: {
value: options.transaction,
},
},
signatures: options.signatures.map((signature) => ({
bcs: {
value: fromBase64(signature),
},
signature: {
oneofKind: undefined,
},
})),
readMask: {
paths,
},
});
return parseTransaction(response.transaction!, options.include);
}
async simulateTransaction<Include extends SuiClientTypes.SimulateTransactionInclude = {}>(
options: SuiClientTypes.SimulateTransactionOptions<Include>,
): Promise<SuiClientTypes.SimulateTransactionResult<Include>> {
const paths = [
'transaction.digest',
'transaction.transaction.digest',
'transaction.signatures',
'transaction.effects.status',
];
if (options.include?.transaction) {
paths.push(
'transaction.transaction.sender',
'transaction.transaction.gas_payment',
'transaction.transaction.expiration',
'transaction.transaction.kind',
);
}
if (options.include?.bcs) {
paths.push('transaction.transaction.bcs');
}
if (options.include?.balanceChanges) {
paths.push('transaction.balance_changes');
}
if (options.include?.effects) {
paths.push('transaction.effects');
}
if (options.include?.events) {
paths.push('transaction.events');
}
if (options.include?.objectTypes) {
// Use effects.changed_objects to match JSON-RPC behavior (which uses objectChanges)
paths.push('transaction.effects.changed_objects.object_type');
paths.push('transaction.effects.changed_objects.object_id');
}
if (options.include?.commandResults) {
paths.push('command_outputs');
}
if (!(options.transaction instanceof Uint8Array)) {
await options.transaction.prepareForSerialization({ client: this });
}
const { response } = await this.#client.transactionExecutionService.simulateTransaction({
transaction:
options.transaction instanceof Uint8Array
? {
bcs: {
value: options.transaction,
},
}
: transactionToGrpcTransaction(options.transaction),
readMask: {
paths,
},
doGasSelection: false,
checks:
options.checksEnabled === false
? SimulateTransactionRequest_TransactionChecks.DISABLED
: SimulateTransactionRequest_TransactionChecks.ENABLED,
});
const transactionResult = parseTransaction(response.transaction!, options.include);
// Add command results if requested
const commandResults =
options.include?.commandResults && response.commandOutputs
? response.commandOutputs.map((output) => ({
returnValues: (output.returnValues ?? []).map((rv) => ({
bcs: rv.value?.value ?? null,
})),
mutatedReferences: (output.mutatedByRef ?? []).map((mr) => ({
bcs: mr.value?.value ?? null,
})),
}))
: undefined;
if (transactionResult.$kind === 'Transaction') {
return {
$kind: 'Transaction',
Transaction: transactionResult.Transaction,
commandResults:
commandResults as SuiClientTypes.SimulateTransactionResult<Include>['commandResults'],
};
} else {
return {
$kind: 'FailedTransaction',
FailedTransaction: transactionResult.FailedTransaction,
commandResults:
commandResults as SuiClientTypes.SimulateTransactionResult<Include>['commandResults'],
};
}
}
async getReferenceGasPrice(): Promise<SuiClientTypes.GetReferenceGasPriceResponse> {
const response = await this.#client.ledgerService.getEpoch({
readMask: {
paths: ['reference_gas_price'],
},
});
return {
referenceGasPrice: response.response.epoch?.referenceGasPrice?.toString() ?? '',
};
}
async getProtocolConfig(): Promise<SuiClientTypes.GetProtocolConfigResponse> {
const response = await this.#client.ledgerService.getEpoch({
readMask: { paths: ['protocol_config'] },
});
const protocolConfig = response.response.epoch?.protocolConfig;
if (!protocolConfig) {
throw new Error('Protocol config not found in response');
}
const featureFlags: Record<string, boolean> = { ...protocolConfig.featureFlags };
const attributes: Record<string, string | null> = {};
if (protocolConfig.attributes) {
for (const [key, value] of Object.entries(protocolConfig.attributes)) {
attributes[key] = value ?? null;
}
}
return {
protocolConfig: {
protocolVersion: protocolConfig.protocolVersion?.toString() ?? (null as never),
featureFlags,
attributes,
},
};
}
async getCurrentSystemState(): Promise<SuiClientTypes.GetCurrentSystemStateResponse> {
const response = await this.#client.ledgerService.getEpoch({
readMask: {
paths: [
'system_state.version',
'system_state.epoch',
'system_state.protocol_version',
'system_state.reference_gas_price',
'system_state.epoch_start_timestamp_ms',
'system_state.safe_mode',
'system_state.safe_mode_storage_rewards',
'system_state.safe_mode_computation_rewards',
'system_state.safe_mode_storage_rebates',
'system_state.safe_mode_non_refundable_storage_fee',
'system_state.parameters',
'system_state.storage_fund',
'system_state.stake_subsidy',
],
},
});
const epoch = response.response.epoch;
const systemState = epoch?.systemState;
if (!systemState) {
throw new Error('System state not found in response');
}
const startMs = epoch?.start?.seconds
? Number(epoch.start.seconds) * 1000 + Math.floor((epoch.start.nanos || 0) / 1_000_000)
: systemState.epochStartTimestampMs
? Number(systemState.epochStartTimestampMs)
: (null as never);
return {
systemState: {
systemStateVersion: systemState.version?.toString() ?? (null as never),
epoch: systemState.epoch?.toString() ?? (null as never),
protocolVersion: systemState.protocolVersion?.toString() ?? (null as never),
referenceGasPrice: systemState.referenceGasPrice?.toString() ?? (null as never),
epochStartTimestampMs: startMs.toString(),
safeMode: systemState.safeMode ?? false,
safeModeStorageRewards: systemState.safeModeStorageRewards?.toString() ?? (null as never),
safeModeComputationRewards:
systemState.safeModeComputationRewards?.toString() ?? (null as never),
safeModeStorageRebates: systemState.safeModeStorageRebates?.toString() ?? (null as never),
safeModeNonRefundableStorageFee:
systemState.safeModeNonRefundableStorageFee?.toString() ?? (null as never),
parameters: {
epochDurationMs: systemState.parameters?.epochDurationMs?.toString() ?? (null as never),
stakeSubsidyStartEpoch:
systemState.parameters?.stakeSubsidyStartEpoch?.toString() ?? (null as never),
maxValidatorCount:
systemState.parameters?.maxValidatorCount?.toString() ?? (null as never),
minValidatorJoiningStake:
systemState.parameters?.minValidatorJoiningStake?.toString() ?? (null as never),
validatorLowStakeThreshold:
systemState.parameters?.validatorLowStakeThreshold?.toString() ?? (null as never),
validatorLowStakeGracePeriod:
systemState.parameters?.validatorLowStakeGracePeriod?.toString() ?? (null as never),
},
storageFund: {
totalObjectStorageRebates:
systemState.storageFund?.totalObjectStorageRebates?.toString() ?? (null as never),
nonRefundableBalance:
systemState.storageFund?.nonRefundableBalance?.toString() ?? (null as never),
},
stakeSubsidy: {
balance: systemState.stakeSubsidy?.balance?.toString() ?? (null as never),
distributionCounter:
systemState.stakeSubsidy?.distributionCounter?.toString() ?? (null as never),
currentDistributionAmount:
systemState.stakeSubsidy?.currentDistributionAmount?.toString() ?? (null as never),
stakeSubsidyPeriodLength:
systemState.stakeSubsidy?.stakeSubsidyPeriodLength?.toString() ?? (null as never),
stakeSubsidyDecreaseRate:
systemState.stakeSubsidy?.stakeSubsidyDecreaseRate ?? (null as never),
},
},
};
}
async listDynamicFields(
options: SuiClientTypes.ListDynamicFieldsOptions,
): Promise<SuiClientTypes.ListDynamicFieldsResponse> {
return this.#client.listDynamicFields(options);
}
async verifyZkLoginSignature(
options: SuiClientTypes.VerifyZkLoginSignatureOptions,
): Promise<SuiClientTypes.ZkLoginVerifyResponse> {
const messageBytes = fromBase64(options.bytes);
// For PersonalMessage, the server expects BCS-encoded vector<u8>
// For TransactionData, the server expects the raw BCS-encoded TransactionData as-is
const messageValue =
options.intentScope === 'PersonalMessage'
? bcs.byteVector().serialize(messageBytes).toBytes()
: messageBytes;
const { response } = await this.#client.signatureVerificationService.verifySignature({
message: {
name: options.intentScope,
value: messageValue,
},
signature: {
bcs: {
value: fromBase64(options.signature),
},
signature: {
oneofKind: undefined,
},
},
address: options.address,
jwks: [],
});
return {
success: response.isValid ?? false,
errors: response.reason ? [response.reason] : [],
};
}
async defaultNameServiceName(
options: SuiClientTypes.DefaultNameServiceNameOptions,
): Promise<SuiClientTypes.DefaultNameServiceNameResponse> {
const name =
(
await this.#client.nameService.reverseLookupName({
address: options.address,
})
).response.record?.name ?? null;
return {
data: {
name,
},
};
}
async getMoveFunction(
options: SuiClientTypes.GetMoveFunctionOptions,
): Promise<SuiClientTypes.GetMoveFunctionResponse> {
const resolvedPackageId = (await this.mvr.resolvePackage({ package: options.packageId }))
.package;
const { response } = await this.#client.movePackageService.getFunction({
packageId: resolvedPackageId,
moduleName: options.moduleName,
name: options.name,
});
let visibility: 'public' | 'private' | 'friend' | 'unknown' = 'unknown';
switch (response.function?.visibility) {
case FunctionDescriptor_Visibility.PUBLIC:
visibility = 'public';
break;
case FunctionDescriptor_Visibility.PRIVATE:
visibility = 'private';
break;
case FunctionDescriptor_Visibility.FRIEND:
visibility = 'friend';
break;
}
return {
function: {
packageId: normalizeSuiAddress(resolvedPackageId),
moduleName: options.moduleName,
name: response.function?.name!,
visibility,
isEntry: response.function?.isEntry ?? false,
typeParameters:
response.function?.typeParameters?.map(({ constraints }) => ({
isPhantom: false,
constraints:
constraints.map((constraint) => {
switch (constraint) {
case Ability.COPY:
return 'copy';
case Ability.DROP:
return 'drop';
case Ability.STORE:
return 'store';
case Ability.KEY:
return 'key';
default:
return 'unknown';
}
}) ?? [],
})) ?? [],
parameters:
response.function?.parameters?.map((param) => parseNormalizedSuiMoveType(param)) ?? [],
returns: response.function?.returns?.map((ret) => parseNormalizedSuiMoveType(ret)) ?? [],
},
};
}
async getChainIdentifier(
_options?: SuiClientTypes.GetChainIdentifierOptions,
): Promise<SuiClientTypes.GetChainIdentifierResponse> {
return this.cache.read(['chainIdentifier'], async () => {
const { response } = await this.#client.ledgerService.getServiceInfo({});
if (!response.chainId) {
throw new Error('Chain identifier not found in service info');
}
return {
chainIdentifier: response.chainId,
};
});
}
resolveTransactionPlugin() {
const client = this.#client;
return async function resolveTransactionData(
transactionData: TransactionDataBuilder,
options: BuildTransactionOptions,
next: () => Promise<void>,
) {
const snapshot = transactionData.snapshot();
// If sender is not set, use a dummy address for resolution purposes
// The resolved transaction will not include the sender if it wasn't set originally
if (!snapshot.sender) {
snapshot.sender = '0x0000000000000000000000000000000000000000000000000000000000000000';
}
const grpcTransaction = transactionDataToGrpcTransaction(snapshot);
let response;
try {
const result = await client.transactionExecutionService.simulateTransaction({
transaction: grpcTransaction,
doGasSelection:
!options.onlyTransactionKind &&
(snapshot.gasData.budget == null || snapshot.gasData.payment == null),
readMask: {
paths: [
'transaction.transaction.sender',
'transaction.transaction.gas_payment',
'transaction.transaction.expiration',
'transaction.transaction.kind',
'transaction.effects.status',
],
},
});
response = result.response;
} catch (error) {
// https://github.com/timostamm/protobuf-ts/pull/739
if (error instanceof Error && error.message) {
throw new SimulationError(decodeURIComponent(error.message), { cause: error });
}
throw error;
}
if (
!options.onlyTransactionKind &&
response.transaction?.effects?.status &&
!response.transaction.effects.status.success
) {
const executionError = response.transaction.effects.status.error
? parseGrpcExecutionError(response.transaction.effects.status.error)
: undefined;
const errorMessage = executionError?.message ?? 'Transaction failed';
throw new SimulationError(`Transaction resolution failed: ${errorMessage}`, {
executionError,
});
}
if (!response.transaction?.transaction) {
throw new Error('simulateTransaction did not return resolved transaction data');
}
applyGrpcResolvedTransaction(transactionData, response.transaction.transaction, options);
return await next();
};
}
}
function mapDisplayProto(
include: boolean | undefined,
display: { output?: Value; errors?: Value } | undefined,
): SuiClientTypes.Display | null | undefined {
if (!include) return undefined;
if (display === undefined) return null;
return {
output:
display.output !== undefined
? (Value.toJson(display.output) as Record<string, string> | null)
: null,
errors:
display.errors !== undefined
? (Value.toJson(display.errors) as Record<string, string> | null)
: null,
};
}
function mapOwner(owner: Owner | null | undefined): SuiClientTypes.ObjectOwner | null {
if (!owner) {
return null;
}
if (owner.kind === Owner_OwnerKind.IMMUTABLE) {
return {
$kind: 'Immutable',
Immutable: true,
};
}
if (owner.kind === Owner_OwnerKind.ADDRESS) {
return {
AddressOwner: owner.address!,
$kind: 'AddressOwner',
};
}
if (owner.kind === Owner_OwnerKind.OBJECT) {
return {
$kind: 'ObjectOwner',
ObjectOwner: owner.address!,
};
}
if (owner.kind === Owner_OwnerKind.SHARED) {
return {
$kind: 'Shared',
Shared: {
initialSharedVersion: owner.version?.toString()!,
},
};
}
if (owner.kind === Owner_OwnerKind.CONSENSUS_ADDRESS) {
return {
$kind: 'ConsensusAddressOwner',
ConsensusAddressOwner: {
startVersion: owner.version?.toString()!,
owner: owner.address!,
},
};
}
throw new Error(
`Unknown owner kind ${JSON.stringify(owner, (_k, v) => (typeof v === 'bigint' ? v.toString() : v))}`,
);
}
function parseGrpcExecutionError(error: GrpcExecutionError): SuiClientTypes.ExecutionError {
const message = error.description ?? 'Unknown error';
const command = error.command != null ? Number(error.command) : undefined;
const details = error.errorDetails;
switch (details?.oneofKind) {
case 'abort': {
const abort = details.abort;
const cleverError = abort.cleverError;
return {
$kind: 'MoveAbort',
message: formatMoveAbortMessage({
command,
location: abort.location,
abortCode: String(abort.abortCode ?? 0n),
cleverError: cleverError
? {
lineNumber:
cleverError.lineNumber != null ? Number(cleverError.lineNumber) : undefined,
constantName: cleverError.constantName,
value:
cleverError.value?.oneofKind === 'rendered'
? cleverError.value.rendered
: undefined,
}
: undefined,
}),
command,
MoveAbort: parseMoveAbort(abort),
};
}
case 'sizeError':
return {
$kind: 'SizeError',
message,
command,
SizeError: {
name: mapErrorName(error.kind),
size: Number(details.sizeError.size ?? 0n),
maxSize: Number(details.sizeError.maxSize ?? 0n),
},
};
case 'commandArgumentError':
return {
$kind: 'CommandArgumentError',
message,
command,
CommandArgumentError: {
argument: details.commandArgumentError.argument ?? 0,
name: mapErrorName(details.commandArgumentError.kind),
},
};
case 'typeArgumentError':
return {
$kind: 'TypeArgumentError',
message,
command,
TypeArgumentError: {
typeArgument: details.typeArgumentError.typeArgument ?? 0,
name: mapErrorName(details.typeArgumentError.kind),
},
};
case 'packageUpgradeError':
return {
$kind: 'PackageUpgradeError',
message,
command,
PackageUpgradeError: {
name: mapErrorName(details.packageUpgradeError.kind),
packageId: details.packageUpgradeError.packageId,
digest: details.packageUpgradeError.digest,
},
};
case 'indexError':
return {
$kind: 'IndexError',
message,
command,
IndexError: {
index: details.indexError.index,
subresult: details.indexError.subresult,
},
};
case 'coinDenyListError':
return {
$kind: 'CoinDenyListError',
message,
command,
CoinDenyListError: {
name: mapErrorName(error.kind),
coinType: details.coinDenyListError.coinType!,
address: details.coinDenyListError.address,
},
};
case 'congestedObjects':
return {
$kind: 'CongestedObjects',
message,
command,
CongestedObjects: {
name: mapErrorName(error.kind),
objects: details.congestedObjects.objects,
},
};
case 'objectId':
return {
$kind: 'ObjectIdError',
message,
command,
ObjectIdError: {
name: mapErrorName(error.kind),
objectId: details.objectId,
},
};
default:
return {
$kind: 'Unknown',
message,
command,
Unknown: null,
};
}
}
function parseMoveAbort(abort: GrpcMoveAbort): SuiClientTypes.MoveAbort {
return {
abortCode: String(abort.abortCode ?? 0n),
location: { ...abort.location },
cleverError: abort.cleverError
? {
errorCode:
abort.cleverError.errorCode != null ? Number(abort.cleverError.errorCode) : undefined,
lineNumber:
abort.cleverError.lineNumber != null ? Number(abort.cleverError.lineNumber) : undefined,
constantName: abort.cleverError.constantName,
constantType: abort.cleverError.constantType,
value:
abort.cleverError.value?.oneofKind === 'rendered'
? abort.cleverError.value.rendered
: abort.cleverError.value?.oneofKind === 'raw'
? toBase64(abort.cleverError.value.raw)
: undefined,
}
: undefined,
};
}
function mapErrorName(
kind:
| ExecutionError_ExecutionErrorKind
| CommandArgumentError_CommandArgumentErrorKind
| TypeArgumentError_TypeArgumentErrorKind
| PackageUpgradeError_PackageUpgradeErrorKind
| undefined,
): string {
if (kind == null) {
return 'Unknown';
}
const name = CommandArgumentError_CommandArgumentErrorKind[kind];
if (!name || name.endsWith('_UNKNOWN')) {
return 'Unknown';
}
return name
.split('_')
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
.join('');
}
function mapIdOperation(
operation: ChangedObject_IdOperation | undefined,
): null | 'Created' | 'Deleted' | 'Unknown' | 'None' {
if (operation == null) {
return null;
}
switch (operation) {
case ChangedObject_IdOperation.CREATED:
return 'Created';
case ChangedObject_IdOperation.DELETED:
return 'Deleted';
case ChangedObject_IdOperation.NONE:
case ChangedObject_IdOperation.ID_OPERATION_UNKNOWN:
return 'None';
default:
operation satisfies never;
return 'Unknown';
}
}
function mapInputObjectState(
state: ChangedObject_InputObjectState | undefined,
): null | 'Exists' | 'DoesNotExist' | 'Unknown' {
if (state == null) {
return null;
}
switch (state) {
case ChangedObject_InputObjectState.EXISTS:
return 'Exists';
case ChangedObject_InputObjectState.DOES_NOT_EXIST:
return 'DoesNotExist';
case ChangedObject_InputObjectState.UNKNOWN:
return 'Unknown';
default:
state satisfies never;
return 'Unknown';
}
}
function mapOutputObjectState(
state: ChangedObject_OutputObjectState | undefined,
): null | 'ObjectWrite' | 'PackageWrite' | 'DoesNotExist' | 'AccumulatorWriteV1' | 'Unknown' {
if (state == null) {
return null;
}
switch (state) {
case ChangedObject_OutputObjectState.OBJECT_WRITE:
return 'ObjectWrite';
case ChangedObject_OutputObjectState.PACKAGE_WRITE:
return 'PackageWrite';
case ChangedObject_OutputObjectState.DOES_NOT_EXIST:
return 'DoesNotExist';
case ChangedObject_OutputObjectState.ACCUMULATOR_WRITE:
return 'AccumulatorWriteV1';
case ChangedObject_OutputObjectState.UNKNOWN:
return 'Unknown';
default:
state satisfies never;
return 'Unknown';
}
}
function mapUnchangedConsensusObjectKind(
kind: UnchangedConsensusObject_UnchangedConsensusObjectKind | undefined,
): null | SuiClientTypes.UnchangedConsensusObject['kind'] {
if (kind == null) {
return null;
}
switch (kind) {
case UnchangedConsensusObject_UnchangedConsensusObjectKind.UNCHANGED_CONSENSUS_OBJECT_KIND_UNKNOWN:
return 'Unknown';
case UnchangedConsensusObject_UnchangedConsensusObjectKind.READ_ONLY_ROOT:
return 'ReadOnlyRoot';
case UnchangedConsensusObject_UnchangedConsensusObjectKind.MUTATE_CONSENSUS_STREAM_ENDED:
return 'MutateConsensusStreamEnded';
case UnchangedConsensusObject_UnchangedConsensusObjectKind.READ_CONSENSUS_STREAM_ENDED:
return 'ReadConsensusStreamEnded';
case UnchangedConsensusObject_UnchangedConsensusObjectKind.CANCELED:
return 'Cancelled';
case UnchangedConsensusObject_UnchangedConsensusObjectKind.PER_EPOCH_CONFIG:
return 'PerEpochConfig';
default:
kind satisfies never;
return 'Unknown';
}
}
export function parseTransactionEffects({
effects,
}: {
effects: TransactionEffects | undefined;
}): SuiClientTypes.TransactionEffects | null {
if (!effects) {
return null;
}
const changedObjects = effects.changedObjects.map((change): SuiClientTypes.ChangedObject => {
return {
objectId: change.objectId!,
inputState: mapInputObjectState(change.inputState)!,
inputVersion: change.inputVersion?.toString() ?? null,
inputDigest: change.inputDigest ?? null,
inputOwner: mapOwner(change.inputOwner),
outputState: mapOutputObjectState(change.outputState)!,
outputVersion: change.outputVersion?.toString() ?? null,
outputDigest: change.outputDigest ?? null,
outputOwner: mapOwner(change.outputOwner),
idOperation: mapIdOperation(change.idOperation)!,
};
});
return {
bcs: effects.bcs?.value!,
version: 2,
status: effects.status?.success
? {
success: true,
error: null,
}
: {
success: false,
error: parseGrpcExecutionError(effects.status!.error!),
},
gasUsed: {
computationCost: effects.gasUsed?.computationCost?.toString()!,
storageCost: effects.gasUsed?.storageCost?.toString()!,
storageRebate: effects.gasUsed?.storageRebate?.toString()!,
nonRefundableStorageFee: effects.gasUsed?.nonRefundableStorageFee?.toString()!,
},
transactionDigest: effects.transactionDigest!,
gasObject: {
objectId: effects.gasObject?.objectId!,
inputState: mapInputObjectState(effects.gasObject?.inputState)!,
inputVersion: effects.gasObject?.inputVersion?.toString() ?? null,
inputDigest: effects.gasObject?.inputDigest ?? null,
inputOwner: mapOwner(effects.gasObject?.inputOwner),
outputState: mapOutputObjectState(effects.gasObject?.outputState)!,
outputVersion: effects.gasObject?.outputVersion?.toString() ?? null,
outputDigest: effects.gasObject?.outputDigest ?? null,
outputOwner: mapOwner(effects.gasObject?.outputOwner),
idOperation: mapIdOperation(effects.gasObject?.idOperation)!,
},
eventsDigest: effects.eventsDigest ?? null,
dependencies: effects.dependencies,
lamportVersion: effects.lamportVersion?.toString() ?? null,
changedObjects,
unchangedConsensusObjects: effects.unchangedConsensusObjects.map(
(object): SuiClientTypes.UnchangedConsensusObject => {
return {
kind: mapUnchangedConsensusObjectKind(object.kind)!,
// TODO: we are inconsistent about id vs objectId
objectId: object.objectId!,
version: object.version?.toString() ?? null,
digest: object.digest ?? null,
};
},
),
auxiliaryDataDigest: effects.auxiliaryDataDigest ?? null,
};
}
function parseTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
transaction: ExecutedTransaction,
include?: Include,
): SuiClientTypes.TransactionResult<Include> {
const objectTypes: Record<string, string> = {};
if (include?.objectTypes) {
transaction.effects?.changedObjects?.forEach((change) => {
if (change.objectId && change.objectType) {
objectTypes[change.objectId] = change.objectType;
}
});
}
let transactionData: SuiClientTypes.TransactionData | undefined;
if (include?.transaction) {
const tx = transaction.transaction;
if (!tx) {
throw new Error('Transaction data is required but missing from gRPC response');
}
const resolved = grpcTransactionToTransactionData(tx);
transactionData = {
gasData: resolved.gasData,
sender: resolved.sender,
expiration: resolved.expiration,
commands: resolved.commands,
inputs: resolved.inputs,
version: resolved.version,
};
}
const bcsBytes = include?.bcs ? transaction.transaction?.bcs?.value : undefined;
const effects = include?.effects
? parseTransactionEffects({
effects: transaction.effects,
})
: undefined;
const status: SuiClientTypes.ExecutionStatus = transaction.effects?.status?.success
? { success: true, error: null }
: {
success: false,
error: transaction.effects?.status?.error
? parseGrpcExecutionError(transaction.effects.status.error)
: {
$kind: 'Unknown',
message: 'Transaction failed',
Unknown: null,
},
};
const result: SuiClientTypes.Transaction<Include> = {
digest: transaction.digest!,
epoch: transaction.effects?.epoch?.toString() ?? null,
status,
effects: effects as SuiClientTypes.Transaction<Include>['effects'],
objectTypes: (include?.objectTypes
? objectTypes
: undefined) as SuiClientTypes.Transaction<Include>['objectTypes'],
transaction: transactionData as SuiClientTypes.Transaction<Include>['transaction'],
bcs: bcsBytes as SuiClientTypes.Transaction<Include>['bcs'],
signatures: transaction.signatures?.map((sig) => toBase64(sig.bcs?.value!)) ?? [],
balanceChanges: (include?.balanceChanges
? (transaction.balanceChanges?.map((change) => ({
coinType: change.coinType!,
address: change.address!,
amount: change.amount!,
})) ?? [])
: undefined) as SuiClientTypes.Transaction<Include>['balanceChanges'],
events: (include?.events
? (transaction.events?.events.map((event) => ({
packageId: normalizeSuiAddress(event.packageId!),
module: event.module!,
sender: normalizeSuiAddress(event.sender!),
eventType: event.eventType!,
bcs: event.contents?.value ?? new Uint8Array(),
json: event.json ? (Value.toJson(event.json) as Record<string, unknown>) : null,
})) ?? [])
: undefined) as SuiClientTypes.Transaction<Include>['events'],
};
return status.success
? {
$kind: 'Transaction',
Transaction: result,
}
: {
$kind: 'FailedTransaction',
FailedTransaction: result,
};
}
function parseNormalizedSuiMoveType(type: OpenSignature): SuiClientTypes.OpenSignature {
let reference: 'mutable' | 'immutable' | null = null;
if (type.reference === OpenSignature_Reference.IMMUTABLE) {
reference = 'immutable';
} else if (type.reference === OpenSignature_Reference.MUTABLE) {
reference = 'mutable';
}
return {
reference,
body: parseNormalizedSuiMoveTypeBody(type.body!),
};
}
function parseNormalizedSuiMoveTypeBody(type: OpenSignatureBody): SuiClientTypes.OpenSignatureBody {
switch (type.type) {
case OpenSignatureBody_Type.TYPE_UNKNOWN:
return { $kind: 'unknown' };
case OpenSignatureBody_Type.ADDRESS:
return { $kind: 'address' };
case OpenSignatureBody_Type.BOOL:
return { $kind: 'bool' };
case OpenSignatureBody_Type.U8:
return { $kind: 'u8' };
case OpenSignatureBody_Type.U16:
return { $kind: 'u16' };
case OpenSignatureBody_Type.U32:
return { $kind: 'u32' };
case OpenSignatureBody_Type.U64:
return { $kind: 'u64' };
case OpenSignatureBody_Type.U128:
return { $kind: 'u128' };
case OpenSignatureBody_Type.U256:
return { $kind: 'u256' };
case OpenSignatureBody_Type.VECTOR:
return {
$kind: 'vector',
vector: parseNormalizedSuiMoveTypeBody(type.typeParameterInstantiation[0]),
};
case OpenSignatureBody_Type.DATATYPE:
return {
$kind: 'datatype',
datatype: {
typeName: type.typeName!,
typeParameters: type.typeParameterInstantiation.map((t) =>
parseNormalizedSuiMoveTypeBody(t),
),
},
};
case OpenSignatureBody_Type.TYPE_PARAMETER:
return {
$kind: 'typeParameter',
index: type.typeParameter!,
};
default:
return { $kind: 'unknown' };
}
}