// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; import type { TadaDocumentNode } from 'gql.tada'; import type { DocumentNode } from 'graphql'; import { print } from 'graphql'; import { BaseClient } from '../client/index.js'; import type { SuiClientTypes } from '../client/index.js'; import { GraphQLCoreClient } from './core.js'; import type { TypedDocumentString } from './generated/queries.js'; import { GetDynamicFieldsDocument } from './generated/queries.js'; import { fromBase64 } from '@mysten/utils'; import { normalizeStructTag } from '../utils/sui-types.js'; import { deriveDynamicFieldID } from '../utils/dynamic-fields.js'; import type { TransactionPlugin } from '../transactions/index.js'; export type GraphQLDocument< Result = Record, Variables = Record, > = | string | DocumentNode | TypedDocumentString | TypedDocumentNode | TadaDocumentNode; export type GraphQLQueryOptions< Result = Record, Variables = Record, > = { query: GraphQLDocument; operationName?: string; extensions?: Record; signal?: AbortSignal; } & (Variables extends { [key: string]: never } ? { variables?: Variables } : { variables: Variables; }); export type GraphQLQueryResult> = { data?: Result; errors?: GraphQLResponseErrors; extensions?: Record; }; export type GraphQLResponseErrors = Array<{ message: string; locations?: { line: number; column: number }[]; path?: (string | number)[]; }>; export interface SuiGraphQLClientOptions> { url: string; fetch?: typeof fetch; headers?: Record; queries?: Queries; network: SuiClientTypes.Network; mvr?: SuiClientTypes.MvrOptions; } export class SuiGraphQLRequestError extends Error {} const SUI_CLIENT_BRAND = Symbol.for('@mysten/SuiGraphQLClient') as never; export function isSuiGraphQLClient(client: unknown): client is SuiGraphQLClient { return ( typeof client === 'object' && client !== null && (client as any)[SUI_CLIENT_BRAND] === true ); } export interface DynamicFieldInclude { value?: boolean; } export type DynamicFieldEntryWithValue = SuiClientTypes.DynamicFieldEntry & { value: Include extends { value: true } ? SuiClientTypes.DynamicFieldValue : undefined; }; export interface ListDynamicFieldsWithValueResponse { hasNextPage: boolean; cursor: string | null; dynamicFields: DynamicFieldEntryWithValue[]; } export class SuiGraphQLClient = {}> extends BaseClient implements SuiClientTypes.TransportMethods { #url: string; #queries: Queries; #headers: Record; #fetch: typeof fetch; core: GraphQLCoreClient; get mvr(): SuiClientTypes.MvrMethods { return this.core.mvr; } get [SUI_CLIENT_BRAND]() { return true; } constructor({ url, fetch: fetchFn = fetch, headers = {}, queries = {} as Queries, network, mvr, }: SuiGraphQLClientOptions) { super({ network, }); this.#url = url; this.#queries = queries; this.#headers = headers; this.#fetch = (...args) => fetchFn(...args); this.core = new GraphQLCoreClient({ graphqlClient: this, mvr, }); } async query, Variables = Record>( options: GraphQLQueryOptions, ): Promise> { const res = await this.#fetch(this.#url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.#headers, }, body: JSON.stringify({ query: typeof options.query === 'string' || options.query instanceof String ? String(options.query) : print(options.query), variables: options.variables, extensions: options.extensions, operationName: options.operationName, }), signal: options.signal, }); if (!res.ok) { throw new SuiGraphQLRequestError(`GraphQL request failed: ${res.statusText} (${res.status})`); } return await res.json(); } async execute< const Query extends Extract, Result = Queries[Query] extends GraphQLDocument ? R : Record, Variables = Queries[Query] extends GraphQLDocument ? V : Record, >( query: Query, options: Omit, 'query'>, ): Promise> { return this.query({ ...(options as { variables: Record }), query: this.#queries[query]!, }) as Promise>; } getObjects( input: SuiClientTypes.GetObjectsOptions, ): Promise> { return this.core.getObjects(input); } getObject( input: SuiClientTypes.GetObjectOptions, ): Promise> { return this.core.getObject(input); } listCoins(input: SuiClientTypes.ListCoinsOptions): Promise { return this.core.listCoins(input); } listOwnedObjects( input: SuiClientTypes.ListOwnedObjectsOptions, ): Promise> { return this.core.listOwnedObjects(input); } getBalance(input: SuiClientTypes.GetBalanceOptions): Promise { return this.core.getBalance(input); } listBalances( input: SuiClientTypes.ListBalancesOptions, ): Promise { return this.core.listBalances(input); } getCoinMetadata( input: SuiClientTypes.GetCoinMetadataOptions, ): Promise { return this.core.getCoinMetadata(input); } getTransaction( input: SuiClientTypes.GetTransactionOptions, ): Promise> { return this.core.getTransaction(input); } executeTransaction( input: SuiClientTypes.ExecuteTransactionOptions, ): Promise> { return this.core.executeTransaction(input); } signAndExecuteTransaction( input: SuiClientTypes.SignAndExecuteTransactionOptions, ): Promise> { return this.core.signAndExecuteTransaction(input); } waitForTransaction( input: SuiClientTypes.WaitForTransactionOptions, ): Promise> { return this.core.waitForTransaction(input); } simulateTransaction( input: SuiClientTypes.SimulateTransactionOptions, ): Promise> { return this.core.simulateTransaction(input); } getReferenceGasPrice(): Promise { return this.core.getReferenceGasPrice(); } async listDynamicFields( input: SuiClientTypes.ListDynamicFieldsOptions & { include?: Include & DynamicFieldInclude }, ): Promise> { const includeValue = input.include?.value ?? false; const { data, errors } = await this.query({ query: GetDynamicFieldsDocument, variables: { parentId: input.parentId, first: input.limit, cursor: input.cursor, includeValue, }, }); if (errors?.length) { throw errors.length === 1 ? new Error(errors[0].message) : new AggregateError(errors.map((e) => new Error(e.message))); } const result = data?.address?.dynamicFields; if (!result) { throw new Error('Missing response data'); } return { dynamicFields: result.nodes.map((dynamicField): DynamicFieldEntryWithValue => { const valueType = dynamicField.value?.__typename === 'MoveObject' ? dynamicField.value.contents?.type?.repr! : dynamicField.value?.type?.repr!; const isDynamicObject = dynamicField.value?.__typename === 'MoveObject'; const derivedNameType = isDynamicObject ? `0x2::dynamic_object_field::Wrapper<${dynamicField.name?.type?.repr}>` : dynamicField.name?.type?.repr!; let value: SuiClientTypes.DynamicFieldValue | undefined; if (includeValue) { let valueBcs: Uint8Array; if (dynamicField.value?.__typename === 'MoveValue') { valueBcs = fromBase64(dynamicField.value.bcs ?? ''); } else if (dynamicField.value?.__typename === 'MoveObject') { valueBcs = fromBase64(dynamicField.value.contents?.bcs ?? ''); } else { valueBcs = new Uint8Array(); } value = { type: valueType, bcs: valueBcs }; } return { $kind: isDynamicObject ? 'DynamicObject' : 'DynamicField', fieldId: deriveDynamicFieldID( input.parentId, derivedNameType, fromBase64(dynamicField.name?.bcs!), ), type: normalizeStructTag( isDynamicObject ? `0x2::dynamic_field::Field<0x2::dynamic_object_field::Wrapper<${dynamicField.name?.type?.repr}>,0x2::object::ID>` : `0x2::dynamic_field::Field<${dynamicField.name?.type?.repr},${valueType}>`, ), name: { type: dynamicField.name?.type?.repr!, bcs: fromBase64(dynamicField.name?.bcs!), }, valueType, childId: isDynamicObject && dynamicField.value?.__typename === 'MoveObject' ? dynamicField.value.address : undefined, value: (includeValue ? value : undefined) as DynamicFieldEntryWithValue['value'], } as DynamicFieldEntryWithValue; }), cursor: result.pageInfo.endCursor ?? null, hasNextPage: result.pageInfo.hasNextPage, }; } getDynamicField( input: SuiClientTypes.GetDynamicFieldOptions, ): Promise { return this.core.getDynamicField(input); } getMoveFunction( input: SuiClientTypes.GetMoveFunctionOptions, ): Promise { return this.core.getMoveFunction(input); } resolveTransactionPlugin(): TransactionPlugin { return this.core.resolveTransactionPlugin(); } verifyZkLoginSignature( input: SuiClientTypes.VerifyZkLoginSignatureOptions, ): Promise { return this.core.verifyZkLoginSignature(input); } defaultNameServiceName( input: SuiClientTypes.DefaultNameServiceNameOptions, ): Promise { return this.core.defaultNameServiceName(input); } }