@mysten/sui
Version:
Sui TypeScript API
1,225 lines (1,112 loc) • 36.9 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { fromBase64, type InferBcsInput } from '@mysten/bcs';
import { bcs, TypeTagSerializer } from '../bcs/index.js';
import type {
DevInspectResults,
DryRunTransactionBlockResponse,
ExecutionStatus as JsonRpcExecutionStatus,
ObjectOwner,
SuiMoveAbilitySet,
SuiMoveAbort,
SuiMoveNormalizedType,
SuiMoveVisibility,
SuiObjectChange,
SuiObjectData,
SuiObjectDataFilter,
SuiTransactionBlockResponse,
TransactionEffects,
} from './types/index.js';
import { Transaction } from '../transactions/Transaction.js';
import { coreClientResolveTransactionPlugin } from '../client/core-resolver.js';
import { TransactionDataBuilder } from '../transactions/TransactionData.js';
import { chunk } from '@mysten/utils';
import { normalizeSuiAddress, normalizeStructTag } from '../utils/sui-types.js';
import { deriveDynamicFieldID } from '../utils/dynamic-fields.js';
import { SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS } from '../utils/constants.js';
import { CoreClient } from '../client/core.js';
import type { SuiClientTypes } from '../client/types.js';
import { ObjectError } from '../client/errors.js';
import {
formatMoveAbortMessage,
parseTransactionBcs,
parseTransactionEffectsBcs,
} from '../client/index.js';
import type { SuiJsonRpcClient } from './client.js';
const MAX_GAS = 50_000_000_000;
function parseJsonRpcExecutionStatus(
status: JsonRpcExecutionStatus,
abortError?: SuiMoveAbort | null,
): SuiClientTypes.ExecutionStatus {
if (status.status === 'success') {
return { success: true, error: null };
}
const rawMessage = status.error ?? 'Unknown';
if (abortError) {
const commandMatch = rawMessage.match(/in command (\d+)/);
const command = commandMatch ? parseInt(commandMatch[1], 10) : undefined;
const instructionMatch = rawMessage.match(/instruction:\s*(\d+)/);
const instruction = instructionMatch ? parseInt(instructionMatch[1], 10) : undefined;
const moduleParts = abortError.module_id?.split('::') ?? [];
const pkg = moduleParts[0] ? normalizeSuiAddress(moduleParts[0]) : undefined;
const module = moduleParts[1];
return {
success: false,
error: {
$kind: 'MoveAbort',
message: formatMoveAbortMessage({
command,
location:
pkg && module
? {
package: pkg,
module,
functionName: abortError.function ?? undefined,
instruction,
}
: undefined,
abortCode: String(abortError.error_code ?? 0),
cleverError: abortError.line != null ? { lineNumber: abortError.line } : undefined,
}),
command,
MoveAbort: {
abortCode: String(abortError.error_code ?? 0),
location: abortError.module_id
? {
package: normalizeSuiAddress(abortError.module_id.split('::')[0] ?? ''),
module: abortError.module_id.split('::')[1] ?? '',
functionName: abortError.function ?? undefined,
instruction,
}
: undefined,
},
},
};
}
return {
success: false,
error: {
$kind: 'Unknown',
message: rawMessage,
Unknown: null,
},
};
}
export class JSONRpcCoreClient extends CoreClient {
#jsonRpcClient: SuiJsonRpcClient;
constructor({
jsonRpcClient,
mvr,
}: {
jsonRpcClient: SuiJsonRpcClient;
mvr?: SuiClientTypes.MvrOptions;
}) {
super({ network: jsonRpcClient.network, base: jsonRpcClient, mvr });
this.#jsonRpcClient = jsonRpcClient;
}
async getObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
options: SuiClientTypes.GetObjectsOptions<Include>,
) {
const batches = chunk(options.objectIds, 50);
const results: SuiClientTypes.GetObjectsResponse<Include>['objects'] = [];
for (const batch of batches) {
const objects = await this.#jsonRpcClient.multiGetObjects({
ids: batch,
options: {
showOwner: true,
showType: true,
showBcs: options.include?.content || options.include?.objectBcs ? true : false,
showPreviousTransaction:
options.include?.previousTransaction || options.include?.objectBcs ? true : false,
showStorageRebate: options.include?.objectBcs ?? false,
showContent: options.include?.json ?? false,
showDisplay: options.include?.display ?? false,
},
signal: options.signal,
});
for (const [idx, object] of objects.entries()) {
if (object.error) {
results.push(ObjectError.fromResponse(object.error, batch[idx]));
} else {
results.push(parseObject(object.data!, options.include));
}
}
}
return {
objects: results,
};
}
async listOwnedObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
options: SuiClientTypes.ListOwnedObjectsOptions<Include>,
) {
let filter: SuiObjectDataFilter | null = null;
if (options.type) {
const parts = options.type.split('::');
if (parts.length === 1) {
filter = { Package: options.type };
} else if (parts.length === 2) {
filter = { MoveModule: { package: parts[0], module: parts[1] } };
} else {
filter = { StructType: options.type };
}
}
const objects = await this.#jsonRpcClient.getOwnedObjects({
owner: options.owner,
limit: options.limit,
cursor: options.cursor,
options: {
showOwner: true,
showType: true,
showBcs: options.include?.content || options.include?.objectBcs ? true : false,
showPreviousTransaction:
options.include?.previousTransaction || options.include?.objectBcs ? true : false,
showStorageRebate: options.include?.objectBcs ?? false,
showContent: options.include?.json ?? false,
showDisplay: options.include?.display ?? false,
},
filter,
signal: options.signal,
});
return {
objects: objects.data.map((result) => {
if (result.error) {
throw ObjectError.fromResponse(result.error);
}
return parseObject(result.data!, options.include);
}),
hasNextPage: objects.hasNextPage,
cursor: objects.nextCursor ?? null,
};
}
async listCoins(options: SuiClientTypes.ListCoinsOptions) {
const coins = await this.#jsonRpcClient.getCoins({
owner: options.owner,
coinType: options.coinType,
limit: options.limit,
cursor: options.cursor,
signal: options.signal,
});
return {
objects: coins.data.map(
(coin): SuiClientTypes.Coin => ({
objectId: coin.coinObjectId,
version: coin.version,
digest: coin.digest,
balance: coin.balance,
type: normalizeStructTag(`0x2::coin::Coin<${coin.coinType}>`),
owner: {
$kind: 'AddressOwner' as const,
AddressOwner: options.owner,
},
}),
),
hasNextPage: coins.hasNextPage,
cursor: coins.nextCursor ?? null,
};
}
async getBalance(options: SuiClientTypes.GetBalanceOptions) {
const balance = await this.#jsonRpcClient.getBalance({
owner: options.owner,
coinType: options.coinType,
signal: options.signal,
});
const addressBalance = balance.fundsInAddressBalance ?? '0';
const coinBalance = String(BigInt(balance.totalBalance) - BigInt(addressBalance));
return {
balance: {
coinType: normalizeStructTag(balance.coinType),
balance: balance.totalBalance,
coinBalance,
addressBalance,
},
};
}
async getCoinMetadata(
options: SuiClientTypes.GetCoinMetadataOptions,
): Promise<SuiClientTypes.GetCoinMetadataResponse> {
const coinType = (await this.mvr.resolveType({ type: options.coinType })).type;
const result = await this.#jsonRpcClient.getCoinMetadata({
coinType,
signal: options.signal,
});
if (!result) {
return { coinMetadata: null };
}
return {
coinMetadata: {
id: result.id ?? null,
decimals: result.decimals,
name: result.name,
symbol: result.symbol,
description: result.description,
iconUrl: result.iconUrl ?? null,
},
};
}
async listBalances(options: SuiClientTypes.ListBalancesOptions) {
const balances = await this.#jsonRpcClient.getAllBalances({
owner: options.owner,
signal: options.signal,
});
return {
balances: balances.map((balance) => {
const addressBalance = balance.fundsInAddressBalance ?? '0';
const coinBalance = String(BigInt(balance.totalBalance) - BigInt(addressBalance));
return {
coinType: normalizeStructTag(balance.coinType),
balance: balance.totalBalance,
coinBalance,
addressBalance,
};
}),
hasNextPage: false,
cursor: null,
};
}
async getTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
options: SuiClientTypes.GetTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
const transaction = await this.#jsonRpcClient.getTransactionBlock({
digest: options.digest,
options: {
// showRawInput is always needed to extract signatures from SenderSignedData
showRawInput: true,
// showEffects is always needed to get status
showEffects: true,
showObjectChanges: options.include?.objectTypes ?? false,
showRawEffects: options.include?.effects ?? false,
showEvents: options.include?.events ?? false,
showBalanceChanges: options.include?.balanceChanges ?? false,
},
signal: options.signal,
});
return parseTransaction(transaction, options.include);
}
async executeTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
options: SuiClientTypes.ExecuteTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
const transaction = await this.#jsonRpcClient.executeTransactionBlock({
transactionBlock: options.transaction,
signature: options.signatures,
options: {
// showRawInput is always needed to extract signatures from SenderSignedData
showRawInput: true,
// showEffects is always needed to get status
showEffects: true,
showRawEffects: options.include?.effects ?? false,
showEvents: options.include?.events ?? false,
showObjectChanges: options.include?.objectTypes ?? false,
showBalanceChanges: options.include?.balanceChanges ?? false,
},
signal: options.signal,
});
return parseTransaction(transaction, options.include);
}
async simulateTransaction<Include extends SuiClientTypes.SimulateTransactionInclude = {}>(
options: SuiClientTypes.SimulateTransactionOptions<Include>,
): Promise<SuiClientTypes.SimulateTransactionResult<Include>> {
if (!(options.transaction instanceof Uint8Array)) {
await options.transaction.build({ client: this, onlyTransactionKind: true });
}
const tx = Transaction.from(options.transaction);
const data =
options.transaction instanceof Uint8Array
? null
: TransactionDataBuilder.restore(options.transaction.getData());
const transactionBytes = data
? data.build({
overrides: {
gasData: {
budget: data.gasData.budget ?? String(MAX_GAS),
price: data.gasData.price ?? String(await this.#jsonRpcClient.getReferenceGasPrice()),
payment: data.gasData.payment ?? [],
},
},
})
: (options.transaction as Uint8Array);
const sender = tx.getData().sender ?? normalizeSuiAddress('0x0');
const checksDisabled = options.checksEnabled === false;
let dryRunResult: DryRunTransactionBlockResponse | null = null;
try {
dryRunResult = await this.#jsonRpcClient.dryRunTransactionBlock({
transactionBlock: transactionBytes,
signal: options.signal,
});
} catch (e) {
if (!checksDisabled) {
throw e;
}
}
let devInspectResult: DevInspectResults | null = null;
if (options.include?.commandResults || checksDisabled) {
try {
devInspectResult = await this.#jsonRpcClient.devInspectTransactionBlock({
sender,
transactionBlock: tx,
signal: options.signal,
});
} catch {}
}
const dryRunFailed = !dryRunResult || dryRunResult.effects.status.status !== 'success';
const effectsSource =
checksDisabled && dryRunFailed && devInspectResult
? devInspectResult
: (dryRunResult ?? devInspectResult);
if (!effectsSource) {
throw new Error('simulateTransaction failed: no results from dryRun or devInspect');
}
const { effects, objectTypes } = parseTransactionEffectsJson({
effects: effectsSource.effects,
objectChanges: dryRunResult?.objectChanges ?? [],
});
const transactionData: SuiClientTypes.Transaction<Include> = {
digest: TransactionDataBuilder.getDigestFromBytes(transactionBytes),
epoch: null,
status: effects.status,
effects: (options.include?.effects
? effects
: undefined) as SuiClientTypes.Transaction<Include>['effects'],
objectTypes: (options.include?.objectTypes
? objectTypes
: undefined) as SuiClientTypes.Transaction<Include>['objectTypes'],
signatures: [],
transaction: (options.include?.transaction
? parseTransactionBcs(
options.transaction instanceof Uint8Array
? options.transaction
: await options.transaction
.build({
client: this,
})
.catch(() => null as never),
)
: undefined) as SuiClientTypes.Transaction<Include>['transaction'],
bcs: (options.include?.bcs
? transactionBytes
: undefined) as SuiClientTypes.Transaction<Include>['bcs'],
balanceChanges: (options.include?.balanceChanges && dryRunResult
? dryRunResult.balanceChanges.map((change) => ({
coinType: normalizeStructTag(change.coinType),
address: parseOwnerAddress(change.owner)!,
amount: change.amount,
}))
: undefined) as SuiClientTypes.Transaction<Include>['balanceChanges'],
events: (options.include?.events
? (effectsSource.events?.map((event) => ({
packageId: event.packageId,
module: event.transactionModule,
sender: event.sender,
eventType: event.type,
bcs: 'bcs' in event ? fromBase64(event.bcs) : new Uint8Array(),
json: (event.parsedJson as Record<string, unknown>) ?? null,
})) ?? [])
: undefined) as SuiClientTypes.Transaction<Include>['events'],
};
let commandResults: SuiClientTypes.CommandResult[] | undefined;
if (options.include?.commandResults && devInspectResult?.results) {
commandResults = devInspectResult.results.map((result) => ({
returnValues: (result.returnValues ?? []).map(([bytes]) => ({
bcs: new Uint8Array(bytes),
})),
mutatedReferences: (result.mutableReferenceOutputs ?? []).map(([, bytes]) => ({
bcs: new Uint8Array(bytes),
})),
}));
}
return effects.status.success
? {
$kind: 'Transaction',
Transaction: transactionData,
commandResults:
commandResults as SuiClientTypes.SimulateTransactionResult<Include>['commandResults'],
}
: {
$kind: 'FailedTransaction',
FailedTransaction: transactionData,
commandResults:
commandResults as SuiClientTypes.SimulateTransactionResult<Include>['commandResults'],
};
}
async getReferenceGasPrice(options?: SuiClientTypes.GetReferenceGasPriceOptions) {
const referenceGasPrice = await this.#jsonRpcClient.getReferenceGasPrice({
signal: options?.signal,
});
return {
referenceGasPrice: String(referenceGasPrice),
};
}
async getProtocolConfig(
options?: SuiClientTypes.GetProtocolConfigOptions,
): Promise<SuiClientTypes.GetProtocolConfigResponse> {
const result = await this.#jsonRpcClient.getProtocolConfig({ signal: options?.signal });
const attributes: Record<string, string | null> = {};
for (const [key, value] of Object.entries(result.attributes)) {
if (value === null) {
attributes[key] = null;
} else if ('u16' in value) {
attributes[key] = value.u16;
} else if ('u32' in value) {
attributes[key] = value.u32;
} else if ('u64' in value) {
attributes[key] = value.u64;
} else if ('f64' in value) {
attributes[key] = value.f64;
} else if ('bool' in value) {
attributes[key] = value.bool;
} else {
const entries = Object.entries(value);
attributes[key] = entries.length === 1 ? String(entries[0][1]) : JSON.stringify(value);
}
}
return {
protocolConfig: {
protocolVersion: result.protocolVersion,
featureFlags: { ...result.featureFlags },
attributes,
},
};
}
async getCurrentSystemState(
options?: SuiClientTypes.GetCurrentSystemStateOptions,
): Promise<SuiClientTypes.GetCurrentSystemStateResponse> {
const systemState = await this.#jsonRpcClient.getLatestSuiSystemState({
signal: options?.signal,
});
return {
systemState: {
systemStateVersion: systemState.systemStateVersion,
epoch: systemState.epoch,
protocolVersion: systemState.protocolVersion,
referenceGasPrice: systemState.referenceGasPrice?.toString() ?? (null as never),
epochStartTimestampMs: systemState.epochStartTimestampMs,
safeMode: systemState.safeMode,
safeModeStorageRewards: systemState.safeModeStorageRewards,
safeModeComputationRewards: systemState.safeModeComputationRewards,
safeModeStorageRebates: systemState.safeModeStorageRebates,
safeModeNonRefundableStorageFee: systemState.safeModeNonRefundableStorageFee,
parameters: {
epochDurationMs: systemState.epochDurationMs,
stakeSubsidyStartEpoch: systemState.stakeSubsidyStartEpoch,
maxValidatorCount: systemState.maxValidatorCount,
minValidatorJoiningStake: systemState.minValidatorJoiningStake,
validatorLowStakeThreshold: systemState.validatorLowStakeThreshold,
validatorLowStakeGracePeriod: systemState.validatorLowStakeGracePeriod,
},
storageFund: {
totalObjectStorageRebates: systemState.storageFundTotalObjectStorageRebates,
nonRefundableBalance: systemState.storageFundNonRefundableBalance,
},
stakeSubsidy: {
balance: systemState.stakeSubsidyBalance,
distributionCounter: systemState.stakeSubsidyDistributionCounter,
currentDistributionAmount: systemState.stakeSubsidyCurrentDistributionAmount,
stakeSubsidyPeriodLength: systemState.stakeSubsidyPeriodLength,
stakeSubsidyDecreaseRate: systemState.stakeSubsidyDecreaseRate,
},
},
};
}
async listDynamicFields(options: SuiClientTypes.ListDynamicFieldsOptions) {
const dynamicFields = await this.#jsonRpcClient.getDynamicFields({
parentId: options.parentId,
limit: options.limit,
cursor: options.cursor,
});
return {
dynamicFields: dynamicFields.data.map((dynamicField): SuiClientTypes.DynamicFieldEntry => {
const isDynamicObject = dynamicField.type === 'DynamicObject';
const fullType = isDynamicObject
? `0x2::dynamic_field::Field<0x2::dynamic_object_field::Wrapper<${dynamicField.name.type}>, 0x2::object::ID>`
: `0x2::dynamic_field::Field<${dynamicField.name.type}, ${dynamicField.objectType}>`;
const bcsBytes = fromBase64(dynamicField.bcsName);
const derivedNameType = isDynamicObject
? `0x2::dynamic_object_field::Wrapper<${dynamicField.name.type}>`
: dynamicField.name.type;
return {
$kind: isDynamicObject ? 'DynamicObject' : 'DynamicField',
fieldId: deriveDynamicFieldID(options.parentId, derivedNameType, bcsBytes),
type: normalizeStructTag(fullType),
name: {
type: dynamicField.name.type,
bcs: bcsBytes,
},
valueType: dynamicField.objectType,
childId: isDynamicObject ? dynamicField.objectId : undefined,
} as SuiClientTypes.DynamicFieldEntry;
}),
hasNextPage: dynamicFields.hasNextPage,
cursor: dynamicFields.nextCursor,
};
}
async verifyZkLoginSignature(options: SuiClientTypes.VerifyZkLoginSignatureOptions) {
const result = await this.#jsonRpcClient.verifyZkLoginSignature({
bytes: options.bytes,
signature: options.signature,
intentScope: options.intentScope,
author: options.address,
});
return {
success: result.success,
errors: result.errors,
};
}
async defaultNameServiceName(
options: SuiClientTypes.DefaultNameServiceNameOptions,
): Promise<SuiClientTypes.DefaultNameServiceNameResponse> {
const name = (await this.#jsonRpcClient.resolveNameServiceNames(options)).data[0];
return {
data: {
name,
},
};
}
resolveTransactionPlugin() {
return coreClientResolveTransactionPlugin;
}
async getMoveFunction(
options: SuiClientTypes.GetMoveFunctionOptions,
): Promise<SuiClientTypes.GetMoveFunctionResponse> {
const resolvedPackageId = (await this.mvr.resolvePackage({ package: options.packageId }))
.package;
const result = await this.#jsonRpcClient.getNormalizedMoveFunction({
package: resolvedPackageId,
module: options.moduleName,
function: options.name,
});
return {
function: {
packageId: normalizeSuiAddress(resolvedPackageId),
moduleName: options.moduleName,
name: options.name,
visibility: parseVisibility(result.visibility),
isEntry: result.isEntry,
typeParameters: result.typeParameters.map((abilities) => ({
isPhantom: false,
constraints: parseAbilities(abilities),
})),
parameters: result.parameters.map((param) => parseNormalizedSuiMoveType(param)),
returns: result.return.map((ret) => parseNormalizedSuiMoveType(ret)),
},
};
}
async getChainIdentifier(
_options?: SuiClientTypes.GetChainIdentifierOptions,
): Promise<SuiClientTypes.GetChainIdentifierResponse> {
return this.cache.read(['chainIdentifier'], async () => {
const checkpoint = await this.#jsonRpcClient.getCheckpoint({ id: '0' });
return {
chainIdentifier: checkpoint.digest,
};
});
}
}
function serializeObjectToBcs(object: SuiObjectData): Uint8Array | undefined {
if (object.bcs?.dataType !== 'moveObject') {
return undefined;
}
try {
// Normalize the type string to ensure consistent address formatting (0x2 vs 0x00...02)
const typeStr = normalizeStructTag(object.bcs.type);
let moveObjectType: InferBcsInput<typeof bcs.MoveObjectType>;
// Normalize constants for comparison
const normalizedSuiFramework = normalizeSuiAddress(SUI_FRAMEWORK_ADDRESS);
const gasCoinType = normalizeStructTag(
`${SUI_FRAMEWORK_ADDRESS}::coin::Coin<${SUI_FRAMEWORK_ADDRESS}::sui::SUI>`,
);
const stakedSuiType = normalizeStructTag(`${SUI_SYSTEM_ADDRESS}::staking_pool::StakedSui`);
const coinPrefix = `${normalizedSuiFramework}::coin::Coin<`;
if (typeStr === gasCoinType) {
moveObjectType = { GasCoin: null };
} else if (typeStr === stakedSuiType) {
moveObjectType = { StakedSui: null };
} else if (typeStr.startsWith(coinPrefix)) {
const innerTypeMatch = typeStr.match(
new RegExp(
`${normalizedSuiFramework.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}::coin::Coin<(.+)>$`,
),
);
if (innerTypeMatch) {
const innerTypeTag = TypeTagSerializer.parseFromStr(innerTypeMatch[1], true);
moveObjectType = { Coin: innerTypeTag };
} else {
throw new Error('Failed to parse Coin type');
}
} else {
const typeTag = TypeTagSerializer.parseFromStr(typeStr, true);
if (typeof typeTag !== 'object' || !('struct' in typeTag)) {
throw new Error('Expected struct type tag');
}
moveObjectType = { Other: typeTag.struct };
}
const contents = fromBase64(object.bcs.bcsBytes);
const owner = convertOwnerToBcs(object.owner!);
return bcs.Object.serialize({
data: {
Move: {
type: moveObjectType,
hasPublicTransfer: object.bcs.hasPublicTransfer,
version: object.bcs.version,
contents,
},
},
owner,
previousTransaction: object.previousTransaction!,
storageRebate: object.storageRebate!,
}).toBytes();
} catch {
// If serialization fails, return undefined
return undefined;
}
}
function parseObject<Include extends SuiClientTypes.ObjectInclude = {}>(
object: SuiObjectData,
include?: Include,
): SuiClientTypes.Object<Include> {
const bcsContent =
object.bcs?.dataType === 'moveObject' ? fromBase64(object.bcs.bcsBytes) : undefined;
const objectBcs = include?.objectBcs ? serializeObjectToBcs(object) : undefined;
// Package objects have type "package" which is not a struct tag, so don't normalize it
const type =
object.type && object.type.includes('::')
? normalizeStructTag(object.type)
: (object.type ?? '');
const jsonContent =
include?.json && object.content?.dataType === 'moveObject'
? (object.content.fields as Record<string, unknown>)
: include?.json
? null
: undefined;
const displayData = include?.display
? object.display?.data != null
? { output: object.display.data as Record<string, string>, errors: null }
: null
: undefined;
return {
objectId: object.objectId,
version: object.version,
digest: object.digest,
type,
content: (include?.content
? bcsContent
: undefined) as SuiClientTypes.Object<Include>['content'],
owner: parseOwner(object.owner!),
previousTransaction: (include?.previousTransaction
? (object.previousTransaction ?? undefined)
: 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'],
};
}
function parseOwner(owner: ObjectOwner): SuiClientTypes.ObjectOwner {
if (owner === 'Immutable') {
return {
$kind: 'Immutable',
Immutable: true,
};
}
if ('ConsensusAddressOwner' in owner) {
return {
$kind: 'ConsensusAddressOwner',
ConsensusAddressOwner: {
owner: owner.ConsensusAddressOwner.owner,
startVersion: owner.ConsensusAddressOwner.start_version,
},
};
}
if ('AddressOwner' in owner) {
return {
$kind: 'AddressOwner',
AddressOwner: owner.AddressOwner,
};
}
if ('ObjectOwner' in owner) {
return {
$kind: 'ObjectOwner',
ObjectOwner: owner.ObjectOwner,
};
}
if ('Shared' in owner) {
return {
$kind: 'Shared',
Shared: {
initialSharedVersion: owner.Shared.initial_shared_version,
},
};
}
throw new Error(`Unknown owner type: ${JSON.stringify(owner)}`);
}
function convertOwnerToBcs(owner: ObjectOwner) {
if (owner === 'Immutable') {
return { Immutable: null };
}
if ('AddressOwner' in owner) {
return { AddressOwner: owner.AddressOwner };
}
if ('ObjectOwner' in owner) {
return { ObjectOwner: owner.ObjectOwner };
}
if ('Shared' in owner) {
return {
Shared: { initialSharedVersion: owner.Shared.initial_shared_version },
};
}
if (typeof owner === 'object' && owner !== null && 'ConsensusAddressOwner' in owner) {
return {
ConsensusAddressOwner: {
startVersion: owner.ConsensusAddressOwner.start_version,
owner: owner.ConsensusAddressOwner.owner,
},
};
}
throw new Error(`Unknown owner type: ${JSON.stringify(owner)}`);
}
function parseOwnerAddress(owner: ObjectOwner): string | null {
if (owner === 'Immutable') {
return null;
}
if ('ConsensusAddressOwner' in owner) {
return owner.ConsensusAddressOwner.owner;
}
if ('AddressOwner' in owner) {
return owner.AddressOwner;
}
if ('ObjectOwner' in owner) {
return owner.ObjectOwner;
}
if ('Shared' in owner) {
return null;
}
throw new Error(`Unknown owner type: ${JSON.stringify(owner)}`);
}
function parseTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
transaction: SuiTransactionBlockResponse,
include?: Include,
): SuiClientTypes.TransactionResult<Include> {
const objectTypes: Record<string, string> = {};
if (include?.objectTypes) {
transaction.objectChanges?.forEach((change) => {
if (change.type !== 'published') {
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
}
});
}
let transactionData: SuiClientTypes.TransactionData | undefined;
let signatures: string[] = [];
let bcsBytes: Uint8Array | undefined;
if (transaction.rawTransaction) {
const parsedTx = bcs.SenderSignedData.parse(fromBase64(transaction.rawTransaction))[0];
signatures = parsedTx.txSignatures;
if (include?.transaction || include?.bcs) {
const bytes = bcs.TransactionData.serialize(parsedTx.intentMessage.value).toBytes();
if (include?.bcs) {
bcsBytes = bytes;
}
if (include?.transaction) {
const data = TransactionDataBuilder.restore({
version: 2,
sender: parsedTx.intentMessage.value.V1.sender,
expiration: parsedTx.intentMessage.value.V1.expiration,
gasData: parsedTx.intentMessage.value.V1.gasData,
inputs: parsedTx.intentMessage.value.V1.kind.ProgrammableTransaction!.inputs,
commands: parsedTx.intentMessage.value.V1.kind.ProgrammableTransaction!.commands,
});
transactionData = { ...data };
}
}
}
// Get status from JSON-RPC response
const status: SuiClientTypes.ExecutionStatus = transaction.effects?.status
? parseJsonRpcExecutionStatus(transaction.effects.status, transaction.effects.abortError)
: {
success: false,
error: {
$kind: 'Unknown',
message: 'Unknown',
Unknown: null,
},
};
const effectsBytes = transaction.rawEffects ? new Uint8Array(transaction.rawEffects) : null;
const result: SuiClientTypes.Transaction<Include> = {
digest: transaction.digest,
epoch: transaction.effects?.executedEpoch ?? null,
status,
effects: (include?.effects && effectsBytes
? parseTransactionEffectsBcs(effectsBytes)
: undefined) 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,
balanceChanges: (include?.balanceChanges
? (transaction.balanceChanges?.map((change) => ({
coinType: normalizeStructTag(change.coinType),
address: parseOwnerAddress(change.owner)!,
amount: change.amount,
})) ?? [])
: undefined) as SuiClientTypes.Transaction<Include>['balanceChanges'],
events: (include?.events
? (transaction.events?.map((event) => ({
packageId: event.packageId,
module: event.transactionModule,
sender: event.sender,
eventType: event.type,
bcs: 'bcs' in event ? fromBase64(event.bcs) : new Uint8Array(),
json: (event.parsedJson as Record<string, unknown>) ?? null,
})) ?? [])
: undefined) as SuiClientTypes.Transaction<Include>['events'],
};
return status.success
? {
$kind: 'Transaction',
Transaction: result,
}
: {
$kind: 'FailedTransaction',
FailedTransaction: result,
};
}
function parseTransactionEffectsJson({
bytes,
effects,
objectChanges,
}: {
bytes?: Uint8Array;
effects: TransactionEffects;
objectChanges: SuiObjectChange[] | null;
}): {
effects: SuiClientTypes.TransactionEffects;
objectTypes: Record<string, string>;
} {
const changedObjects: SuiClientTypes.ChangedObject[] = [];
const unchangedConsensusObjects: SuiClientTypes.UnchangedConsensusObject[] = [];
const objectTypes: Record<string, string> = {};
objectChanges?.forEach((change) => {
switch (change.type) {
case 'published':
changedObjects.push({
objectId: change.packageId,
inputState: 'DoesNotExist',
inputVersion: null,
inputDigest: null,
inputOwner: null,
outputState: 'PackageWrite',
outputVersion: change.version,
outputDigest: change.digest,
outputOwner: null,
idOperation: 'Created',
});
break;
case 'transferred':
changedObjects.push({
objectId: change.objectId,
inputState: 'Exists',
inputVersion: change.version,
inputDigest: change.digest,
inputOwner: {
$kind: 'AddressOwner' as const,
AddressOwner: change.sender,
},
outputState: 'ObjectWrite',
outputVersion: change.version,
outputDigest: change.digest,
outputOwner: parseOwner(change.recipient),
idOperation: 'None',
});
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
break;
case 'mutated':
changedObjects.push({
objectId: change.objectId,
inputState: 'Exists',
inputVersion: change.previousVersion,
inputDigest: null,
inputOwner: parseOwner(change.owner),
outputState: 'ObjectWrite',
outputVersion: change.version,
outputDigest: change.digest,
outputOwner: parseOwner(change.owner),
idOperation: 'None',
});
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
break;
case 'deleted':
changedObjects.push({
objectId: change.objectId,
inputState: 'Exists',
inputVersion: change.version,
inputDigest: effects.deleted?.find((d) => d.objectId === change.objectId)?.digest ?? null,
inputOwner: null,
outputState: 'DoesNotExist',
outputVersion: null,
outputDigest: null,
outputOwner: null,
idOperation: 'Deleted',
});
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
break;
case 'wrapped':
changedObjects.push({
objectId: change.objectId,
inputState: 'Exists',
inputVersion: change.version,
inputDigest: null,
inputOwner: {
$kind: 'AddressOwner' as const,
AddressOwner: change.sender,
},
outputState: 'ObjectWrite',
outputVersion: change.version,
outputDigest:
effects.wrapped?.find((w) => w.objectId === change.objectId)?.digest ?? null,
outputOwner: {
$kind: 'ObjectOwner' as const,
ObjectOwner: change.sender,
},
idOperation: 'None',
});
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
break;
case 'created':
changedObjects.push({
objectId: change.objectId,
inputState: 'DoesNotExist',
inputVersion: null,
inputDigest: null,
inputOwner: null,
outputState: 'ObjectWrite',
outputVersion: change.version,
outputDigest: change.digest,
outputOwner: parseOwner(change.owner),
idOperation: 'Created',
});
objectTypes[change.objectId] = normalizeStructTag(change.objectType);
break;
}
});
return {
objectTypes,
effects: {
bcs: bytes ?? null,
version: 2,
status: parseJsonRpcExecutionStatus(effects.status, effects.abortError),
gasUsed: effects.gasUsed,
transactionDigest: effects.transactionDigest,
gasObject: {
objectId: effects.gasObject?.reference.objectId,
inputState: 'Exists',
inputVersion: null,
inputDigest: null,
inputOwner: null,
outputState: 'ObjectWrite',
outputVersion: effects.gasObject.reference.version,
outputDigest: effects.gasObject.reference.digest,
outputOwner: parseOwner(effects.gasObject.owner),
idOperation: 'None',
},
eventsDigest: effects.eventsDigest ?? null,
dependencies: effects.dependencies ?? [],
lamportVersion: effects.gasObject.reference.version,
changedObjects,
unchangedConsensusObjects,
auxiliaryDataDigest: null,
},
};
}
function parseNormalizedSuiMoveType(type: SuiMoveNormalizedType): SuiClientTypes.OpenSignature {
if (typeof type !== 'string') {
if ('Reference' in type) {
return {
reference: 'immutable',
body: parseNormalizedSuiMoveTypeBody(type.Reference),
};
}
if ('MutableReference' in type) {
return {
reference: 'mutable',
body: parseNormalizedSuiMoveTypeBody(type.MutableReference),
};
}
}
return {
reference: null,
body: parseNormalizedSuiMoveTypeBody(type),
};
}
function parseNormalizedSuiMoveTypeBody(
type: SuiMoveNormalizedType,
): SuiClientTypes.OpenSignatureBody {
switch (type) {
case 'Address':
return { $kind: 'address' };
case 'Bool':
return { $kind: 'bool' };
case 'U8':
return { $kind: 'u8' };
case 'U16':
return { $kind: 'u16' };
case 'U32':
return { $kind: 'u32' };
case 'U64':
return { $kind: 'u64' };
case 'U128':
return { $kind: 'u128' };
case 'U256':
return { $kind: 'u256' };
}
if (typeof type === 'string') {
throw new Error(`Unknown type: ${type}`);
}
if ('Vector' in type) {
return {
$kind: 'vector',
vector: parseNormalizedSuiMoveTypeBody(type.Vector),
};
}
if ('Struct' in type) {
return {
$kind: 'datatype',
datatype: {
typeName: `${normalizeSuiAddress(type.Struct.address)}::${type.Struct.module}::${type.Struct.name}`,
typeParameters: type.Struct.typeArguments.map((t) => parseNormalizedSuiMoveTypeBody(t)),
},
};
}
if ('TypeParameter' in type) {
return {
$kind: 'typeParameter',
index: type.TypeParameter,
};
}
throw new Error(`Unknown type: ${JSON.stringify(type)}`);
}
function parseAbilities(abilitySet: SuiMoveAbilitySet): SuiClientTypes.Ability[] {
return abilitySet.abilities.map((ability) => {
switch (ability) {
case 'Copy':
return 'copy';
case 'Drop':
return 'drop';
case 'Store':
return 'store';
case 'Key':
return 'key';
default:
return 'unknown';
}
});
}
function parseVisibility(visibility: SuiMoveVisibility): SuiClientTypes.Visibility {
switch (visibility) {
case 'Public':
return 'public';
case 'Private':
return 'private';
case 'Friend':
return 'friend';
default:
return 'unknown';
}
}