@msafe/sui3-sdk 0.0.90 → 1.0.4

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,17 +1,30 @@
1
- import { GasCostSummary } from '@mysten/sui.js/client';
2
- import { TransactionBlock } from '@mysten/sui.js/transactions';
1
+ import type { SuiClientTypes } from '@mysten/sui/client';
2
+ import { Transaction } from '@mysten/sui/transactions';
3
3
 
4
4
  // Default to 120% o the reference gas price
5
5
  import { MSafeGlobals } from '@/globals/MSafeGlobals';
6
- import { SimulationResult } from '@/types';
6
+ import type { GasObjectReference, SimulationResult, SimulateDryRunResponse } from '@/types';
7
7
 
8
8
  const GAS_SAFE_OVERHEAD = 1000n;
9
9
 
10
+ function formatExecutionError(error: SuiClientTypes.ExecutionError): string {
11
+ return error.message;
12
+ }
13
+
14
+ function gasObjectToReference(gas: SuiClientTypes.ChangedObject | null | undefined): GasObjectReference {
15
+ if (!gas?.objectId) {
16
+ throw new Error('Gas object missing from simulation effects');
17
+ }
18
+ const version = gas.outputVersion ?? gas.inputVersion ?? '0';
19
+ const digest = gas.outputDigest ?? gas.inputDigest ?? '';
20
+ return { objectId: gas.objectId, version, digest };
21
+ }
22
+
10
23
  export class Simulator {
11
24
  constructor(private globals: MSafeGlobals) {}
12
25
 
13
- public async simulate(input: { txb: TransactionBlock; sender: string }): Promise<SimulationResult> {
14
- const tx = this.copyTransactionBlock(input.txb);
26
+ public async simulate(input: { txb: Transaction; sender: string }): Promise<SimulationResult> {
27
+ const tx = this.copyTransaction(input.txb);
15
28
 
16
29
  const { suiClient } = this.globals;
17
30
 
@@ -19,36 +32,66 @@ export class Simulator {
19
32
 
20
33
  tx.setGasPrice(gasPrice);
21
34
 
22
- const inspectResult = await suiClient.dryRunTransactionBlock({
23
- transactionBlock: await tx.build({ client: suiClient }),
24
- });
25
- const success = inspectResult.effects.status.status === 'success';
35
+ let built: Uint8Array;
36
+ try {
37
+ built = await tx.build({ client: suiClient });
38
+ } catch (e: unknown) {
39
+ const message = e instanceof Error ? e.message : String(e);
40
+ return {
41
+ success: false,
42
+ gasPrice,
43
+ simulationError: message,
44
+ };
45
+ }
46
+
47
+ let inspectResult: SimulateDryRunResponse;
48
+ try {
49
+ inspectResult = await suiClient.simulateTransaction({
50
+ transaction: built,
51
+ include: { effects: true },
52
+ });
53
+ } catch (e: unknown) {
54
+ const message = e instanceof Error ? e.message : String(e);
55
+ return {
56
+ success: false,
57
+ gasPrice,
58
+ simulationError: message,
59
+ };
60
+ }
61
+ const txRow = inspectResult.Transaction ?? inspectResult.FailedTransaction;
62
+ const { effects } = txRow;
63
+ if (!effects) {
64
+ throw new Error('simulateTransaction did not return effects');
65
+ }
66
+ const success = effects.status.success === true;
26
67
 
27
68
  if (!success) {
69
+ const err = effects.status.success === false ? effects.status.error : null;
28
70
  return {
29
71
  success,
30
72
  gasPrice,
31
73
  response: inspectResult,
32
- simulationError: inspectResult.effects.status.error!,
74
+ simulationError: err ? formatExecutionError(err) : 'Simulation failed',
33
75
  };
34
76
  }
35
- const gasBudget = this.toGasBudget(inspectResult.effects.gasUsed, gasPrice);
77
+ const gasBudget = this.toGasBudget(effects.gasUsed, gasPrice);
36
78
 
37
79
  return {
38
80
  success,
39
81
  gasPrice,
40
- gasObject: inspectResult.effects.gasObject.reference,
41
- gasUsed: inspectResult.effects.gasUsed,
82
+ gasObject: gasObjectToReference(effects.gasObject),
83
+ gasUsed: effects.gasUsed,
42
84
  gasBudget,
43
85
  response: inspectResult,
44
86
  };
45
87
  }
46
88
 
47
89
  private async getGasPrice() {
48
- return this.globals.suiClient.getReferenceGasPrice();
90
+ const { referenceGasPrice } = await this.globals.suiClient.getReferenceGasPrice();
91
+ return BigInt(referenceGasPrice);
49
92
  }
50
93
 
51
- private toGasBudget(gasUsed: GasCostSummary, gasPrice: bigint) {
94
+ private toGasBudget(gasUsed: SuiClientTypes.GasCostSummary, gasPrice: bigint) {
52
95
  const { computationCost, storageCost, storageRebate } = gasUsed;
53
96
  const safeOverhead = GAS_SAFE_OVERHEAD * gasPrice;
54
97
 
@@ -61,7 +104,7 @@ export class Simulator {
61
104
  return gasBudget > baseComputationCostWithOverhead ? gasBudget : baseComputationCostWithOverhead;
62
105
  }
63
106
 
64
- private copyTransactionBlock(tx: TransactionBlock): TransactionBlock {
65
- return TransactionBlock.from(tx.serialize());
107
+ private copyTransaction(tx: Transaction): Transaction {
108
+ return Transaction.from(tx.serialize());
66
109
  }
67
110
  }
@@ -1,4 +1,6 @@
1
- import { CoinMetadata } from '@mysten/sui.js/client';
1
+ import type { SuiClientTypes } from '@mysten/sui/client';
2
+
3
+ export type CoinMetadata = SuiClientTypes.CoinMetadata;
2
4
 
3
5
  export interface OwnedCoin {
4
6
  type: string;
@@ -1,6 +1,14 @@
1
1
  import { IGasOption, TransactionType, TxIntention } from '@msafe/sui3-utils';
2
- import { DryRunTransactionBlockResponse, GasCostSummary, SuiObjectRef } from '@mysten/sui.js/client';
3
- import { SerializedSignature } from '@mysten/sui.js/cryptography';
2
+ import type { SuiClientTypes } from '@mysten/sui/client';
3
+
4
+ /** Gas coin reference derived from simulation / execution effects. */
5
+ export interface GasObjectReference {
6
+ objectId: string;
7
+ version: string;
8
+ digest: string;
9
+ }
10
+
11
+ export type SimulateDryRunResponse = SuiClientTypes.SimulateTransactionResult<{ effects: true }>;
4
12
 
5
13
  export interface PendingTx {
6
14
  application: string;
@@ -77,7 +85,7 @@ export interface ProposeIntention {
77
85
  sequenceNumber: number;
78
86
  userAddress: string;
79
87
  msafeAddress: string;
80
- signature: SerializedSignature;
88
+ signature: string;
81
89
  }
82
90
 
83
91
  export type SimulationResult = SimulationSuccessResult | SimulationFailedResult;
@@ -85,16 +93,16 @@ export type SimulationResult = SimulationSuccessResult | SimulationFailedResult;
85
93
  export interface SimulationSuccessResult {
86
94
  success: true;
87
95
  gasPrice: bigint;
88
- gasUsed: GasCostSummary;
96
+ gasUsed: SuiClientTypes.GasCostSummary;
89
97
  gasBudget: bigint;
90
- gasObject: SuiObjectRef;
91
- response: DryRunTransactionBlockResponse;
98
+ gasObject: GasObjectReference;
99
+ response: SimulateDryRunResponse;
92
100
  }
93
101
 
94
102
  export interface SimulationFailedResult {
95
103
  success: false;
96
104
  gasPrice: bigint;
97
- response: DryRunTransactionBlockResponse;
105
+ response?: SimulateDryRunResponse;
98
106
  simulationError: string;
99
107
  }
100
108
 
@@ -1,10 +1,6 @@
1
- import {
2
- ExecuteTransactionRequestType,
3
- SuiTransactionBlockResponseOptions,
4
- SuiTransactionBlockResponse,
5
- } from '@mysten/sui.js/client';
6
- import { SignatureWithBytes } from '@mysten/sui.js/cryptography';
7
- import { TransactionBlock } from '@mysten/sui.js/transactions';
1
+ import type { SuiClientTypes } from '@mysten/sui/client';
2
+ import { SignatureWithBytes } from '@mysten/sui/cryptography';
3
+ import { Transaction } from '@mysten/sui/transactions';
8
4
 
9
5
  export interface IWallet {
10
6
  walletType: string;
@@ -13,11 +9,10 @@ export interface IWallet {
13
9
 
14
10
  signPersonalMessage(input: { messageStr: string }): Promise<SignatureWithBytes>;
15
11
 
16
- signTransactionBlock(input: { transactionBlock: TransactionBlock | Uint8Array }): Promise<SignatureWithBytes>;
12
+ signTransactionBlock(input: { transactionBlock: Transaction | Uint8Array }): Promise<SignatureWithBytes>;
17
13
 
18
14
  signAndExecuteTransactionBlock(input: {
19
- transactionBlock: TransactionBlock;
20
- requestType?: ExecuteTransactionRequestType;
21
- options?: SuiTransactionBlockResponseOptions;
22
- }): Promise<SuiTransactionBlockResponse>;
15
+ transactionBlock: Transaction | Uint8Array;
16
+ include?: SuiClientTypes.TransactionInclude;
17
+ }): Promise<SuiClientTypes.TransactionResult>;
23
18
  }
package/src/utils/coin.ts CHANGED
@@ -1,13 +1,15 @@
1
- import { CoinMetadata, SuiClient, SuiObjectData } from '@mysten/sui.js/client';
2
- import { normalizeStructTag } from '@mysten/sui.js/utils';
1
+ import type { SuiGrpcClient } from '@mysten/sui/grpc';
2
+ import { normalizeStructTag } from '@mysten/sui/utils';
3
+
4
+ import type { CoinMetadata } from '@/types/assets';
3
5
 
4
6
  // CoinHelper is the coin helper to query for coin metadata.
5
7
  export class CoinHelper {
6
- private _client: SuiClient;
8
+ private _client: SuiGrpcClient;
7
9
 
8
10
  private _coinMetaReg: Map<string, CoinMetadata>;
9
11
 
10
- constructor(client: SuiClient) {
12
+ constructor(client: SuiGrpcClient) {
11
13
  this._client = client;
12
14
  this._coinMetaReg = new Map();
13
15
  }
@@ -26,7 +28,7 @@ export class CoinHelper {
26
28
 
27
29
  private async queryCoinMeta(coinType: string): Promise<CoinMetadata | undefined> {
28
30
  const res = await this._client.getCoinMetadata({ coinType });
29
- return res || undefined;
31
+ return res.coinMetadata ?? undefined;
30
32
  }
31
33
  }
32
34
 
@@ -48,14 +50,25 @@ export class Coin {
48
50
  return res || null;
49
51
  }
50
52
 
51
- static getBalance(data: SuiObjectData): bigint | undefined {
53
+ static getBalance(data: {
54
+ type?: string | null;
55
+ json?: Record<string, unknown> | null;
56
+ content?: { dataType?: string; fields?: { balance?: string } };
57
+ }): bigint | undefined {
52
58
  if (!Coin.isCoin(data.type)) {
53
59
  return undefined;
54
60
  }
61
+ if (data.json && typeof data.json === 'object' && 'balance' in data.json) {
62
+ const { balance } = data.json as { balance?: string };
63
+ if (balance === undefined) {
64
+ return undefined;
65
+ }
66
+ return BigInt(balance);
67
+ }
55
68
  if (data.content?.dataType !== 'moveObject') {
56
69
  return undefined;
57
70
  }
58
- const { balance } = data.content?.fields as any;
71
+ const { balance } = data.content?.fields as { balance?: string };
59
72
  if (balance === undefined) {
60
73
  return undefined;
61
74
  }
@@ -1,5 +1,4 @@
1
- import { SerializedSignature } from '@mysten/sui.js/cryptography';
2
- import { verifyPersonalMessage, verifyTransactionBlock } from '@mysten/sui.js/verify';
1
+ import { verifyPersonalMessageSignature, verifyTransactionSignature } from '@mysten/sui/verify';
3
2
 
4
3
  import { stringToBuffer } from '@/utils/buffer';
5
4
  import { Formatter } from '@/utils/format';
@@ -8,15 +7,15 @@ export class SignatureVerifier {
8
7
  static async getPublicKeyFromSignature(input: {
9
8
  message: Uint8Array;
10
9
  messageType: 'TransactionBlock' | 'Personal';
11
- signature: SerializedSignature;
10
+ signature: string;
12
11
  }) {
13
12
  if (input.messageType === 'TransactionBlock') {
14
- return verifyTransactionBlock(input.message, input.signature);
13
+ return verifyTransactionSignature(input.message, input.signature);
15
14
  }
16
- return verifyPersonalMessage(input.message, input.signature);
15
+ return verifyPersonalMessageSignature(input.message, input.signature);
17
16
  }
18
17
 
19
- static async getPublicKeyFromPersonalSignature(input: { messageStr: string; signature: SerializedSignature }) {
18
+ static async getPublicKeyFromPersonalSignature(input: { messageStr: string; signature: string }) {
20
19
  const message = stringToBuffer(input.messageStr);
21
20
  return this.getPublicKeyFromSignature({
22
21
  message,
@@ -28,18 +27,14 @@ export class SignatureVerifier {
28
27
  static async verifySignature(input: {
29
28
  message: Uint8Array;
30
29
  messageType: 'TransactionBlock' | 'Personal';
31
- signature: SerializedSignature;
30
+ signature: string;
32
31
  targetAddress: string;
33
32
  }) {
34
33
  const publicKey = await SignatureVerifier.getPublicKeyFromSignature(input);
35
34
  return Formatter.isSuiAddressEqual(publicKey.toSuiAddress(), input.targetAddress);
36
35
  }
37
36
 
38
- static async verifyPersonalSignature(input: {
39
- messageStr: string;
40
- signature: SerializedSignature;
41
- targetAddress: string;
42
- }) {
37
+ static async verifyPersonalSignature(input: { messageStr: string; signature: string; targetAddress: string }) {
43
38
  const message = stringToBuffer(input.messageStr);
44
39
  return this.verifySignature({
45
40
  message,
@@ -51,7 +46,7 @@ export class SignatureVerifier {
51
46
 
52
47
  static async verifyTransactionSignature(input: {
53
48
  payload: Uint8Array;
54
- signature: SerializedSignature;
49
+ signature: string;
55
50
  targetAddress: string;
56
51
  }): Promise<boolean> {
57
52
  return this.verifySignature({
@@ -1,4 +1,4 @@
1
- import { normalizeSuiAddress, normalizeStructTag } from '@mysten/sui.js/utils';
1
+ import { normalizeSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
2
2
 
3
3
  import { Coin } from '@/utils/coin';
4
4
 
@@ -3,3 +3,4 @@ export * from './crypto';
3
3
  export * from './format';
4
4
  export * from './sui';
5
5
  export * from './coin';
6
+ export * from './transaction';
@@ -1,11 +1,15 @@
1
- import { SuiObjectResponse, SuiObjectDataOptions, SuiObjectData, SuiClient } from '@mysten/sui.js/client';
1
+ import type { SuiClientTypes } from '@mysten/sui/client';
2
+ import type { SuiGrpcClient } from '@mysten/sui/grpc';
2
3
 
3
4
  import { EntryIterator, getAllFromIterator, REQUEST_PAGE_SIZE, SuiIterator } from './iterator';
4
5
  import { PagedData, Requester } from './requester';
5
6
 
6
7
  // TODO: setup testnet faucet and test this file
7
8
 
8
- export type ObjectFilter = (objRes: SuiObjectResponse) => boolean;
9
+ /** Owned object row with Move JSON content (for helpers like Coin.getBalance). */
10
+ export type OwnedObjectData = SuiClientTypes.Object<{ json: true }>;
11
+
12
+ export type ObjectFilter = (obj: OwnedObjectData) => boolean;
9
13
 
10
14
  // OidIter is the iterator to give the list of object ids
11
15
  export type OidIter = SuiIterator<string>;
@@ -13,20 +17,27 @@ export type OidIter = SuiIterator<string>;
13
17
  export interface BatchObjectOptions {
14
18
  filter?: ObjectFilter;
15
19
  pageSize?: number;
16
- objectOptions?: SuiObjectDataOptions;
20
+ /** Extra fields to request from the node; defaults include Move JSON for struct fields. */
21
+ objectInclude?: SuiClientTypes.ObjectInclude;
22
+ }
23
+
24
+ const defaultObjectInclude: SuiClientTypes.ObjectInclude = { json: true };
25
+
26
+ function mergeInclude(options?: BatchObjectOptions): SuiClientTypes.ObjectInclude {
27
+ return { ...defaultObjectInclude, ...options?.objectInclude };
17
28
  }
18
29
 
19
30
  // getObjectsById get the list of objects by id.
20
31
  // Compared with the multiGetObject method defined by SUI, this method will do the pagination
21
32
  // for get object requests.
22
33
  export async function getObjectsById(
23
- provider: SuiClient,
34
+ provider: SuiGrpcClient,
24
35
  ids: string[],
25
36
  options?: BatchObjectOptions,
26
- ): Promise<(SuiObjectData | undefined)[]> {
37
+ ): Promise<(OwnedObjectData | undefined)[]> {
27
38
  const oidIter = new ListOidIterator(ids);
28
39
  const iter = new ObjectBatchIterator(provider, oidIter, options);
29
- return (await getAllFromIterator(iter)) as (SuiObjectData | undefined)[];
40
+ return (await getAllFromIterator(iter)) as (OwnedObjectData | undefined)[];
30
41
  }
31
42
 
32
43
  // ListOidIterator is the iterator that iterate through a list of ids.
@@ -53,9 +64,9 @@ export class ListOidIterator implements OidIter {
53
64
  }
54
65
  }
55
66
 
56
- export class ObjectBatchIterator extends EntryIterator<SuiObjectData> {
67
+ export class ObjectBatchIterator extends EntryIterator<OwnedObjectData> {
57
68
  constructor(
58
- public readonly provider: SuiClient,
69
+ public readonly provider: SuiGrpcClient,
59
70
  public readonly idIter: OidIter,
60
71
  public readonly options?: BatchObjectOptions,
61
72
  ) {
@@ -64,27 +75,24 @@ export class ObjectBatchIterator extends EntryIterator<SuiObjectData> {
64
75
  }
65
76
 
66
77
  // TODO: Test with customize objects
67
- export class ObjectBatchRequester implements Requester<SuiObjectData> {
78
+ export class ObjectBatchRequester implements Requester<OwnedObjectData> {
68
79
  filter: ObjectFilter | undefined;
69
80
 
70
81
  pageSize: number;
71
82
 
72
- objectOptions: SuiObjectDataOptions;
83
+ objectInclude: SuiClientTypes.ObjectInclude;
73
84
 
74
85
  constructor(
75
- public readonly provider: SuiClient,
86
+ public readonly provider: SuiGrpcClient,
76
87
  public readonly stringIter: OidIter,
77
88
  public options?: BatchObjectOptions,
78
89
  ) {
79
90
  this.filter = options?.filter;
80
91
  this.pageSize = options?.pageSize || REQUEST_PAGE_SIZE;
81
- this.objectOptions = options?.objectOptions || {
82
- showType: true,
83
- showContent: true,
84
- };
92
+ this.objectInclude = mergeInclude(options);
85
93
  }
86
94
 
87
- async doNextRequest(): Promise<PagedData<SuiObjectData>> {
95
+ async doNextRequest(): Promise<PagedData<OwnedObjectData>> {
88
96
  const requestPage: string[] = [];
89
97
  while (requestPage.length < this.pageSize) {
90
98
  const hasNext = await this.stringIter.hasNext();
@@ -96,36 +104,44 @@ export class ObjectBatchRequester implements Requester<SuiObjectData> {
96
104
  requestPage.push(objId);
97
105
  }
98
106
  }
99
- const res: SuiObjectResponse[] = await this.provider.multiGetObjects({
100
- ids: requestPage,
101
- options: this.objectOptions,
107
+ const { objects } = await this.provider.getObjects({
108
+ objectIds: requestPage,
109
+ include: this.objectInclude,
102
110
  });
103
- let filtered: SuiObjectResponse[];
111
+ const rows: OwnedObjectData[] = [];
112
+ // eslint-disable-next-line no-restricted-syntax
113
+ for (const entry of objects) {
114
+ if (entry instanceof Error) {
115
+ continue;
116
+ }
117
+ rows.push(entry as OwnedObjectData);
118
+ }
119
+ let filtered: OwnedObjectData[];
104
120
  if (this.filter) {
105
121
  const { filter } = this;
106
- filtered = res.filter((r: SuiObjectResponse) => filter?.(r));
122
+ filtered = rows.filter((r) => filter(r));
107
123
  } else {
108
- filtered = res;
124
+ filtered = rows;
109
125
  }
110
126
  return {
111
- data: filtered.map((objRes) => objRes.data).filter((data) => data) as SuiObjectData[],
127
+ data: filtered,
112
128
  hasNext: await this.stringIter.hasNext(),
113
129
  };
114
130
  }
115
131
  }
116
132
 
117
133
  export async function getAllOwnedObjects(
118
- provider: SuiClient,
134
+ provider: SuiGrpcClient,
119
135
  owner: string,
120
136
  options?: BatchObjectOptions,
121
- ): Promise<SuiObjectData[]> {
137
+ ): Promise<OwnedObjectData[]> {
122
138
  const iter = new OwnedObjectIterator(provider, owner, options);
123
- return (await getAllFromIterator(iter)) as SuiObjectData[];
139
+ return (await getAllFromIterator(iter)) as OwnedObjectData[];
124
140
  }
125
141
 
126
- export class OwnedObjectIterator extends EntryIterator<SuiObjectData> implements SuiIterator<SuiObjectData> {
142
+ export class OwnedObjectIterator extends EntryIterator<OwnedObjectData> implements SuiIterator<OwnedObjectData> {
127
143
  constructor(
128
- public readonly provider: SuiClient,
144
+ public readonly provider: SuiGrpcClient,
129
145
  public readonly owner: string,
130
146
  public readonly options?: BatchObjectOptions,
131
147
  ) {
@@ -133,46 +149,43 @@ export class OwnedObjectIterator extends EntryIterator<SuiObjectData> implements
133
149
  }
134
150
  }
135
151
 
136
- export class OwnedObjectRequester implements Requester<SuiObjectData> {
152
+ export class OwnedObjectRequester implements Requester<OwnedObjectData> {
137
153
  nextCursor: string | null;
138
154
 
139
155
  public filter: ObjectFilter | undefined;
140
156
 
141
157
  public pageSize: number;
142
158
 
143
- public objectOptions: SuiObjectDataOptions;
159
+ public objectInclude: SuiClientTypes.ObjectInclude;
144
160
 
145
161
  constructor(
146
- public readonly provider: SuiClient,
162
+ public readonly provider: SuiGrpcClient,
147
163
  public readonly owner: string,
148
164
  public readonly options?: BatchObjectOptions,
149
165
  ) {
150
166
  this.nextCursor = null;
151
167
  this.filter = options?.filter;
152
168
  this.pageSize = options?.pageSize || REQUEST_PAGE_SIZE;
153
- this.objectOptions = options?.objectOptions || {
154
- showType: true,
155
- showContent: true,
156
- };
169
+ this.objectInclude = mergeInclude(options);
157
170
  }
158
171
 
159
- async doNextRequest(): Promise<PagedData<SuiObjectData>> {
160
- const res = await this.provider.getOwnedObjects({
172
+ async doNextRequest(): Promise<PagedData<OwnedObjectData>> {
173
+ const res = await this.provider.listOwnedObjects({
161
174
  owner: this.owner,
162
- options: this.objectOptions,
175
+ include: this.objectInclude,
163
176
  cursor: this.nextCursor,
164
177
  limit: this.pageSize,
165
178
  });
166
- this.nextCursor = res.nextCursor as string;
167
- let filtered: SuiObjectResponse[];
179
+ this.nextCursor = res.cursor;
180
+ let filtered: OwnedObjectData[];
168
181
  if (this.filter) {
169
182
  const { filter } = this;
170
- filtered = res.data.filter((obj: SuiObjectResponse) => filter?.(obj));
183
+ filtered = res.objects.filter((obj) => filter(obj as OwnedObjectData));
171
184
  } else {
172
- filtered = res.data;
185
+ filtered = res.objects as OwnedObjectData[];
173
186
  }
174
187
  return {
175
- data: filtered.map((r: SuiObjectResponse) => r.data).filter((data) => data) as SuiObjectData[],
188
+ data: filtered,
176
189
  hasNext: res.hasNextPage,
177
190
  };
178
191
  }