UNPKG

@paraswap/sdk

Version:
310 lines (270 loc) 10.3 kB
import type { WithGasPrice, WithMaxFee } from '../../gas'; import type { ConstructFetchInput, Address, FetcherPostInput, PriceString, OptimalRate, } from '../../types'; import { assert } from 'ts-essentials'; import { API_URL, SwapSide } from '../../constants'; import { constructSearchString } from '../../helpers/misc'; import type { OrderData } from '../limitOrders/buildOrder'; import { sanitizeOrderData as sanitizeLimitOrderData } from '../limitOrders/helpers/misc'; import { sanitizeOrderData as sanitizeNFTOrderData } from '../nftOrders/helpers/misc'; import { AssetTypeVariant } from '../nftOrders/helpers/types'; export interface TransactionParams { to: string; from: string; value: string; data: string; gas?: string; chainId: number; // either gasPrice or maxFeePerGas & maxPriorityFeePerGas will be present gasPrice?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; } export type SwappableOrder = OrderData & { permitMakerAsset?: string; signature: string; }; export type SwappableNFTOrder = SwappableOrder & { makerAssetId: string; takerAssetId: string; makerAssetType: AssetTypeVariant; takerAssetType: AssetTypeVariant; }; // when priceRoute with side=SELL, slippage can replace destAmount export type TxInputAmountsPartSell = { slippage: number; srcAmount: PriceString; destAmount?: never; // disallowed }; // when priceRoute with side=BUY, slippage can replace srcAmount export type TxInputAmountsPartBuy = { slippage: number; srcAmount?: never; // disallowed destAmount: PriceString; }; // both srcAmount and destAmount can be present in absence of slippage export type TxInputAmountsPartBuyOrSell = { slippage?: never; // disallowed srcAmount: PriceString; destAmount: PriceString; }; // more details in the docs https://developers.paraswap.network/api/build-parameters-for-transaction#request-body export type BuildTxInputBase = { srcToken: Address; destToken: Address; userAddress: Address; /** @description Whenever msg.sender (`userAddress`) i.e. address calling the ParaSwap contract is different than the address sending the transaction, `txOrigin` must be passed along with `userAddress` */ txOrigin?: string; /** @description used with referral link */ referrer?: string; partner?: string; partnerAddress?: string; partnerFeeBps?: number; /** @description If user should receive surplus instead of partner. Default: false */ isSurplusToUser?: boolean; /** @description If fees should be sent directly to the partner instead of registering them on FeeClaimer. v6 only. Default: false */ isDirectFeeTransfer?: boolean; /** @deprecated Use "takeSurplus" instead. Positive slippage goes to user, true by default */ positiveSlippageToUser?: boolean; /** @description Set to true to take positive slippage. Works with partnerAddress. Default: false */ takeSurplus?: boolean; /** @description Cap the surplus at 1% maximum. Default: true */ isCapSurplus?: boolean; receiver?: Address; srcDecimals?: number; destDecimals?: number; permit?: string; deadline?: string; }; // for Swap transaction export type BuildSwapTxInput = BuildTxInputBase & { priceRoute: OptimalRate; } & ( | TxInputAmountsPartSell | TxInputAmountsPartBuy | TxInputAmountsPartBuyOrSell ); // this union doesn't allow to mix srcAmount & destAmount & slippage together // building block for LimitOrders and NFT Orders swaps // can only use priceRoute.side=BUY and related TxInputAmountsPart* type BuildTxInputBaseBUYForOrders< // to Omit extra keys // can't do Omit<> around union, breaks discriminated union K extends keyof TxInputAmountsPartBuy | keyof BuildTxInputBase = never > = Omit<BuildTxInputBase, K> & // destAmount is sum(orders[].makerAmount) (| Omit<TxInputAmountsPartBuy, 'destAmount' | K> | Omit<TxInputAmountsPartBuyOrSell, 'destAmount' | K> ); // for LimitOrder Fill, without swap export type BuildLimitOrderTxInput = BuildTxInputBaseBUYForOrders & { orders: SwappableOrder[]; srcDecimals: number; destDecimals: number; }; // for NFT Order Fill, without swap export type BuildNFTOrderTxInput = // @TODO if NFT can ever be srcToken, change logic // for NFT token destDecimals = 0 is acceptable BuildTxInputBaseBUYForOrders<'destDecimals'> & { orders: SwappableNFTOrder[]; srcDecimals: number; }; export interface BuildSwapAndLimitOrderTxInput0 // destAmount is sum(orders[].makerAmount) extends Omit<BuildTxInputBase, 'destAmount'> { priceRoute: OptimalRate; // priceRoute.side=BUY orders: SwappableOrder[]; destDecimals: number; } // for Swap + LimitOrder, priceRoute must have side=BUY export type BuildSwapAndLimitOrderTxInput = // destAmount is sum(orders[].makerAmount) BuildTxInputBaseBUYForOrders & { priceRoute: OptimalRate; // priceRoute.side=BUY & priceRoute.contractMethod=simpleBuy orders: SwappableOrder[]; destDecimals: number; }; // with slippage for a swap and fill - p2p - order, without to fill a p2p order directly with the intended taker asset // for Swap + NFT Order, priceRoute must have side=BUY export type BuildSwapAndNFTOrderTxInput = // destAmount is sum(orders[].makerAmount) BuildTxInputBaseBUYForOrders & { priceRoute: OptimalRate; // priceRoute.side=BUY & priceRoute.contractMethod=simpleBuy orders: SwappableNFTOrder[]; }; export type BuildTxInput = | BuildSwapTxInput | BuildLimitOrderTxInput | BuildNFTOrderTxInput | BuildSwapAndLimitOrderTxInput | BuildSwapAndNFTOrderTxInput; export type BuildOptionsBase = { /** @description Allows the API to skip performing onchain checks such as balances, allowances, as well as transaction simulations. The response does not contain `gas` parameter when set to `true` */ ignoreChecks?: boolean; /** @description Allows the API to skip gas checks. The response does not contain `gas` parameter when set to `true` */ ignoreGasEstimate?: boolean; /** @description Allows the API to skip performing onchain allowance checks. */ ignoreAllowance?: boolean; /** @description Allows the API to return the contract parameters only. */ onlyParams?: boolean; }; export type BuildOptionsWithGasPrice = BuildOptionsBase & Partial<WithGasPrice>; export type BuildOptionsWitWithMaxFee = BuildOptionsBase & Partial<WithMaxFee>; export type BuildOptions = BuildOptionsWithGasPrice | BuildOptionsWitWithMaxFee; type BuildTx = ( params: BuildTxInput, options?: BuildOptions, signal?: AbortSignal ) => Promise<TransactionParams>; export type BuildTxFunctions = { buildTx: BuildTx; }; type SearchStringParams = BuildOptions; export const constructBuildTx = ({ apiURL = API_URL, chainId, fetcher, }: ConstructFetchInput): BuildTxFunctions => { const transactionsURL = `${apiURL}/transactions/${chainId}` as const; const buildTx: BuildTx = async (params, options = {}, signal) => { if ( 'priceRoute' in params && 'destAmount' in params && // isn't provided together with `orders` !('orders' in params) // when present, destAmount becomes sum(orders[].makerAmount) ) { const { priceRoute, priceRoute: { side }, } = params; const AmountMismatchError = side === SwapSide.SELL ? 'Source Amount Mismatch' : 'Destination Amount Mismatch'; // user provides srcAmount or slippage but not both. so we only validate accordingly. assert( areAmountsCorrect({ queryParams: params, side, priceRoute, }), AmountMismatchError ); } // always pass explicit type to make sure UrlSearchParams are correct const search = constructSearchString<SearchStringParams>(options); const fetchURL = `${transactionsURL}/${search}` as const; const sanitizedParams = 'orders' in params && params.orders.length > 0 ? { ...params, // make sure we don't pass more with orders than API expects orders: params.orders.map((order) => { const sanitizedOrderData = 'makerAssetId' in order ? sanitizeNFTOrderData(order) // assetType is provided here, because Order.*Asset may be address : // if Order received from API by hash sanitizeLimitOrderData(order); const sanitizedOrder: SwappableOrder = { ...sanitizedOrderData, signature: order.signature, }; if (order.permitMakerAsset) { sanitizedOrder.permitMakerAsset = order.permitMakerAsset; } return sanitizedOrder; }), } : params; const takeSurplus = params.takeSurplus ?? (params.positiveSlippageToUser !== undefined ? !params.positiveSlippageToUser : undefined); if ('positiveSlippageToUser' in sanitizedParams) { // positiveSlippageToUser & takeSurplus together will Error in API delete sanitizedParams.positiveSlippageToUser; } if (takeSurplus !== undefined) { sanitizedParams.takeSurplus = takeSurplus; } const fetchParams: FetcherPostInput = { url: fetchURL, method: 'POST', data: sanitizedParams, signal, }; const builtTx = await fetcher<TransactionParams>(fetchParams); return builtTx; }; return { buildTx }; }; interface AreAmountsCorrectInput { queryParams: { srcAmount?: string; destAmount?: string; slippage?: number }; side: SwapSide; priceRoute: OptimalRate; } function areAmountsCorrect({ queryParams, side, priceRoute, }: AreAmountsCorrectInput): boolean { // return early after a simpler check if the user was swapping before filling if (queryParams.slippage) { return ( (side === SwapSide.BUY && queryParams.destAmount === priceRoute.destAmount) || (side === SwapSide.SELL && queryParams.srcAmount === priceRoute.srcAmount) ); } // provided amounts match the previously queried price route const [inputAmount, priceRouteAmount] = side === SwapSide.SELL ? [queryParams.srcAmount, priceRoute.srcAmount] : [queryParams.destAmount, priceRoute.destAmount]; return inputAmount === priceRouteAmount; }