浏览代码

rough impl of odos

ani 1 年之前
父节点
当前提交
d15fe1a881

+ 1 - 0
express_relay/swap-beacon/package.json

@@ -16,6 +16,7 @@
   "dependencies": {
     "@graphprotocol/graph-ts": "^0.35.1",
     "@pythnetwork/express-relay-evm-js": "^0.8.1",
+    "axios": "^1.7.3",
     "viem": "^2.18.6"
   }
 }

+ 101 - 12
express_relay/swap-beacon/src/adapter/odos.ts

@@ -1,27 +1,116 @@
-import { Adapter, Pair, ExtendedTargetCall, TokenToSend } from "../types";
+import { Adapter, ExtendedTargetCall, TokenToSend } from "../types";
 import { Address, Hex } from "viem";
 import { TokenAmount } from "@pythnetwork/express-relay-evm-js";
+import axios, { AxiosInstance } from "axios";
+import { getSwapAdapterConfig } from "../index";
 
 export class OdosAdapter implements Adapter {
   chainIds: string[] = ["34443"];
-  getPrice(chainId: string, pair: Pair): Promise<number> {
-    return Promise.resolve(1);
+  private httpClient: AxiosInstance;
+  constructor(timeout?: number) {
+    this.httpClient = axios.create({
+      baseURL: "https://api.odos.xyz/api",
+      timeout: timeout || 5000,
+    });
   }
-  constructSwaps(
+  async getPrice(
     chainId: string,
     tokenIn: Address,
     tokenOut: Address,
     amountIn?: bigint,
     amountOut?: bigint
-  ): ExtendedTargetCall[] {
-    // construct the swap calldata manually
-    let targetContract: Address = "0x59F78DE21a0b05d96Ae00c547BA951a3B905602f";
-    let targetCalldata: Hex = "0x";
-    let targetCallValue: bigint = 0n;
-    let tokensToSend: TokenToSend[] = [];
+  ): Promise<number> {
+    if (typeof amountIn === "undefined") {
+      throw new Error("amountIn must be defined");
+    }
+    if (typeof amountOut !== "undefined") {
+      throw new Error("amountOut must not be defined");
+    }
 
-    // TODO: figure out this
-    let tokensToReceive: TokenAmount[] = [];
+    const response = await this.httpClient.get("/sor/quote/v2", {
+      params: {
+        chainId: chainId,
+        inputTokens: [
+          {
+            amount: amountIn.toString(),
+            tokenAddress: tokenIn,
+          },
+        ],
+        outputTokens: [
+          {
+            proportion: 1,
+            tokenAddress: tokenOut,
+          },
+        ],
+        slippageLimitPercent: 0.5,
+        userAddr: getSwapAdapterConfig(chainId).multicallAdapter,
+      },
+    });
+    return response.data.outTokens[0] / response.data.inTokens[0];
+  }
+  async constructSwaps(
+    chainId: string,
+    tokenIn: Address,
+    tokenOut: Address,
+    amountIn?: bigint,
+    amountOut?: bigint
+  ): Promise<ExtendedTargetCall[]> {
+    if (typeof amountIn === "undefined") {
+      throw new Error("amountIn must be defined");
+    }
+
+    const responseQuote = await this.httpClient.get("/sor/quote/v2", {
+      params: {
+        chainId: chainId,
+        inputTokens: [
+          {
+            amount: amountIn.toString(),
+            tokenAddress: tokenIn,
+          },
+        ],
+        outputTokens: [
+          {
+            proportion: 1,
+            tokenAddress: tokenOut,
+          },
+        ],
+        slippageLimitPercent: 0.5,
+        userAddr: getSwapAdapterConfig(chainId).multicallAdapter,
+      },
+    });
+    if (typeof amountOut !== "undefined") {
+      if (responseQuote.data.outTokens[0] < amountOut) {
+        throw new Error("Not enough output tokens");
+      }
+    }
+    const pathId = responseQuote.data.pathId;
+
+    const responseTx = await this.httpClient.get("/sor/assemble", {
+      params: {
+        pathId: pathId,
+        simulate: false,
+        userAddr: getSwapAdapterConfig(chainId).multicallAdapter,
+      },
+    });
+    const targetCalldata: Hex = responseTx.data.transaction.data;
+    const targetContract: Address = responseTx.data.transaction.to;
+    const targetCallValue: bigint = BigInt(responseTx.data.transaction.value);
+
+    const tokensToSend: TokenToSend[] = responseTx.data.inputTokens.map(
+      (inputToken: { tokenAddress: Address; amount: string }) => ({
+        tokenAmount: {
+          token: inputToken.tokenAddress as Address,
+          amount: inputToken.amount,
+        },
+        destination: responseTx.data.transaction.to,
+      })
+    );
+    const tokensToReceive: TokenAmount[] = responseTx.data.outputTokens.map(
+      (outputToken: { tokenAddress: Address; amount: string }) => ({
+        token: outputToken.tokenAddress as Address,
+        amount: outputToken.amount,
+      })
+    );
 
     return [
       {

+ 28 - 26
express_relay/swap-beacon/src/index.ts

@@ -3,7 +3,6 @@ import { SWAP_ADAPTER_CONFIGS } from "./const";
 import {
   Client,
   Opportunity,
-  OpportunityParams,
   ChainId,
   TokenAmount,
   OPPORTUNITY_ADAPTER_CONFIGS,
@@ -14,7 +13,7 @@ import { multicallAbi } from "./abi";
 
 export class SwapBeaconError extends Error {}
 
-function getSwapAdapterConfig(chainId: string) {
+export function getSwapAdapterConfig(chainId: string) {
   const swapAdapterConfig = SWAP_ADAPTER_CONFIGS[chainId];
   if (!swapAdapterConfig) {
     throw new SwapBeaconError(
@@ -44,12 +43,10 @@ export class SwapBeacon {
     tokenIn: Address,
     tokenOut: Address
   ) {
-    const pair = {
-      token0: tokenIn,
-      token1: tokenOut,
-    };
     const prices = await Promise.all(
-      this.adapters.map((adapter) => adapter.getPrice(chainId, pair))
+      this.adapters.map((adapter) =>
+        adapter.getPrice(chainId, tokenIn, tokenOut)
+      )
     );
 
     return this.adapters[
@@ -199,34 +196,39 @@ export class SwapBeacon {
       Promise.all(promisesOptimalAdaptersBuy),
     ]);
 
-    const swapsSell = optimalAdaptersSell
-      .map((adapter, index) =>
-        adapter.constructSwaps(
-          opportunity.chainId,
-          base,
-          opportunity.sellTokens[index].token,
-          undefined,
-          opportunity.sellTokens[index].amount
+    const swapsSell = (
+      await Promise.all(
+        optimalAdaptersSell.map(
+          async (adapter, index) =>
+            await adapter.constructSwaps(
+              opportunity.chainId,
+              base,
+              opportunity.sellTokens[index].token,
+              undefined,
+              opportunity.sellTokens[index].amount
+            )
         )
       )
-      .reduce((acc, val) => acc.concat(val), []);
-    const swapsBuy = optimalAdaptersBuy
-      .map((adapter, index) =>
-        adapter.constructSwaps(
-          opportunity.chainId,
-          opportunity.buyTokens[index].token,
-          base,
-          opportunity.buyTokens[index].amount,
-          undefined
+    ).reduce((acc, val) => acc.concat(val), []);
+    const swapsBuy = (
+      await Promise.all(
+        optimalAdaptersBuy.map(
+          async (adapter, index) =>
+            await adapter.constructSwaps(
+              opportunity.chainId,
+              opportunity.buyTokens[index].token,
+              base,
+              opportunity.buyTokens[index].amount,
+              undefined
+            )
         )
       )
-      .reduce((acc, val) => acc.concat(val), []);
+    ).reduce((acc, val) => acc.concat(val), []);
 
     return this.createSwapOpportunity(opportunity, base, swapsSell, swapsBuy);
   }
 
   async opportunityHandler(opportunity: Opportunity) {
-    // check opportunity
     const swapAdapterConfig = getSwapAdapterConfig(opportunity.chainId);
 
     await Promise.all(

+ 8 - 3
express_relay/swap-beacon/src/types.ts

@@ -33,19 +33,24 @@ export type TargetCall = {
   tokensToSend: TokenToSend[];
 };
 
-// TODO: better name
 export type ExtendedTargetCall = TargetCall & {
   tokensToReceive: TokenAmount[];
 };
 
 export interface Adapter {
   chainIds: string[];
-  getPrice: (chainId: string, pair: Pair) => Promise<number>;
+  getPrice: (
+    chainId: string,
+    tokenIn: Address,
+    tokenOut: Address,
+    amountIn?: bigint,
+    amountOut?: bigint
+  ) => Promise<number>;
   constructSwaps: (
     chainId: string,
     tokenIn: Address,
     tokenOut: Address,
     amountIn?: bigint,
     amountOut?: bigint
-  ) => ExtendedTargetCall[];
+  ) => Promise<ExtendedTargetCall[]>;
 }

+ 38 - 11
pnpm-lock.yaml

@@ -4,16 +4,6 @@ settings:
   autoInstallPeers: true
   excludeLinksFromLockfile: false
 
-overrides:
-  '@injectivelabs/sdk-ts@1.10.72>@injectivelabs/token-metadata': 1.10.42
-  eslint-config-next>@typescript-eslint/parser: ^7.0.0
-  '@solana/web3.js@^1.93.0': 1.92.3
-
-patchedDependencies:
-  eccrypto@1.1.6:
-    hash: rjcfmtfgn3z72mudpdif5oxmye
-    path: patches/eccrypto@1.1.6.patch
-
 importers:
 
   .:
@@ -554,7 +544,10 @@ importers:
         version: 0.35.1
       '@pythnetwork/express-relay-evm-js':
         specifier: ^0.8.1
-        version: link:../sdk/js
+        version: 0.8.1(axios@1.7.3)(bufferutil@4.0.8)(js-yaml@4.1.0)(typescript@5.5.4)(utf-8-validate@6.0.4)(zod@3.23.8)
+      axios:
+        specifier: ^1.7.3
+        version: 1.7.3
       viem:
         specifier: ^2.18.6
         version: 2.18.6(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@6.0.4)(zod@3.23.8)
@@ -8612,6 +8605,9 @@ packages:
   axios@1.7.2:
     resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
 
+  axios@1.7.3:
+    resolution: {integrity: sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==}
+
   axobject-query@3.2.1:
     resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
 
@@ -26230,6 +26226,21 @@ snapshots:
       - utf-8-validate
       - zod
 
+  '@pythnetwork/express-relay-evm-js@0.8.1(axios@1.7.3)(bufferutil@4.0.8)(js-yaml@4.1.0)(typescript@5.5.4)(utf-8-validate@6.0.4)(zod@3.23.8)':
+    dependencies:
+      isomorphic-ws: 5.0.0(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      openapi-client-axios: 7.5.5(axios@1.7.3)(js-yaml@4.1.0)
+      openapi-fetch: 0.8.2
+      viem: 2.18.6(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@6.0.4)(zod@3.23.8)
+      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - axios
+      - bufferutil
+      - js-yaml
+      - typescript
+      - utf-8-validate
+      - zod
+
   '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
       '@pythnetwork/price-service-sdk': 1.7.1
@@ -31256,6 +31267,14 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
+  axios@1.7.3:
+    dependencies:
+      follow-redirects: 1.15.6
+      form-data: 4.0.0
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
   axobject-query@3.2.1:
     dependencies:
       dequal: 2.0.3
@@ -40094,6 +40113,14 @@ snapshots:
       js-yaml: 4.1.0
       openapi-types: 12.1.3
 
+  openapi-client-axios@7.5.5(axios@1.7.3)(js-yaml@4.1.0):
+    dependencies:
+      axios: 1.7.3
+      bath-es5: 3.0.3
+      dereference-json-schema: 0.2.1
+      js-yaml: 4.1.0
+      openapi-types: 12.1.3
+
   openapi-fetch@0.8.2:
     dependencies:
       openapi-typescript-helpers: 0.0.5