@scallop-io/sui-scallop-sdk 2.0.1 → 2.0.2-switchboard-alpha.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scallop-io/sui-scallop-sdk",
3
- "version": "2.0.1",
3
+ "version": "2.0.2-switchboard-alpha.2",
4
4
  "description": "Typescript sdk for interacting with Scallop contract on SUI",
5
5
  "keywords": [
6
6
  "sui",
@@ -44,8 +44,9 @@
44
44
  "@noble/hashes": "^1.3.2",
45
45
  "@pythnetwork/price-service-client": "^1.8.2",
46
46
  "@pythnetwork/pyth-sui-js": "2.1.0",
47
- "@scallop-io/sui-kit": "1.3.1",
47
+ "@scallop-io/sui-kit": "1.3.5",
48
48
  "@scure/bip39": "^1.2.1",
49
+ "@switchboard-xyz/sui-sdk": "0.0.22-beta.1",
49
50
  "@tanstack/query-core": "5.51.15",
50
51
  "axios": "^1.6.0",
51
52
  "bech32": "^2.0.0",
@@ -82,7 +83,7 @@
82
83
  "peerDependencies": {
83
84
  "@mysten/bcs": "^1.2.0",
84
85
  "@mysten/sui": "1.3.1",
85
- "@scallop-io/sui-kit": "1.3.1",
86
+ "@scallop-io/sui-kit": "1.3.5",
86
87
  "bn.js": "^5.2.1"
87
88
  },
88
89
  "lint-staged": {
@@ -149,6 +150,8 @@
149
150
  "scripts": {
150
151
  "clean": "rm -rf tsconfig.tsbuildinfo ./dist",
151
152
  "build": "pnpm run build:tsup",
153
+ "build-sourcemap": "pnpm run build:sourcemap",
154
+ "build:sourcemap": "tsup ./src/index.ts --format esm,cjs --splitting --minify --treeshake --dts --sourcemap",
152
155
  "build:tsup": "tsup ./src/index.ts --format esm,cjs --splitting --minify --treeshake --dts",
153
156
  "watch:tsup": "tsup ./src/index.ts --format esm,cjs --clean --splitting --watch",
154
157
  "watch:types": "tsc --watch",
@@ -9,6 +9,7 @@ import type {
9
9
  } from 'src/types';
10
10
  import { xOracleList as X_ORACLE_LIST } from 'src/constants';
11
11
  import { updatePythPriceFeeds } from './pyth';
12
+ import { updateSwitchboardAggregators } from './switchboard';
12
13
 
13
14
  /**
14
15
  * Update the price of the oracle for multiple coin.
@@ -62,7 +63,22 @@ export const updateOracles = async (
62
63
  filterAssetCoinNames(assetCoinName, 'pyth')
63
64
  );
64
65
  if (pythAssetCoinNames.length > 0)
65
- await updatePythPriceFeeds(builder, assetCoinNames, txBlock);
66
+ await updatePythPriceFeeds(builder, pythAssetCoinNames, txBlock);
67
+ }
68
+
69
+ if (flattenedRules.has('switchboard')) {
70
+ const switchboardAssetCoinNames = assetCoinNames.filter((assetCoinName) =>
71
+ filterAssetCoinNames(assetCoinName, 'switchboard')
72
+ );
73
+
74
+ if (switchboardAssetCoinNames.length > 0) {
75
+ await updateSwitchboardAggregators(
76
+ builder,
77
+ switchboardAssetCoinNames,
78
+ txBlock,
79
+ builder.params.switchboardSolanaRpc
80
+ );
81
+ }
66
82
  }
67
83
 
68
84
  // Remove duplicate coin names.
@@ -0,0 +1,233 @@
1
+ import { ScallopBuilder } from 'src/models';
2
+ import { type SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit';
3
+ import {
4
+ Aggregator,
5
+ AggregatorData,
6
+ ObjectParsingHelper,
7
+ // suiQueueCache,
8
+ // QueueData,
9
+ SwitchboardClient,
10
+ } from '@switchboard-xyz/sui-sdk';
11
+ import { queryMultipleObjects } from 'src/queries';
12
+ import { SuiParsedData } from '@mysten/sui/client';
13
+ import { toHex } from '@mysten/bcs';
14
+
15
+ const getFieldsFromObject = (
16
+ response: SuiParsedData | null | undefined
17
+ ): any => {
18
+ // Check if 'data' and 'content' exist and are of the expected type
19
+ if (response && response.dataType === 'moveObject') {
20
+ // Safely return 'fields' from 'content'
21
+ return response.fields as any;
22
+ }
23
+
24
+ throw new Error('Invalid response data');
25
+ };
26
+
27
+ const parseFeedConfigs = (responses: SuiParsedData[]): AggregatorData[] => {
28
+ return responses.map(getFieldsFromObject).map((aggregatorData) => {
29
+ const currentResult = (aggregatorData.current_result as any).fields;
30
+ const updateState = (aggregatorData.update_state as any).fields;
31
+
32
+ // build the data object
33
+ const data: AggregatorData = {
34
+ id: ObjectParsingHelper.asId(aggregatorData.id),
35
+ authority: ObjectParsingHelper.asString(aggregatorData.authority),
36
+ createdAtMs: ObjectParsingHelper.asNumber(aggregatorData.created_at_ms),
37
+ currentResult: {
38
+ maxResult: ObjectParsingHelper.asBN(currentResult.max_result),
39
+ maxTimestamp: ObjectParsingHelper.asNumber(
40
+ currentResult.max_timestamp_ms
41
+ ),
42
+ mean: ObjectParsingHelper.asBN(currentResult.mean),
43
+ minResult: ObjectParsingHelper.asBN(currentResult.min_result),
44
+ minTimestamp: ObjectParsingHelper.asNumber(
45
+ currentResult.min_timestamp_ms
46
+ ),
47
+ range: ObjectParsingHelper.asBN(currentResult.range),
48
+ result: ObjectParsingHelper.asBN(currentResult.result),
49
+ stdev: ObjectParsingHelper.asBN(currentResult.stdev),
50
+ },
51
+ feedHash: toHex(
52
+ ObjectParsingHelper.asUint8Array(aggregatorData.feed_hash)
53
+ ),
54
+ maxStalenessSeconds: ObjectParsingHelper.asNumber(
55
+ aggregatorData.max_staleness_seconds
56
+ ),
57
+ maxVariance: ObjectParsingHelper.asNumber(aggregatorData.max_variance),
58
+ minResponses: ObjectParsingHelper.asNumber(aggregatorData.min_responses),
59
+ minSampleSize: ObjectParsingHelper.asNumber(
60
+ aggregatorData.min_sample_size
61
+ ),
62
+ name: ObjectParsingHelper.asString(aggregatorData.name),
63
+ queue: ObjectParsingHelper.asString(aggregatorData.queue),
64
+ updateState: {
65
+ currIdx: ObjectParsingHelper.asNumber(updateState.curr_idx),
66
+ results: updateState.results.map((r: any) => {
67
+ const oracleId = r.fields.oracle;
68
+ const value = ObjectParsingHelper.asBN(r.fields.result.fields);
69
+ const timestamp = parseInt(r.fields.timestamp_ms);
70
+ return {
71
+ oracle: oracleId,
72
+ value,
73
+ timestamp,
74
+ };
75
+ }),
76
+ },
77
+ };
78
+
79
+ return data;
80
+ });
81
+ };
82
+
83
+ // const parseQueueAddresses = async (
84
+ // cache: ScallopCache,
85
+ // queueAddresses: string[]
86
+ // ): Promise<void> => {
87
+ // const queueParsedDatas = (
88
+ // await queryMultipleObjects(cache, queueAddresses)
89
+ // ).map((q) => getFieldsFromObject(q.content));
90
+
91
+ // // Get the existing_oracles table ids
92
+ // const existingOracleTableIds = queueParsedDatas.map((q: any) => {
93
+ // return q.existing_oracles.fields.id.id;
94
+ // });
95
+
96
+ // for (let i = 0; i < existingOracleTableIds.length; i++) {
97
+ // const parentId = existingOracleTableIds[i];
98
+ // const queueAddress = queueAddresses[i];
99
+ // const queueResponse = queueParsedDatas[i];
100
+ // const queueCachedData = await suiQueueCache.get(queueAddress);
101
+ // if (queueCachedData) {
102
+ // return queueCachedData;
103
+ // }
104
+
105
+ // let cursor = null;
106
+ // let nextPage = true;
107
+
108
+ // const existingOraclesIds = [];
109
+ // while (nextPage) {
110
+ // const resp = await cache.queryGetDynamicFields({
111
+ // parentId,
112
+ // limit: 25,
113
+ // cursor,
114
+ // });
115
+
116
+ // if (!resp) break;
117
+ // const { nextCursor, data, hasNextPage } = resp;
118
+ // nextPage = hasNextPage;
119
+ // cursor = nextCursor;
120
+
121
+ // existingOraclesIds.push(...data.map((t) => t.objectId));
122
+ // }
123
+
124
+ // const existingOracles = await queryMultipleObjects(
125
+ // cache,
126
+ // existingOraclesIds
127
+ // ).then((res) => {
128
+ // return res.map((t) => {
129
+ // const fields: any = getFieldsFromObject(t.content);
130
+ // return {
131
+ // oracleId: ObjectParsingHelper.asString(fields.value.fields.oracle_id),
132
+ // oracleKey: toBase58(
133
+ // ObjectParsingHelper.asUint8Array(fields.value.fields.oracle_key)
134
+ // ),
135
+ // };
136
+ // });
137
+ // });
138
+
139
+ // const queueData = {
140
+ // authority: ObjectParsingHelper.asString(queueResponse.authority),
141
+ // existingOracles,
142
+ // // get fee number (though encoded as string)
143
+ // fee: ObjectParsingHelper.asNumber(queueResponse.fee),
144
+
145
+ // // fee recipient address
146
+ // feeRecipient: ObjectParsingHelper.asString(queueResponse.fee_recipient),
147
+
148
+ // // accepted fee coin types
149
+ // feeTypes: ObjectParsingHelper.asArray(queueResponse.fee_types).map(
150
+ // (ft: any) => ft.fields.name
151
+ // ),
152
+
153
+ // // guardian queue id
154
+ // guardianQueueId: ObjectParsingHelper.asString(
155
+ // queueResponse.guardian_queue_id
156
+ // ),
157
+
158
+ // // queue id
159
+ // id: ObjectParsingHelper.asId(queueResponse.id),
160
+
161
+ // // last queue override ms
162
+ // lastQueueOverrideMs: ObjectParsingHelper.asNumber(
163
+ // queueResponse.last_queue_override_ms
164
+ // ),
165
+
166
+ // // minimum attestations
167
+ // minAttestations: ObjectParsingHelper.asNumber(
168
+ // queueResponse.min_attestations
169
+ // ),
170
+
171
+ // // queue name
172
+ // name: ObjectParsingHelper.asString(queueResponse.name),
173
+ // oracleValidityLengthMs: ObjectParsingHelper.asNumber(
174
+ // queueResponse.oracle_validity_length_ms
175
+ // ),
176
+
177
+ // // get source queue key
178
+ // queueKey: toBase58(
179
+ // ObjectParsingHelper.asUint8Array(queueResponse.queue_key) as Uint8Array
180
+ // ),
181
+ // } as QueueData;
182
+
183
+ // suiQueueCache.set(queueAddress, queueData);
184
+ // }
185
+ // };
186
+
187
+ export const updateSwitchboardAggregators = async (
188
+ builder: ScallopBuilder,
189
+ assetCoinNames: string[],
190
+ txBlock: SuiKitTxBlock,
191
+ solanaRPCUrl: string = 'https://crossbar.switchboard.xyz/rpc/mainnet'
192
+ ) => {
193
+ const switchboardClient = new SwitchboardClient(builder.suiKit.client());
194
+ const onDemandAggObjects = await queryMultipleObjects(
195
+ builder.cache,
196
+ await builder.query.getSwitchboardOnDemandAggregatorObjectIds(
197
+ assetCoinNames
198
+ )
199
+ );
200
+
201
+ const feedConfigs = parseFeedConfigs(
202
+ onDemandAggObjects.map((t) => t.content) as SuiParsedData[]
203
+ );
204
+
205
+ // Get queue addresses
206
+ // const queueAddresses = feedConfigs.map((fc) => fc.queue);
207
+
208
+ // await parseQueueAddresses(builder.cache, queueAddresses);
209
+
210
+ for (let idx = 0; idx < assetCoinNames.length; idx++) {
211
+ // console.log({ coinName: assetCoinNames[idx] });
212
+ const switchboardAgg = new Aggregator(
213
+ switchboardClient,
214
+ onDemandAggObjects[idx].objectId
215
+ );
216
+
217
+ const { responses, failures } = await switchboardAgg.fetchUpdateTx(
218
+ txBlock.txBlock,
219
+ {
220
+ feedConfigs: feedConfigs[idx],
221
+ solanaRPCUrl,
222
+ }
223
+ );
224
+
225
+ console.log({ responses, failures });
226
+ if (failures.length > 0) {
227
+ throw new Error(
228
+ `Failed to update aggregator for ${assetCoinNames[idx]}: ${failures.join(',')}`
229
+ );
230
+ }
231
+ }
232
+ return;
233
+ };
@@ -443,7 +443,7 @@ export class ScallopAddress {
443
443
  instance?: ScallopAddressInstanceParams
444
444
  ) {
445
445
  const { addressId, auth, network, forceAddressesInterface } = params;
446
- this.cache = instance?.cache ?? new ScallopCache({});
446
+ this.cache = instance?.cache ?? new ScallopCache(params);
447
447
 
448
448
  this._requestClient = axios.create({
449
449
  baseURL: API_BASE_URL,
@@ -246,7 +246,7 @@ export class ScallopCache {
246
246
  return query;
247
247
  }
248
248
 
249
- public async queryGetNormalizedMoveFunction(target: string) {
249
+ private async queryGetNormalizedMoveFunction(target: string) {
250
250
  const { address, module, name } = parseStructTag(target);
251
251
  return this.queryClient.fetchQuery({
252
252
  queryKey: queryKeys.rpc.getNormalizedMoveFunction(target),
@@ -460,11 +460,7 @@ export class ScallopCache {
460
460
  input: GetBalanceParams
461
461
  ): Promise<CoinBalance | null> {
462
462
  if (!input.coinType) return null;
463
-
464
- return (
465
- ((await this.queryGetAllCoinBalances(input.owner)) ?? {})[
466
- normalizeStructTag(input.coinType)
467
- ] ?? '0'
468
- );
463
+ const coinBalances = await this.queryGetAllCoinBalances(input.owner);
464
+ return coinBalances[normalizeStructTag(input.coinType)] ?? null;
469
465
  }
470
466
  }
@@ -1,11 +1,26 @@
1
1
  import { SuiKit, SuiKitParams } from '@scallop-io/sui-kit';
2
2
  import { RPC_PROVIDERS } from 'src/constants/rpc';
3
3
 
4
- export const newSuiKit = (params: SuiKitParams) => {
4
+ export const newSuiKit = (params: Partial<SuiKitParams>) => {
5
+ let initParams;
6
+ if (
7
+ 'suiClients' in params &&
8
+ params.suiClients &&
9
+ params.suiClients?.length > 0
10
+ ) {
11
+ initParams = {
12
+ suiClients: params.suiClients,
13
+ };
14
+ } else {
15
+ initParams = {
16
+ fullnodeUrls:
17
+ 'fullnodeUrls' in params
18
+ ? (params?.fullnodeUrls ?? RPC_PROVIDERS)
19
+ : RPC_PROVIDERS,
20
+ };
21
+ }
5
22
  return new SuiKit({
6
23
  ...params,
7
- fullnodeUrls: Array.from(
8
- new Set([...(params.fullnodeUrls ?? []), ...RPC_PROVIDERS])
9
- ),
24
+ ...initParams,
10
25
  });
11
26
  };
@@ -74,7 +74,7 @@ export type ScallopCacheParams = {
74
74
  walletAddress?: string;
75
75
  cacheOptions?: QueryClientConfig;
76
76
  config?: ScallopCacheConfig;
77
- } & SuiKitParams;
77
+ } & Partial<SuiKitParams>;
78
78
 
79
79
  export type ScallopIndexerParams = ScallopCacheParams & {
80
80
  indexerApiUrl?: string;
@@ -99,6 +99,7 @@ export type ScallopConstantsParams = ScallopAddressParams & {
99
99
  export type ScallopUtilsParams = ScallopAddressParams &
100
100
  ScallopConstantsParams & {
101
101
  pythEndpoints?: string[];
102
+ switchboardSolanaRpc?: string;
102
103
  };
103
104
 
104
105
  export type ScallopQueryParams = ScallopUtilsParams & ScallopIndexerParams;
@@ -109,7 +110,7 @@ export type ScallopBuilderParams = ScallopQueryParams & {
109
110
  };
110
111
 
111
112
  export type ScallopClientParams = ScallopBuilderParams;
112
- export type ScallopParams = SuiKitParams &
113
+ export type ScallopParams = Partial<SuiKitParams> &
113
114
  ScallopAddressParams &
114
115
  ScallopConstantsParams & {
115
116
  walletAddress?: string;