@scallop-io/sui-kit 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +1,39 @@
1
- import { SuiInteractorParams, NetworkType } from '../../types/index.js';
2
- import { SuiOwnedObject, SuiSharedObject } from '../suiModel/index.js';
3
- import { batch, delay } from './util.js';
4
- import { SuiGrpcClient, type SuiGrpcClientOptions } from '@mysten/sui/grpc';
5
- import type { ClientWithCoreApi, SuiClientTypes } from '@mysten/sui/client';
1
+ import type { ClientWithCoreApi, SuiClientTypes } from "@mysten/sui/client";
2
+ import { SuiGrpcClient, type SuiGrpcClientOptions } from "@mysten/sui/grpc";
3
+ import type { NetworkType, SuiInteractorParams } from "../../types/index.js";
4
+ import { SuiOwnedObject, SuiSharedObject } from "../suiModel/index.js";
5
+ import { batch, delay } from "./util.js";
6
6
 
7
7
  const MAX_OBJECTS_PER_REQUEST = 50;
8
8
 
9
9
  // Helper to create gRPC client options with baseUrl
10
10
  function createGrpcClientOptions(
11
- url: string,
12
- network: NetworkType
11
+ url: string,
12
+ network: NetworkType,
13
13
  ): SuiGrpcClientOptions {
14
- return { baseUrl: url, network } satisfies SuiGrpcClientOptions;
14
+ return { baseUrl: url, network } satisfies SuiGrpcClientOptions;
15
15
  }
16
16
 
17
17
  // Helper to get fullnode URLs for each network
18
18
  function getFullnodeUrl(network: NetworkType): string {
19
- switch (network) {
20
- case 'mainnet':
21
- return 'https://fullnode.mainnet.sui.io:443';
22
- case 'testnet':
23
- return 'https://fullnode.testnet.sui.io:443';
24
- case 'devnet':
25
- return 'https://fullnode.devnet.sui.io:443';
26
- case 'localnet':
27
- return 'http://127.0.0.1:9000';
28
- default:
29
- throw new Error(`Unknown network: ${network}`);
30
- }
19
+ switch (network) {
20
+ case "mainnet":
21
+ return "https://fullnode.mainnet.sui.io:443";
22
+ case "testnet":
23
+ return "https://fullnode.testnet.sui.io:443";
24
+ case "devnet":
25
+ return "https://fullnode.devnet.sui.io:443";
26
+ case "localnet":
27
+ return "http://127.0.0.1:9000";
28
+ default:
29
+ throw new Error(`Unknown network: ${network}`);
30
+ }
31
31
  }
32
32
 
33
33
  // Object data type from SDK v2
34
34
  export type SuiObjectData = SuiClientTypes.Object<{
35
- content: true;
36
- json: true;
35
+ content: true;
36
+ json: true;
37
37
  }>;
38
38
 
39
39
  // Options for getObjects (SDK v2 naming)
@@ -41,283 +41,290 @@ export type SuiObjectDataOptions = SuiClientTypes.ObjectInclude;
41
41
 
42
42
  // Simulate transaction response type
43
43
  export type SimulateTransactionResponse =
44
- SuiClientTypes.SimulateTransactionResult<{
45
- effects: true;
46
- events: true;
47
- balanceChanges: true;
48
- commandResults: true;
49
- }>;
44
+ SuiClientTypes.SimulateTransactionResult<{
45
+ effects: true;
46
+ events: true;
47
+ balanceChanges: true;
48
+ commandResults: true;
49
+ }>;
50
50
 
51
51
  /**
52
52
  * Encapsulates all functions that interact with the sui sdk
53
53
  */
