Skip to content

Commit d7cc5ed

Browse files
authored
Add first draft of new experimental client with extensions (#110)
* Add first draft of new experimental client with extensions * Add basic transport methods * Implement jsonrpc transport * Add network to base client * remove client instantiation
1 parent df3466a commit d7cc5ed

File tree

6 files changed

+584
-1
lines changed

6 files changed

+584
-1
lines changed

packages/typescript/src/client/client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import { fromBase58, toBase64, toHex } from '@mysten/bcs';
44

55
import type { Signer } from '../cryptography/index.js';
6+
import type { Experimental_SuiClient } from '../experimental/client.js';
7+
import { JSONRpcTransport } from '../experimental/transports/jsonRPC.js';
8+
import type { SelfRegisteringClientExtension } from '../experimental/types.js';
69
import type { Transaction } from '../transactions/index.js';
710
import { isTransaction } from '../transactions/index.js';
811
import {
@@ -126,7 +129,7 @@ export function isSuiClient(client: unknown): client is SuiClient {
126129
);
127130
}
128131

129-
export class SuiClient {
132+
export class SuiClient implements SelfRegisteringClientExtension {
130133
protected transport: SuiTransport;
131134

132135
get [SUI_CLIENT_BRAND]() {
@@ -826,4 +829,14 @@ export class SuiClient {
826829
// This should never happen, because the above case should always throw, but just adding it in the event that something goes horribly wrong.
827830
throw new Error('Unexpected error while waiting for transaction block.');
828831
}
832+
833+
experimental_asClientExtension(this: SuiClient) {
834+
return {
835+
name: 'jsonRPC',
836+
register: (client: Experimental_SuiClient) => {
837+
client.$registerTransport(new JSONRpcTransport(this));
838+
return this;
839+
},
840+
} as const;
841+
}
829842
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
/* eslint-disable @typescript-eslint/ban-types */
4+
5+
import type {
6+
ClientWithExtensions,
7+
Experimental_SuiClientTypes,
8+
Simplify,
9+
SuiClientRegistration,
10+
UnionToIntersection,
11+
} from './types.js';
12+
13+
export class Experimental_SuiClient implements Experimental_SuiClientTypes.TransportMethods {
14+
#transports: Experimental_SuiClientTypes.TransportMethods[] = [];
15+
network: Experimental_SuiClientTypes.Network;
16+
17+
constructor({ network }: Experimental_SuiClientTypes.SuiClientOptions) {
18+
this.network = network;
19+
}
20+
21+
#transportMethod<T extends keyof Experimental_SuiClientTypes.TransportMethods>(
22+
method: T,
23+
...args: Parameters<NonNullable<Experimental_SuiClientTypes.TransportMethods[T]>>
24+
): ReturnType<NonNullable<Experimental_SuiClientTypes.TransportMethods[T]>> {
25+
for (const transport of this.#transports) {
26+
if (transport[method]) {
27+
return (transport[method] as (...args: any[]) => any)(...args);
28+
}
29+
}
30+
throw new Error(`No transport method found for ${method}`);
31+
}
32+
33+
getBalance(options: Experimental_SuiClientTypes.GetBalanceOptions) {
34+
return this.#transportMethod('getBalance', options);
35+
}
36+
37+
getAllBalances(options: Experimental_SuiClientTypes.GetAllBalancesOptions) {
38+
return this.#transportMethod('getAllBalances', options);
39+
}
40+
41+
getTransaction(options: Experimental_SuiClientTypes.GetTransactionOptions) {
42+
return this.#transportMethod('getTransaction', options);
43+
}
44+
45+
executeTransaction(options: Experimental_SuiClientTypes.ExecuteTransactionOptions) {
46+
return this.#transportMethod('executeTransaction', options);
47+
}
48+
49+
dryRunTransaction(options: Experimental_SuiClientTypes.DryRunTransactionOptions) {
50+
return this.#transportMethod('dryRunTransaction', options);
51+
}
52+
53+
getReferenceGasPrice() {
54+
return this.#transportMethod('getReferenceGasPrice');
55+
}
56+
57+
$registerTransport(transport: Experimental_SuiClientTypes.TransportMethods) {
58+
this.#transports.push(transport);
59+
}
60+
61+
$extend<const Registrations extends SuiClientRegistration<this>[]>(
62+
...registrations: Registrations
63+
) {
64+
return Object.create(
65+
this,
66+
Object.fromEntries(
67+
registrations.map((registration) => {
68+
if ('experimental_asClientExtension' in registration) {
69+
const { name, register } = registration.experimental_asClientExtension();
70+
return [name, { value: register(this) }];
71+
}
72+
return [registration.name, { value: registration.register(this) }];
73+
}),
74+
),
75+
) as ClientWithExtensions<
76+
Simplify<
77+
Omit<
78+
{
79+
[K in keyof this]: this[K];
80+
},
81+
keyof Experimental_SuiClient
82+
> &
83+
UnionToIntersection<
84+
{
85+
[K in keyof Registrations]: Registrations[K] extends SuiClientRegistration<
86+
this,
87+
infer Name extends string,
88+
infer Extension
89+
>
90+
? {
91+
[K2 in Name]: Extension;
92+
}
93+
: never;
94+
}[number]
95+
>
96+
>
97+
>;
98+
}
99+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { ObjectResponseError } from '../client/index.js';
5+
6+
export class SuiClientError extends Error {}
7+
8+
export class ObjectError extends SuiClientError {
9+
code: string;
10+
11+
constructor(code: string, message: string) {
12+
super(message);
13+
this.code = code;
14+
}
15+
16+
static fromResponse(response: ObjectResponseError, objectId?: string): ObjectError {
17+
switch (response.code) {
18+
case 'notExists':
19+
return new ObjectError(response.code, `Object ${response.object_id} does not exist`);
20+
case 'dynamicFieldNotFound':
21+
return new ObjectError(
22+
response.code,
23+
`Dynamic field not found for object ${response.parent_object_id}`,
24+
);
25+
case 'deleted':
26+
return new ObjectError(response.code, `Object ${response.object_id} has been deleted`);
27+
case 'displayError':
28+
return new ObjectError(response.code, `Display error: ${response.error}`);
29+
case 'unknown':
30+
default:
31+
return new ObjectError(
32+
response.code,
33+
`Unknown error while loading object${objectId ? ` ${objectId}` : ''}`,
34+
);
35+
}
36+
}
37+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { fromBase64 } from '@mysten/bcs';
5+
6+
import { bcs } from '../../bcs/index.js';
7+
import type {
8+
ObjectOwner,
9+
SuiClient,
10+
SuiObjectData,
11+
SuiTransactionBlockResponse,
12+
} from '../../client/index.js';
13+
import { batch } from '../../transactions/plugins/utils.js';
14+
import { Transaction } from '../../transactions/Transaction.js';
15+
import { ObjectError } from '../errors.js';
16+
import type { Experimental_SuiClientTypes } from '../types.js';
17+
18+
export class JSONRpcTransport implements Experimental_SuiClientTypes.TransportMethods {
19+
#jsonRpcClient: SuiClient;
20+
21+
constructor(jsonRpcClient: SuiClient) {
22+
this.#jsonRpcClient = jsonRpcClient;
23+
}
24+
25+
async getObjects(options: Experimental_SuiClientTypes.GetObjectsOptions) {
26+
const batches = batch(options.objectIds, 50);
27+
const results: Experimental_SuiClientTypes.GetObjectsResponse['objects'] = [];
28+
29+
for (const batch of batches) {
30+
const objects = await this.#jsonRpcClient.multiGetObjects({
31+
ids: batch,
32+
options: {
33+
showOwner: true,
34+
showType: true,
35+
},
36+
});
37+
38+
for (const [idx, object] of objects.entries()) {
39+
if (object.error) {
40+
results.push(ObjectError.fromResponse(object.error, batch[idx]));
41+
} else {
42+
results.push(parseObject(object.data!));
43+
}
44+
}
45+
}
46+
47+
return {
48+
objects: results,
49+
};
50+
}
51+
async getOwnedObjects(options: Experimental_SuiClientTypes.GetOwnedObjectsOptions) {
52+
const objects = await this.#jsonRpcClient.getOwnedObjects({
53+
owner: options.address,
54+
limit: options.limit,
55+
cursor: options.cursor,
56+
});
57+
58+
return {
59+
objects: objects.data.map((result) => {
60+
if (result.error) {
61+
throw ObjectError.fromResponse(result.error);
62+
}
63+
64+
return parseObject(result.data!);
65+
}),
66+
hasNextPage: objects.hasNextPage,
67+
cursor: objects.nextCursor ?? null,
68+
};
69+
}
70+
async getBalance(options: Experimental_SuiClientTypes.GetBalanceOptions) {
71+
const balance = await this.#jsonRpcClient.getBalance({
72+
owner: options.address,
73+
coinType: options.coinType,
74+
});
75+
76+
return {
77+
balance: {
78+
coinType: balance.coinType,
79+
balance: BigInt(balance.totalBalance),
80+
},
81+
};
82+
}
83+
async getAllBalances(options: Experimental_SuiClientTypes.GetAllBalancesOptions) {
84+
const balances = await this.#jsonRpcClient.getAllBalances({
85+
owner: options.address,
86+
});
87+
88+
return {
89+
balances: balances.map((balance) => ({
90+
coinType: balance.coinType,
91+
balance: BigInt(balance.totalBalance),
92+
})),
93+
hasNextPage: false,
94+
cursor: null,
95+
};
96+
}
97+
async getTransaction(options: Experimental_SuiClientTypes.GetTransactionOptions) {
98+
const transaction = await this.#jsonRpcClient.getTransactionBlock({
99+
digest: options.digest,
100+
options: {
101+
showRawInput: true,
102+
showObjectChanges: true,
103+
showRawEffects: true,
104+
showEvents: true,
105+
},
106+
});
107+
108+
return {
109+
transaction: parseTransaction(transaction),
110+
};
111+
}
112+
async executeTransaction(options: Experimental_SuiClientTypes.ExecuteTransactionOptions) {
113+
const transaction = await this.#jsonRpcClient.executeTransactionBlock({
114+
transactionBlock: options.transaction,
115+
signature: options.signatures,
116+
options: {
117+
showEffects: true,
118+
showEvents: true,
119+
},
120+
});
121+
122+
return {
123+
transaction: parseTransaction(transaction),
124+
};
125+
}
126+
async dryRunTransaction(options: Experimental_SuiClientTypes.DryRunTransactionOptions) {
127+
const tx = Transaction.from(options.transaction);
128+
const result = await this.#jsonRpcClient.dryRunTransactionBlock({
129+
transactionBlock: options.transaction,
130+
});
131+
132+
return {
133+
transaction: {
134+
digest: await tx.getDigest(),
135+
// TODO: Effects aren't returned as bcs from dryRun, once we define structured effects we can return those instead
136+
effects: result.effects as never,
137+
signatures: [],
138+
bcs: options.transaction,
139+
},
140+
};
141+
}
142+
async getReferenceGasPrice() {
143+
const referenceGasPrice = await this.#jsonRpcClient.getReferenceGasPrice();
144+
return {
145+
referenceGasPrice,
146+
};
147+
}
148+
}
149+
150+
function parseObject(object: SuiObjectData): Experimental_SuiClientTypes.ObjectResponse {
151+
return {
152+
id: object.objectId,
153+
version: object.version,
154+
digest: object.digest,
155+
type: object.type!,
156+
content:
157+
object.bcs?.dataType === 'moveObject' ? fromBase64(object.bcs.bcsBytes) : new Uint8Array(),
158+
owner: parseOwner(object.owner!),
159+
};
160+
}
161+
162+
function parseOwner(owner: ObjectOwner): Experimental_SuiClientTypes.ObjectOwner {
163+
if (owner === 'Immutable') {
164+
return {
165+
$kind: 'Immutable',
166+
Immutable: true,
167+
};
168+
}
169+
170+
if ('ConsensusV2' in owner) {
171+
return {
172+
$kind: 'ConsensusV2',
173+
ConsensusV2Owner: {
174+
authenticator: {
175+
$kind: 'SingleOwner',
176+
SingleOwner: owner.ConsensusV2.authenticator.SingleOwner,
177+
},
178+
startVersion: owner.ConsensusV2.start_version,
179+
},
180+
};
181+
}
182+
183+
if ('AddressOwner' in owner) {
184+
return {
185+
$kind: 'AddressOwner',
186+
AddressOwner: owner.AddressOwner,
187+
};
188+
}
189+
190+
if ('ObjectOwner' in owner) {
191+
return {
192+
$kind: 'ObjectOwner',
193+
ObjectOwner: owner.ObjectOwner,
194+
};
195+
}
196+
197+
if ('Shared' in owner) {
198+
return {
199+
$kind: 'Shared',
200+
Shared: {
201+
initialSharedVersion: owner.Shared.initial_shared_version,
202+
},
203+
};
204+
}
205+
206+
throw new Error(`Unknown owner type: ${JSON.stringify(owner)}`);
207+
}
208+
209+
function parseTransaction(
210+
transaction: SuiTransactionBlockResponse,
211+
): Experimental_SuiClientTypes.TransactionResponse {
212+
const parsedTx = bcs.SenderSignedData.parse(fromBase64(transaction.rawTransaction!))[0];
213+
214+
return {
215+
digest: transaction.digest,
216+
effects: new Uint8Array(transaction.rawEffects!),
217+
bcs: bcs.TransactionData.serialize(parsedTx.intentMessage.value).toBytes(),
218+
signatures: parsedTx.txSignatures,
219+
};
220+
}

0 commit comments

Comments
 (0)