@cetusprotocol/aggregator-sdk 0.3.31 → 0.4.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.
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 = {
@@ -258,6 +260,78 @@ export class AggregatorClient {
258
260
  return getRouterResult(this.endpoint, params)
259
261
  }
260
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
333
+ }
334
+
261
335
  async expectInputSwap(
262
336
  txb: Transaction,
263
337
  inputCoin: TransactionObjectArgument,
@@ -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(
@@ -610,7 +747,9 @@ export class AggregatorClient {
610
747
  // return "0x347dd58bbd11cd82c8b386b344729717c04a998da73386e82a239cc196d5706b" // version 7
611
748
  // return "0xf2fcea41dc217385019828375764fa06d9bd25e8e4726ba1962680849fb8d613" // version 8
612
749
  // return "0xa2d8a4279d69d8fec04b2fea8852d0d467d3cc0d39c5890180d439ae7a9953ed" // version 9
613
- return "0x34ef25b60b51f9d07cd9b7dc5b08dfdf26c7b0ff00c57bb17454c161fa6b6b83" // version 10
750
+ // return "0x34ef25b60b51f9d07cd9b7dc5b08dfdf26c7b0ff00c57bb17454c161fa6b6b83" // version 10
751
+ // return "0xf57be4b9f9036034b1c5484d299d8fb68d5f43862d6afe8886d67db293dfc4bc" // version 11
752
+ return "0x200e762fa2c49f3dc150813038fbf22fd4f894ac6f23ebe1085c62f2ef97f1ca" // version 12
614
753
  } else {
615
754
  return "0xabb6a81c8a216828e317719e06125de5bb2cb0fe8f9916ff8c023ca5be224c78"
616
755
  }
@@ -711,6 +850,8 @@ export class AggregatorClient {
711
850
  return new Steamm(this.env)
712
851
  case METASTABLE:
713
852
  return new Metastable(this.env, pythPriceIDs)
853
+ case OBRIC:
854
+ return new Obric(this.env, pythPriceIDs)
714
855
  default:
715
856
  throw new Error(`Unsupported dex ${provider}`)
716
857
  }
@@ -821,6 +962,14 @@ export function findPythPriceIDs(routes: Router[]): string[] {
821
962
  priceIDs.add(path.extendedDetails.metastableETHPriceSeed)
822
963
  }
823
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
+ }
824
973
  }
825
974
  }
826
975
  return Array.from(priceIDs)
