import axios, { AxiosResponse } from "axios"; import BigNumber from "bignumber.js"; import invariant from "tiny-invariant"; import Web3 from "web3"; import { TransactionConfig } from "web3-eth"; import { Token } from "../Tokens"; import { Address, BigNumberString, ChainId, formatRouteRequest, } from "../constants"; import { getRouterContract } from "../constants/contracts"; import { applySlippage } from "../utils"; import { Wallet } from "../wallet"; export type NodeRoute = { path: string[]; pairs: string[]; extras: (string | number[])[]; inputAmount: string | number; expectedOutputAmount: string | number; deadline: string | number; partner: string | number; sig: string | number[]; }; export type RouterResponse = { expectedOut: BigNumberString; routerAddress: Address; details: NodeRoute; txn: TransactionConfig | { error: string }; minimumExpectedOut: BigNumberString; priceImpact: { numerator: number; denominator: number; }; }; export type RouterPayloadRequest = { tokenIn: Address; tokenOut: Address; amountIn: BigNumberString; deadlineMs?: number; chainId?: ChainId; to?: Address; from?: Address; slippage?: number; maxHops?: number; includeTxn?: boolean; priceImpact?: boolean; }; export function convertNodeRouteToContractPayload( route: NodeRoute, slippage: number, to: Address ): [ string[], string[], (string | number[])[], string | number, string | number, string | number, string, string | number, string | number, string | number[] ] { const minimum = applySlippage( new BigNumber(route.expectedOutputAmount), slippage ); return [ route.path, route.pairs, route.extras, route.inputAmount, minimum.toFixed(0), route.expectedOutputAmount, to, route.deadline, route.partner, route.sig, ]; } /** * @class SmartRouter * Utility Class to Wrap Minima Functionality and facilitate swaps * * @example * * ```ts * const router = new SmartRouter('abc-123-987-beef') * const {details, txn} = await router.getRouteBase(CELO, CUSD, '10000', { includeTxn: true, maxHops: 3 }) * const await provider.signTransaction(txn); * ``` */ export class SmartRouter { constructor(private apiKey?: string, private wallet?: Wallet) {} /** * Pings the minima service to find the most efficient trade to go from input to output. * * @param input Token to be traded in. Provide either the address or the Token object * @param output Token to be traded for. Provide either the address or the Token object * @param amountIn Total input amount for the trade, can be BigNumber or Stringified BigNumber * @param options Additional options * @returns Trade information, including a pre-formatted txn to sign and send to execute a trade */ public async getRouteBase( input: Token | string, output: Token | string, amountIn: BigNumber | BigNumberString, options?: Partial ): Promise<{ details?: NodeRoute; routerAddress?: Address; expectedOut?: BigNumber; error?: string; txn?: TransactionConfig | { error: string }; priceImpact?: number; }> { input = typeof input === "string" ? input : input.address; output = typeof output === "string" ? output : output.address; amountIn = typeof amountIn === "string" ? amountIn : amountIn.toFixed(0); try { const url = formatRouteRequest(); const resp = await axios.get< RouterResponse, AxiosResponse, RouterPayloadRequest >(url, { params: { tokenIn: input, tokenOut: output, amountIn, ...options, }, headers: { ["x-api-key"]: this.apiKey ?? "", }, }); const { expectedOut, priceImpact, ...rest } = resp.data; return { expectedOut: new BigNumber(expectedOut), priceImpact: priceImpact ? priceImpact.numerator / priceImpact.denominator : undefined, ...rest, }; } catch (e) { let error = "failed"; if (axios.isAxiosError(e)) { error = e.message; } return { error }; } } /** * * Formats the route as a transaction to sign and execute. * * @param route Route returned from `getRouteBase` * @param to Recipient address of trade * @param slippage Allowed slippage for the trade, in bips * @param routerAddress Address of router that will execute the trade * @returns TransactionConfig corresponding to the given route * * @example * * ```ts * const { details, routerAddress } = await router.getRouteBase(CELO, CUSD, '10000') * const transactionConfig = router.formatTransaction(details, wallet.address, 10, routerAddress) * ``` */ public static formatTransaction( route: NodeRoute, to: Address, slippage: number, routerAddress: Address ) { const contract = getRouterContract(new Web3(), routerAddress); const data = contract.methods .swapExactInputForOutput( convertNodeRouteToContractPayload(route, slippage, to) ) .encodeABI(); return { to: routerAddress, data, gas: "1000000", }; } /** * * If a Wallet object is attached, will execute the given swal * * @param route * @param to * @param slippage * @param routerAddress * @returns transaction receipt */ public performSwap( route: NodeRoute, to: Address, slippage: number, routerAddress: Address ) { invariant(!!this.wallet, "Wallet required to do a swap"); const txnInfo = SmartRouter.formatTransaction( route, to, slippage, routerAddress ); return this.wallet.signAndSendTransaction([txnInfo]); } }