54
54
  export class SuiInteractor {
55
- private clients: ClientWithCoreApi[] = [];
56
- public currentClient: ClientWithCoreApi;
57
- private fullNodes: string[] = [];
58
- private network: NetworkType;
55
+ private clients: ClientWithCoreApi[] = [];
56
+ public currentClient: ClientWithCoreApi;
57
+ private fullNodes: string[] = [];
58
+ private network: NetworkType;
59
59
 
60
- constructor(params: Partial<SuiInteractorParams>) {
61
- // Default network
62
- this.network = 'mainnet';
60
+ constructor(params: Partial<SuiInteractorParams>) {
61
+ // Default network
62
+ this.network = "mainnet";
63
63
 
64
- if ('fullnodeUrls' in params && params.fullnodeUrls) {
65
- this.network = params.network ?? 'mainnet';
66
- this.fullNodes = params.fullnodeUrls;
67
- this.clients = this.fullNodes.map(
68
- (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network))
69
- );
70
- } else if ('suiClients' in params && params.suiClients) {
71
- this.clients = params.suiClients;
72
- } else {
73
- this.fullNodes = [getFullnodeUrl(this.network)];
74
- this.clients = [
75
- new SuiGrpcClient(
76
- createGrpcClientOptions(this.fullNodes[0], this.network)
77
- ),
78
- ];
79
- }
80
- this.currentClient = this.clients[0];
81
- }
64
+ if ("fullnodeUrls" in params && params.fullnodeUrls) {
65
+ this.network = params.network ?? "mainnet";
66
+ this.fullNodes = params.fullnodeUrls;
67
+ this.clients = this.fullNodes.map(
68
+ (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network)),
69
+ );
70
+ } else if ("suiClients" in params && params.suiClients) {
71
+ this.clients = params.suiClients;
72
+ } else {
73
+ this.fullNodes = [getFullnodeUrl(this.network)];
74
+ this.clients = [
75
+ new SuiGrpcClient(
76
+ createGrpcClientOptions(this.fullNodes[0], this.network),
77
+ ),
78
+ ];
79
+ }
80
+ this.currentClient = this.clients[0];
81
+ }
82
82
 
83
- switchToNextClient() {
84
- const currentClientIdx = this.clients.indexOf(this.currentClient);
85
- this.currentClient =
86
- this.clients[(currentClientIdx + 1) % this.clients.length];
87
- }
83
+ getClient(idx: number) {
84
+ if (idx < 0 || idx >= this.clients.length) {
85
+ throw new Error("Client index out of bounds");
86
+ }
87
+ return this.clients[idx];
88
+ }
88
89
 
89
- switchFullNodes(fullNodes: string[], network?: NetworkType) {
90
- if (fullNodes.length === 0) {
91
- throw new Error('fullNodes cannot be empty');
92
- }
93
- this.fullNodes = fullNodes;
94
- if (network) {
95
- this.network = network;
96
- }
97
- this.clients = fullNodes.map(
98
- (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network))
99
- );
100
- this.currentClient = this.clients[0];
101
- }
90
+ switchToNextClient() {
91
+ const currentClientIdx = this.clients.indexOf(this.currentClient);
92
+ this.currentClient =
93
+ this.clients[(currentClientIdx + 1) % this.clients.length];
94
+ }
102
95
 
103
- get currentFullNode() {
104
- if (this.fullNodes.length === 0) {
105
- throw new Error('No full nodes available');
106
- }
96
+ switchFullNodes(fullNodes: string[], network?: NetworkType) {
97
+ if (fullNodes.length === 0) {
98
+ throw new Error("fullNodes cannot be empty");
99
+ }
100
+ this.fullNodes = fullNodes;
101
+ if (network) {
102
+ this.network = network;
103
+ }
104
+ this.clients = fullNodes.map(
105
+ (url) => new SuiGrpcClient(createGrpcClientOptions(url, this.network)),
106
+ );
107
+ this.currentClient = this.clients[0];
108
+ }
107
109
 
108
- const clientIdx = this.clients.indexOf(this.currentClient);
109
- if (clientIdx === -1) {
110
- throw new Error('Current client not found');
111
- }
110
+ get currentFullNode() {
111
+ if (this.fullNodes.length === 0) {
112
+ throw new Error("No full nodes available");
113
+ }
112
114
 
113
- return this.fullNodes[clientIdx];
114
- }
115
+ const clientIdx = this.clients.indexOf(this.currentClient);
116
+ if (clientIdx === -1) {
117
+ throw new Error("Current client not found");
118
+ }
115
119
 
116
- async sendTx(
117
- transactionBlock: Uint8Array | string,
118
- signature: string | string[]
119
- ): Promise<
120
- SuiClientTypes.TransactionResult<{
121
- balanceChanges: true;
122
- effects: true;
123
- events: true;
124
- objectTypes: true;
125
- }>
126
- > {
127
- const txBytes =
128
- typeof transactionBlock === 'string'
129
- ? Uint8Array.from(Buffer.from(transactionBlock, 'base64'))
130
- : transactionBlock;
120
+ return this.fullNodes[clientIdx];
121
+ }
131
122
 
132
- const signatures = Array.isArray(signature) ? signature : [signature];
123
+ async sendTx(
124
+ transactionBlock: Uint8Array | string,
125
+ signature: string | string[],
126
+ ): Promise<
127
+ SuiClientTypes.TransactionResult<{
128
+ balanceChanges: true;
129
+ effects: true;
130
+ events: true;
131
+ objectTypes: true;
132
+ }>
133
+ > {
134
+ const txBytes =
135
+ typeof transactionBlock === "string"
136
+ ? Uint8Array.from(Buffer.from(transactionBlock, "base64"))
137
+ : transactionBlock;
133
138
 
134
- for (const clientIdx in this.clients) {
135
- try {
136
- return await this.clients[clientIdx].core.executeTransaction({
137
- transaction: txBytes,
138
- signatures,
139
- include: {
140
- balanceChanges: true,
141
- effects: true,
142
- events: true,
143
- objectTypes: true,
144
- },
145
- });
146
- } catch (err) {
147
- console.warn(
148
- `Failed to send transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`
149
- );
150
- await delay(2000);
151
- }
152
- }
153
- throw new Error('Failed to send transaction with all fullnodes');
154
- }
139
+ const signatures = Array.isArray(signature) ? signature : [signature];
155
140
 
156
- async dryRunTx(
157
- transactionBlock: Uint8Array
158
- ): Promise<SimulateTransactionResponse> {
159
- for (const clientIdx in this.clients) {
160
- try {
161
- return await this.clients[clientIdx].core.simulateTransaction({
162
- transaction: transactionBlock,
163
- include: {
164
- effects: true,
165
- events: true,
166
- balanceChanges: true,
167
- commandResults: true,
168
- },
169
- });
170
- } catch (err) {
171
- console.warn(
172
- `Failed to dry run transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`
173
- );
174
- await delay(2000);
175
- }
176
- }
177
- throw new Error('Failed to dry run transaction with all fullnodes');
178
- }
141
+ for (const clientIdx in this.clients) {
142
+ try {
143
+ return await this.clients[clientIdx].core.executeTransaction({
144
+ transaction: txBytes,
145
+ signatures,
146
+ include: {
147
+ balanceChanges: true,
148
+ effects: true,
149
+ events: true,
150
+ objectTypes: true,
151
+ },
152
+ });
153
+ } catch (err) {
154
+ console.warn(
155
+ `Failed to send transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`,
156
+ );
157
+ await delay(2000);
158
+ }
159
+ }
160
+ throw new Error("Failed to send transaction with all fullnodes");
161
+ }
179
162
 