@@ -870,7 +1019,8 @@ export function parseRouterResponse(
870
1019
  path.provider === SCALLOP ||
871
1020
  path.provider === HAEDALPMM ||
872
1021
  path.provider === STEAMM ||
873
- path.provider === METASTABLE
1022
+ path.provider === METASTABLE ||
1023
+ path.provider === OBRIC
874
1024
  ) {
875
1025
  extendedDetails = {
876
1026
  aftermathLpSupplyType:
@@ -898,6 +1048,10 @@ export function parseRouterResponse(
898
1048
  metastableCreateCapModule: path.extended_details?.metastable_create_cap_module,
899
1049
  metastableCreateCapAllTypeParams: path.extended_details?.metastable_create_cap_all_type_params,
900
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,
901
1055
  }
902
1056
  }
903
1057
 
package/src/errors.ts CHANGED
@@ -51,7 +51,8 @@ export enum AggregatorServerErrorCode {
51
51
  NumberTooLarge = 10001,
52
52
  NoRouter = 10002,
53
53
  InsufficientLiquidity = 10003,
54
- HoneyPot = 10004
54
+ HoneyPot = 10004,
55
+ RateLimitExceeded = 10005,
55
56
  }
56
57
 
57
58
  export function getAggregatorServerErrorMessage(
@@ -68,6 +69,8 @@ export function getAggregatorServerErrorMessage(
68
69
  return "Insufficient Liquidity"
69
70
  case AggregatorServerErrorCode.HoneyPot:
70
71
  return "Target token is detected as a HoneyPot scam"
72
+ case AggregatorServerErrorCode.RateLimitExceeded:
73
+ return "Too many requests. Please try again later"
71
74
  default:
72
75
  return "Unknown error"
73
76
  }
@@ -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
+ }
@@ -3,28 +3,38 @@ import BN from 'bn.js'
3
3
  import Decimal from "decimal.js"
4
4
 
5
5
  export const dealWithFastRouterSwapParamsForMsafe = (data: any) => {
6
- const result = data?.map((item: any) => {
7
- return {
8
- ...item,
9
- amountIn: item?.amountIn?.toString(),
10
- amountOut: item?.amountOut?.toString(),
11
- initialPrice: item?.initialPrice?.toString()
12
- }
13
- } )
6
+ const result = {
7
+ ...data,
8
+ amountIn: data?.amountIn?.toString(),
9
+ amountOut: data?.amountIn?.toString(),
10
+ routes: data?.routes?.map((item:any) => {
11
+ return {
12
+ ...item,
13
+ amountIn: item?.amountIn?.toString(),
14
+ amountOut: item?.amountOut?.toString(),
15
+ initialPrice: item?.initialPrice?.toString()
16
+ }
17
+ })
18
+ }
14
19
 
15
20
  return result
16
21
  }
17
22
 
18
23
 
19
24
  export const restituteMsafeFastRouterSwapParams = (data: any) => {
20
- const result = data?.map((item: any) => {
21
- return {
22
- ...item,
23
- amountIn: new BN(item?.amountIn),
24
- amountOut: new BN(item?.amountOut),
25
- initialPrice: new Decimal(item?.initialPrice)
26
- }
27
- } )
25
+ const result = {
26
+ ...data,
27
+ amountIn: new BN(data?.amountIn),
28
+ amountOut: new BN(data?.amountIn),
29
+ routes: data?.routes?.map((item:any) => {
30
+ return {
31
+ ...item,
32
+ amountIn: new BN(item?.amountIn),
33
+ amountOut: new BN(item?.amountOut),
34
+ initialPrice: new Decimal(item?.initialPrice?.toString())
35
+ }
36
+ })
37
+ }
28
38
 
29
39
  return result
30
- }
40
+ }
@@ -0,0 +1,197 @@
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 obric provider", () => {
21
+ let client: AggregatorClient
22
+ let keypair: Ed25519Keypair
23
+
24
+ const T_SUI = "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
25
+ const WH_USDC = "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN"
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 = keypair.getPublicKey().toSuiAddress()
40
+
41
+ const wallet = "0x935029ca5219502a47ac9b69f556ccf6e2198b5e7815cf50f68846f723739cbd" // has 80 eth
42
+ console.log("wallet: ", wallet)
43
+
44
+ const endpoint = aggregatorURL
45
+
46
+ const suiClient = new SuiClient({
47
+ url: fullNodeURL,
48
+ })
49
+ client = new AggregatorClient(endpoint, wallet, suiClient, Env.Mainnet)
50
+ })
51
+
52
+ test("Find Routers --> SUI -> USDC, locked", async () => {
53
+ // const amounts = ["1000", "1000000", "100000000", "5000000000", "10000000000000"]
54
+ const amounts = ["9990", "5000000000"]
55
+
56
+ for (const amount of amounts) {
57
+ const res = await client.findRouters({
58
+ from: T_SUI,
59
+ target: WH_USDC,
60
+ amount: new BN(amount),
61
+ byAmountIn: true,
62
+ depth: 3,
63
+ splitCount: 1,
64
+ providers: ["OBRIC"],
65
+ })
66
+
67
+ if (res != null) {
68
+ console.log(JSON.stringify(res, null, 2))
69
+ }
70
+ console.log("amount in", res?.amountIn.toString())
71
+ console.log("amount out", res?.amountOut.toString())
72
+
73
+ const txb = new Transaction()
74
+
75
+ if (res != null) {
76
+ console.log(JSON.stringify(res, null, 2))
77
+ await client.fastRouterSwap({
78
+ routers: res,
79
+ txb,
80
+ slippage: 0.01,
81
+ refreshAllCoins: true,
82
+ payDeepFeeAmount: 0,
83
+ })
84
+
85
+ txb.setSender(client.signer)
86
+ const buildTxb = await txb.build({ client: client.client })
87
+ // const buildTxb = await txb.getData()
88
+
89
+ console.log("buildTxb", buildTxb)
90
+ printTransaction(txb)
91
+
92
+ let result = await client.devInspectTransactionBlock(txb)
93
+ console.log("🚀 ~ file: router.test.ts:180 ~ test ~ result:", result)
94
+ for (const event of result.events) {
95
+ console.log("event", JSON.stringify(event, null, 2))
96
+ }
97
+ }
98
+ }
99
+ }, 50000)
100
+
101
+ test("Find Routers --> WUSDC --> SUI, no pyth mode", async () => {
102
+ // const amounts = ["1000", "1000000", "100000000", "5000000000", "10000000000000"]
103
+ // const amounts = ["1000", "1000000", "900000000"]
104
+ const amounts = ["90000000"]
105
+
106
+ for (const amount of amounts) {
107
+ const res = await client.findRouters({
108
+ from: T_USDC,
109
+ target: T_SUI,
110
+ amount: new BN(amount),
111
+ byAmountIn: true,
112
+ depth: 3,
113
+ splitCount: 1,
114
+ providers: ["OBRIC"],
115
+ })
116
+
117
+ if (res != null) {
118
+ console.log(JSON.stringify(res, null, 2))
119
+ }
120
+ console.log("amount in", res?.amountIn.toString())
121
+ console.log("amount out", res?.amountOut.toString())
122
+
123
+ const txb = new Transaction()
124
+
125
+ if (res != null) {
126
+ await client.fastRouterSwap({
127
+ routers: res,
128
+ txb,
129
+ slippage: 0.001,
130
+ refreshAllCoins: true,
131
+ payDeepFeeAmount: 0,
132
+ })
133
+
134
+ txb.setSender(client.signer)
135
+ const buildTxb = await txb.build({ client: client.client })
136
+ // const buildTxb = await txb.getData()
137
+
138
+ // printTransaction(txb)
139
+
140
+ let result = await client.devInspectTransactionBlock(txb)
141
+ console.log("🚀 ~ file: router.test.ts:180 ~ test ~ result:", result)
142
+ for (const event of result.events) {
143
+ console.log("event", JSON.stringify(event, null, 2))
144
+ }
145
+ }
146
+ }
147
+ })
148
+
149
+ test("Build Router TX", async () => {
150
+ const amount = "1000000"
151
+
152
+ const res = await client.findRouters({
153
+ from: T_USDC,
154
+ target: T_SUI,
155
+ amount: new BN(amount),
156
+ byAmountIn: true,
157
+ depth: 3,
158
+ providers: ["OBRIC"],
159
+ })
160
+
161
+ console.log("amount in", res?.amountIn.toString())
162
+ console.log("amount out", res?.amountOut.toString())
163
+
164
+ const txb = new Transaction()
165
+
166
+ if (res != null) {
167
+ console.log(JSON.stringify(res, null, 2))
168
+ await client.fastRouterSwap({
169
+ routers: res,
170
+ txb,
171
+ slippage: 0.01,
172
+ refreshAllCoins: true,
173
+ payDeepFeeAmount: 0,
174
+ })
175
+
176
+ txb.setSender(client.signer)
177
+ const buildTxb = await txb.build({ client: client.client })
178
+ // const buildTxb = await txb.getData()
179
+
180
+ console.log("buildTxb", buildTxb)
181
+ // printTransaction(txb)
182
+
183
+ let result = await client.devInspectTransactionBlock(txb)
184
+ console.log("🚀 ~ file: router.test.ts:180 ~ test ~ result:", result)
185
+ for (const event of result.events) {
186
+ console.log("event", JSON.stringify(event, null, 2))
187
+ }
188
+
189
+ // if (result.effects.status.status === "success") {
190
+ // const result = await client.signAndExecuteTransaction(txb, keypair)
191
+ // console.log("result", result)
192
+ // } else {
193
+ // console.log("result", result)
194
+ // }
195
+ }
196
+ }, 600000)
197
+ })