UNPKG

@mysten/sui

Version:
309 lines (259 loc) 9.67 kB
// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 import { TypeTagSerializer } from '../bcs/type-tag-serializer.js'; import type { TransactionPlugin } from '../transactions/index.js'; import { deriveDynamicFieldID } from '../utils/dynamic-fields.js'; import { normalizeStructTag, parseStructTag, SUI_ADDRESS_LENGTH } from '../utils/sui-types.js'; import { BaseClient } from './client.js'; import type { ClientWithExtensions, SuiClientTypes } from './types.js'; import { MvrClient } from './mvr.js'; import { bcs } from '../bcs/index.js'; export type ClientWithCoreApi = ClientWithExtensions<{ core: CoreClient; }>; export interface CoreClientOptions extends SuiClientTypes.SuiClientOptions { base: BaseClient; mvr?: SuiClientTypes.MvrOptions; } const DEFAULT_MVR_URLS: Record<string, string> = { mainnet: 'https://mainnet.mvr.mystenlabs.com', testnet: 'https://testnet.mvr.mystenlabs.com', }; export abstract class CoreClient extends BaseClient implements SuiClientTypes.TransportMethods { core = this; mvr: SuiClientTypes.MvrMethods; constructor(options: CoreClientOptions) { super(options); this.mvr = new MvrClient({ cache: this.cache.scope('core.mvr'), url: options.mvr?.url ?? DEFAULT_MVR_URLS[this.network], pageSize: options.mvr?.pageSize, overrides: options.mvr?.overrides, }); } abstract getObjects<Include extends SuiClientTypes.ObjectInclude = {}>( options: SuiClientTypes.GetObjectsOptions<Include>, ): Promise<SuiClientTypes.GetObjectsResponse<Include>>; async getObject<Include extends SuiClientTypes.ObjectInclude = {}>( options: SuiClientTypes.GetObjectOptions<Include>, ): Promise<SuiClientTypes.GetObjectResponse<Include>> { const { objectId } = options; const { objects: [result], } = await this.getObjects({ objectIds: [objectId], signal: options.signal, include: options.include, }); if (result instanceof Error) { throw result; } return { object: result }; } abstract listCoins( options: SuiClientTypes.ListCoinsOptions, ): Promise<SuiClientTypes.ListCoinsResponse>; abstract listOwnedObjects<Include extends SuiClientTypes.ObjectInclude = {}>( options: SuiClientTypes.ListOwnedObjectsOptions<Include>, ): Promise<SuiClientTypes.ListOwnedObjectsResponse<Include>>; abstract getBalance( options: SuiClientTypes.GetBalanceOptions, ): Promise<SuiClientTypes.GetBalanceResponse>; abstract listBalances( options: SuiClientTypes.ListBalancesOptions, ): Promise<SuiClientTypes.ListBalancesResponse>; abstract getCoinMetadata( options: SuiClientTypes.GetCoinMetadataOptions, ): Promise<SuiClientTypes.GetCoinMetadataResponse>; abstract getTransaction<Include extends SuiClientTypes.TransactionInclude = {}>( options: SuiClientTypes.GetTransactionOptions<Include>, ): Promise<SuiClientTypes.TransactionResult<Include>>; abstract executeTransaction<Include extends SuiClientTypes.TransactionInclude = {}>( options: SuiClientTypes.ExecuteTransactionOptions<Include>, ): Promise<SuiClientTypes.TransactionResult<Include>>; abstract simulateTransaction<Include extends SuiClientTypes.SimulateTransactionInclude = {}>( options: SuiClientTypes.SimulateTransactionOptions<Include>, ): Promise<SuiClientTypes.SimulateTransactionResult<Include>>; abstract getReferenceGasPrice( options?: SuiClientTypes.GetReferenceGasPriceOptions, ): Promise<SuiClientTypes.GetReferenceGasPriceResponse>; abstract getCurrentSystemState( options?: SuiClientTypes.GetCurrentSystemStateOptions, ): Promise<SuiClientTypes.GetCurrentSystemStateResponse>; abstract getProtocolConfig( options?: SuiClientTypes.GetProtocolConfigOptions, ): Promise<SuiClientTypes.GetProtocolConfigResponse>; abstract getChainIdentifier( options?: SuiClientTypes.GetChainIdentifierOptions, ): Promise<SuiClientTypes.GetChainIdentifierResponse>; abstract listDynamicFields( options: SuiClientTypes.ListDynamicFieldsOptions, ): Promise<SuiClientTypes.ListDynamicFieldsResponse>; abstract resolveTransactionPlugin(): TransactionPlugin; abstract verifyZkLoginSignature( options: SuiClientTypes.VerifyZkLoginSignatureOptions, ): Promise<SuiClientTypes.ZkLoginVerifyResponse>; abstract getMoveFunction( options: SuiClientTypes.GetMoveFunctionOptions, ): Promise<SuiClientTypes.GetMoveFunctionResponse>; abstract defaultNameServiceName( options: SuiClientTypes.DefaultNameServiceNameOptions, ): Promise<SuiClientTypes.DefaultNameServiceNameResponse>; async getDynamicField( options: SuiClientTypes.GetDynamicFieldOptions, ): Promise<SuiClientTypes.GetDynamicFieldResponse> { const normalizedNameType = TypeTagSerializer.parseFromStr( ( await this.core.mvr.resolveType({ type: options.name.type, }) ).type, ); const fieldId = deriveDynamicFieldID(options.parentId, normalizedNameType, options.name.bcs); const { objects: [fieldObject], } = await this.getObjects({ objectIds: [fieldId], signal: options.signal, include: { previousTransaction: true, content: true, }, }); if (fieldObject instanceof Error) { throw fieldObject; } const fieldType = parseStructTag(fieldObject.type); const content = await fieldObject.content; const nameTypeParam = fieldType.typeParams[0]; const isDynamicObject = typeof nameTypeParam !== 'string' && nameTypeParam.module === 'dynamic_object_field' && nameTypeParam.name === 'Wrapper'; const valueBcs = content.slice(SUI_ADDRESS_LENGTH + options.name.bcs.length); const valueType = typeof fieldType.typeParams[1] === 'string' ? fieldType.typeParams[1] : normalizeStructTag(fieldType.typeParams[1]); return { dynamicField: { $kind: isDynamicObject ? 'DynamicObject' : 'DynamicField', fieldId: fieldObject.objectId, digest: fieldObject.digest, version: fieldObject.version, type: fieldObject.type, previousTransaction: fieldObject.previousTransaction, name: { type: typeof nameTypeParam === 'string' ? nameTypeParam : normalizeStructTag(nameTypeParam), bcs: options.name.bcs, }, value: { type: valueType, bcs: valueBcs, }, childId: isDynamicObject ? bcs.Address.parse(valueBcs) : undefined, } as SuiClientTypes.GetDynamicFieldResponse['dynamicField'], }; } async getDynamicObjectField<Include extends SuiClientTypes.ObjectInclude = {}>( options: SuiClientTypes.GetDynamicObjectFieldOptions<Include>, ): Promise<SuiClientTypes.GetDynamicObjectFieldResponse<Include>> { const resolvedNameType = ( await this.core.mvr.resolveType({ type: options.name.type, }) ).type; const wrappedType = `0x2::dynamic_object_field::Wrapper<${resolvedNameType}>`; const { dynamicField } = await this.getDynamicField({ parentId: options.parentId, name: { type: wrappedType, bcs: options.name.bcs, }, signal: options.signal, }); const { object } = await this.getObject({ objectId: dynamicField.childId!, signal: options.signal, include: options.include, }); return { object }; } async waitForTransaction<Include extends SuiClientTypes.TransactionInclude = {}>( options: SuiClientTypes.WaitForTransactionOptions<Include>, ): Promise<SuiClientTypes.TransactionResult<Include>> { const { signal, timeout = 60 * 1000, pollSchedule, include } = options; const digest = 'result' in options && options.result ? (options.result.Transaction ?? options.result.FailedTransaction)!.digest : options.digest; const abortSignal = signal ? AbortSignal.any([AbortSignal.timeout(timeout), signal]) : AbortSignal.timeout(timeout); const abortPromise = new Promise((_, reject) => { abortSignal.addEventListener('abort', () => reject(abortSignal.reason)); }); abortPromise.catch(() => { // Swallow unhandled rejections that might be thrown after early return }); // Default schedule tuned to testnet measurements: // - Fullnode (gRPC/JSON-RPC): p50=130ms, p95=330ms // - GraphQL indexer: p50=1300ms, p95=1430ms // After schedule exhausted, repeats the last interval. const schedule = pollSchedule ?? [0, 300, 600, 1500, 3500]; const t0 = Date.now(); let scheduleIndex = 0; const lastInterval = schedule.length > 0 ? schedule[schedule.length - 1] - (schedule[schedule.length - 2] ?? 0) : 2_000; while (true) { if (scheduleIndex < schedule.length) { const remaining = t0 + schedule[scheduleIndex] - Date.now(); scheduleIndex++; if (remaining > 0) { await Promise.race([ new Promise((resolve) => setTimeout(resolve, remaining)), abortPromise, ]); } } else { await Promise.race([ new Promise((resolve) => setTimeout(resolve, lastInterval)), abortPromise, ]); } abortSignal.throwIfAborted(); try { return await this.getTransaction({ digest, include, signal: abortSignal, }); } catch {} } } async signAndExecuteTransaction<Include extends SuiClientTypes.TransactionInclude = {}>({ transaction, signer, additionalSignatures = [], ...input }: SuiClientTypes.SignAndExecuteTransactionOptions<Include>): Promise< SuiClientTypes.TransactionResult<Include> > { let transactionBytes; if (transaction instanceof Uint8Array) { transactionBytes = transaction; } else { transaction.setSenderIfNotSet(signer.toSuiAddress()); transactionBytes = await transaction.build({ client: this }); } const { signature } = await signer.signTransaction(transactionBytes); return this.executeTransaction({ transaction: transactionBytes, signatures: [signature, ...additionalSignatures], ...input, }); } }