diff --git a/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.test.ts new file mode 100644 index 000000000..9d2d387f3 --- /dev/null +++ b/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.test.ts @@ -0,0 +1,127 @@ +import { Currency, Ether, Token, TradeType } from "@uniswap/sdk-core"; +import { BigNumber, constants, ethers } from "ethers"; + +import { UnsignedPriorityOrderInfo } from "../order"; + +import { NativeAssets } from "./utils"; + +import { PriorityOrderTrade } from "."; + +const USDC = new Token( + 1, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + 6, + "USDC" +); +const DAI = new Token( + 1, + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + 18, + "DAI" +); + +describe("PriorityOrderTrade", () => { + const NON_FEE_OUTPUT_AMOUNT = BigNumber.from("1000000000000000000"); + + const orderInfo: UnsignedPriorityOrderInfo = { + deadline: Math.floor(new Date().getTime() / 1000) + 1000, + reactor: "0x0000000000000000000000000000000000000000", + swapper: "0x0000000000000000000000000000000000000000", + nonce: BigNumber.from(10), + cosigner: "0x0000000000000000000000000000000000000000", + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + auctionStartBlock: BigNumber.from(100000), + baselinePriorityFeeWei: BigNumber.from(2), + input: { + token: USDC.address, + amount: BigNumber.from(1000), + mpsPerPriorityFeeWei: BigNumber.from(0), + }, + outputs: [ + { + token: DAI.address, + amount: NON_FEE_OUTPUT_AMOUNT, + mpsPerPriorityFeeWei: BigNumber.from(5), + recipient: "0x0000000000000000000000000000000000000000", + }, + { + token: DAI.address, + amount: BigNumber.from("1000"), + mpsPerPriorityFeeWei: BigNumber.from(5), + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + + const trade = new PriorityOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT, + }); + + it("returns the right input amount for an exact-in trade", () => { + expect(trade.inputAmount.quotient.toString()).toEqual( + orderInfo.input.amount.toString() + ); + }); + + it("returns the correct non-fee output amount", () => { + expect(trade.outputAmount.quotient.toString()).toEqual( + NON_FEE_OUTPUT_AMOUNT.toString() + ); + }); + + it("returns the correct minimum amount out", () => { + expect(trade.minimumAmountOut().quotient.toString()).toEqual( + NON_FEE_OUTPUT_AMOUNT.toString() + ); + }); + + it("works for native output trades", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: NativeAssets.ETH, + amount: NON_FEE_OUTPUT_AMOUNT, + mpsPerPriorityFeeWei: BigNumber.from(5), + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new PriorityOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); + + it("works for native output trades where order info has 0 address", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: constants.AddressZero, + amount: NON_FEE_OUTPUT_AMOUNT, + mpsPerPriorityFeeWei: BigNumber.from(5), + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new PriorityOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); +}); diff --git a/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.ts b/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.ts new file mode 100644 index 000000000..c48f8d829 --- /dev/null +++ b/sdks/uniswapx-sdk/src/trade/PriorityOrderTrade.ts @@ -0,0 +1,154 @@ +import { Currency, CurrencyAmount, Price, TradeType } from "@uniswap/sdk-core"; + +import { UnsignedPriorityOrder, UnsignedPriorityOrderInfo } from "../order"; + +import { areCurrenciesEqual } from "./utils"; + +export class PriorityOrderTrade< + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType +> { + public readonly tradeType: TTradeType; + public readonly order: UnsignedPriorityOrder; + + private _inputAmount: CurrencyAmount | undefined; + private _outputAmounts: CurrencyAmount[] | undefined; + + private _currencyIn: TInput; + private _currenciesOut: TOutput[]; + + public constructor({ + currencyIn, + currenciesOut, + orderInfo, + tradeType, + }: { + currencyIn: TInput; + currenciesOut: TOutput[]; + orderInfo: UnsignedPriorityOrderInfo; + tradeType: TTradeType; + }) { + this._currencyIn = currencyIn; + this._currenciesOut = currenciesOut; + this.tradeType = tradeType; + + // assume single-chain for now + this.order = new UnsignedPriorityOrder(orderInfo, currencyIn.chainId); + } + + public get inputAmount(): CurrencyAmount { + if (this._inputAmount) return this._inputAmount; + + const amount = CurrencyAmount.fromRawAmount( + this._currencyIn, + this.order.info.input.amount.toString() + ); + this._inputAmount = amount; + return amount; + } + + public get outputAmounts(): CurrencyAmount[] { + if (this._outputAmounts) return this._outputAmounts; + + const amounts = this.order.info.outputs.map((output) => { + // assume single chain ids across all outputs for now + const currencyOut = this._currenciesOut.find((currency) => + areCurrenciesEqual(currency, output.token, currency.chainId) + ); + + if (!currencyOut) { + throw new Error("currency not found in output array"); + } + + return CurrencyAmount.fromRawAmount( + currencyOut, + output.amount.toString() + ); + }); + + this._outputAmounts = amounts; + return amounts; + } + + private _firstNonFeeOutputAmount: + | CurrencyAmount + | undefined; + + private getFirstNonFeeOutputAmount(): CurrencyAmount { + if (this._firstNonFeeOutputAmount) + return this._firstNonFeeOutputAmount; + + if (this.order.info.outputs.length === 0) { + throw new Error("there must be at least one output token"); + } + const output = this.order.info.outputs[0]; + + // assume single chain ids across all outputs for now + const currencyOut = this._currenciesOut.find((currency) => + areCurrenciesEqual(currency, output.token, currency.chainId) + ); + + if (!currencyOut) { + throw new Error( + "currency output from order must exist in currenciesOut list" + ); + } + + const amount = + CurrencyAmount.fromRawAmount( + currencyOut, + output.amount.toString() + ); + + this._firstNonFeeOutputAmount = amount; + return amount; + } + + // TODO: revise when there are actually multiple output amounts. for now, assume only one non-fee output at a time + public get outputAmount(): CurrencyAmount { + // TODO: estimate epected amount, using classic quote or expected priority + return this.getFirstNonFeeOutputAmount(); + } + + public minimumAmountOut(): CurrencyAmount { + return this.getFirstNonFeeOutputAmount(); + } + + public maximumAmountIn(): CurrencyAmount { + return CurrencyAmount.fromRawAmount( + this._currencyIn, + this.order.info.input.amount.toString() + ); + } + + private _executionPrice: Price | undefined; + + /** + * The price expressed in terms of output amount/input amount. + */ + public get executionPrice(): Price { + return ( + this._executionPrice ?? + (this._executionPrice = new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.inputAmount.quotient, + this.outputAmount.quotient + )) + ); + } + + /** + * Return the execution price after accounting for slippage tolerance + * @returns The execution price + */ + public worstExecutionPrice(): Price { + return new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.maximumAmountIn().quotient, + this.minimumAmountOut().quotient + ); + } +} diff --git a/sdks/uniswapx-sdk/src/trade/index.ts b/sdks/uniswapx-sdk/src/trade/index.ts index c16f72c32..37d79ca70 100644 --- a/sdks/uniswapx-sdk/src/trade/index.ts +++ b/sdks/uniswapx-sdk/src/trade/index.ts @@ -1,3 +1,4 @@ export * from "./DutchOrderTrade"; export * from "./V2DutchOrderTrade"; +export * from "./PriorityOrderTrade"; export * from "./RelayOrderTrade";