180
- async getObjects(
181
- ids: string[],
182
- options?: {
183
- include?: SuiObjectDataOptions;
184
- batchSize?: number;
185
- switchClientDelay?: number;
186
- }
187
- ): Promise<SuiObjectData[]> {
188
- const include = options?.include ?? { content: true, json: true };
163
+ async dryRunTx(
164
+ transactionBlock: Uint8Array,
165
+ ): Promise<SimulateTransactionResponse> {
166
+ for (const clientIdx in this.clients) {
167
+ try {
168
+ return await this.clients[clientIdx].core.simulateTransaction({
169
+ transaction: transactionBlock,
170
+ include: {
171
+ effects: true,
172
+ events: true,
173
+ balanceChanges: true,
174
+ commandResults: true,
175
+ },
176
+ });
177
+ } catch (err) {
178
+ console.warn(
179
+ `Failed to dry run transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`,
180
+ );
181
+ await delay(2000);
182
+ }
183
+ }
184
+ throw new Error("Failed to dry run transaction with all fullnodes");
185
+ }
189
186
 
190
- const batchIds = batch(
191
- ids,
192
- Math.max(
193
- options?.batchSize ?? MAX_OBJECTS_PER_REQUEST,
194
- MAX_OBJECTS_PER_REQUEST
195
- )
196
- );
197
- const results: SuiObjectData[] = [];
198
- let lastError = null;
187
+ async getObjects(
188
+ ids: string[],
189
+ options?: {
190
+ include?: SuiObjectDataOptions;
191
+ batchSize?: number;
192
+ switchClientDelay?: number;
193
+ },
194
+ ): Promise<SuiObjectData[]> {
195
+ const include = options?.include ?? { content: true, json: true };
199
196
 
200
- for (const batchChunk of batchIds) {
201
- for (const clientIdx in this.clients) {
202
- try {
203
- const response = await this.clients[clientIdx].core.getObjects({
204
- objectIds: batchChunk,
205
- include,
206
- });
197
+ const batchIds = batch(
198
+ ids,
199
+ Math.max(
200
+ options?.batchSize ?? MAX_OBJECTS_PER_REQUEST,
201
+ MAX_OBJECTS_PER_REQUEST,
202
+ ),
203
+ );
204
+ const results: SuiObjectData[] = [];
205
+ let lastError = null;
207
206
 
208
- const parsedObjects = response.objects
209
- .map((obj) => {
210
- if (obj instanceof Error) {
211
- return null;
212
- }
213
- return obj as SuiObjectData;
214
- })
215
- .filter((object): object is SuiObjectData => object !== null);
207
+ for (const batchChunk of batchIds) {
208
+ for (const clientIdx in this.clients) {
209
+ try {
210
+ const response = await this.clients[clientIdx].core.getObjects({
211
+ objectIds: batchChunk,
212
+ include,
213
+ });
216
214
 
217
- results.push(...parsedObjects);
218
- lastError = null;
219
- break; // Exit the client loop if successful
220
- } catch (err) {
221
- lastError = err instanceof Error ? err : new Error(String(err));
222
- await delay(options?.switchClientDelay ?? 2000);
223
- console.warn(
224
- `Failed to get objects with fullnode ${this.fullNodes[clientIdx]}: ${err}`
225
- );
226
- }
227
- }
228
- if (lastError) {
229
- throw new Error(
230
- `Failed to get objects with all fullnodes: ${lastError}`
231
- );
232
- }
233
- }
215
+ const parsedObjects = response.objects
216
+ .map((obj) => {
217
+ if (obj instanceof Error) {
218
+ return null;
219
+ }
220
+ return obj as SuiObjectData;
221
+ })
222
+ .filter((object): object is SuiObjectData => object !== null);
234
223
 
235
- return results;
236
- }
224
+ results.push(...parsedObjects);
225
+ lastError = null;
226
+ break; // Exit the client loop if successful
227
+ } catch (err) {
228
+ lastError = err instanceof Error ? err : new Error(String(err));
229
+ await delay(options?.switchClientDelay ?? 2000);
230
+ console.warn(
231
+ `Failed to get objects with fullnode ${this.fullNodes[clientIdx]}: ${err}`,
232
+ );
233
+ }
234
+ }
235
+ if (lastError) {
236
+ throw new Error(
237
+ `Failed to get objects with all fullnodes: ${lastError}`,
238
+ );
239
+ }
240
+ }
237
241
 
238
- async getObject(id: string, options?: { include?: SuiObjectDataOptions }) {
239
- const objects = await this.getObjects([id], options);
240
- return objects[0];
241
- }
242
+ return results;
243
+ }
242
244
 
