@mysten/sui
Version:
Sui TypeScript API(Work in Progress)
422 lines (379 loc) • 11.8 kB
text/typescript
// 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!,
};
}