-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add priority order trade (#101)
- Loading branch information
Showing
3 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Currency, Currency, TradeType>({ | ||
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<Currency, Currency, TradeType>( | ||
{ | ||
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<Currency, Currency, TradeType>( | ||
{ | ||
currencyIn: USDC, | ||
currenciesOut: [Ether.onChain(1)], | ||
orderInfo: ethOutputOrderInfo, | ||
tradeType: TradeType.EXACT_INPUT, | ||
} | ||
); | ||
expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TInput> | undefined; | ||
private _outputAmounts: CurrencyAmount<TOutput>[] | 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<TInput> { | ||
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<TOutput>[] { | ||
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<TOutput> | ||
| undefined; | ||
|
||
private getFirstNonFeeOutputAmount(): CurrencyAmount<TOutput> { | ||
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<TOutput> { | ||
// TODO: estimate epected amount, using classic quote or expected priority | ||
return this.getFirstNonFeeOutputAmount(); | ||
} | ||
|
||
public minimumAmountOut(): CurrencyAmount<TOutput> { | ||
return this.getFirstNonFeeOutputAmount(); | ||
} | ||
|
||
public maximumAmountIn(): CurrencyAmount<TInput> { | ||
return CurrencyAmount.fromRawAmount( | ||
this._currencyIn, | ||
this.order.info.input.amount.toString() | ||
); | ||
} | ||
|
||
private _executionPrice: Price<TInput, TOutput> | undefined; | ||
|
||
/** | ||
* The price expressed in terms of output amount/input amount. | ||
*/ | ||
public get executionPrice(): Price<TInput, TOutput> { | ||
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<TInput, TOutput> { | ||
return new Price( | ||
this.inputAmount.currency, | ||
this.outputAmount.currency, | ||
this.maximumAmountIn().quotient, | ||
this.minimumAmountOut().quotient | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from "./DutchOrderTrade"; | ||
export * from "./V2DutchOrderTrade"; | ||
export * from "./PriorityOrderTrade"; | ||
export * from "./RelayOrderTrade"; |