UNPKG

@mysten/sui

Version:

Sui TypeScript API(Work in Progress)

422 lines (379 loc) 11.8 kB
// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 import { Experimental_CoreClient } from '../core.js'; import type { Experimental_SuiClientTypes } from '../types.js'; import type { GraphQLQueryOptions, SuiGraphQLClient } from '../../graphql/client.js'; import type { Object_Owner_FieldsFragment, Object_FieldsFragment, Transaction_FieldsFragment, } from '../../graphql/generated/queries.js'; import { DryRunTransactionBlockDocument, ExecuteTransactionBlockDocument, GetAllBalancesDocument, GetBalanceDocument, GetCoinsDocument, GetDynamicFieldsDocument, GetOwnedObjectsDocument, GetReferenceGasPriceDocument, GetTransactionBlockDocument, MultiGetObjectsDocument, VerifyZkLoginSignatureDocument, ZkLoginIntentScope, } from '../../graphql/generated/queries.js'; import { ObjectError } from '../errors.js'; import { fromBase64, toBase64 } from '@mysten/utils'; import { normalizeStructTag, normalizeSuiAddress } from '../../utils/sui-types.js'; import { deriveDynamicFieldID } from '../../utils/dynamic-fields.js'; import { parseTransactionEffects } from './utils.js'; export class GraphQLTransport extends Experimental_CoreClient { #graphqlClient: SuiGraphQLClient; constructor(graphqlClient: SuiGraphQLClient) { super({ network: graphqlClient.network }); this.#graphqlClient = graphqlClient; } async #graphqlQuery< Result = Record<string, unknown>, Variables = Record<string, unknown>, Data = Result, >( options: GraphQLQueryOptions<Result, Variables>, getData?: (result: Result) => Data, ): Promise<NonNullable<Data>> { const { data, errors } = await this.#graphqlClient.query(options); handleGraphQLErrors(errors); const extractedData = data && (getData ? getData(data) : data); if (extractedData == null) { throw new Error('Missing response data'); } return extractedData as NonNullable<Data>; } async getObjects( options: Experimental_SuiClientTypes.GetObjectsOptions, ): Promise<Experimental_SuiClientTypes.GetObjectsResponse> { const objects: Object_FieldsFragment[] = []; let hasNextPage = true; let cursor: string | null = null; while (hasNextPage) { const objectsPage = await this.#graphqlQuery( { query: MultiGetObjectsDocument, variables: { objectIds: options.objectIds, cursor, }, }, (result) => result.objects, ); objects.push(...objectsPage.nodes); hasNextPage = objectsPage.pageInfo.hasNextPage; cursor = (objectsPage.pageInfo.endCursor ?? null) as string | null; } return { objects: options.objectIds .map((id) => normalizeSuiAddress(id)) .map( (id) => objects.find((obj) => obj.address === id) ?? new ObjectError('notFound', `Object ${id} not found`), ) .map((obj) => { if (obj instanceof ObjectError) { return obj; } return { id: obj.address, version: obj.version, digest: obj.digest!, owner: mapOwner(obj.owner!), type: obj.asMoveObject?.contents?.type?.repr!, content: fromBase64(obj.asMoveObject?.contents?.bcs!), }; }), }; } async getOwnedObjects( options: Experimental_SuiClientTypes.GetOwnedObjectsOptions, ): Promise<Experimental_SuiClientTypes.GetOwnedObjectsResponse> { const objects = await this.#graphqlQuery( { query: GetOwnedObjectsDocument, variables: { owner: options.address, limit: options.limit, cursor: options.cursor, filter: options.type ? { type: options.type } : undefined, }, }, (result) => result.address?.objects, ); return { objects: objects.nodes.map((obj) => ({ id: obj.address, version: obj.version, digest: obj.digest!, owner: mapOwner(obj.owner!), type: obj.contents?.type?.repr!, content: fromBase64(obj.contents?.bcs!), })), hasNextPage: objects.pageInfo.hasNextPage, cursor: objects.pageInfo.endCursor ?? null, }; } async getCoins( options: Experimental_SuiClientTypes.GetCoinsOptions, ): Promise<Experimental_SuiClientTypes.GetCoinsResponse> { const coins = await this.#graphqlQuery( { query: GetCoinsDocument, variables: { owner: options.address, cursor: options.cursor, first: options.limit, type: options.coinType, }, }, (result) => result.address?.coins, ); return { cursor: coins.pageInfo.endCursor ?? null, hasNextPage: coins.pageInfo.hasNextPage, objects: coins.nodes.map((coin) => ({ id: coin.address, version: coin.version, digest: coin.digest!, owner: mapOwner(coin.owner!), type: coin.contents?.type?.repr!, balance: coin.coinBalance, content: fromBase64(coin.contents?.bcs!), })), }; } async getBalance( options: Experimental_SuiClientTypes.GetBalanceOptions, ): Promise<Experimental_SuiClientTypes.GetBalanceResponse> { const result = await this.#graphqlQuery( { query: GetBalanceDocument, variables: { owner: options.address, type: options.coinType }, }, (result) => result.address?.balance, ); return { balance: { coinType: result.coinType.repr, balance: result.totalBalance, }, }; } async getAllBalances( options: Experimental_SuiClientTypes.GetAllBalancesOptions, ): Promise<Experimental_SuiClientTypes.GetAllBalancesResponse> { const balances = await this.#graphqlQuery( { query: GetAllBalancesDocument, variables: { owner: options.address }, }, (result) => result.address?.balances, ); return { cursor: balances.pageInfo.endCursor ?? null, hasNextPage: balances.pageInfo.hasNextPage, balances: balances.nodes.map((balance) => ({ coinType: balance.coinType.repr, balance: balance.totalBalance, })), }; } async getTransaction( options: Experimental_SuiClientTypes.GetTransactionOptions, ): Promise<Experimental_SuiClientTypes.GetTransactionResponse> { const result = await this.#graphqlQuery( { query: GetTransactionBlockDocument, variables: { digest: options.digest }, }, (result) => result.transactionBlock, ); return { transaction: parseTransaction(result), }; } async executeTransaction( options: Experimental_SuiClientTypes.ExecuteTransactionOptions, ): Promise<Experimental_SuiClientTypes.ExecuteTransactionResponse> { const result = await this.#graphqlQuery( { query: ExecuteTransactionBlockDocument, variables: { txBytes: toBase64(options.transaction), signatures: options.signatures }, }, (result) => result.executeTransactionBlock, ); if (result.errors) { if (result.errors.length === 1) { throw new Error(result.errors[0]); } throw new AggregateError(result.errors.map((error) => new Error(error))); } return { transaction: parseTransaction(result.effects.transactionBlock!), }; } async dryRunTransaction( options: Experimental_SuiClientTypes.DryRunTransactionOptions, ): Promise<Experimental_SuiClientTypes.DryRunTransactionResponse> { const result = await this.#graphqlQuery( { query: DryRunTransactionBlockDocument, variables: { txBytes: toBase64(options.transaction) }, }, (result) => result.dryRunTransactionBlock, ); if (result.error) { throw new Error(result.error); } return { transaction: parseTransaction(result.transaction!), }; } async getReferenceGasPrice(): Promise<Experimental_SuiClientTypes.GetReferenceGasPriceResponse> { const result = await this.#graphqlQuery( { query: GetReferenceGasPriceDocument, }, (result) => result.epoch?.referenceGasPrice, ); return { referenceGasPrice: result.referenceGasPrice, }; } async getDynamicFields( options: Experimental_SuiClientTypes.GetDynamicFieldsOptions, ): Promise<Experimental_SuiClientTypes.GetDynamicFieldsResponse> { const result = await this.#graphqlQuery( { query: GetDynamicFieldsDocument, variables: { parentId: options.parentId }, }, (result) => result.owner?.dynamicFields, ); return { dynamicFields: result.nodes.map((dynamicField) => { const valueType = dynamicField.value?.__typename === 'MoveObject' ? dynamicField.value.contents?.type?.repr! : dynamicField.value?.type.repr!; return { id: deriveDynamicFieldID( options.parentId, dynamicField.name?.type.repr!, dynamicField.name?.bcs!, ), type: normalizeStructTag( dynamicField.value?.__typename === 'MoveObject' ? `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, }; }), cursor: result.pageInfo.endCursor ?? null, hasNextPage: result.pageInfo.hasNextPage, }; } async verifyZkLoginSignature( options: Experimental_SuiClientTypes.VerifyZkLoginSignatureOptions, ): Promise<Experimental_SuiClientTypes.ZkLoginVerifyResponse> { const intentScope = options.intentScope === 'TransactionData' ? ZkLoginIntentScope.TransactionData : ZkLoginIntentScope.PersonalMessage; const result = await this.#graphqlQuery( { query: VerifyZkLoginSignatureDocument, variables: { bytes: options.bytes, signature: options.signature, intentScope, author: options.author, }, }, (result) => result.verifyZkloginSignature, ); return { success: result.success, errors: result.errors, }; } } export type GraphQLResponseErrors = Array<{ message: string; locations?: { line: number; column: number }[]; path?: (string | number)[]; }>; function handleGraphQLErrors(errors: GraphQLResponseErrors | undefined): void { if (!errors || errors.length === 0) return; const errorInstances = errors.map((error) => new GraphQLResponseError(error)); if (errorInstances.length === 1) { throw errorInstances[0]; } throw new AggregateError(errorInstances); } class GraphQLResponseError extends Error { locations?: Array<{ line: number; column: number }>; constructor(error: GraphQLResponseErrors[0]) { super(error.message); this.locations = error.locations; } } function mapOwner(owner: Object_Owner_FieldsFragment): Experimental_SuiClientTypes.ObjectOwner { switch (owner.__typename) { case 'AddressOwner': return { $kind: 'AddressOwner', AddressOwner: owner.owner?.asAddress?.address }; case 'ConsensusV2': return { $kind: 'ConsensusV2', ConsensusV2: owner.authenticator!.address }; case 'Immutable': return { $kind: 'Immutable', Immutable: true }; case 'Parent': return { $kind: 'ObjectOwner', ObjectOwner: owner.parent?.address }; case 'Shared': return { $kind: 'Shared', Shared: owner.initialSharedVersion }; } } function parseTransaction( transaction: Transaction_FieldsFragment, ): Experimental_SuiClientTypes.TransactionResponse { const objectTypes: Record<string, string> = {}; transaction.effects?.unchangedSharedObjects.nodes.forEach((node) => { if (node.__typename === 'SharedObjectRead') { const type = node.object?.asMoveObject?.contents?.type.repr; const address = node.object?.asMoveObject?.address; if (type && address) { objectTypes[address] = type; } } }); transaction.effects?.objectChanges.nodes.forEach((node) => { const address = node.address; const type = node.inputState?.asMoveObject?.contents?.type.repr ?? node.outputState?.asMoveObject?.contents?.type.repr; if (address && type) { objectTypes[address] = type; } }); return { digest: transaction.digest!, effects: parseTransactionEffects({ effects: new Uint8Array(transaction.effects?.bcs!), objectTypes, }), bcs: transaction.bcs!, signatures: transaction.signatures!, }; }