Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: /swap endpoint #1269

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/dependency-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
with:
fail-on-severity: high
# Comma-separated list of GHSA IDs to allow.
# We allow @openzeppelin/[email protected] critical vulnerabilities
# We allow the following critical vulnerabilities
# because they are not exploitable in our usage of the package.
allow-ghsas: GHSA-fg47-3c2x-m2wr, GHSA-88g8-f5mf-f5rj, GHSA-3h5v-q93c-6h6q
# - @openzeppelin/[email protected]
# - @openzeppelin/[email protected]
allow-ghsas: GHSA-fg47-3c2x-m2wr, GHSA-88g8-f5mf-f5rj, GHSA-3h5v-q93c-6h6q, GHSA-qh9x-gcfh-pcrw, GHSA-4g63-c64m-25w9, GHSA-xrc4-737v-9q75, GHSA-4h98-2769-gh6h, GHSA-4h98-2769-gh6h, GHSA-93hq-5wgc-jc82
25 changes: 11 additions & 14 deletions api/_dexes/1inch.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import axios from "axios";

import { AcrossSwap, SwapQuoteAndCalldata } from "./types";
import { Swap, OriginSwapQuoteAndCalldata } from "./types";
import { getSwapAndBridgeAddress } from "./utils";

export async function get1inchQuoteAndCalldata(
swap: AcrossSwap
): Promise<SwapQuoteAndCalldata> {
const swapAndBridgeAddress = getSwapAndBridgeAddress(
"1inch",
swap.swapToken.chainId
);
const apiBaseUrl = `https://api.1inch.dev/swap/v6.0/${swap.swapToken.chainId}`;
export async function get1inchQuoteForOriginSwapExactInput(
swap: Omit<Swap, "recipient">
): Promise<OriginSwapQuoteAndCalldata> {
const swapAndBridgeAddress = getSwapAndBridgeAddress("1inch", swap.chainId);
const apiBaseUrl = `https://api.1inch.dev/swap/v6.0/${swap.chainId}`;
const apiHeaders = {
Authorization: `Bearer ${process.env.ONEINCH_API_KEY}`,
accept: "application/json",
};

const swapParams = {
src: swap.swapToken.address,
dst: swap.acrossInputToken.address,
amount: swap.swapTokenAmount,
src: swap.tokenIn.address,
dst: swap.tokenOut.address,
amount: swap.amount,
from: swapAndBridgeAddress,
slippage: swap.slippage,
slippage: swap.slippageTolerance,
disableEstimate: true,
allowPartialFill: false,
receiver: swapAndBridgeAddress,
Expand All @@ -47,6 +44,6 @@ export async function get1inchQuoteAndCalldata(
value: response.data.tx.value,
swapAndBridgeAddress,
dex: "1inch",
slippage: swap.slippage,
slippage: swap.slippageTolerance,
};
}
142 changes: 142 additions & 0 deletions api/_dexes/cross-swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
isRouteEnabled,
isInputTokenBridgeable,
isOutputTokenBridgeable,
getBridgeQuoteForMinOutput,
} from "../_utils";
import {
getUniswapCrossSwapQuotesForMinOutputB2A,
getUniswapCrossSwapQuotesForMinOutputA2B,
getBestUniswapCrossSwapQuotesForMinOutputA2A,
} from "./uniswap";
import { CrossSwap, CrossSwapQuotes } from "./types";

export type CrossSwapType =
(typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE];

export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE];

export const AMOUNT_TYPE = {
EXACT_INPUT: "exactInput",
MIN_OUTPUT: "minOutput",
} as const;

export const CROSS_SWAP_TYPE = {
BRIDGEABLE_TO_BRIDGEABLE: "bridgeableToBridgeable",
BRIDGEABLE_TO_ANY: "bridgeableToAny",
ANY_TO_BRIDGEABLE: "anyToBridgeable",
ANY_TO_ANY: "anyToAny",
} as const;

export const PREFERRED_BRIDGE_TOKENS = ["WETH", "USDC"];

export async function getCrossSwapQuotes(
crossSwap: CrossSwap
): Promise<CrossSwapQuotes> {
if (crossSwap.type === AMOUNT_TYPE.EXACT_INPUT) {
// @TODO: Add support for exact input amount
throw new Error("Not implemented yet");
}

if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) {
return getCrossSwapQuotesForMinOutput(crossSwap);
}

throw new Error("Invalid amount type");
}

export async function getCrossSwapQuotesForMinOutput(crossSwap: CrossSwap) {
const crossSwapType = getCrossSwapType({
inputToken: crossSwap.inputToken.address,
originChainId: crossSwap.inputToken.chainId,
outputToken: crossSwap.outputToken.address,
destinationChainId: crossSwap.outputToken.chainId,
});

if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) {
return getCrossSwapQuotesForMinOutputB2B(crossSwap);
}

