@mysten/sui
Version:
Sui TypeScript API
349 lines (304 loc) • 11.1 kB
text/typescript
// 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<string, unknown>,
Variables = Record<string, unknown>,
> =
| string
| DocumentNode
| TypedDocumentString<Result, Variables>
| TypedDocumentNode<Result, Variables>
| TadaDocumentNode<Result, Variables>;
export type GraphQLQueryOptions<
Result = Record<string, unknown>,
Variables = Record<string, unknown>,
> = {
query: GraphQLDocument<Result, Variables>;
operationName?: string;
extensions?: Record<string, unknown>;
signal?: AbortSignal;
} & (Variables extends { [key: string]: never }
? { variables?: Variables }
: {
variables: Variables;
});
export type GraphQLQueryResult<Result = Record<string, unknown>> = {
data?: Result;
errors?: GraphQLResponseErrors;
extensions?: Record<string, unknown>;
};
export type GraphQLResponseErrors = Array<{
message: string;
locations?: { line: number; column: number }[];
path?: (string | number)[];
}>;
export interface SuiGraphQLClientOptions<Queries extends Record<string, GraphQLDocument>> {
url: string;
fetch?: typeof fetch;
headers?: Record<string, string>;
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<Include extends DynamicFieldInclude = {}> =
SuiClientTypes.DynamicFieldEntry & {
value: Include extends { value: true } ? SuiClientTypes.DynamicFieldValue : undefined;
};
export interface ListDynamicFieldsWithValueResponse<Include extends DynamicFieldInclude = {}> {
hasNextPage: boolean;
cursor: string | null;
dynamicFields: DynamicFieldEntryWithValue<Include>[];
}
export class SuiGraphQLClient<Queries extends Record<string, GraphQLDocument> = {}>
extends BaseClient
implements SuiClientTypes.TransportMethods
{
#url: string;
#queries: Queries;
#headers: Record<string, string>;
#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<Queries>) {
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<Result = Record<string, unknown>, Variables = Record<string, unknown>>(
options: GraphQLQueryOptions<Result, Variables>,
): Promise<GraphQLQueryResult<Result>> {
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<keyof Queries, string>,
Result = Queries[Query] extends GraphQLDocument<infer R, unknown> ? R : Record<string, unknown>,
Variables = Queries[Query] extends GraphQLDocument<unknown, infer V>
? V
: Record<string, unknown>,
>(
query: Query,
options: Omit<GraphQLQueryOptions<Result, Variables>, 'query'>,
): Promise<GraphQLQueryResult<Result>> {
return this.query({
...(options as { variables: Record<string, unknown> }),
query: this.#queries[query]!,
}) as Promise<GraphQLQueryResult<Result>>;
}
getObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
input: SuiClientTypes.GetObjectsOptions<Include>,
): Promise<SuiClientTypes.GetObjectsResponse<Include>> {
return this.core.getObjects(input);
}
getObject<Include extends SuiClientTypes.ObjectInclude = {}>(
input: SuiClientTypes.GetObjectOptions<Include>,
): Promise<SuiClientTypes.GetObjectResponse<Include>> {
return this.core.getObject(input);
}
listCoins(input: SuiClientTypes.ListCoinsOptions): Promise<SuiClientTypes.ListCoinsResponse> {
return this.core.listCoins(input);
}
listOwnedObjects<Include extends SuiClientTypes.ObjectInclude = {}>(
input: SuiClientTypes.ListOwnedObjectsOptions<Include>,
): Promise<SuiClientTypes.ListOwnedObjectsResponse<Include>> {
return this.core.listOwnedObjects(input);
}
getBalance(input: SuiClientTypes.GetBalanceOptions): Promise<SuiClientTypes.GetBalanceResponse> {
return this.core.getBalance(input);
}
listBalances(
input: SuiClientTypes.ListBalancesOptions,
): Promise<SuiClientTypes.ListBalancesResponse> {
return this.core.listBalances(input);
}
getCoinMetadata(
input: SuiClientTypes.GetCoinMetadataOptions,
): Promise<SuiClientTypes.GetCoinMetadataResponse> {
return this.core.getCoinMetadata(input);
}
getTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
input: SuiClientTypes.GetTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
return this.core.getTransaction(input);
}
executeTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
input: SuiClientTypes.ExecuteTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
return this.core.executeTransaction(input);
}
signAndExecuteTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
input: SuiClientTypes.SignAndExecuteTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
return this.core.signAndExecuteTransaction(input);
}
waitForTransaction<Include extends SuiClientTypes.TransactionInclude = {}>(
input: SuiClientTypes.WaitForTransactionOptions<Include>,
): Promise<SuiClientTypes.TransactionResult<Include>> {
return this.core.waitForTransaction(input);
}
simulateTransaction<Include extends SuiClientTypes.SimulateTransactionInclude = {}>(
input: SuiClientTypes.SimulateTransactionOptions<Include>,
): Promise<SuiClientTypes.SimulateTransactionResult<Include>> {
return this.core.simulateTransaction(input);
}
getReferenceGasPrice(): Promise<SuiClientTypes.GetReferenceGasPriceResponse> {
return this.core.getReferenceGasPrice();
}
async listDynamicFields<Include extends DynamicFieldInclude = {}>(
input: SuiClientTypes.ListDynamicFieldsOptions & { include?: Include & DynamicFieldInclude },
): Promise<ListDynamicFieldsWithValueResponse<Include>> {
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<Include> => {
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<Include>['value'],
} as DynamicFieldEntryWithValue<Include>;
}),
cursor: result.pageInfo.endCursor ?? null,
hasNextPage: result.pageInfo.hasNextPage,
};
}
getDynamicField(
input: SuiClientTypes.GetDynamicFieldOptions,
): Promise<SuiClientTypes.GetDynamicFieldResponse> {
return this.core.getDynamicField(input);
}
getMoveFunction(
input: SuiClientTypes.GetMoveFunctionOptions,
): Promise<SuiClientTypes.GetMoveFunctionResponse> {
return this.core.getMoveFunction(input);
}
resolveTransactionPlugin(): TransactionPlugin {
return this.core.resolveTransactionPlugin();
}
verifyZkLoginSignature(
input: SuiClientTypes.VerifyZkLoginSignatureOptions,
): Promise<SuiClientTypes.ZkLoginVerifyResponse> {
return this.core.verifyZkLoginSignature(input);
}
defaultNameServiceName(
input: SuiClientTypes.DefaultNameServiceNameOptions,
): Promise<SuiClientTypes.DefaultNameServiceNameResponse> {
return this.core.defaultNameServiceName(input);
}
}