Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Commit

Permalink
/prices endpoint (#58)
Browse files Browse the repository at this point in the history
* Initial /price endpoint

* Optimizationns

* Batch fetch orders for /price endpoint

* Rename to records

* Remove 0s, rename to prices

* Bump to 20 per batch
  • Loading branch information
dekz authored Feb 12, 2020
1 parent 689455c commit 45cf9cf
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 16 deletions.
19 changes: 18 additions & 1 deletion src/handlers/swap_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { SwapService } from '../services/swap_service';
import { TokenMetadatasForChains } from '../token_metadatas_for_networks';
import { ChainId, GetSwapQuoteRequestParams } from '../types';
import { schemaUtils } from '../utils/schema_utils';
import { findTokenAddress, isETHSymbol } from '../utils/token_metadata_utils';
import { findTokenAddress, getTokenMetadataIfExists, isETHSymbol } from '../utils/token_metadata_utils';

export class SwapHandlers {
private readonly _swapService: SwapService;
Expand Down Expand Up @@ -101,6 +101,23 @@ export class SwapHandlers {
const filteredTokens = tokens.filter(t => t.address !== NULL_ADDRESS);
res.status(HttpStatus.OK).send({ records: filteredTokens });
}
// tslint:disable-next-line:prefer-function-over-method
public async getTokenPricesAsync(req: express.Request, res: express.Response): Promise<void> {
const symbolOrAddress = req.query.sellToken || 'WETH';
const baseAsset = getTokenMetadataIfExists(symbolOrAddress, CHAIN_ID);
if (!baseAsset) {
throw new ValidationError([
{
field: 'sellToken',
code: ValidationErrorCodes.ValueOutOfRange,
reason: `Could not find token ${symbolOrAddress}`,
},
]);
}
const unitAmount = new BigNumber(1);
const records = await this._swapService.getTokenPricesAsync(baseAsset, unitAmount);
res.status(HttpStatus.OK).send({ records });
}
}

const findTokenAddressOrThrowApiError = (address: string, field: string, chainId: ChainId): string => {
Expand Down
3 changes: 3 additions & 0 deletions src/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export const config: ConnectionOptions = {
synchronize: true,
logging: true,
logger: 'debug',
extra: {
connectionLimit: 50,
},
};
1 change: 1 addition & 0 deletions src/routers/swap_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export function createSwapRouter(swapService: SwapService): express.Router {
router.get('/', asyncHandler(SwapHandlers.rootAsync.bind(SwapHandlers)));
router.get('/quote', asyncHandler(handlers.getSwapQuoteAsync.bind(handlers)));
router.get('/tokens', asyncHandler(handlers.getSwapTokensAsync.bind(handlers)));
router.get('/prices', asyncHandler(handlers.getTokenPricesAsync.bind(handlers)));
return router;
}
37 changes: 28 additions & 9 deletions src/services/orderbook_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WSClient } from '@0x/mesh-rpc-client';
import { assetDataUtils } from '@0x/order-utils';
import { AssetPairsItem, OrdersRequestOpts, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { Connection } from 'typeorm';
import { Connection, In } from 'typeorm';

import { SignedOrderEntity } from '../entities';
import { ValidationError } from '../errors';
Expand Down Expand Up @@ -67,16 +67,18 @@ export class OrderBookService {
baseAssetData: string,
quoteAssetData: string,
): Promise<OrderbookResponse> {
const bidSignedOrderEntities = (await this._connection.manager.find(SignedOrderEntity, {
where: { takerAssetData: baseAssetData, makerAssetData: quoteAssetData },
})) as Array<Required<SignedOrderEntity>>;
const askSignedOrderEntities = (await this._connection.manager.find(SignedOrderEntity, {
where: { takerAssetData: quoteAssetData, makerAssetData: baseAssetData },
})) as Array<Required<SignedOrderEntity>>;
const bidApiOrders: APIOrder[] = bidSignedOrderEntities
const [bidSignedOrderEntities, askSignedOrderEntities] = await Promise.all([
this._connection.manager.find(SignedOrderEntity, {
where: { takerAssetData: baseAssetData, makerAssetData: quoteAssetData },
}),
this._connection.manager.find(SignedOrderEntity, {
where: { takerAssetData: quoteAssetData, makerAssetData: baseAssetData },
}),
]);
const bidApiOrders: APIOrder[] = (bidSignedOrderEntities as Array<Required<SignedOrderEntity>>)
.map(orderUtils.deserializeOrderToAPIOrder)
.sort((orderA, orderB) => orderUtils.compareBidOrder(orderA.order, orderB.order));
const askApiOrders: APIOrder[] = askSignedOrderEntities
const askApiOrders: APIOrder[] = (askSignedOrderEntities as Array<Required<SignedOrderEntity>>)
.map(orderUtils.deserializeOrderToAPIOrder)
.sort((orderA, orderB) => orderUtils.compareAskOrder(orderA.order, orderB.order));
const paginatedBidApiOrders = paginationUtils.paginate(bidApiOrders, page, perPage);
Expand Down Expand Up @@ -154,6 +156,23 @@ export class OrderBookService {
const paginatedApiOrders = paginationUtils.paginate(apiOrders, page, perPage);
return paginatedApiOrders;
}
public async getBatchOrdersAsync(
page: number,
perPage: number,
makerAssetDatas: string[],
takerAssetDatas: string[],
): Promise<PaginatedCollection<APIOrder>> {
const filterObject = {
makerAssetData: In(makerAssetDatas),
takerAssetData: In(takerAssetDatas),
};
const signedOrderEntities = (await this._connection.manager.find(SignedOrderEntity, {
where: filterObject,
})) as Array<Required<SignedOrderEntity>>;
const apiOrders = _.map(signedOrderEntities, orderUtils.deserializeOrderToAPIOrder);
const paginatedApiOrders = paginationUtils.paginate(apiOrders, page, perPage);
return paginatedApiOrders;
}
constructor(connection: Connection, meshClient?: WSClient) {
this._meshClient = meshClient;
this._connection = connection;
Expand Down
56 changes: 54 additions & 2 deletions src/services/swap_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
import { assetDataUtils, SupportedProvider } from '@0x/order-utils';
import { AbiEncoder, BigNumber, decodeThrownErrorAsRevertError, RevertError } from '@0x/utils';
import { TxData, Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';

import { ASSET_SWAPPER_MARKET_ORDERS_OPTS, CHAIN_ID, FEE_RECIPIENT_ADDRESS } from '../config';
import { DEFAULT_TOKEN_DECIMALS, PERCENTAGE_SIG_DIGITS, QUOTE_ORDER_EXPIRATION_BUFFER_MS } from '../constants';
import { logger } from '../logger';
import { CalculateSwapQuoteParams, GetSwapQuoteResponse, GetSwapQuoteResponseLiquiditySource } from '../types';
import { TokenMetadatasForChains } from '../token_metadatas_for_networks';
import { CalculateSwapQuoteParams, GetSwapQuoteResponse, GetSwapQuoteResponseLiquiditySource, GetTokenPricesResponse, TokenMetadata } from '../types';
import { orderUtils } from '../utils/order_utils';
import { findTokenDecimalsIfExists } from '../utils/token_metadata_utils';

Expand Down Expand Up @@ -50,9 +52,9 @@ export class SwapService {
affiliateAddress,
} = params;
const assetSwapperOpts = {
...ASSET_SWAPPER_MARKET_ORDERS_OPTS,
slippagePercentage,
gasPrice: providedGasPrice,
...ASSET_SWAPPER_MARKET_ORDERS_OPTS,
excludedSources, // TODO(dave4506): overrides the excluded sources selected by chainId
};
if (sellAmount !== undefined) {
Expand Down Expand Up @@ -135,6 +137,56 @@ export class SwapService {
return apiSwapQuote;
}

public async getTokenPricesAsync(sellToken: TokenMetadata, unitAmount: BigNumber): Promise<GetTokenPricesResponse> {
// Gets the price for buying 1 unit (not base unit as this is different between tokens with differing decimals)
// returns price in sellToken units, e.g What is the price of 1 ZRX (in DAI)
// Equivalent to performing multiple swap quotes selling sellToken and buying 1 whole buy token
const takerAssetData = assetDataUtils.encodeERC20AssetData(sellToken.tokenAddress);
const queryAssetData = TokenMetadatasForChains.filter(m => m.symbol !== sellToken.symbol);
const chunkSize = 20;
const assetDataChunks = _.chunk(queryAssetData, chunkSize);
const allResults = _.flatten(
await Promise.all(
assetDataChunks.map(async a => {
const encodedAssetData = a.map(m =>
assetDataUtils.encodeERC20AssetData(m.tokenAddresses[CHAIN_ID]),
);
const amounts = a.map(m => Web3Wrapper.toBaseUnitAmount(unitAmount, m.decimals));
const quotes = await this._swapQuoter.getBatchMarketBuySwapQuoteForAssetDataAsync(
encodedAssetData,
takerAssetData,
amounts,
{
...ASSET_SWAPPER_MARKET_ORDERS_OPTS,
slippagePercentage: 0,
bridgeSlippage: 0,
numSamples: 3,
},
);
return quotes;
}),
),
);

const prices = allResults.map((quote, i) => {
if (!quote) {
return undefined;
}
const buyTokenDecimals = queryAssetData[i].decimals;
const sellTokenDecimals = sellToken.decimals;
const { makerAssetAmount, totalTakerAssetAmount } = quote.bestCaseQuoteInfo;
const unitMakerAssetAmount = Web3Wrapper.toUnitAmount(makerAssetAmount, buyTokenDecimals);
const unitTakerAssetAmount = Web3Wrapper.toUnitAmount(totalTakerAssetAmount, sellTokenDecimals);
const price = unitTakerAssetAmount
.dividedBy(unitMakerAssetAmount)
.decimalPlaces(buyTokenDecimals);
return {
symbol: queryAssetData[i].symbol,
price,
};
}).filter(p => p) as GetTokenPricesResponse;
return prices;
}
// tslint:disable-next-line: prefer-function-over-method
private _convertSourceBreakdownToArray(sourceBreakdown: SwapQuoteOrdersBreakdown): GetSwapQuoteResponseLiquiditySource[] {
const breakdown: GetSwapQuoteResponseLiquiditySource[] = [];
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,13 @@ export interface GetSwapQuoteResponse {
from?: string;
}

export interface Price {
symbol: string;
price: BigNumber;
}

export type GetTokenPricesResponse = Price[];

export interface GetSwapQuoteRequestParams {
sellToken: string;
buyToken: string;
Expand Down
28 changes: 24 additions & 4 deletions src/utils/order_store_db_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,30 @@ export class OrderStoreDbAdapter extends OrderStore {
}
// Currently not handling deletes as this is handled by Mesh
}
public async hasAsync(assetPairKey: string): Promise<boolean> {
const [assetA, assetB] = OrderStore.assetPairKeyToAssets(assetPairKey);
const pairs = await this._orderbookService.getAssetPairsAsync(FIRST_PAGE, MAX_QUERY_SIZE, assetA, assetB);
return pairs.total !== 0;
public async getBatchOrderSetsForAssetsAsync(
makerAssetDatas: string[],
takerAssetDatas: string[],
): Promise<OrderSet[]> {
const { records: apiOrders } = await this._orderbookService.getBatchOrdersAsync(
FIRST_PAGE,
MAX_QUERY_SIZE,
makerAssetDatas,
takerAssetDatas,
);
const orderSets: { [makerAssetData: string]: OrderSet } = {};
makerAssetDatas.forEach(m =>
takerAssetDatas.forEach(t => (orderSets[OrderStore.getKeyForAssetPair(m, t)] = new OrderSet())),
);
await Promise.all(
apiOrders.map(async o =>
orderSets[OrderStore.getKeyForAssetPair(o.order.makerAssetData, o.order.takerAssetData)].addAsync(o),
),
);
return Object.values(orderSets);
}
// tslint:disable-next-line:prefer-function-over-method
public async hasAsync(_assetPairKey: string): Promise<boolean> {
return true;
}
public async valuesAsync(assetPairKey: string): Promise<APIOrder[]> {
return Array.from((await this.getOrderSetForAssetPairAsync(assetPairKey)).values());
Expand Down

0 comments on commit 45cf9cf

Please sign in to comment.