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: add stablecoins for a2a bridging #1373

Merged
merged 13 commits into from
Jan 15, 2025
76 changes: 50 additions & 26 deletions api/_dexes/cross-swap-service.ts
dohaki marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
buildMinOutputBridgeTokenMessage,
getCrossSwapType,
getQuoteFetchStrategy,
NoQuoteFoundError,
PREFERRED_BRIDGE_TOKENS,
QuoteFetchStrategies,
} from "./utils";
Expand Down Expand Up @@ -79,7 +80,7 @@
}

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

Check warning on line 83 in api/_dexes/cross-swap-service.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");
}

Expand Down Expand Up @@ -376,7 +377,7 @@
strategies: QuoteFetchStrategies
) {
const preferredBridgeTokens = PREFERRED_BRIDGE_TOKENS;
const bridgeRoutesLimit = 1;
const bridgeRoutesToCompareChunkSize = 2;

const originSwapChainId = crossSwap.inputToken.chainId;
const destinationSwapChainId = crossSwap.outputToken.chainId;
Expand All @@ -399,36 +400,59 @@
const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) =>
preferredBridgeTokens.includes(toTokenSymbol)
);
const bridgeRoutesToCompare = (
preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes
).slice(0, bridgeRoutesLimit);
const bridgeRoutes =
preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes;

let chunkStart = 0;
while (chunkStart < bridgeRoutes.length) {
const bridgeRoutesToCompare = bridgeRoutes.slice(
chunkStart,
chunkStart + bridgeRoutesToCompareChunkSize
);

if (bridgeRoutesToCompare.length === 0) {
throw new Error(
`No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}`
if (bridgeRoutesToCompare.length === 0) {
throw new Error(
`No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}`
);
}

const crossSwapQuotesResults = await Promise.allSettled(
bridgeRoutesToCompare.map((bridgeRoute) =>
getCrossSwapQuotesForOutputByRouteA2A(
crossSwap,
bridgeRoute,
originStrategy,
destinationStrategy
)
)
);
}

const crossSwapQuotes = await Promise.all(
bridgeRoutesToCompare.map((bridgeRoute) =>
getCrossSwapQuotesForOutputByRouteA2A(
crossSwap,
bridgeRoute,
originStrategy,
destinationStrategy
const crossSwapQuotes = crossSwapQuotesResults
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);

if (crossSwapQuotes.length === 0) {
chunkStart += bridgeRoutesToCompareChunkSize;
continue;
}

// Compare quotes by lowest input amount
const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) =>
prev.originSwapQuote!.maximumAmountIn.lt(
curr.originSwapQuote!.maximumAmountIn
)
)
);
? prev
: curr
);
return bestCrossSwapQuote;
}

// Compare quotes by lowest input amount
const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) =>
prev.originSwapQuote!.maximumAmountIn.lt(
curr.originSwapQuote!.maximumAmountIn
)
? prev
: curr
);
return bestCrossSwapQuote;
throw new NoQuoteFoundError({
originSwapChainId,
inputTokenSymbol: crossSwap.inputToken.symbol,
destinationSwapChainId,
outputTokenSymbol: crossSwap.outputToken.symbol,
});
}

export async function getCrossSwapQuotesForOutputByRouteA2A(
Expand Down
71 changes: 41 additions & 30 deletions api/_dexes/uniswap/trading-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TradeType } from "@uniswap/sdk-core";
import axios from "axios";
import axios, { AxiosError } from "axios";

