Skip to content
This repository has been archived by the owner on Jun 24, 2022. It is now read-only.

Commit

Permalink
[1689 - Quote endpoint] New Quote endpoint for price & fee (#1835)
Browse files Browse the repository at this point in the history
* [1689 - Quote endpoint] Consolidate waterfall PRs:

* useGetGpApiStatus hook (mocked)

1. checks api to use
2. mocked right now

* export type

* [1689 - Quote endpoint] useOrderValidTo hook (#1728)

* useOrderValidTo hook

- syntactic sugar for getting validTo time (unix u32) from deadline TTL

* [1689 - Quote endpoint] update Error objects to include new fee > sell error (#1758)

* update Error objects to include new fee > sell error

* [1689 - Quote endpoint] update gp v2 contracts 1.1.2 & set env check (#1759)

* add default Gp_Api_Status to env

* update to 1.1.2 gp-v2-contracts

yarn lock [email protected]

* [1689 - Quote Endpoint] Add new quote getting methods in API and Price (fix types) (#1772)

* utils/price: add new quote logic

* gnosisProtocol API - add new quote logic

1. map old params to new
2. add post logic

* add legacy/new to index

* use gpStatus and new api method in useRefetch and Unfillable

* set mock to LEGACY (temp)

* add validTo to necessary places

* [1689 - Quote Endpoint] getQuote > replace getFeeQuote and getPriceQuote (#1773)

* change getPriceQuote and getFeeQuote

- getQuote - one endpoint

* [1689 - Quote endpoint] Fix Cypress Fee test (#1775)

* setup fee test for new endpoint

* add mock quote data and fix command for stubs

* fix package name and update yarn

yarn lock

* add receiver to quote params

* better GP API default

* removed unused import

* rename gpQuoteStatus to gpPriceStrategy

* comments

* comment

* pass new params to useQuoteAndSwap/useFallbackPi

* better types/param name

* round validTo UP @alfetopito

* get correct ETH address

* create validTo adjuster. return more from useOrderValidTo

* MINIMUM_VALID_TO = 120

* paths, fixed type, and renaming stuff

* set the validTo in the callback

* export resolveLast best quote call

* use resolveLast for quote promise

* add manual price setting for dev

* logs

* pass order validTo thanks !@alfetopito

* timestamp

* add param to stubResponse cypress

* fix incorrect version name

* yarn lock update

* Bumpbing bignumber.js and dex-js to the same version

* Removed bignumber.js from dependency as it's only used on dex-js

Co-authored-by: Leandro Boscariol <[email protected]>
  • Loading branch information
W3stside and Leandro Boscariol authored Dec 16, 2021
1 parent 8f6b47f commit eb44f91
Show file tree
Hide file tree
Showing 23 changed files with 2,782 additions and 2,923 deletions.
96 changes: 59 additions & 37 deletions cypress-custom/integration/fee.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
import { WETH9 as WETH } from '@uniswap/sdk-core'
import { OrderKind } from '@gnosis.pm/gp-v2-contracts'
import { FeeQuoteParams, FeeInformation } from '../../src/custom/utils/price'
import { GetQuoteResponse } from '@gnosis.pm/gp-v2-contracts'
import { parseUnits } from 'ethers/lib/utils'

const DAI = '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'
const FOUR_HOURS = 3600 * 4 * 1000
const DEFAULT_SELL_TOKEN = WETH[4]
const DEFAULT_APP_DATA = '0x0000000000000000000000000000000000000000000000000000000000000000'
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'

const FEE_QUERY = `https://protocol-rinkeby.dev.gnosisdev.com/api/v1/quote`

const baseParams = {
from: ZERO_ADDRESS,
receiver: ZERO_ADDRESS,
validTo: Math.ceil(Date.now() / 1000 + 500),
appData: DEFAULT_APP_DATA,
sellTokenBalance: 'erc20',
buyTokenBalance: 'erc20',
partiallyFillable: false,
}

const getFeeQuery = ({ sellToken, buyToken, amount, kind }: Omit<FeeQuoteParams, 'chainId'>) =>
`https://protocol-rinkeby.dev.gnosisdev.com/api/v1/fee?sellToken=${sellToken}&buyToken=${buyToken}&amount=${amount}&kind=${kind}`
const mockQuoteResponse = {
quote: {
// arb props here..
sellToken: '0x6810e776880c02933d47db1b9fc05908e5386b96',
buyToken: '0x6810e776880c02933d47db1b9fc05908e5386b96',
receiver: '0x6810e776880c02933d47db1b9fc05908e5386b96',
sellAmount: '1234567890',
buyAmount: '1234567890',
validTo: 0,
appData: '0x0000000000000000000000000000000000000000000000000000000000000000',
feeAmount: '1234567890',
kind: 'buy',
partiallyFillable: true,
sellTokenBalance: 'erc20',
buyTokenBalance: 'erc20',
},
from: ZERO_ADDRESS,
}

function _assertFeeData(fee: FeeInformation | string): void {
function _assertFeeData(fee: GetQuoteResponse): void {
if (typeof fee === 'string') {
fee = JSON.parse(fee)
}
expect(fee).to.have.property('amount')
expect(fee).to.have.property('expirationDate')
expect(fee).to.have.property('quote')
expect(fee).to.have.property('expiration')
expect(fee.quote).to.have.property('feeAmount')
}

/* Fee not currently being saved in local so commenting this out
Expand Down Expand Up @@ -54,18 +84,25 @@ function _assertFeeFetched(token: string): Cypress.Chainable {

describe('Fee endpoint', () => {
it('Returns the expected info', () => {
const FEE_QUERY = getFeeQuery({
const params = {
sellToken: DEFAULT_SELL_TOKEN.address,
buyToken: DAI,
amount: parseUnits('0.1', DEFAULT_SELL_TOKEN.decimals).toString(),
kind: OrderKind.SELL,
sellAmountBeforeFee: parseUnits('0.1', DEFAULT_SELL_TOKEN.decimals).toString(),
kind: 'sell',
fromDecimals: DEFAULT_SELL_TOKEN.decimals,
toDecimals: 6,
})
// BASE PARAMS
...baseParams,
}

// GIVEN: -
// WHEN: Call fee API
cy.request(FEE_QUERY)
cy.request({
method: 'POST',
url: FEE_QUERY,
body: params,
log: true,
})
.its('body')
// THEN: The API response has the expected data
.should(_assertFeeData)
Expand All @@ -74,27 +111,19 @@ describe('Fee endpoint', () => {

describe('Fee: Complex fetch and persist fee', () => {
const INPUT_AMOUNT = '0.1'
const FEE_QUERY = getFeeQuery({
sellToken: DEFAULT_SELL_TOKEN.address,
buyToken: DAI,
amount: parseUnits(INPUT_AMOUNT, DEFAULT_SELL_TOKEN.decimals).toString(),
kind: OrderKind.SELL,
fromDecimals: DEFAULT_SELL_TOKEN.decimals,
toDecimals: 6,
})

// Needs to run first to pass because of Cypress async issues between tests
it('Re-fetched when it expires', () => {
// GIVEN: input token Fee expiration is always 6 hours from now
const SIX_HOURS = FOUR_HOURS * 1.5
const LATER_TIME = new Date(Date.now() + SIX_HOURS).toISOString()
const LATER_FEE = {
expirationDate: LATER_TIME,
amount: '0',
...mockQuoteResponse,
expiration: LATER_TIME,
}

// only override Date functions (default is to override all time based functions)
cy.stubResponse({ url: FEE_QUERY, alias: 'feeRequest', body: LATER_FEE })
cy.stubResponse({ method: 'POST', url: FEE_QUERY, alias: 'feeRequest', body: LATER_FEE })

// GIVEN: user visits app, selects 0.1 WETH as sell, DAI as buy
// and goes AFK
Expand All @@ -116,36 +145,29 @@ describe('Fee: Complex fetch and persist fee', () => {
const mockedTime = new Date($clock.details().now)

// THEN: fee time is properly stubbed and
expect(body.expirationDate).to.equal(LATER_TIME)
expect(body.expiration).to.equal(LATER_TIME)
// THEN: the mocked later date is indeed less than the new fee (read: the fee is valid)
expect(new Date(body.expirationDate)).to.be.greaterThan(mockedTime)
expect(new Date(body.expiration)).to.be.greaterThan(mockedTime)
})
})
})
})

describe('Fee: simple checks it exists', () => {
const INPUT_AMOUNT = '0.1'
const FEE_QUERY = getFeeQuery({
sellToken: DEFAULT_SELL_TOKEN.address,
buyToken: DAI,
amount: parseUnits(INPUT_AMOUNT, DEFAULT_SELL_TOKEN.decimals).toString(),
kind: OrderKind.SELL,
fromDecimals: DEFAULT_SELL_TOKEN.decimals,
toDecimals: 6,
})
const FEE_RESP = {
const QUOTE_RESP = {
...mockQuoteResponse,
// 1 min in future
expirationDate: new Date(Date.now() + 60000).toISOString(),
amount: parseUnits('0.05', DEFAULT_SELL_TOKEN.decimals).toString(),
expiration: new Date(Date.now() + 60000).toISOString(),
}

it('Fetch fee when selecting both tokens', () => {
// Stub responses from fee endpoint
cy.stubResponse({
method: 'POST',
url: FEE_QUERY,
alias: 'feeRequest',
body: FEE_RESP,
body: QUOTE_RESP,
})
// GIVEN: A user loads the swap page
// WHEN: Select DAI token as output and sells 0.1 WETH
Expand Down
12 changes: 11 additions & 1 deletion cypress-custom/support/commands.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ declare namespace Cypress {
*
* @example cy.stubResponse({ url: '/api/v1/someEndpoint/', alias: 'endpoint', body: { foo: 'foo' } })
*/
stubResponse({ url, alias, body }: { url: string; alias?: string; body?: any }): Chainable<Subject>
stubResponse({
method,
url,
alias,
body,
}: {
method: 'GET' | 'POST' | 'DELETE'
url: string
alias?: string
body?: any
}): Chainable<Subject>
}
}
4 changes: 2 additions & 2 deletions cypress-custom/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function enterOutputAmount(tokenAddress, amount, selectToken = false) {
cy.get('#swap-currency-input .token-amount-output').type(amount.toString(), { force: true, delay: 400 })
}

function stubResponse({ url, alias = 'stubbedResponse', body }) {
cy.intercept({ method: 'GET', url }, _responseHandlerFactory(body)).as(alias)
function stubResponse({ method, url, alias = 'stubbedResponse', body }) {
cy.intercept({ method, url }, _responseHandlerFactory(body)).as(alias)
}

Cypress.Commands.add('swapClickInputToken', () => clickInputToken)
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,14 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@gnosis.pm/cow-runner-game": "^0.2.9",
"@gnosis.pm/dex-js": "^0.13.0",
"@gnosis.pm/gp-v2-contracts": "^1.0.2",
"@gnosis.pm/dex-js": "^0.14.0",
"@gnosis.pm/gp-v2-contracts": "^1.1.2",
"@gnosis.pm/safe-service-client": "^0.1.1",
"@pinata/sdk": "^1.1.23",
"@sentry/react": "^6.11.0",
"@sentry/tracing": "^6.11.0",
"@uniswap/default-token-list": "^2.0.0",
"@walletconnect/web3-provider": "^1.6.6",
"bignumber.js": "9.0.1",
"bnc-sdk": "^3.5.0",
"fast-safe-stringify": "^2.0.8",
"paraswap": "^5.0.1",
Expand Down
82 changes: 57 additions & 25 deletions src/custom/api/gnosisProtocol/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SupportedChainId as ChainId } from 'constants/chains'
import { OrderKind } from '@gnosis.pm/gp-v2-contracts'
import { OrderKind, QuoteQuery } from '@gnosis.pm/gp-v2-contracts'
import { stringify } from 'qs'
import { getSigningSchemeApiValue, OrderCreation, OrderCancellation, SigningSchemeValue } from 'utils/signatures'
import { APP_DATA_HASH } from 'constants/index'
Expand All @@ -17,11 +17,13 @@ import QuoteError, {
GpQuoteErrorDetails,
} from 'api/gnosisProtocol/errors/QuoteError'
import { toErc20Address } from 'utils/tokens'
import { FeeInformation, FeeQuoteParams, PriceInformation, PriceQuoteParams } from 'utils/price'
import { FeeQuoteParams, PriceInformation, PriceQuoteParams, SimpleGetQuoteResponse } from 'utils/price'

import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists'
import { GAS_FEE_ENDPOINTS } from 'constants/index'
import * as Sentry from '@sentry/browser'
import { ZERO_ADDRESS } from 'constants/misc'
import { getAppDataHash } from 'constants/appDataHash'

function getGnosisProtocolUrl(): Partial<Record<ChainId, string>> {
if (isLocal || isDev || isPr || isBarn) {
Expand Down Expand Up @@ -247,7 +249,10 @@ const UNHANDLED_ORDER_ERROR: ApiErrorObject = {
description: ApiErrorCodeDetails.UNHANDLED_CREATE_ERROR,
}

async function _handleQuoteResponse(response: Response, params?: FeeQuoteParams) {
async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(
response: Response,
params?: P
): Promise<T> {
if (!response.ok) {
const errorObj: ApiErrorObject = await response.json()

Expand All @@ -267,7 +272,7 @@ async function _handleQuoteResponse(response: Response, params?: FeeQuoteParams)
// report to sentry
Sentry.captureException(sentryError, {
tags: { errorType: 'getFeeQuote' },
contexts: { params },
contexts: { params: { ...params } },
})
}

Expand All @@ -277,7 +282,45 @@ async function _handleQuoteResponse(response: Response, params?: FeeQuoteParams)
}
}

export async function getPriceQuote(params: PriceQuoteParams): Promise<PriceInformation | null> {
function _mapNewToLegacyParams(params: FeeQuoteParams): QuoteQuery {
const { amount, kind, userAddress, receiver, validTo, sellToken, buyToken, chainId } = params
const fallbackAddress = userAddress || ZERO_ADDRESS

const baseParams = {
sellToken: toErc20Address(sellToken, chainId),
buyToken: toErc20Address(buyToken, chainId),
from: fallbackAddress,
receiver: receiver || fallbackAddress,
appData: getAppDataHash(),
validTo,
partiallyFillable: false,
}

const finalParams: QuoteQuery =
kind === OrderKind.SELL
? {
kind: OrderKind.SELL,
sellAmountBeforeFee: amount,
...baseParams,
}
: {
kind: OrderKind.BUY,
buyAmountAfterFee: amount,
...baseParams,
}

return finalParams
}

export async function getQuote(params: FeeQuoteParams) {
const { chainId } = params
const quoteParams = _mapNewToLegacyParams(params)
const response = await _post(chainId, '/quote', quoteParams)

return _handleQuoteResponse<SimpleGetQuoteResponse>(response)
}

export async function getPriceQuoteLegacy(params: PriceQuoteParams): Promise<PriceInformation | null> {
const { baseToken, quoteToken, amount, kind, chainId } = params
console.log(`[api:${API_NAME}] Get price from API`, params)

Expand All @@ -293,25 +336,7 @@ export async function getPriceQuote(params: PriceQuoteParams): Promise<PriceInfo
throw new QuoteError(UNHANDLED_QUOTE_ERROR)
})

return _handleQuoteResponse(response)
}

export async function getFeeQuote(params: FeeQuoteParams): Promise<FeeInformation> {
const { sellToken, buyToken, amount, kind, chainId } = params
console.log(`[api:${API_NAME}] Get fee from API`, params)

const response = await _get(
chainId,
`/fee?sellToken=${toErc20Address(sellToken, chainId)}&buyToken=${toErc20Address(
buyToken,
chainId
)}&amount=${amount}&kind=${kind}`
).catch((error) => {
console.error('Error getting fee quote:', error)
throw new QuoteError(UNHANDLED_QUOTE_ERROR)
})

return _handleQuoteResponse(response, params)
return _handleQuoteResponse<PriceInformation | null>(response)
}

export async function getOrder(chainId: ChainId, orderId: string): Promise<OrderMetaData | null> {
Expand Down Expand Up @@ -418,5 +443,12 @@ export async function getGasPrices(chainId: ChainId = DEFAULT_NETWORK_FOR_LISTS)

// Register some globals for convenience
registerOnWindow({
operator: { getFeeQuote, getTrades, getOrder, sendSignedOrder: sendOrder, apiGet: _get, apiPost: _post },
operator: {
getQuote,
getTrades,
getOrder,
sendSignedOrder: sendOrder,
apiGet: _get,
apiPost: _post,
},
})
5 changes: 5 additions & 0 deletions src/custom/api/gnosisProtocol/errors/QuoteError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export function mapOperatorErrorToQuoteError(error?: ApiErrorObject): GpQuoteErr
errorType: GpQuoteErrorCodes.UnsupportedToken,
description: error.description,
}
case ApiErrorCodes.SellAmountDoesNotCoverFee:
return {
errorType: GpQuoteErrorCodes.FeeExceedsFrom,
description: error.description,
}
default:
return { errorType: GpQuoteErrorCodes.UNHANDLED_ERROR, description: GpQuoteErrorDetails.UNHANDLED_ERROR }
}
Expand Down
4 changes: 2 additions & 2 deletions src/custom/api/gnosisProtocol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const {
getOrderLink = realApi.getOrderLink,
sendOrder = realApi.sendOrder,
sendSignedOrderCancellation = realApi.sendSignedOrderCancellation,
getPriceQuote = realApi.getPriceQuote,
getFeeQuote = realApi.getFeeQuote,
getQuote = realApi.getQuote,
getPriceQuoteLegacy = realApi.getPriceQuoteLegacy,
getOrder = realApi.getOrder,
getTrades = realApi.getTrades,
// functions that only have a mock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { darken } from 'polished'
import { useSetUserSlippageTolerance, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
import { L2_CHAIN_IDS } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { INPUT_OUTPUT_EXPLANATION } from 'constants/index'
import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ORDER_VALID_TO_TIME_SECONDS } from 'constants/index'

enum SlippageError {
InvalidInput = 'InvalidInput',
Expand Down Expand Up @@ -142,7 +142,7 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
} else {
try {
const parsed: number = Math.floor(Number.parseFloat(value) * 60)
if (!Number.isInteger(parsed) || parsed < 60 || parsed > 180 * 60) {
if (!Number.isInteger(parsed) || parsed < MINIMUM_ORDER_VALID_TO_TIME_SECONDS || parsed > 180 * 60) {
setDeadlineError(DeadlineError.InvalidInput)
} else {
setDeadline(parsed)
Expand Down
Loading

0 comments on commit eb44f91

Please sign in to comment.