@scallop-io/sui-scallop-sdk 2.0.0-alpha.6 → 2.0.0-alpha.7

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.0-alpha.6",
3
+ "version": "2.0.0-alpha.7",
4
4
  "description": "Typescript sdk for interacting with Scallop contract on SUI",
5
5
  "keywords": [
6
6
  "sui",
@@ -46,6 +46,9 @@
46
46
  "@pythnetwork/pyth-sui-js": "2.1.0",
47
47
  "@scallop-io/sui-kit": "1.3.1",
48
48
  "@scure/bip39": "^1.2.1",
49
+ "@switchboard-xyz/common": "^3.0.6",
50
+ "@switchboard-xyz/on-demand": "^2.2.0",
51
+ "@switchboard-xyz/sui-sdk": "0.0.21",
49
52
  "@tanstack/query-core": "5.51.15",
50
53
  "axios": "^1.6.0",
51
54
  "bech32": "^2.0.0",
@@ -2,7 +2,7 @@ import { Transaction } from '@mysten/sui/transactions';
2
2
  import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui/utils';
3
3
  import { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit';
4
4
  import { getObligations } from '../queries';
5
- import { updateOracles } from './oracle';
5
+ import { updateOracles } from './oracles';
6
6
  import { requireSender } from '../utils';
7
7
  import type { SuiObjectArg, TransactionResult } from '@scallop-io/sui-kit';
8
8
  import type { ScallopBuilder } from '../models';
@@ -361,39 +361,16 @@ const generateCoreQuickMethod: GenerateCoreQuickMethod = ({
361
361
  const sender = requireSender(txBlock);
362
362
  const marketCoinName = builder.utils.parseMarketCoinName(poolCoinName);
363
363
 
364
- // check if user has sCoin instead of marketCoin
365
- try {
366
- const sCoinName = builder.utils.parseSCoinName(poolCoinName);
367
- if (!sCoinName) throw new Error(`No sCoin for ${poolCoinName}`);
368
- const {
369
- leftCoin,
370
- takeCoin: sCoins,
371
- totalAmount,
372
- } = await builder.selectSCoin(txBlock, sCoinName, amount, sender);
373
- txBlock.transferObjects([leftCoin], sender);
374
- const marketCoins = txBlock.burnSCoin(sCoinName, sCoins);
364
+ const sCoinName = builder.utils.parseSCoinName(poolCoinName);
365
+ if (!sCoinName) throw new Error(`No sCoin for ${poolCoinName}`);
375
366
 
376
- // check amount
377
- amount -= totalAmount;
378
- try {
379
- if (amount > 0) {
380
- // sCoin is not enough, try market coin
381
- const { leftCoin, takeCoin: walletMarketCoins } =
382
- await builder.selectMarketCoin(
383
- txBlock,
384
- marketCoinName,
385
- amount,
386
- sender
387
- );
388
- txBlock.transferObjects([leftCoin], sender);
389
- txBlock.mergeCoins(marketCoins, [walletMarketCoins]);
390
- }
391
- } catch (_e) {
392
- // ignore
393
- }
394
- return txBlock.withdraw(marketCoins, poolCoinName);
395
- } catch (_e) {
396
- // no sCoin found
367
+ // check if user has sCoin instead of marketCoin
368
+ const {
369
+ leftCoin,
370
+ takeCoin: sCoins,
371
+ totalAmount,
372
+ } = await builder.selectSCoin(txBlock, sCoinName, amount, sender);
373
+ if (totalAmount === 0) {
397
374
  const { leftCoin, takeCoin: walletMarketCoins } =
398
375
  await builder.selectMarketCoin(
399
376
  txBlock,
@@ -404,6 +381,29 @@ const generateCoreQuickMethod: GenerateCoreQuickMethod = ({
404
381
  txBlock.transferObjects([leftCoin], sender);
405
382
  return txBlock.withdraw(walletMarketCoins, poolCoinName);
406
383
  }
384
+
385
+ txBlock.transferObjects([leftCoin], sender);
386
+ const marketCoins = txBlock.burnSCoin(sCoinName, sCoins);
387
+
388
+ // check amount
389
+ amount -= totalAmount;
390
+ try {
391
+ if (amount > 0) {
392
+ // sCoin is not enough, try market coin
393
+ const { leftCoin, takeCoin: walletMarketCoins } =
394
+ await builder.selectMarketCoin(
395
+ txBlock,
396
+ marketCoinName,
397
+ amount,
398
+ sender
399
+ );
400
+ txBlock.transferObjects([leftCoin], sender);
401
+ txBlock.mergeCoins(marketCoins, [walletMarketCoins]);
402
+ }
403
+ } catch (_e) {
404
+ // ignore
405
+ }
406
+ return txBlock.withdraw(marketCoins, poolCoinName);
407
407
  },
408
408
  borrowQuick: async (amount, poolCoinName, obligationId, obligationKey) => {
409
409
  const obligationInfo = await requireObligationInfo(
@@ -1,13 +1,15 @@
1
1
  import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui/utils';
2
- import {
3
- SuiPythClient,
4
- SuiPriceServiceConnection,
5
- } from '@pythnetwork/pyth-sui-js';
6
2
  import type { TransactionArgument } from '@mysten/sui/transactions';
7
3
  import type { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit';
8
- import type { ScallopBuilder } from '../models';
9
- import type { xOracleRules, xOracleRuleType } from '../types';
4
+ import type { ScallopBuilder } from 'src/models';
5
+ import type {
6
+ SupportOracleType,
7
+ xOracleRules,
8
+ xOracleRuleType,
9
+ } from 'src/types';
10
10
  import { xOracleList as X_ORACLE_LIST } from 'src/constants';
11
+ import { updatePythPriceFeeds } from './pyth';
12
+ import { updateSwitchboardAggregators } from './switchboard';
11
13
 
12
14
  /**
13
15
  * Update the price of the oracle for multiple coin.
@@ -36,46 +38,45 @@ export const updateOracles = async (
36
38
  : X_ORACLE_LIST;
37
39
 
38
40
  // const rules: SupportOracleType[] = builder.isTestnet ? ['pyth'] : ['pyth'];
39
- const flattenedRules = [
40
- ...new Set(
41
- Object.values(xOracleList).flatMap(({ primary, secondary }) => [
42
- ...primary,
43
- ...secondary,
44
- ])
45
- ),
46
- ];
41
+ const flattenedRules = new Set(
42
+ Object.values(xOracleList).flatMap(({ primary, secondary }) => [
43
+ ...primary,
44
+ ...secondary,
45
+ ])
46
+ );
47
47
 
48
- if (flattenedRules.includes('pyth') && usePythPullModel) {
49
- const pythClient = new SuiPythClient(
50
- builder.suiKit.client(),
51
- builder.address.get('core.oracles.pyth.state'),
52
- builder.address.get('core.oracles.pyth.wormholeState')
53
- );
54
- const priceIds = assetCoinNames.map((assetCoinName) =>
55
- builder.address.get(`core.coins.${assetCoinName}.oracle.pyth.feed`)
48
+ const filterAssetCoinNames = (
49
+ assetCoinName: string,
50
+ rule: SupportOracleType
51
+ ) => {
52
+ const assetXOracle = xOracleList[assetCoinName];
53
+ return (
54
+ assetXOracle &&
55
+ (assetXOracle.primary.includes(rule) ||
56
+ assetXOracle.secondary.includes(rule))
56
57
  );
58
+ };
57
59
 
58
- // iterate through the endpoints
59
- const endpoints = builder.params.pythEndpoints ?? [
60
- ...builder.constants.whitelist.pythEndpoints,
61
- ];
62
- for (const endpoint of endpoints) {
63
- try {
64
- const pythConnection = new SuiPriceServiceConnection(endpoint);
65
- const priceUpdateData =
66
- await pythConnection.getPriceFeedsUpdateData(priceIds);
67
- await pythClient.updatePriceFeeds(
68
- txBlock.txBlock, // convert txBlock to TransactionBlock because pyth sdk not support new @mysten/sui yet
69
- priceUpdateData,
70
- priceIds
71
- );
60
+ // Handle Pyth price feed
61
+ if (flattenedRules.has('pyth') && usePythPullModel) {
62
+ const pythAssetCoinNames = assetCoinNames.filter((assetCoinName) =>
63
+ filterAssetCoinNames(assetCoinName, 'pyth')
64
+ );
65
+ if (pythAssetCoinNames.length > 0)
66
+ await updatePythPriceFeeds(builder, assetCoinNames, txBlock);
67
+ }
72
68
 
73
- break;
74
- } catch (e) {
75
- console.warn(
76
- `Failed to update price feeds with endpoint ${endpoint}: ${e}`
77
- );
78
- }
69
+ // Handle Switchboard on-demand aggregator
70
+ if (flattenedRules.has('switchboard')) {
71
+ const switchboardAssetCoinNames = assetCoinNames.filter((assetCoinName) =>
72
+ filterAssetCoinNames(assetCoinName, 'switchboard')
73
+ );
74
+ if (switchboardAssetCoinNames.length > 0) {
75
+ await updateSwitchboardAggregators(
76
+ builder,
77
+ switchboardAssetCoinNames,
78
+ txBlock
79
+ );
79
80
  }
80
81
  }
81
82
 
@@ -324,7 +325,8 @@ const updateSwitchboardPrice = (
324
325
  coinType: string
325
326
  ) => {
326
327
  txBlock.moveCall(
327
- `${packageId}::rule::set_price_as_${type}`,
328
+ // `${packageId}::rule::set_price_as_${type}`,
329
+ `${packageId}::rule::set_as_${type}_price`,
328
330
  [
329
331
  request,
330
332
  aggregatorId,
@@ -0,0 +1,44 @@
1
+ import {
2
+ SuiPriceServiceConnection,
3
+ SuiPythClient,
4
+ } from '@pythnetwork/pyth-sui-js';
5
+ import { ScallopBuilder } from 'src/models';
6
+ import type { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit';
7
+
8
+ export const updatePythPriceFeeds = async (
9
+ builder: ScallopBuilder,
10
+ assetCoinNames: string[],
11
+ txBlock: SuiKitTxBlock
12
+ ) => {
13
+ const pythClient = new SuiPythClient(
14
+ builder.suiKit.client(),
15
+ builder.address.get('core.oracles.pyth.state'),
16
+ builder.address.get('core.oracles.pyth.wormholeState')
17
+ );
18
+ const priceIds = assetCoinNames.map((assetCoinName) =>
19
+ builder.address.get(`core.coins.${assetCoinName}.oracle.pyth.feed`)
20
+ );
21
+
22
+ // iterate through the endpoints
23
+ const endpoints = builder.params.pythEndpoints ?? [
24
+ ...builder.constants.whitelist.pythEndpoints,
25
+ ];
26
+ for (const endpoint of endpoints) {
27
+ try {
28
+ const pythConnection = new SuiPriceServiceConnection(endpoint);
29
+ const priceUpdateData =
30
+ await pythConnection.getPriceFeedsUpdateData(priceIds);
31
+ await pythClient.updatePriceFeeds(
32
+ txBlock.txBlock, // convert txBlock to TransactionBlock because pyth sdk not support new @mysten/sui yet
33
+ priceUpdateData,
34
+ priceIds
35
+ );
36
+
37
+ break;
38
+ } catch (e) {
39
+ console.warn(
40
+ `Failed to update price feeds with endpoint ${endpoint}: ${e}`
41
+ );
42
+ }
43
+ }
44
+ };
@@ -0,0 +1,270 @@
1
+ import { ScallopBuilder } from 'src/models';
2
+ import {
3
+ // SUI_CLOCK_OBJECT_ID,
4
+ // SUI_TYPE_ARG,
5
+ type SuiTxBlock as SuiKitTxBlock,
6
+ } from '@scallop-io/sui-kit';
7
+ import {
8
+ Aggregator,
9
+ AggregatorData,
10
+ ObjectParsingHelper,
11
+ // AggregatorData,
12
+ // FeedEvalResponse,
13
+ // ObjectParsingHelper,
14
+ // Queue,
15
+ SwitchboardClient,
16
+ } from '@switchboard-xyz/sui-sdk';
17
+ import { queryMultipleObjects } from 'src/queries';
18
+ import { MoveValue, SuiParsedData } from '@mysten/sui/client';
19
+ import { toHex } from '@mysten/bcs';
20
+ // import { CrossbarClient, IOracleJob, OracleJob } from '@switchboard-xyz/common';
21
+ // import axios from 'axios';
22
+
23
+ const getFieldsFromObject = (
24
+ response: SuiParsedData
25
+ ): {
26
+ [key: string]: MoveValue;
27
+ } => {
28
+ // Check if 'data' and 'content' exist and are of the expected type
29
+ if (response.dataType === 'moveObject') {
30
+ // Safely return 'fields' from 'content'
31
+ return response.fields as any;
32
+ }
33
+
34
+ throw new Error('Invalid response data');
35
+ };
36
+
37
+ const parseFeedConfigs = (responses: SuiParsedData[]): AggregatorData[] => {
38
+ return responses.map(getFieldsFromObject).map((aggregatorData) => {
39
+ const currentResult = (aggregatorData.current_result as any).fields;
40
+ const updateState = (aggregatorData.update_state as any).fields;
41
+
42
+ // build the data object
43
+ const data: AggregatorData = {
44
+ id: ObjectParsingHelper.asId(aggregatorData.id),
45
+ authority: ObjectParsingHelper.asString(aggregatorData.authority),
46
+ createdAtMs: ObjectParsingHelper.asNumber(aggregatorData.created_at_ms),
47
+ currentResult: {
48
+ maxResult: ObjectParsingHelper.asBN(currentResult.max_result),
49
+ maxTimestamp: ObjectParsingHelper.asNumber(
50
+ currentResult.max_timestamp_ms
51
+ ),
52
+ mean: ObjectParsingHelper.asBN(currentResult.mean),
53
+ minResult: ObjectParsingHelper.asBN(currentResult.min_result),
54
+ minTimestamp: ObjectParsingHelper.asNumber(
55
+ currentResult.min_timestamp_ms
56
+ ),
57
+ range: ObjectParsingHelper.asBN(currentResult.range),
58
+ result: ObjectParsingHelper.asBN(currentResult.result),
59
+ stdev: ObjectParsingHelper.asBN(currentResult.stdev),
60
+ },
61
+ feedHash: toHex(
62
+ ObjectParsingHelper.asUint8Array(aggregatorData.feed_hash)
63
+ ),
64
+ maxStalenessSeconds: ObjectParsingHelper.asNumber(
65
+ aggregatorData.max_staleness_seconds
66
+ ),
67
+ maxVariance: ObjectParsingHelper.asNumber(aggregatorData.max_variance),
68
+ minResponses: ObjectParsingHelper.asNumber(aggregatorData.min_responses),
69
+ minSampleSize: ObjectParsingHelper.asNumber(
70
+ aggregatorData.min_sample_size
71
+ ),
72
+ name: ObjectParsingHelper.asString(aggregatorData.name),
73
+ queue: ObjectParsingHelper.asString(aggregatorData.queue),
74
+ updateState: {
75
+ currIdx: ObjectParsingHelper.asNumber(updateState.curr_idx),
76
+ results: updateState.results.map((r: any) => {
77
+ const oracleId = r.fields.oracle;
78
+ const value = ObjectParsingHelper.asBN(r.fields.result.fields);
79
+ const timestamp = parseInt(r.fields.timestamp_ms);
80
+ return {
81
+ oracle: oracleId,
82
+ value,
83
+ timestamp,
84
+ };
85
+ }),
86
+ },
87
+ };
88
+
89
+ return data;
90
+ });
91
+ };
92
+
93
+ // const encodeJobs = (jobArray: OracleJob[]) => {
94
+ // return jobArray.map((job) => serializeOracleJob(job).toString('base64'));
95
+ // };
96
+
97
+ // const normalizeOracleJob = (
98
+ // data: string | IOracleJob | Record<string, any>
99
+ // ): OracleJob => {
100
+ // const parseJobObject = (jobData: Record<string, any>) => {
101
+ // if (!jobData) {
102
+ // throw new Error(`No job data provided: ${jobData}`);
103
+ // } else if (!('tasks' in jobData)) {
104
+ // throw new Error('"tasks" property is required');
105
+ // } else if (!(Array.isArray(jobData.tasks) && jobData.tasks.length > 0)) {
106
+ // throw new Error('"tasks" property must be a non-empty array');
107
+ // }
108
+ // return OracleJob.fromObject(jobData);
109
+ // };
110
+ // const parseJobString = (jobString: string) => {
111
+ // // Strip comments using regex from https://regex101.com/r/B8WkuX/1
112
+ // const cleanJson = jobString.replace(
113
+ // /\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/g,
114
+ // ''
115
+ // );
116
+ // return parseJobObject(JSON.parse(cleanJson));
117
+ // };
118
+ // return typeof data === 'string' ? parseJobString(data) : parseJobObject(data);
119
+ // };
120
+
121
+ // const serializeOracleJob = (
122
+ // data: string | IOracleJob | Record<string, any>
123
+ // ): Buffer => {
124
+ // const job = normalizeOracleJob(data);
125
+ // return Buffer.from(OracleJob.encodeDelimited(job).finish());
126
+ // };
127
+
128
+ // const fetchSignatures = async (
129
+ // feedConfig: AggregatorData
130
+ // ): Promise<{
131
+ // responses: FeedEvalResponse[];
132
+ // failures: string[];
133
+ // }> => {
134
+ // const crossbarClient = new CrossbarClient('https://crossbar.switchboard.xyz');
135
+
136
+ // const jobs: OracleJob[] = await crossbarClient
137
+ // .fetch(feedConfig.feedHash)
138
+ // .then((res) => res.jobs.map((job) => OracleJob.fromObject(job)));
139
+
140
+ // const encodedJobs = encodeJobs(jobs);
141
+ // const maxVariance = Math.floor(feedConfig.maxVariance / 1e9) * 1e9;
142
+ // const minResponses = feedConfig.minResponses;
143
+ // const numSignatures = feedConfig.minSampleSize;
144
+ // const recentHash = toBase58(new Uint8Array(32));
145
+ // const useTimestamp = true;
146
+
147
+ // const GATEWAY_URL = 'https://api.mainnet-beta.solana.com';
148
+ // const TIMEOUT = 10000;
149
+ // const url = `${GATEWAY_URL}/gateway/api/v1/fetch_signatures`;
150
+ // const headers = { 'Content-Type': 'application/json' };
151
+
152
+ // const body = JSON.stringify({
153
+ // api_version: '1.0.0',
154
+ // jobs_b64_encoded: encodedJobs,
155
+ // recent_chainhash: recentHash,
156
+ // signature_scheme: 'Secp256k1',
157
+ // hash_scheme: 'Sha256',
158
+ // num_oracles: numSignatures,
159
+ // max_variance: maxVariance,
160
+ // min_responses: minResponses,
161
+ // use_timestamp: useTimestamp,
162
+ // });
163
+
164
+ // return await axios
165
+ // .post(url, body, {
166
+ // headers,
167
+ // timeout: TIMEOUT,
168
+ // })
169
+ // .then((r) => r.data);
170
+ // };
171
+
172
+ export const updateSwitchboardAggregators = async (
173
+ builder: ScallopBuilder,
174
+ assetCoinNames: string[],
175
+ txBlock: SuiKitTxBlock
176
+ ) => {
177
+ const switchboardClient = new SwitchboardClient(builder.suiKit.client());
178
+ const onDemandAggObjects = await queryMultipleObjects(
179
+ builder.cache,
180
+ await builder.query.getSwitchboardOnDemandAggregatorObjectIds(
181
+ assetCoinNames
182
+ )
183
+ );
184
+
185
+ const feedConfigs = parseFeedConfigs(
186
+ onDemandAggObjects.map((t) => t.content) as SuiParsedData[]
187
+ );
188
+
189
+ for (const idx in assetCoinNames) {
190
+ // const { switchboardAddress, oracleQueueId } =
191
+ // await switchboardClient.fetchState();
192
+
193
+ // const feedConfig = feedConfigs[idx];
194
+
195
+ // const suiQueue = await new Queue(
196
+ // switchboardClient,
197
+ // oracleQueueId
198
+ // ).loadData();
199
+
200
+ // const { responses, failures } = await fetchSignatures(feedConfig);
201
+ // const validOracles = new Set(
202
+ // suiQueue.existingOracles.map((o) => o.oracleKey)
203
+ // );
204
+
205
+ // const validResponses = responses.filter((r) => {
206
+ // return validOracles.has(toBase58(fromHex(r.oracle_pubkey)));
207
+ // });
208
+
209
+ // // if we have no valid responses (or not enough), fail out
210
+ // if (
211
+ // !validResponses.length ||
212
+ // validResponses.length < feedConfig.minSampleSize
213
+ // ) {
214
+ // // maybe retry by recursing into the same function / add a retry count
215
+ // throw new Error('Not enough valid oracle responses.');
216
+ // }
217
+
218
+ // // split the gas coin into the right amount for each response
219
+ // const coins = txBlock.splitCoins(
220
+ // txBlock.gas,
221
+ // validResponses.map(() => suiQueue.fee)
222
+ // );
223
+
224
+ // // map the responses into the tx
225
+ // validResponses.forEach((response, i) => {
226
+ // const oracle = suiQueue.existingOracles.find(
227
+ // (o) => o.oracleKey === toBase58(fromHex(response.oracle_pubkey))
228
+ // )!;
229
+
230
+ // const signature = Array.from(fromBase64(response.signature));
231
+ // signature.push(response.recovery_id);
232
+
233
+ // txBlock.moveCall(
234
+ // `${switchboardAddress}::aggregator_submit_result_action::run`,
235
+ // [
236
+ // txBlock.object(onDemandAggObjects[idx].objectId),
237
+ // txBlock.object(suiQueue.id),
238
+ // txBlock.pure.u128(response.success_value),
239
+ // txBlock.pure.bool(response.success_value.startsWith('-')),
240
+ // txBlock.pure.u64(response.timestamp!),
241
+ // txBlock.object(oracle.oracleId),
242
+ // txBlock.pure.vector('u8', signature),
243
+ // txBlock.sharedObjectRef({
244
+ // objectId: SUI_CLOCK_OBJECT_ID,
245
+ // initialSharedVersion: '1',
246
+ // mutable: false,
247
+ // }),
248
+ // coins[i],
249
+ // ],
250
+ // [SUI_TYPE_ARG]
251
+ // );
252
+ // });
253
+
254
+ // return { responses, failures };
255
+
256
+ const switchboardAgg = new Aggregator(
257
+ switchboardClient,
258
+ onDemandAggObjects[idx].objectId
259
+ );
260
+
261
+ const { responses, failures } = await switchboardAgg.fetchUpdateTx(
262
+ txBlock.txBlock,
263
+ {
264
+ feedConfigs: feedConfigs[idx],
265
+ }
266
+ );
267
+
268
+ return { responses, failures };
269
+ }
270
+ };