@cetusprotocol/aggregator-sdk 0.3.32 → 0.4.1

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/src/client.ts CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  } from "@pythnetwork/pyth-sui-js"
50
50
  import { Steamm } from "./transaction/steamm"
51
51
  import { Metastable } from "./transaction/metastable"
52
+ import { Obric } from "./transaction/obric"
52
53
 
53
54
  export const CETUS = "CETUS"
54
55
  export const DEEPBOOKV2 = "DEEPBOOK"
@@ -71,6 +72,7 @@ export const ALPHAFI = "ALPHAFI"
71
72
  export const SPRINGSUI = "SPRINGSUI"
72
73
  export const STEAMM = "STEAMM"
73
74
  export const METASTABLE = "METASTABLE"
75
+ export const OBRIC = "OBRIC"
74
76
  export const DEFAULT_ENDPOINT = "https://api-sui.cetus.zone/router_v2"
75
77
 
76
78
  export type BuildRouterSwapParams = {
@@ -124,7 +126,6 @@ export interface SwapInPoolsParams {
124
126
  }
125
127
 
126
128
  interface PythConfig {
127
- connections: SuiPriceServiceConnection[]
128
129
  wormholeStateId: string
129
130
  pythStateId: string
130
131
  }
@@ -146,11 +147,21 @@ function isBuilderFastRouterSwapParams(
146
147
  return Array.isArray((params as BuildFastRouterSwapParams).routers)
147
148
  }
148
149
 
150
+ export type AggregatorClientParams = {
151
+ endpoint?: string
152
+ signer?: string
153
+ client?: SuiClient
154
+ env?: Env
155
+ pythUrls?: string[]
156
+ apiKey?: string
157
+ }
158
+
149
159
  export class AggregatorClient {
150
160
  public endpoint: string
151
161
  public signer: string
152
162
  public client: SuiClient
153
163
  public env: Env
164
+ public apiKey: string
154
165
  private allCoins: Map<string, CoinAsset[]>
155
166
 
156
167
  private pythConnections: SuiPriceServiceConnection[]
@@ -158,26 +169,12 @@ export class AggregatorClient {
158
169
 
159
170
  private static readonly CONFIG: Record<Env, PythConfig> = {
160
171
  [Env.Testnet]: {
161
- connections: [
162
- new SuiPriceServiceConnection("https://hermes-beta.pyth.network"),
163
- ],
164
172
  wormholeStateId:
165
173
  "0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790",
166
174
  pythStateId:
167
175
  "0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c",
168
176
  },
169
177
  [Env.Mainnet]: {
170
- connections: [
171
- new SuiPriceServiceConnection(
172
- "https://cetus-pythnet-a648.mainnet.pythnet.rpcpool.com/219cf7a8-6d75-432d-a648-d487a6dd5dc3/hermes",
173
- {
174
- timeout: 3000,
175
- }
176
- ),
177
- new SuiPriceServiceConnection("https://hermes.pyth.network", {
178
- timeout: 3000,
179
- }),
180
- ],
181
178
  wormholeStateId:
182
179
  "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
183
180
  pythStateId:
@@ -185,25 +182,30 @@ export class AggregatorClient {
185
182
  },
186
183
  }
187
184
 
188
- constructor(
189
- endpoint?: string,
190
- signer?: string,
191
- client?: SuiClient,
192
- env?: Env
193
- ) {
194
- this.endpoint = endpoint ? processEndpoint(endpoint) : DEFAULT_ENDPOINT
195
- this.client = client || new SuiClient({ url: getFullnodeUrl("mainnet") })
196
- this.signer = signer || ""
197
- this.env = env || Env.Mainnet
185
+ constructor(params: AggregatorClientParams) {
186
+ this.endpoint = params.endpoint ? processEndpoint(params.endpoint) : DEFAULT_ENDPOINT
187
+ this.client = params.client || new SuiClient({ url: getFullnodeUrl("mainnet") })
188
+ this.signer = params.signer || ""
189
+ this.env = params.env || Env.Mainnet
198
190
  this.allCoins = new Map<string, CoinAsset[]>()
199
191
 
200
192
  const config = AggregatorClient.CONFIG[this.env]
201
- this.pythConnections = config.connections
193
+ this.pythConnections = this.newPythClients(params.pythUrls ?? [])
202
194
  this.pythClient = new SuiPythClient(
203
195
  this.client,
204
196
  config.pythStateId,
205
197
  config.wormholeStateId
206
198
  )
199
+ this.apiKey = params.apiKey || ""
200
+ }
201
+
202
+ newPythClients(pythUrls: string[]) {
203
+ if (!pythUrls.includes("https://hermes.pyth.network")) {
204
+ pythUrls.push("https://hermes.pyth.network")
205
+ }
206
+
207
+ const connections = pythUrls.map(url => new SuiPriceServiceConnection(url, { timeout: 3000 }))
208
+ return connections
207
209
  }
208
210
 
209
211
  async getCoins(
@@ -255,7 +257,79 @@ export class AggregatorClient {
255
257
  }
256
258
 
257
259
  async findRouters(params: FindRouterParams): Promise<RouterData | null> {
258
- return getRouterResult(this.endpoint, params)
260
+ return getRouterResult(this.endpoint, this.apiKey, params)
261
+ }
262
+
263
+ async executeFlexibleInputSwap(
264
+ txb: Transaction,
265
+ inputCoin: TransactionObjectArgument,
266
+ routers: Router[],
267
+ amountOutLimit: BN,
268
+ pythPriceIDs: Map<string, string>,
269
+ partner?: string,
270
+ deepbookv3DeepFee?: TransactionObjectArgument,
271
+ packages?: Map<string, string>
272
+ ) {
273
+ if (routers.length === 0) {
274
+ throw new Error("No router found")
275
+ }
276
+
277
+ const outputCoinType = routers[0].path[routers[0].path.length - 1].target
278
+ const outputCoins = []
279
+
280
+ for (let i = 0; i < routers.length - 1; i++) {
281
+ if (routers[i].path.length === 0) {
282
+ throw new Error("Empty path")
283
+ }
284
+ // 为每条路径分割所需的代币
285
+ const splitCoin = txb.splitCoins(inputCoin, [routers[i].amountIn.toString()])
286
+ let nextCoin = splitCoin[0] as TransactionObjectArgument
287
+
288
+ for (const path of routers[i].path) {
289
+ const dex = this.newDex(path.provider, pythPriceIDs, partner)
290
+ nextCoin = await dex.swap(
291
+ this,
292
+ txb,
293
+ path,
294
+ nextCoin,
295
+ packages,
296
+ deepbookv3DeepFee
297
+ )
298
+ }
299
+ outputCoins.push(nextCoin)
300
+ }
301
+
302
+ // 处理最后一条路径,使用剩余的所有代币
303
+ if (routers[routers.length - 1].path.length === 0) {
304
+ throw new Error("Empty path")
305
+ }
306
+ let lastCoin = inputCoin
307
+ for (const path of routers[routers.length - 1].path) {
308
+ const dex = this.newDex(path.provider, pythPriceIDs, partner)
309
+ lastCoin = await dex.swap(
310
+ this,
311
+ txb,
312
+ path,
313
+ lastCoin,
314
+ packages,
315
+ deepbookv3DeepFee
316
+ )
317
+ }
318
+ outputCoins.push(lastCoin)
319
+
320
+ const aggregatorV2PublishedAt = getAggregatorV2PublishedAt(
321
+ this.publishedAtV2(),
322
+ packages
323
+ )
324
+
325
+ const mergedTargetCoin = this.checkCoinThresholdAndMergeCoin(
326
+ txb,
327
+ outputCoins,
328
+ outputCoinType,
329
+ amountOutLimit,
330
+ aggregatorV2PublishedAt
331
+ )
332
+ return mergedTargetCoin
259
333
  }
260
334
 
261
335
  async expectInputSwap(
@@ -476,6 +550,69 @@ export class AggregatorClient {
476
550
  return targetCoin
477
551
  }
478
552
 
553
+ async fixableRouterSwap(
554
+ params: BuildRouterSwapParamsV2
555
+ ): Promise<TransactionObjectArgument> {
556
+ const { routers, inputCoin, slippage, txb, partner, deepbookv3DeepFee } =
557
+ params
558
+
559
+ const routerData = Array.isArray(routers) ? routers : routers.routes
560
+ const byAmountIn = params.routers.byAmountIn
561
+
562
+ const amountIn = routerData.reduce(
563
+ (acc, router) => acc.add(router.amountIn),
564
+ new BN(0)
565
+ )
566
+ const amountOut = routerData.reduce(
567
+ (acc, router) => acc.add(router.amountOut),
568
+ new BN(0)
569
+ )
570
+
571
+ const amountLimit = CalculateAmountLimitBN(
572
+ byAmountIn ? amountOut : amountIn,
573
+ byAmountIn,
574
+ slippage
575
+ )
576
+
577
+ const packages = isBuilderRouterSwapParams(params)
578
+ ? undefined
579
+ : params.routers.packages
580
+
581
+ const aggregatorV2PublishedAt = getAggregatorV2PublishedAt(
582
+ this.publishedAtV2(),
583
+ packages
584
+ )
585
+
586
+ const priceIDs = findPythPriceIDs(routerData)
587
+
588
+ const priceInfoObjectIds =
589
+ priceIDs.length > 0
590
+ ? await this.updatePythPriceIDs(priceIDs, txb)
591
+ : new Map<string, string>()
592
+
593
+ if (byAmountIn) {
594
+ const targetCoin = await this.executeFlexibleInputSwap(
595
+ txb,
596
+ inputCoin,
597
+ routerData,
598
+ amountLimit,
599
+ priceInfoObjectIds,
600
+ partner,
601
+ deepbookv3DeepFee,
602
+ packages
603
+ )
604
+ return targetCoin
605
+ }
606
+
607
+ const targetCoin = await this.expectOutputSwap(
608
+ txb,
609
+ inputCoin,
610
+ routerData,
611
+ partner
612
+ )
613
+ return targetCoin
614
+ }
615
+
479
616
  // auto build input coin
480
617
  // auto merge, transfer or destory target coin.
481
618
  async fastRouterSwap(
@@ -611,7 +748,8 @@ export class AggregatorClient {
611
748
  // return "0xf2fcea41dc217385019828375764fa06d9bd25e8e4726ba1962680849fb8d613" // version 8
612
749
  // return "0xa2d8a4279d69d8fec04b2fea8852d0d467d3cc0d39c5890180d439ae7a9953ed" // version 9
613
750
  // return "0x34ef25b60b51f9d07cd9b7dc5b08dfdf26c7b0ff00c57bb17454c161fa6b6b83" // version 10
614
- return "0xf57be4b9f9036034b1c5484d299d8fb68d5f43862d6afe8886d67db293dfc4bc" // version 11
751
+ // return "0xf57be4b9f9036034b1c5484d299d8fb68d5f43862d6afe8886d67db293dfc4bc" // version 11
752
+ return "0x200e762fa2c49f3dc150813038fbf22fd4f894ac6f23ebe1085c62f2ef97f1ca" // version 12
615
753
  } else {
616
754
  return "0xabb6a81c8a216828e317719e06125de5bb2cb0fe8f9916ff8c023ca5be224c78"
617
755
  }
@@ -712,6 +850,8 @@ export class AggregatorClient {
712
850
  return new Steamm(this.env)
713
851
  case METASTABLE:
714
852
  return new Metastable(this.env, pythPriceIDs)
853
+ case OBRIC:
854
+ return new Obric(this.env, pythPriceIDs)
715
855
  default:
716
856
  throw new Error(`Unsupported dex ${provider}`)
717
857
  }
@@ -776,7 +916,7 @@ export class AggregatorClient {
776
916
 
777
917
  if (priceUpdateData == null) {
778
918
  throw new Error(
779
- `No pyth price seeds update data found: ${lastError?.message}`
919
+ `All Pyth price nodes are unavailable. Cannot fetch price data. Please switch to or add new available Pyth nodes. Detailed error: ${lastError?.message}`
780
920
  )
781
921
  }
782
922
 
@@ -788,7 +928,7 @@ export class AggregatorClient {
788
928
  priceIDs
789
929
  )
790
930
  } catch (e) {
791
- throw new Error(`Failed to update price feeds: ${e}`)
931
+ throw new Error(`All Pyth price nodes are unavailable. Cannot fetch price data. Please switch to or add new available Pyth nodes in the pythUrls parameter when initializing AggregatorClient, for example: new AggregatorClient({ pythUrls: ["https://your-pyth-node-url"] }). Detailed error: ${e}`)
792
932
  }
793
933
 
794
934
  let priceInfoObjectIdsMap = new Map<string, string>()
@@ -822,6 +962,14 @@ export function findPythPriceIDs(routes: Router[]): string[] {
822
962
  priceIDs.add(path.extendedDetails.metastableETHPriceSeed)
823
963
  }
824
964
  }
965
+ if (path.provider === OBRIC) {
966
+ if (path.extendedDetails && path.extendedDetails.obricCoinAPriceSeed) {
967
+ priceIDs.add(path.extendedDetails.obricCoinAPriceSeed)
968
+ }
969
+ if (path.extendedDetails && path.extendedDetails.obricCoinBPriceSeed) {
970
+ priceIDs.add(path.extendedDetails.obricCoinBPriceSeed)
971
+ }
972
+ }
825
973
  }
826
974
  }
827
975
  return Array.from(priceIDs)
@@ -871,7 +1019,8 @@ export function parseRouterResponse(
871
1019
  path.provider === SCALLOP ||
872
1020
  path.provider === HAEDALPMM ||
873
1021
  path.provider === STEAMM ||
874
- path.provider === METASTABLE
1022
+ path.provider === METASTABLE ||
1023
+ path.provider === OBRIC
875
1024
  ) {
876
1025
  extendedDetails = {
877
1026
  aftermathLpSupplyType:
@@ -899,6 +1048,10 @@ export function parseRouterResponse(
899
1048
  metastableCreateCapModule: path.extended_details?.metastable_create_cap_module,
900
1049
  metastableCreateCapAllTypeParams: path.extended_details?.metastable_create_cap_all_type_params,
901
1050
  metastableRegistryId: path.extended_details?.metastable_registry_id,
1051
+ obricCoinAPriceSeed: path.extended_details?.obric_coin_a_price_seed,
1052
+ obricCoinBPriceSeed: path.extended_details?.obric_coin_b_price_seed,
1053
+ obricCoinAPriceId: path.extended_details?.obric_coin_a_price_id,
1054
+ obricCoinBPriceId: path.extended_details?.obric_coin_b_price_id,
902
1055
  }
903
1056
  }
904
1057
 
@@ -0,0 +1,90 @@
1
+ import {
2
+ Transaction,
3
+ TransactionArgument,
4
+ TransactionObjectArgument,
5
+ } from "@mysten/sui/transactions"
6
+ import {
7
+ AggregatorClient,
8
+ CLOCK_ADDRESS,
9
+ Dex,
10
+ Env,
11
+ getAggregatorV2ExtendPublishedAt,
12
+ Path,
13
+ } from ".."
14
+
15
+ export class Obric implements Dex {
16
+ private pythPriceIDs: Map<string, string>
17
+ private pythStateObjectId: string
18
+
19
+ constructor(env: Env, pythPriceIDs: Map<string, string>) {
20
+ if (env === Env.Testnet) {
21
+ throw new Error("Obric is not supported on testnet")
22
+ }
23
+ this.pythPriceIDs = pythPriceIDs
24
+ this.pythStateObjectId = "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8"
25
+ }
26
+
27
+ async swap(
28
+ client: AggregatorClient,
29
+ txb: Transaction,
30
+ path: Path,
31
+ inputCoin: TransactionObjectArgument,
32
+ packages?: Map<string, string>
33
+ ): Promise<TransactionObjectArgument> {
34
+ const { direction, from, target } = path
35
+ const [func, coinAType, coinBType] = direction
36
+ ? ["swap_a2b", from, target]
37
+ : ["swap_b2a", target, from]
38
+
39
+ let coinAPriceSeed
40
+ let coinBPriceSeed
41
+
42
+ let coinAPriceInfoObjectId
43
+ let coinBPriceInfoObjectId
44
+
45
+ if (path.extendedDetails == null) {
46
+ throw new Error("Extended details not supported in obric")
47
+ } else {
48
+ if (
49
+ path.extendedDetails.obricCoinAPriceSeed && path.extendedDetails.obricCoinBPriceSeed
50
+ ) {
51
+ coinAPriceSeed = path.extendedDetails.obricCoinAPriceSeed
52
+ coinAPriceInfoObjectId = this.pythPriceIDs.get(coinAPriceSeed!)
53
+ coinBPriceSeed = path.extendedDetails.obricCoinBPriceSeed
54
+ coinBPriceInfoObjectId = this.pythPriceIDs.get(coinBPriceSeed!)
55
+ } else {
56
+ if (!path.extendedDetails.obricCoinAPriceId || !path.extendedDetails.obricCoinBPriceId) {
57
+ throw new Error("Base price id or quote price id not supported")
58
+ } else {
59
+ coinAPriceInfoObjectId = path.extendedDetails.obricCoinAPriceId
60
+ coinBPriceInfoObjectId = path.extendedDetails.obricCoinBPriceId
61
+ }
62
+ }
63
+ }
64
+
65
+ if (!coinAPriceInfoObjectId || !coinBPriceInfoObjectId) {
66
+ throw new Error(
67
+ "Base price info object id or quote price info object id not found"
68
+ )
69
+ }
70
+
71
+ const args = [
72
+ txb.object(path.id),
73
+ inputCoin,
74
+ txb.object(this.pythStateObjectId),
75
+ txb.object(coinAPriceInfoObjectId),
76
+ txb.object(coinBPriceInfoObjectId),
77
+ txb.object(CLOCK_ADDRESS),
78
+ ]
79
+ const publishedAt = getAggregatorV2ExtendPublishedAt(
80
+ client.publishedAtV2Extend(),
81
+ packages
82
+ )
83
+ const res = txb.moveCall({
84
+ target: `${publishedAt}::obric::${func}`,
85
+ typeArguments: [coinAType, coinBType],
86
+ arguments: args,
87
+ }) as TransactionArgument
88
+ return res
89
+ }
90
+ }
@@ -0,0 +1,132 @@
1
+ import { describe, test } from "@jest/globals"
2
+ import dotenv from "dotenv"
3
+ import { AggregatorClient } from "~/client"
4
+ import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"
5
+ import { printTransaction } from "~/utils/transaction"
6
+ import BN from "bn.js"
7
+ import { fromB64 } from "@mysten/sui/utils"
8
+ import { SuiClient } from "@mysten/sui/client"
9
+ import { Env } from "~/index"
10
+ import { Transaction } from "@mysten/sui/transactions"
11
+
12
+ dotenv.config()
13
+
14
+ export function buildTestAccount(): Ed25519Keypair {
15
+ const mnemonics = process.env.SUI_WALLET_MNEMONICS || ""
16
+ const testAccountObject = Ed25519Keypair.deriveKeypair(mnemonics)
17
+ return testAccountObject
18
+ }
19
+
20
+ describe("Test scallop provider", () => {
21
+ let client: AggregatorClient
22
+ let keypair: Ed25519Keypair
23
+
24
+ const T_SUI = "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
25
+ const LSDT_SUI = "0xd1b72982e40348d069bb1ff701e634c117bb5f741f44dff91e472d3b01461e55::stsui::STSUI"
26
+ const T_USDC = "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"
27
+
28
+ beforeAll(() => {
29
+ const fullNodeURL = process.env.SUI_RPC!
30
+ const aggregatorURL = process.env.CETUS_AGGREGATOR!
31
+ const secret = process.env.SUI_WALLET_SECRET!
32
+
33
+ if (secret) {
34
+ keypair = Ed25519Keypair.fromSecretKey(fromB64(secret).slice(1, 33))
35
+ } else {
36
+ keypair = buildTestAccount()
37
+ }
38
+
39
+ const wallet =
40
+ "0x4dde66fc52ec16d5e6c0fbd0968580cdf0d962cbb970591ec1e47617b9265617"
41
+
42
+ console.log("wallet: ", wallet)
43
+
44
+ const endpoint = aggregatorURL
45
+
46
+ const suiClient = new SuiClient({
47
+ url: fullNodeURL,
48
+ })
49
+
50
+ client = new AggregatorClient({
51
+ endpoint,
52
+ signer: wallet,
53
+ client: suiClient,
54
+ env: Env.Mainnet,
55
+ pythUrls: ["https://cetus-pythnet-a648.mainnet.pythnet.rpcpool.com/219cf7a8-6d75-432d-a648-d487a6dd5dc3/hermes"],
56
+ apiKey: "8MJDUzLDPJxCgbc7I0bHXSg994mVfh8NRMqV6hcQ",
57
+ })
58
+ })
59
+
60
+ test("Find Routers", async () => {
61
+ const amounts = ["1000", "1000000", "100000000", "5000000000", "1000000000000000000000000000"]
62
+
63
+ while (true) {
64
+ const res = await client.findRouters({
65
+ from: LSDT_SUI,
66
+ target: T_USDC,
67
+ amount: new BN("1000000000000"),
68
+ byAmountIn: true,
69
+ depth: 3,
70
+ splitCount: 1,
71
+ providers: ["ALPHAFI"],
72
+ })
73
+
74
+ if (res != null) {
75
+ console.log(JSON.stringify(res, null, 2))
76
+ }
77
+ console.log("amount in", res?.amountIn.toString())
78
+ console.log("amount out", res?.amountOut.toString())
79
+ }
80
+ }, 6000000)
81
+
82
+ test("Build Router TX", async () => {
83
+ const amount = "100000"
84
+
85
+ const res = await client.findRouters({
86
+ from: LSDT_SUI,
87
+ target: T_SUI,
88
+ amount: new BN(amount),
89
+ byAmountIn: true,
90
+ depth: 3,
91
+ splitCount: 2,
92
+ providers: ["ALPHAFI", "CETUS"],
93
+ })
94
+
95
+ console.log("amount in", res?.amountIn.toString())
96
+ console.log("amount out", res?.amountOut.toString())
97
+
98
+ const txb = new Transaction()
99
+
100
+ if (res != null) {
101
+ console.log(JSON.stringify(res, null, 2))
102
+ await client.fastRouterSwap({
103
+ routers: res,
104
+ txb,
105
+ slippage: 0.01,
106
+ refreshAllCoins: true,
107
+ payDeepFeeAmount: 0,
108
+ })
109
+
110
+ txb.setSender(client.signer)
111
+ const buildTxb = await txb.build({ client: client.client })
112
+ // const buildTxb = await txb.getData()
113
+
114
+ console.log("buildTxb", buildTxb)
115
+
116
+ printTransaction(txb)
117
+
118
+ let result = await client.devInspectTransactionBlock(txb)
119
+ console.log("🚀 ~ file: router.test.ts:180 ~ test ~ result:", result)
120
+ for (const event of result.events) {
121
+ console.log("event", JSON.stringify(event, null, 2))
122
+ }
123
+
124
+ if (result.effects.status.status === "success") {
125
+ const result = await client.signAndExecuteTransaction(txb, keypair)
126
+ console.log("result", result)
127
+ } else {
128
+ console.log("result", result)
129
+ }
130
+ }
131
+ }, 600000)
132
+ })
@@ -57,7 +57,13 @@ describe("Test metastable provider", () => {
57
57
  const suiClient = new SuiClient({
58
58
  url: fullNodeURL,
59
59
  })
60
- client = new AggregatorClient(endpoint, wallet, suiClient, Env.Mainnet)
60
+ client = new AggregatorClient({
61
+ endpoint,
62
+ signer: wallet,
63
+ client: suiClient,
64
+ env: Env.Mainnet,
65
+ pythUrls: ["https://cetus-pythnet-a648.mainnet.pythnet.rpcpool.com/219cf7a8-6d75-432d-a648-d487a6dd5dc3/hermes"],
66
+ })
61
67
  })
62
68
 
63
69
  test("Find Routers --> SUI -> SUPER_SUI", async () => {