if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) {
return getCrossSwapQuotesForMinOutputB2A(crossSwap);
}

if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) {
return getCrossSwapQuotesForMinOutputA2B(crossSwap);
}

if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) {
return getCrossSwapQuotesForMinOutputA2A(crossSwap);
}

throw new Error("Invalid cross swap type");
}

// @TODO: Implement the following function
export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) {

Check warning on line 76 in api/_dexes/cross-swap.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'crossSwap' is defined but never used. Allowed unused args must match /^_/u
throw new Error("Not implemented yet");
}

export async function getCrossSwapQuotesForMinOutputB2B(crossSwap: CrossSwap) {
const bridgeQuote = await getBridgeQuoteForMinOutput({
inputToken: crossSwap.inputToken,
outputToken: crossSwap.outputToken,
minOutputAmount: crossSwap.amount,
// @TODO: handle ETH/WETH message generation
message: "0x",
});
return {
crossSwap,
destinationSwapQuote: undefined,
bridgeQuote,
originSwapQuote: undefined,
};
}

export async function getCrossSwapQuotesForMinOutputB2A(crossSwap: CrossSwap) {
// @TODO: Add support for other DEXes / aggregators
return getUniswapCrossSwapQuotesForMinOutputB2A(crossSwap);
}

export async function getCrossSwapQuotesForMinOutputA2B(crossSwap: CrossSwap) {
// @TODO: Add support for other DEXes / aggregators
return getUniswapCrossSwapQuotesForMinOutputA2B(crossSwap);
}

export async function getCrossSwapQuotesForMinOutputA2A(crossSwap: CrossSwap) {
// @TODO: Add support for other DEXes / aggregators
return getBestUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, {
preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS,
bridgeRoutesLimit: 2,
});
}

export function getCrossSwapType(params: {
inputToken: string;
originChainId: number;
outputToken: string;
destinationChainId: number;
}): CrossSwapType {
if (
isRouteEnabled(
params.originChainId,
params.destinationChainId,
params.inputToken,
params.outputToken
)
) {
return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE;
}

if (isInputTokenBridgeable(params.inputToken, params.originChainId)) {
return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY;
}

if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) {
return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE;
}

return CROSS_SWAP_TYPE.ANY_TO_ANY;
}

export function calcFees() {}
68 changes: 56 additions & 12 deletions api/_dexes/types.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,74 @@
import { BigNumber } from "ethers";
import { getSuggestedFees } from "../_utils";
import { AmountType, CrossSwapType } from "./cross-swap";

export type { AmountType, CrossSwapType };

export type Token = {
address: string;
decimals: number;
symbol: string;
chainId: number;
};

/**
* @property `swapToken` - Address of the token that will be swapped for acrossInputToken.
* @property `acrossInputToken` - Address of the token that will be bridged via Across as the inputToken.
* @property `swapTokenAmount` - The amount of swapToken to be swapped for acrossInputToken.
* @property `slippage` - The slippage tolerance for the swap in decimals, e.g. 1 for 1%.
*/
export type AcrossSwap = {
swapToken: Token;
acrossInputToken: Token;
swapTokenAmount: string;
slippage: number;
export type Swap = {
chainId: number;
tokenIn: Token;
tokenOut: Token;
amount: string;
recipient: string;
slippageTolerance: number;
type: AmountType;
};

export type CrossSwap = {
amount: BigNumber;
inputToken: Token;
outputToken: Token;
recipient: string;
slippageTolerance: number;
type: AmountType;
refundOnOrigin: boolean;
refundAddress?: string;
};

export type SupportedDex = "1inch" | "uniswap";

export type SwapQuoteAndCalldata = {
export type OriginSwapQuoteAndCalldata = {
minExpectedInputTokenAmount: string;
routerCalldata: string;
value: string;
swapAndBridgeAddress: string;
dex: SupportedDex;
slippage: number;
};

export type SwapQuote = {
maximumAmountIn: BigNumber;
minAmountOut: BigNumber;
expectedAmountOut: BigNumber;
expectedAmountIn: BigNumber;
slippageTolerance: number;
swapTx: {
to: string;
data: string;
value: string;
};
tokenIn: Token;
tokenOut: Token;
};

export type CrossSwapQuotes = {
crossSwap: CrossSwap;
bridgeQuote: {
message?: string;
inputToken: Token;
outputToken: Token;
inputAmount: BigNumber;
outputAmount: BigNumber;
minOutputAmount: BigNumber;
suggestedFees: Awaited<ReturnType<typeof getSuggestedFees>>;
};
destinationSwapQuote?: SwapQuote;
originSwapQuote?: SwapQuote;
};
Loading
Loading