import { Swap } from "../types";
import { V2PoolInRoute, V3PoolInRoute } from "./adapter";
Expand Down Expand Up @@ -76,38 +76,49 @@ export async function getUniswapClassicQuoteFromApi(

export async function getUniswapClassicIndicativeQuoteFromApi(
swap: UniswapParamForApi,
tradeType: TradeType
tradeType: TradeType,
useFallback: boolean = true
) {
const response = await axios.post<{
requestId: string;
input: {
amount: string;
chainId: number;
token: string;
};
output: {
amount: string;
chainId: number;
token: string;
};
}>(
`${UNISWAP_TRADING_API_BASE_URL}/indicative_quote`,
{
type:
tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT",
amount: swap.amount,
tokenInChainId: swap.tokenIn.chainId,
tokenOutChainId: swap.tokenOut.chainId,
tokenIn: swap.tokenIn.address,
tokenOut: swap.tokenOut.address,
},
{
headers: {
"x-api-key": UNISWAP_API_KEY,
try {
const response = await axios.post<{
requestId: string;
input: {
amount: string;
chainId: number;
token: string;
};
output: {
amount: string;
chainId: number;
token: string;
};
}>(
`${UNISWAP_TRADING_API_BASE_URL}/indicative_quote`,
{
type:
tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT",
amount: swap.amount,
tokenInChainId: swap.tokenIn.chainId,
tokenOutChainId: swap.tokenOut.chainId,
tokenIn: swap.tokenIn.address,
tokenOut: swap.tokenOut.address,
},
{
headers: {
"x-api-key": UNISWAP_API_KEY,
},
}
);
return response.data;
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 404) {
if (useFallback) {
const { quote } = await getUniswapClassicQuoteFromApi(swap, tradeType);
return quote;
}
}
);
return response.data;
throw error;
}
}

export async function getUniswapClassicCalldataFromApi(
Expand Down
15 changes: 14 additions & 1 deletion api/_dexes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const CROSS_SWAP_TYPE = {
ANY_TO_ANY: "anyToAny",
} as const;

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

export const defaultQuoteFetchStrategy: QuoteFetchStrategy =
// This will be our default strategy until the periphery contract is audited
Expand Down Expand Up @@ -416,3 +416,16 @@ export function assertMinOutputAmount(
);
}
}

export class NoQuoteFoundError extends Error {
constructor(params: {
originSwapChainId: number;
inputTokenSymbol: string;
destinationSwapChainId: number;
outputTokenSymbol: string;
}) {
super(
`No quote found for ${params.originSwapChainId} ${params.inputTokenSymbol} -> ${params.destinationSwapChainId} ${params.outputTokenSymbol}`
);
}
}
6 changes: 4 additions & 2 deletions api/swap/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ export function buildBaseSwapResponseJson(params: {
data: string;
value?: BigNumber;
gas?: BigNumber;
gasPrice: BigNumber;
maxFeePerGas?: BigNumber;
maxPriorityFeePerGas?: BigNumber;
};
permitSwapTx?: AuthTxPayload | PermitTxPayload;
}) {
Expand Down Expand Up @@ -368,7 +369,8 @@ export function buildBaseSwapResponseJson(params: {
data: params.approvalSwapTx.data,
value: params.approvalSwapTx.value,
gas: params.approvalSwapTx.gas,
gasPrice: params.approvalSwapTx.gasPrice,
maxFeePerGas: params.approvalSwapTx.maxFeePerGas,
maxPriorityFeePerGas: params.approvalSwapTx.maxPriorityFeePerGas,
}
: params.permitSwapTx
? params.permitSwapTx.swapTx
Expand Down
142 changes: 142 additions & 0 deletions api/swap/approval/_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { BigNumber, constants } from "ethers";

import { getProvider, latestGasPriceCache } from "../../_utils";
import { buildCrossSwapTxForAllowanceHolder } from "./_utils";
import {
handleBaseSwapQueryParams,
BaseSwapQueryParams,
getApprovalTxns,
buildBaseSwapResponseJson,
} from "../_utils";
import { getBalanceAndAllowance } from "../../_erc20";
import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service";
import { QuoteFetchStrategies } from "../../_dexes/utils";
import { TypedVercelRequest } from "../../_types";
import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02";

// For approval-based flows, we use the `UniversalSwapAndBridge` strategy with Uniswap V3's `SwapRouter02`
const quoteFetchStrategies: QuoteFetchStrategies = {
default: getSwapRouter02Strategy("UniversalSwapAndBridge"),
};

export async function handleApprovalSwap(
request: TypedVercelRequest<BaseSwapQueryParams>
) {
const {
integratorId,
skipOriginTxEstimation,
isInputNative,
isOutputNative,
inputToken,
outputToken,
amount,
amountType,
refundOnOrigin,
refundAddress,
recipient,
depositor,
slippageTolerance,
refundToken,
} = await handleBaseSwapQueryParams(request.query);

const crossSwapQuotes = await getCrossSwapQuotes(
{
amount,
inputToken,
outputToken,
depositor,
recipient: recipient || depositor,
slippageTolerance: Number(slippageTolerance),
type: amountType,
refundOnOrigin,
refundAddress,
isInputNative,
isOutputNative,
},
quoteFetchStrategies
);

const crossSwapTx = await buildCrossSwapTxForAllowanceHolder(
crossSwapQuotes,
integratorId
);

const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } =
crossSwapQuotes;

const originChainId = crossSwap.inputToken.chainId;
const inputTokenAddress = isInputNative
? constants.AddressZero
: crossSwap.inputToken.address;
const inputAmount =
originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount;

const { allowance, balance } = await getBalanceAndAllowance({
chainId: originChainId,
tokenAddress: inputTokenAddress,
owner: crossSwap.depositor,
spender: crossSwapTx.to,
});

const isSwapTxEstimationPossible =
!skipOriginTxEstimation &&
allowance.gte(inputAmount) &&
balance.gte(inputAmount);

let originTxGas: BigNumber | undefined;
let originTxGasPrice:
| {
maxFeePerGas: BigNumber;
maxPriorityFeePerGas: BigNumber;
}
| undefined;
if (isSwapTxEstimationPossible) {
const provider = getProvider(originChainId);
[originTxGas, originTxGasPrice] = await Promise.all([
provider.estimateGas({
...crossSwapTx,
from: crossSwap.depositor,
}),
latestGasPriceCache(originChainId).get(),
]);
} else {
originTxGasPrice = await latestGasPriceCache(originChainId).get();
}

let approvalTxns:
| {
chainId: number;
to: string;
data: string;
}[]
| undefined;
// @TODO: Allow for just enough approval amount to be set.
const approvalAmount = constants.MaxUint256;
if (allowance.lt(inputAmount)) {
approvalTxns = getApprovalTxns({
token: crossSwap.inputToken,
spender: crossSwapTx.to,
amount: approvalAmount,
});
}

const responseJson = buildBaseSwapResponseJson({
originChainId,
inputTokenAddress,
inputAmount,
approvalSwapTx: {
...crossSwapTx,
gas: originTxGas,
maxFeePerGas: originTxGasPrice?.maxFeePerGas,
maxPriorityFeePerGas: originTxGasPrice?.maxPriorityFeePerGas,
},
allowance,
balance,
approvalTxns,
originSwapQuote,
bridgeQuote,
destinationSwapQuote,
refundToken,
});
return responseJson;
}
Loading
Loading