Skip to content

Commit

Permalink
feat: add stablecoins for a2a bridging (#1373)
Browse files Browse the repository at this point in the history
* feat: add stablecoins for a2a bridging

* prevent infinite loop 508

* fixup

* another try to prevent 508

* fix: override vercel dev typescript version

* fixup

* fixup

* fixup

* fixup

* use fallback in indicative quote

* use services
  • Loading branch information
dohaki authored Jan 15, 2025
1 parent e3acd1c commit 9f02d5d
Show file tree
Hide file tree
Showing 12 changed files with 520 additions and 426 deletions.
76 changes: 50 additions & 26 deletions api/_dexes/cross-swap-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
buildMinOutputBridgeTokenMessage,
getCrossSwapType,
getQuoteFetchStrategy,
NoQuoteFoundError,
PREFERRED_BRIDGE_TOKENS,
QuoteFetchStrategies,
} from "./utils";
Expand Down Expand Up @@ -376,7 +377,7 @@ export async function getCrossSwapQuotesForOutputA2A(
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 @@ export async function getCrossSwapQuotesForOutputA2A(
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

0 comments on commit 9f02d5d

Please sign in to comment.