243
- /**
244
- * @description Update objects in a batch
245
- * @param suiObjects
246
- */
247
- async updateObjects(suiObjects: (SuiOwnedObject | SuiSharedObject)[]) {
248
- const objectIds = suiObjects.map((obj) => obj.objectId);
249
- const objects = await this.getObjects(objectIds);
250
- for (const object of objects) {
251
- const suiObject = suiObjects.find(
252
- (obj) => obj.objectId === object?.objectId
253
- );
254
- if (suiObject instanceof SuiSharedObject) {
255
- const owner = object.owner;
256
- if (owner && typeof owner === 'object' && 'Shared' in owner) {
257
- suiObject.initialSharedVersion = (
258
- owner as { Shared: { initialSharedVersion: string } }
259
- ).Shared.initialSharedVersion;
260
- } else {
261
- suiObject.initialSharedVersion = undefined;
262
- }
263
- } else if (suiObject instanceof SuiOwnedObject) {
264
- suiObject.version = object?.version;
265
- suiObject.digest = object?.digest;
266
- }
267
- }
268
- }
245
+ async getObject(id: string, options?: { include?: SuiObjectDataOptions }) {
246
+ const objects = await this.getObjects([id], options);
247
+ return objects[0];
248
+ }
269
249
 
270
- /**
271
- * @description Select coins that add up to the given amount.
272
- * @param addr the address of the owner
273
- * @param amount the amount that is needed for the coin
274
- * @param coinType the coin type, default is '0x2::SUI::SUI'
275
- */
276
- async selectCoins(
277
- addr: string,
278
- amount: number,
279
- coinType: string = '0x2::SUI::SUI'
280
- ) {
281
- const selectedCoins: {
282
- objectId: string;
283
- digest: string;
284
- version: string;
285
- balance: string;
286
- }[] = [];
287
- let totalAmount = 0;
288
- let hasNext = true,
289
- nextCursor: string | null | undefined = null;
290
- while (hasNext && totalAmount < amount) {
291
- const { objects, hasNextPage, cursor } =
292
- await this.currentClient.core.listCoins({
293
- owner: addr,
294
- coinType: coinType,
295
- cursor: nextCursor,
296
- });
297
- // Sort the coins by balance in descending order
298
- objects.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
299
- for (const coinData of objects) {
300
- selectedCoins.push({
301
- objectId: coinData.objectId,
302
- digest: coinData.digest,
303
- version: coinData.version,
304
- balance: coinData.balance,
305
- });
306
- totalAmount = totalAmount + parseInt(coinData.balance);
307
- if (totalAmount >= amount) {
308
- break;
309
- }
310
- }
250
+ /**
251
+ * @description Update objects in a batch
252
+ * @param suiObjects
253
+ */
254
+ async updateObjects(suiObjects: (SuiOwnedObject | SuiSharedObject)[]) {
255
+ const objectIds = suiObjects.map((obj) => obj.objectId);
256
+ const objects = await this.getObjects(objectIds);
257
+ for (const object of objects) {
258
+ const suiObject = suiObjects.find(
259
+ (obj) => obj.objectId === object?.objectId,
260
+ );
261
+ if (suiObject instanceof SuiSharedObject) {
262
+ const owner = object.owner;
263
+ if (owner && typeof owner === "object" && "Shared" in owner) {
264
+ suiObject.initialSharedVersion = (
265
+ owner as { Shared: { initialSharedVersion: string } }
266
+ ).Shared.initialSharedVersion;
267
+ } else {
268
+ suiObject.initialSharedVersion = undefined;
269
+ }
270
+ } else if (suiObject instanceof SuiOwnedObject) {
271
+ suiObject.version = object?.version;
272
+ suiObject.digest = object?.digest;
273
+ }
274
+ }
275
+ }
311
276
 
312
- nextCursor = cursor;
313
- hasNext = hasNextPage;
314
- }
277
+ /**
278
+ * @description Select coins that add up to the given amount.
279
+ * @param addr the address of the owner
280
+ * @param amount the amount that is needed for the coin
281
+ * @param coinType the coin type, default is '0x2::SUI::SUI'
282
+ */
283
+ async selectCoins(
284
+ addr: string,
285
+ amount: number,
286
+ coinType: string = "0x2::SUI::SUI",
287
+ ) {
288
+ const selectedCoins: {
289
+ objectId: string;
290
+ digest: string;
291
+ version: string;
292
+ balance: string;
293
+ }[] = [];
294
+ let totalAmount = 0;
295
+ let hasNext = true,
296
+ nextCursor: string | null | undefined = null;
297
+ while (hasNext && totalAmount < amount) {
298
+ const { objects, hasNextPage, cursor } =
299
+ await this.currentClient.core.listCoins({
300
+ owner: addr,
301
+ coinType: coinType,
302
+ cursor: nextCursor,
303
+ });
304
+ // Sort the coins by balance in descending order
305
+ objects.sort((a, b) => parseInt(b.balance, 10) - parseInt(a.balance, 10));
306
+ for (const coinData of objects) {
307
+ selectedCoins.push({
308
+ objectId: coinData.objectId,
309
+ digest: coinData.digest,
310
+ version: coinData.version,
311
+ balance: coinData.balance,
312
+ });
313
+ totalAmount = totalAmount + parseInt(coinData.balance, 10);
314
+ if (totalAmount >= amount) {
315
+ break;
316
+ }
317
+ }
315
318
 
316
- if (!selectedCoins.length) {
317
- throw new Error('No valid coins found for the transaction.');
318
- }
319
- return selectedCoins;
320
- }
319
+ nextCursor = cursor;
320
+ hasNext = hasNextPage;
321
+ }
322
+
323
+ if (!selectedCoins.length) {
324
+ throw new Error("No valid coins found for the transaction.");
325
+ }
326
+ return selectedCoins;
327
+ }
321
328
  }
322
329
 
323
330
  export { getFullnodeUrl };
@@ -1,10 +1,10 @@
1
1
  export const delay = (ms: number) =>
2
- new Promise((resolve) => setTimeout(resolve, ms));
2
+ new Promise((resolve) => setTimeout(resolve, ms));
3
3
 
4
4
  export const batch = <T>(arr: T[], size: number): T[][] => {
5
- const batches = [];
6
- for (let i = 0; i < arr.length; i += size) {
7
- batches.push(arr.slice(i, i + size));
8
- }
9
- return batches;
5
+ const batches = [];
6
+ for (let i = 0; i < arr.length; i += size) {
7
+ batches.push(arr.slice(i, i + size));
8
+ }
9
+ return batches;
10
10
  };
@@ -1,2 +1,2 @@
1
- export { SuiOwnedObject } from './suiOwnedObject.js';
2
- export { SuiSharedObject } from './suiSharedObject.js';
1
+ export { SuiOwnedObject } from "./suiOwnedObject.js";
2
+ export { SuiSharedObject } from "./suiSharedObject.js";