Merge branch 'development' into supportEIP1193Providers

* development: (37 commits)
  Factor out redundant source param in parse order functions
  Fix marquee typo
  Throw error if cannot find error in table
  Integrate one-time dump and API for nonfungible.com (#1603)
  Change optional text props to mandatory in LedgerSignNote
  Add radar_orders parseRadarOrder test
  Add parser test for Radar orderbook parsing
  Moved calculateSlippage from parsers to transformers
  Show greater than less than when minority has a vote
  Prettier
  Recieved -> Received
  Fix style for address table
  Hide ledger sign note on error
  Revert broken formatting
  Added ledger sign notification
  Fix bug in ledger address selector where provider does not exist.
  fix null column constraint issue
  Add parsing radar order test
  Rename migration to what it is called on prod and qa
  Fix type bug around Radar response
  ...
This commit is contained in:
Fabio Berger 2019-02-20 14:52:58 -08:00
commit d892d16b51
47 changed files with 1450 additions and 121 deletions

View File

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
const nftTrades = new Table({
name: 'raw.nonfungible_dot_com_trades',
columns: [
{ name: 'publisher', type: 'varchar', isPrimary: true },
{ name: 'transaction_hash', type: 'varchar', isPrimary: true },
{ name: 'asset_id', type: 'varchar', isPrimary: true },
{ name: 'block_number', type: 'bigint', isPrimary: true },
{ name: 'log_index', type: 'integer', isPrimary: true },
{ name: 'block_timestamp', type: 'bigint' },
{ name: 'asset_descriptor', type: 'varchar' },
{ name: 'market_address', type: 'varchar(42)' },
{ name: 'total_price', type: 'numeric' },
{ name: 'usd_price', type: 'numeric' },
{ name: 'buyer_address', type: 'varchar(42)' },
{ name: 'seller_address', type: 'varchar(42)' },
{ name: 'meta', type: 'jsonb' },
],
});
export class CreateNftTrades1543540108767 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(nftTrades);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable(nftTrades);
}
}

View File

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
const slippage = new Table({
name: 'raw.slippage',
columns: [
{ name: 'observed_timestamp', type: 'bigint', isPrimary: true },
{ name: 'symbol', type: 'varchar', isPrimary: true },
{ name: 'exchange', type: 'varchar', isPrimary: true },
{ name: 'usd_amount', type: 'numeric', isPrimary: true },
{ name: 'token_amount', type: 'numeric', isNullable: false },
{ name: 'avg_price_in_eth_buy', type: 'numeric', isNullable: true },
{ name: 'avg_price_in_eth_sell', type: 'numeric', isNullable: true },
{ name: 'slippage', type: 'numeric', isNullable: true },
],
});
export class CreateSlippageTable1549856835629 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(slippage);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable(slippage);
}
}

View File

@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
const TOKEN_ORDERBOOK_SNAPSHOT_TABLE = 'raw.token_orderbook_snapshots';
const NEW_COLUMN_NAME = 'maker_address';
export class TokenOrderBookSnapshotsAddMakerAddress1550163069315 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
const snapshotTable = await queryRunner.getTable(TOKEN_ORDERBOOK_SNAPSHOT_TABLE);
if (snapshotTable) {
const newColumn = new TableColumn({
name: NEW_COLUMN_NAME,
type: 'varchar',
isNullable: true,
});
await queryRunner.addColumn(TOKEN_ORDERBOOK_SNAPSHOT_TABLE, newColumn);
// backfill null values
await queryRunner.query(`
UPDATE ${TOKEN_ORDERBOOK_SNAPSHOT_TABLE}
SET ${NEW_COLUMN_NAME}='unknown'
WHERE ${NEW_COLUMN_NAME} is NULL;
`);
await queryRunner.query(`
ALTER TABLE ${TOKEN_ORDERBOOK_SNAPSHOT_TABLE}
DROP CONSTRAINT "token_orderbook_snapshots_pkey1",
ADD PRIMARY KEY (observed_timestamp, source, order_type, price, base_asset_symbol, quote_asset_symbol, maker_address);
`);
} else {
throw new Error(`Could not find table with name ${TOKEN_ORDERBOOK_SNAPSHOT_TABLE}`);
}
}
public async down(queryRunner: QueryRunner): Promise<any> {
const snapshotTable = await queryRunner.getTable(TOKEN_ORDERBOOK_SNAPSHOT_TABLE);
if (snapshotTable) {
await queryRunner.dropColumn(snapshotTable, NEW_COLUMN_NAME);
} else {
throw new Error(`Could not find table with name ${TOKEN_ORDERBOOK_SNAPSHOT_TABLE}`);
}
}
}

View File

@ -49,6 +49,7 @@
"@0x/types": "^2.0.2", "@0x/types": "^2.0.2",
"@0x/utils": "^4.1.0", "@0x/utils": "^4.1.0",
"@0x/web3-wrapper": "^5.0.0", "@0x/web3-wrapper": "^5.0.0",
"@radarrelay/types": "^1.2.1",
"@types/dockerode": "^2.5.9", "@types/dockerode": "^2.5.9",
"@types/p-limit": "^2.0.0", "@types/p-limit": "^2.0.0",
"async-parallel": "^1.2.3", "async-parallel": "^1.2.3",

View File

@ -0,0 +1,41 @@
import { fetchAsync } from '@0x/utils';
const EDPS_BASE_URL = 'http://35.185.219.196:1337';
export type EdpsResponse = EdpsWrapper[];
export interface EdpsWrapper {
[key: string]: EdpsExchange;
}
export interface EdpsExchange {
exchangeName: string;
totalPrice: number;
tokenAmount: number;
tokenSymbol: string;
avgPrice: number;
timestamp: number;
error: string;
}
// tslint:disable:prefer-function-over-method
// ^ Keep consistency with other sources and help logical organization
export class EdpsSource {
/**
* Call Ethereum DEX Price Service API.
*/
public async getEdpsAsync(direction: string, symbol: string, amount: number): Promise<EdpsWrapper> {
const edpsUrl = `${EDPS_BASE_URL}/${direction}?amount=${amount}&symbol=${symbol}&decimals=`;
const resp = await fetchAsync(edpsUrl);
const respJson: EdpsResponse = await resp.json();
const allExchanges: EdpsWrapper = {};
// The below unwraps the response so we get 1 single EdpsWrapper object
// instead of a list of singletons
for (const entry of respJson) {
for (const key of Object.keys(entry)) {
allExchanges[key] = entry[key];
}
}
return allExchanges;
}
}

View File

@ -0,0 +1,220 @@
import { stringify } from 'querystring';
import { logUtils } from '@0x/utils';
import { fetchSuccessfullyOrThrowAsync } from '../../utils';
// URL to use for getting nft trades from nonfungible.com.
export const NONFUNGIBLE_DOT_COM_URL = 'https://nonfungible.com/api/v1';
// Number of trades to get at once. This is a hard limit enforced by the API.
const MAX_TRADES_PER_QUERY = 100;
// Note(albrow): For now this will have to be manually updated by checking
// https://nonfungible.com/
export const knownPublishers = [
'axieinfinity',
// 'cryptokitties', // disabled until we get updated initial dump that isn't truncated
'cryptopunks',
'cryptovoxels',
'decentraland',
'decentraland_estate',
'etherbots',
'etheremon',
'ethtown',
// 'knownorigin', // disabled because of null characters in data being rejected by postgres
// 'mythereum', // worked at one time, but now seems dead
'superrare',
];
export interface NonfungibleDotComHistoryResponse {
data: NonfungibleDotComTradeResponse[];
}
export interface NonfungibleDotComTradeResponse {
_id: string;
transactionHash: string;
blockNumber: number;
logIndex: number;
blockTimestamp: string;
assetId: string;
assetDescriptor: string;
nftAddress: string;
marketAddress: string;
tokenTicker: string;
totalDecimalPrice: number;
totalPrice: string;
usdPrice: number;
currencyTransfer: object;
buyer: string;
seller: string;
meta: object;
image: string;
composedOf: string;
asset_link: string;
seller_address_link: string;
buyer_address_link: string;
}
/**
* Gets and returns all trades for the given publisher, starting at the given block number.
* Automatically handles pagination.
* @param publisher A valid "publisher" for the nonfungible.com API. (e.g. "cryptokitties")
* @param blockNumberStart The block number to start querying from.
*/
export async function getTradesAsync(
publisher: string,
blockNumberStart: number,
): Promise<NonfungibleDotComTradeResponse[]> {
const allTrades: NonfungibleDotComTradeResponse[] = [];
/**
* due to high data volumes and rate limiting, we procured an initial data
* dump from nonfungible.com. If the requested starting block number is
* contained in that initial dump, then pull relevant trades from there
* first. Later (below) we'll get the more recent trades from the API itself.
*/
if (blockNumberStart < highestBlockNumbersInIntialDump[publisher]) {
logUtils.log('getting trades from one-time dump');
// caller needs trades that are in the initial data dump, so get them
// from there, then later go to the API for the rest.
const initialDumpResponse: NonfungibleDotComHistoryResponse = await fetchSuccessfullyOrThrowAsync(
getInitialDumpUrl(publisher),
);
const initialDumpTrades = initialDumpResponse.data;
for (const initialDumpTrade of initialDumpTrades) {
if (!shouldProcessTrade(initialDumpTrade, allTrades)) {
continue;
}
ensureNonNull(initialDumpTrade);
allTrades.push(initialDumpTrade);
}
logUtils.log(`got ${allTrades.length} from one-time dump`);
}
const fullUrl = getFullUrlForPublisher(publisher);
/**
* API returns trades in reverse chronological order, so highest block
* numbers first. The `start` query parameter indicates how far back in
* time (in number of trades) the results should start. Here we iterate
* over both start parameter values and block numbers simultaneously.
* Start parameter values count up from zero. Block numbers count down
* until reaching the highest block number in the initial dump.
*/
const blockNumberStop = Math.max(highestBlockNumbersInIntialDump[publisher] + 1, blockNumberStart);
for (
let startParam = 0, blockNumber = Number.MAX_SAFE_INTEGER;
blockNumber > blockNumberStop;
startParam += MAX_TRADES_PER_QUERY
) {
const response = await _getTradesWithOffsetAsync(fullUrl, publisher, startParam);
const tradesFromApi = response.data;
logUtils.log(
`got ${
tradesFromApi.length
} trades from API. blockNumber=${blockNumber}. blockNumberStop=${blockNumberStop}`,
);
for (const tradeFromApi of tradesFromApi) {
if (tradeFromApi.blockNumber <= blockNumberStop) {
blockNumber = blockNumberStop;
break;
}
if (!shouldProcessTrade(tradeFromApi, allTrades)) {
continue;
}
ensureNonNull(tradeFromApi);
allTrades.push(tradeFromApi);
blockNumber = tradeFromApi.blockNumber;
}
}
return allTrades;
}
function shouldProcessTrade(
trade: NonfungibleDotComTradeResponse,
existingTrades: NonfungibleDotComTradeResponse[],
): boolean {
// check to see if this trade is already in existingTrades
const existingTradeIndex = existingTrades.findIndex(
// HACK! making assumptions about composition of primary key
e =>
e.transactionHash === trade.transactionHash &&
e.logIndex === trade.logIndex &&
e.blockNumber === trade.blockNumber,
);
if (existingTradeIndex !== -1) {
logUtils.log("we've already captured this trade. deciding whether to use the existing record or this one.");
if (trade.blockNumber > existingTrades[existingTradeIndex].blockNumber) {
logUtils.log('throwing out existing trade');
existingTrades.splice(existingTradeIndex, 1);
} else {
logUtils.log('letting existing trade stand, and skipping processing of this trade');
return false;
}
}
return true;
}
const highestBlockNumbersInIntialDump: { [publisher: string]: number } = {
axieinfinity: 7065913,
cryptokitties: 4658171,
cryptopunks: 7058897,
cryptovoxels: 7060783,
decentraland_estate: 7065181,
decentraland: 6938962,
etherbots: 5204980,
etheremon: 7065370,
ethtown: 7064126,
knownorigin: 7065160,
mythereum: 7065311,
superrare: 7065955,
};
async function _getTradesWithOffsetAsync(
url: string,
publisher: string,
offset: number,
): Promise<NonfungibleDotComHistoryResponse> {
const resp: NonfungibleDotComHistoryResponse = await fetchSuccessfullyOrThrowAsync(
`${url}?${stringify({
publisher,
start: offset,
length: MAX_TRADES_PER_QUERY,
})}`,
);
return resp;
}
function getFullUrlForPublisher(publisher: string): string {
return `${NONFUNGIBLE_DOT_COM_URL}/market/${publisher}/history`;
}
function getInitialDumpUrl(publisher: string): string {
return `https://nonfungible-dot-com-one-time-data-dump.s3.amazonaws.com/sales_summary_${publisher}.json`;
}
function ensureNonNull(trade: NonfungibleDotComTradeResponse): void {
// these fields need to be set in order to avoid non-null
// constraint exceptions upon database insertion.
if (trade.logIndex === undefined) {
// for cryptopunks
trade.logIndex = 0;
}
if (trade.assetDescriptor === undefined) {
// for cryptopunks
trade.assetDescriptor = '';
}
if (trade.meta === undefined) {
// for cryptopunks
trade.meta = {};
}
if (trade.marketAddress === null) {
// for decentraland_estate
trade.marketAddress = '';
}
}

View File

@ -33,6 +33,10 @@ export interface CryptoCompareOHLCVParams {
toTs?: number; toTs?: number;
} }
export interface CryptoCompareUsdPrice {
USD: number;
}
const ONE_HOUR = 60 * 60 * 1000; // tslint:disable-line:custom-no-magic-numbers const ONE_HOUR = 60 * 60 * 1000; // tslint:disable-line:custom-no-magic-numbers
const ONE_SECOND = 1000; const ONE_SECOND = 1000;
const ONE_HOUR_AGO = new Date().getTime() - ONE_HOUR; const ONE_HOUR_AGO = new Date().getTime() - ONE_HOUR;
@ -45,6 +49,7 @@ export class CryptoCompareOHLCVSource {
public readonly defaultExchange = 'CCCAGG'; public readonly defaultExchange = 'CCCAGG';
public readonly interval = this.intervalBetweenRecords * MAX_PAGE_SIZE; // the hourly API returns data for one interval at a time public readonly interval = this.intervalBetweenRecords * MAX_PAGE_SIZE; // the hourly API returns data for one interval at a time
private readonly _url: string = 'https://min-api.cryptocompare.com/data/histohour?'; private readonly _url: string = 'https://min-api.cryptocompare.com/data/histohour?';
private readonly _priceUrl: string = 'https://min-api.cryptocompare.com/data/price?';
// rate-limit for all API calls through this class instance // rate-limit for all API calls through this class instance
private readonly _limiter: Bottleneck; private readonly _limiter: Bottleneck;
@ -96,6 +101,13 @@ export class CryptoCompareOHLCVSource {
}; };
return R.unfold(f, pair); return R.unfold(f, pair);
} }
public async getUsdPriceAsync(symbol: string): Promise<number> {
const usdUrl = `${this._priceUrl}tsyms=USD&fsym=${symbol}`;
const resp = await fetchAsync(usdUrl);
const respJson: CryptoCompareUsdPrice = await resp.json();
return respJson.USD;
}
} }
function hasData(record: CryptoCompareOHLCVRecord): boolean { function hasData(record: CryptoCompareOHLCVRecord): boolean {

View File

@ -0,0 +1,53 @@
import { orderParsingUtils } from '@0x/order-utils';
import { fetchAsync, logUtils } from '@0x/utils';
import { RadarBook, RadarMarket, RadarSignedOrder } from '@radarrelay/types';
const RADAR_BASE_URL = 'https://api.radarrelay.com/v2/';
const ACTIVE_MARKETS_URL = `${RADAR_BASE_URL}/markets`;
const MAX_PER_PAGE = 10000;
export const RADAR_SOURCE = 'radar';
// tslint:disable:prefer-function-over-method
// ^ Keep consistency with other sources and help logical organization
export class RadarSource {
public static parseRadarOrderResponse(radarOrderResponse: any): RadarSignedOrder {
return {
...radarOrderResponse,
...orderParsingUtils.convertStringsFieldsToBigNumbers(radarOrderResponse, [
'remainingBaseTokenAmount',
'remainingQuoteTokenAmount',
'price',
]),
signedOrder: orderParsingUtils.convertOrderStringFieldsToBigNumber(radarOrderResponse.signedOrder),
};
}
/**
* Call Radar API to find out which markets they are maintaining orderbooks for.
*/
public async getActiveMarketsAsync(): Promise<RadarMarket[]> {
logUtils.log('Getting all active Radar markets');
const resp = await fetchAsync(`${ACTIVE_MARKETS_URL}?perPage=${MAX_PER_PAGE}`);
const markets: RadarMarket[] = await resp.json();
logUtils.log(`Got ${markets.length} markets.`);
return markets;
}
/**
* Retrieve orderbook from Radar API for a given market.
* @param marketId String identifying the market we want data for. Eg. 'REP/AUG'
*/
public async getMarketOrderbookAsync(marketId: string): Promise<RadarBook> {
logUtils.log(`${marketId}: Retrieving orderbook.`);
const marketOrderbookUrl = `${ACTIVE_MARKETS_URL}/${marketId}/book?perPage=${MAX_PER_PAGE}`;
const resp = await fetchAsync(marketOrderbookUrl);
const jsonResp = await resp.json();
return {
...jsonResp,
// tslint:disable-next-line:no-unbound-method
bids: jsonResp.bids.map(RadarSource.parseRadarOrderResponse),
// tslint:disable-next-line:no-unbound-method
asks: jsonResp.asks.map(RadarSource.parseRadarOrderResponse),
};
}
}

View File

@ -7,8 +7,10 @@ export { DexTrade } from './dex_trade';
export { ExchangeCancelEvent } from './exchange_cancel_event'; export { ExchangeCancelEvent } from './exchange_cancel_event';
export { ExchangeCancelUpToEvent } from './exchange_cancel_up_to_event'; export { ExchangeCancelUpToEvent } from './exchange_cancel_up_to_event';
export { ExchangeFillEvent } from './exchange_fill_event'; export { ExchangeFillEvent } from './exchange_fill_event';
export { NonfungibleDotComTrade } from './nonfungible_dot_com_trade';
export { OHLCVExternal } from './ohlcv_external'; export { OHLCVExternal } from './ohlcv_external';
export { Relayer } from './relayer'; export { Relayer } from './relayer';
export { Slippage } from './slippage';
export { SraOrder } from './sra_order'; export { SraOrder } from './sra_order';
export { SraOrdersObservedTimeStamp, createObservedTimestampForOrder } from './sra_order_observed_timestamp'; export { SraOrdersObservedTimeStamp, createObservedTimestampForOrder } from './sra_order_observed_timestamp';
export { TokenMetadata } from './token_metadata'; export { TokenMetadata } from './token_metadata';

View File

@ -0,0 +1,35 @@
import { BigNumber } from '@0x/utils';
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { bigNumberTransformer, numberToBigIntTransformer } from '../utils';
@Entity({ name: 'nonfungible_dot_com_trades', schema: 'raw' })
export class NonfungibleDotComTrade {
@PrimaryColumn({ name: 'transaction_hash' })
public transactionHash!: string;
@PrimaryColumn({ name: 'publisher' })
public publisher!: string;
@PrimaryColumn({ name: 'block_number', type: 'bigint', transformer: numberToBigIntTransformer })
public blockNumber!: number;
@PrimaryColumn({ name: 'log_index' })
public logIndex!: number;
@PrimaryColumn({ name: 'asset_id' })
public assetId!: string;
@Column({ name: 'block_timestamp', type: 'bigint', transformer: numberToBigIntTransformer })
public blockTimestamp!: number;
@Column({ name: 'asset_descriptor' })
public assetDescriptor!: string;
@Column({ name: 'market_address' })
public marketAddress!: string;
@Column({ name: 'total_price', type: 'numeric', transformer: bigNumberTransformer })
public totalPrice!: BigNumber;
@Column({ name: 'usd_price', type: 'numeric', transformer: bigNumberTransformer })
public usdPrice!: BigNumber;
@Column({ name: 'buyer_address' })
public buyerAddress!: string;
@Column({ name: 'seller_address' })
public sellerAddress!: string;
@Column({ type: 'jsonb' })
public meta!: object;
}

View File

@ -0,0 +1,25 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { BigNumber } from '@0x/utils';
import { bigNumberTransformer, numberToBigIntTransformer } from '../utils';
@Entity({ name: 'slippage', schema: 'raw' })
export class Slippage {
@PrimaryColumn({ name: 'observed_timestamp', type: 'bigint', transformer: numberToBigIntTransformer })
public observedTimestamp!: number;
@PrimaryColumn({ name: 'symbol' })
public symbol!: string;
@PrimaryColumn({ name: 'exchange' })
public exchange!: string;
@PrimaryColumn({ name: 'usd_amount', type: 'numeric', transformer: bigNumberTransformer })
public usdAmount!: BigNumber;
@Column({ name: 'token_amount', type: 'numeric', transformer: bigNumberTransformer })
public tokenAmount!: BigNumber;
@Column({ name: 'avg_price_in_eth_sell', type: 'numeric', transformer: bigNumberTransformer })
public avgPriceInEthSell?: BigNumber;
@Column({ name: 'avg_price_in_eth_buy', type: 'numeric', transformer: bigNumberTransformer })
public avgPriceInEthBuy?: BigNumber;
@Column({ name: 'slippage', type: 'numeric', transformer: bigNumberTransformer })
public slippage?: BigNumber;
}

View File

@ -15,12 +15,14 @@ export class TokenOrderbookSnapshot {
public price!: BigNumber; public price!: BigNumber;
@PrimaryColumn({ name: 'base_asset_symbol' }) @PrimaryColumn({ name: 'base_asset_symbol' })
public baseAssetSymbol!: string; public baseAssetSymbol!: string;
@PrimaryColumn({ name: 'quote_asset_symbol' })
public quoteAssetSymbol!: string;
@PrimaryColumn({ type: String, name: 'maker_address', default: 'unknown' })
public makerAddress!: string;
@Column({ nullable: true, type: String, name: 'base_asset_address' }) @Column({ nullable: true, type: String, name: 'base_asset_address' })
public baseAssetAddress!: string | null; public baseAssetAddress!: string | null;
@Column({ name: 'base_volume', type: 'numeric', transformer: bigNumberTransformer }) @Column({ name: 'base_volume', type: 'numeric', transformer: bigNumberTransformer })
public baseVolume!: BigNumber; public baseVolume!: BigNumber;
@PrimaryColumn({ name: 'quote_asset_symbol' })
public quoteAssetSymbol!: string;
@Column({ nullable: true, type: String, name: 'quote_asset_address' }) @Column({ nullable: true, type: String, name: 'quote_asset_address' })
public quoteAssetAddress!: string | null; public quoteAssetAddress!: string | null;
@Column({ name: 'quote_volume', type: 'numeric', transformer: bigNumberTransformer }) @Column({ name: 'quote_volume', type: 'numeric', transformer: bigNumberTransformer })

View File

@ -12,8 +12,10 @@ import {
ExchangeCancelEvent, ExchangeCancelEvent,
ExchangeCancelUpToEvent, ExchangeCancelUpToEvent,
ExchangeFillEvent, ExchangeFillEvent,
NonfungibleDotComTrade,
OHLCVExternal, OHLCVExternal,
Relayer, Relayer,
Slippage,
SraOrder, SraOrder,
SraOrdersObservedTimeStamp, SraOrdersObservedTimeStamp,
TokenMetadata, TokenMetadata,
@ -33,8 +35,10 @@ const entities = [
ExchangeCancelUpToEvent, ExchangeCancelUpToEvent,
ExchangeFillEvent, ExchangeFillEvent,
ERC20ApprovalEvent, ERC20ApprovalEvent,
NonfungibleDotComTrade,
OHLCVExternal, OHLCVExternal,
Relayer, Relayer,
Slippage,
SraOrder, SraOrder,
SraOrdersObservedTimeStamp, SraOrdersObservedTimeStamp,
TokenMetadata, TokenMetadata,

View File

@ -2,33 +2,27 @@ import { BigNumber } from '@0x/utils';
import { aggregateOrders } from '../utils'; import { aggregateOrders } from '../utils';
import { DdexMarket, DdexOrderbook } from '../../data_sources/ddex'; import { DDEX_SOURCE, DdexMarket, DdexOrderbook } from '../../data_sources/ddex';
import { TokenOrderbookSnapshot as TokenOrder } from '../../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../../entities';
import { OrderType } from '../../types'; import { OrderType } from '../../types';
/** /**
* Marque function of this file. * Marquee function of this file.
* 1) Takes in orders from an orderbook, * 1) Takes in orders from an orderbook,
* other information attached. * other information attached.
* @param ddexOrderbook A raw orderbook that we pull from the Ddex API. * @param ddexOrderbook A raw orderbook that we pull from the Ddex API.
* @param ddexMarket An object containing market data also directly from the API. * @param ddexMarket An object containing market data also directly from the API.
* @param observedTimestamp Time at which the orders for the market were pulled. * @param observedTimestamp Time at which the orders for the market were pulled.
* @param source The exchange where these orders are placed. In this case 'ddex'.
*/ */
export function parseDdexOrders( export function parseDdexOrders(
ddexOrderbook: DdexOrderbook, ddexOrderbook: DdexOrderbook,
ddexMarket: DdexMarket, ddexMarket: DdexMarket,
observedTimestamp: number, observedTimestamp: number,
source: string,
): TokenOrder[] { ): TokenOrder[] {
const aggregatedBids = aggregateOrders(ddexOrderbook.bids); const aggregatedBids = aggregateOrders(ddexOrderbook.bids);
const aggregatedAsks = aggregateOrders(ddexOrderbook.asks); const aggregatedAsks = aggregateOrders(ddexOrderbook.asks);
const parsedBids = aggregatedBids.map(order => const parsedBids = aggregatedBids.map(order => parseDdexOrder(ddexMarket, observedTimestamp, OrderType.Bid, order));
parseDdexOrder(ddexMarket, observedTimestamp, OrderType.Bid, source, order), const parsedAsks = aggregatedAsks.map(order => parseDdexOrder(ddexMarket, observedTimestamp, OrderType.Ask, order));
);
const parsedAsks = aggregatedAsks.map(order =>
parseDdexOrder(ddexMarket, observedTimestamp, OrderType.Ask, source, order),
);
return parsedBids.concat(parsedAsks); return parsedBids.concat(parsedAsks);
} }
@ -46,14 +40,13 @@ export function parseDdexOrder(
ddexMarket: DdexMarket, ddexMarket: DdexMarket,
observedTimestamp: number, observedTimestamp: number,
orderType: OrderType, orderType: OrderType,
source: string,
ddexOrder: [string, BigNumber], ddexOrder: [string, BigNumber],
): TokenOrder { ): TokenOrder {
const tokenOrder = new TokenOrder(); const tokenOrder = new TokenOrder();
const price = new BigNumber(ddexOrder[0]); const price = new BigNumber(ddexOrder[0]);
const amount = ddexOrder[1]; const amount = ddexOrder[1];
tokenOrder.source = source; tokenOrder.source = DDEX_SOURCE;
tokenOrder.observedTimestamp = observedTimestamp; tokenOrder.observedTimestamp = observedTimestamp;
tokenOrder.orderType = orderType; tokenOrder.orderType = orderType;
tokenOrder.price = price; tokenOrder.price = price;
@ -65,5 +58,7 @@ export function parseDdexOrder(
tokenOrder.quoteAssetSymbol = ddexMarket.quoteToken; tokenOrder.quoteAssetSymbol = ddexMarket.quoteToken;
tokenOrder.quoteAssetAddress = ddexMarket.quoteTokenAddress; tokenOrder.quoteAssetAddress = ddexMarket.quoteTokenAddress;
tokenOrder.quoteVolume = price.times(amount); tokenOrder.quoteVolume = price.times(amount);
tokenOrder.makerAddress = 'unknown';
return tokenOrder; return tokenOrder;
} }

View File

@ -2,28 +2,25 @@ import { BigNumber } from '@0x/utils';
import { aggregateOrders } from '../utils'; import { aggregateOrders } from '../utils';
import { IdexOrderbook, IdexOrderParam } from '../../data_sources/idex'; import { IDEX_SOURCE, IdexOrderbook, IdexOrderParam } from '../../data_sources/idex';
import { TokenOrderbookSnapshot as TokenOrder } from '../../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../../entities';
import { OrderType } from '../../types'; import { OrderType } from '../../types';
/** /**
* Marque function of this file. * Marquee function of this file.
* 1) Takes in orders from an orderbook, * 1) Takes in orders from an orderbook,
* 2) Aggregates them by price point, * 2) Aggregates them by price point,
* 3) Parses them into entities which are then saved into the database. * 3) Parses them into entities which are then saved into the database.
* @param idexOrderbook raw orderbook that we pull from the Idex API. * @param idexOrderbook raw orderbook that we pull from the Idex API.
* @param observedTimestamp Time at which the orders for the market were pulled. * @param observedTimestamp Time at which the orders for the market were pulled.
* @param source The exchange where these orders are placed. In this case 'idex'.
*/ */
export function parseIdexOrders(idexOrderbook: IdexOrderbook, observedTimestamp: number, source: string): TokenOrder[] { export function parseIdexOrders(idexOrderbook: IdexOrderbook, observedTimestamp: number): TokenOrder[] {
const aggregatedBids = aggregateOrders(idexOrderbook.bids); const aggregatedBids = aggregateOrders(idexOrderbook.bids);
// Any of the bid orders' params will work // Any of the bid orders' params will work
const idexBidOrder = idexOrderbook.bids[0]; const idexBidOrder = idexOrderbook.bids[0];
const parsedBids = const parsedBids =
aggregatedBids.length > 0 aggregatedBids.length > 0
? aggregatedBids.map(order => ? aggregatedBids.map(order => parseIdexOrder(idexBidOrder.params, observedTimestamp, OrderType.Bid, order))
parseIdexOrder(idexBidOrder.params, observedTimestamp, OrderType.Bid, source, order),
)
: []; : [];
const aggregatedAsks = aggregateOrders(idexOrderbook.asks); const aggregatedAsks = aggregateOrders(idexOrderbook.asks);
@ -31,9 +28,7 @@ export function parseIdexOrders(idexOrderbook: IdexOrderbook, observedTimestamp:
const idexAskOrder = idexOrderbook.asks[0]; const idexAskOrder = idexOrderbook.asks[0];
const parsedAsks = const parsedAsks =
aggregatedAsks.length > 0 aggregatedAsks.length > 0
? aggregatedAsks.map(order => ? aggregatedAsks.map(order => parseIdexOrder(idexAskOrder.params, observedTimestamp, OrderType.Ask, order))
parseIdexOrder(idexAskOrder.params, observedTimestamp, OrderType.Ask, source, order),
)
: []; : [];
return parsedBids.concat(parsedAsks); return parsedBids.concat(parsedAsks);
} }
@ -45,26 +40,25 @@ export function parseIdexOrders(idexOrderbook: IdexOrderbook, observedTimestamp:
* trades have been placed. * trades have been placed.
* @param observedTimestamp The time when the API response returned back to us. * @param observedTimestamp The time when the API response returned back to us.
* @param orderType 'bid' or 'ask' enum. * @param orderType 'bid' or 'ask' enum.
* @param source Exchange where these orders were placed.
* @param idexOrder A <price, amount> tuple which we will convert to volume-basis. * @param idexOrder A <price, amount> tuple which we will convert to volume-basis.
*/ */
export function parseIdexOrder( export function parseIdexOrder(
idexOrderParam: IdexOrderParam, idexOrderParam: IdexOrderParam,
observedTimestamp: number, observedTimestamp: number,
orderType: OrderType, orderType: OrderType,
source: string,
idexOrder: [string, BigNumber], idexOrder: [string, BigNumber],
): TokenOrder { ): TokenOrder {
const tokenOrder = new TokenOrder(); const tokenOrder = new TokenOrder();
const price = new BigNumber(idexOrder[0]); const price = new BigNumber(idexOrder[0]);
const amount = idexOrder[1]; const amount = idexOrder[1];
tokenOrder.source = source; tokenOrder.source = IDEX_SOURCE;
tokenOrder.observedTimestamp = observedTimestamp; tokenOrder.observedTimestamp = observedTimestamp;
tokenOrder.orderType = orderType; tokenOrder.orderType = orderType;
tokenOrder.price = price; tokenOrder.price = price;
tokenOrder.baseVolume = amount; tokenOrder.baseVolume = amount;
tokenOrder.quoteVolume = price.times(amount); tokenOrder.quoteVolume = price.times(amount);
tokenOrder.makerAddress = 'unknown';
if (orderType === OrderType.Bid) { if (orderType === OrderType.Bid) {
tokenOrder.baseAssetSymbol = idexOrderParam.buySymbol; tokenOrder.baseAssetSymbol = idexOrderParam.buySymbol;

View File

@ -0,0 +1,42 @@
import { BigNumber } from '@0x/utils';
import * as R from 'ramda';
import { NonfungibleDotComTradeResponse } from '../../data_sources/nonfungible_dot_com';
import { NonfungibleDotComTrade } from '../../entities';
/**
* Parses a raw trades from the nonfungible.com API and returns an array of
* NonfungibleDotComTrade entities.
* @param rawTrades A raw order response from an SRA endpoint.
*/
export function parseNonFungibleDotComTrades(
rawTrades: NonfungibleDotComTradeResponse[],
publisher: string,
): NonfungibleDotComTrade[] {
return R.map(_parseNonFungibleDotComTrade.bind(null, publisher), rawTrades);
}
/**
* Converts a single trade from nonfungible.com into an NonfungibleDotComTrade entity.
* @param rawTrade A single trade from the response from the nonfungible.com API.
*/
export function _parseNonFungibleDotComTrade(
publisher: string,
rawTrade: NonfungibleDotComTradeResponse,
): NonfungibleDotComTrade {
const nonfungibleDotComTrade = new NonfungibleDotComTrade();
nonfungibleDotComTrade.assetDescriptor = rawTrade.assetDescriptor;
nonfungibleDotComTrade.assetId = rawTrade.assetId;
nonfungibleDotComTrade.blockNumber = rawTrade.blockNumber;
nonfungibleDotComTrade.blockTimestamp = new Date(rawTrade.blockTimestamp).getTime();
nonfungibleDotComTrade.buyerAddress = rawTrade.buyer;
nonfungibleDotComTrade.logIndex = rawTrade.logIndex;
nonfungibleDotComTrade.marketAddress = rawTrade.marketAddress;
nonfungibleDotComTrade.meta = rawTrade.meta;
nonfungibleDotComTrade.sellerAddress = rawTrade.seller;
nonfungibleDotComTrade.totalPrice = new BigNumber(rawTrade.totalPrice);
nonfungibleDotComTrade.transactionHash = rawTrade.transactionHash;
nonfungibleDotComTrade.usdPrice = new BigNumber(rawTrade.usdPrice);
nonfungibleDotComTrade.publisher = publisher;
return nonfungibleDotComTrade;
}

View File

@ -3,33 +3,31 @@ import * as R from 'ramda';
import { aggregateOrders } from '../utils'; import { aggregateOrders } from '../utils';
import { OasisMarket, OasisOrder } from '../../data_sources/oasis'; import { OASIS_SOURCE, OasisMarket, OasisOrder } from '../../data_sources/oasis';
import { TokenOrderbookSnapshot as TokenOrder } from '../../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../../entities';
import { OrderType } from '../../types'; import { OrderType } from '../../types';
/** /**
* Marque function of this file. * Marquee function of this file.
* 1) Takes in orders from an orderbook, * 1) Takes in orders from an orderbook,
* 2) Aggregates them according to price point, * 2) Aggregates them according to price point,
* 3) Builds TokenOrder entity with other information attached. * 3) Builds TokenOrder entity with other information attached.
* @param oasisOrderbook A raw orderbook that we pull from the Oasis API. * @param oasisOrderbook A raw orderbook that we pull from the Oasis API.
* @param oasisMarket An object containing market data also directly from the API. * @param oasisMarket An object containing market data also directly from the API.
* @param observedTimestamp Time at which the orders for the market were pulled. * @param observedTimestamp Time at which the orders for the market were pulled.
* @param source The exchange where these orders are placed. In this case 'oasis'.
*/ */
export function parseOasisOrders( export function parseOasisOrders(
oasisOrderbook: OasisOrder[], oasisOrderbook: OasisOrder[],
oasisMarket: OasisMarket, oasisMarket: OasisMarket,
observedTimestamp: number, observedTimestamp: number,
source: string,
): TokenOrder[] { ): TokenOrder[] {
const aggregatedBids = aggregateOrders(R.filter(R.propEq('act', OrderType.Bid), oasisOrderbook)); const aggregatedBids = aggregateOrders(R.filter(R.propEq('act', OrderType.Bid), oasisOrderbook));
const aggregatedAsks = aggregateOrders(R.filter(R.propEq('act', OrderType.Ask), oasisOrderbook)); const aggregatedAsks = aggregateOrders(R.filter(R.propEq('act', OrderType.Ask), oasisOrderbook));
const parsedBids = aggregatedBids.map(order => const parsedBids = aggregatedBids.map(order =>
parseOasisOrder(oasisMarket, observedTimestamp, OrderType.Bid, source, order), parseOasisOrder(oasisMarket, observedTimestamp, OrderType.Bid, order),
); );
const parsedAsks = aggregatedAsks.map(order => const parsedAsks = aggregatedAsks.map(order =>
parseOasisOrder(oasisMarket, observedTimestamp, OrderType.Ask, source, order), parseOasisOrder(oasisMarket, observedTimestamp, OrderType.Ask, order),
); );
return parsedBids.concat(parsedAsks); return parsedBids.concat(parsedAsks);
} }
@ -48,14 +46,13 @@ export function parseOasisOrder(
oasisMarket: OasisMarket, oasisMarket: OasisMarket,
observedTimestamp: number, observedTimestamp: number,
orderType: OrderType, orderType: OrderType,
source: string,
oasisOrder: [string, BigNumber], oasisOrder: [string, BigNumber],
): TokenOrder { ): TokenOrder {
const tokenOrder = new TokenOrder(); const tokenOrder = new TokenOrder();
const price = new BigNumber(oasisOrder[0]); const price = new BigNumber(oasisOrder[0]);
const amount = oasisOrder[1]; const amount = oasisOrder[1];
tokenOrder.source = source; tokenOrder.source = OASIS_SOURCE;
tokenOrder.observedTimestamp = observedTimestamp; tokenOrder.observedTimestamp = observedTimestamp;
tokenOrder.orderType = orderType; tokenOrder.orderType = orderType;
tokenOrder.price = price; tokenOrder.price = price;
@ -67,5 +64,6 @@ export function parseOasisOrder(
tokenOrder.quoteAssetSymbol = oasisMarket.quote; tokenOrder.quoteAssetSymbol = oasisMarket.quote;
tokenOrder.quoteAssetAddress = null; // Oasis doesn't provide address information tokenOrder.quoteAssetAddress = null; // Oasis doesn't provide address information
tokenOrder.quoteVolume = price.times(amount); tokenOrder.quoteVolume = price.times(amount);
tokenOrder.makerAddress = 'unknown';
return tokenOrder; return tokenOrder;
} }

View File

@ -1,30 +1,28 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { ParadexMarket, ParadexOrder, ParadexOrderbookResponse } from '../../data_sources/paradex'; import { PARADEX_SOURCE, ParadexMarket, ParadexOrder, ParadexOrderbookResponse } from '../../data_sources/paradex';
import { TokenOrderbookSnapshot as TokenOrder } from '../../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../../entities';
import { OrderType } from '../../types'; import { OrderType } from '../../types';
/** /**
* Marque function of this file. * Marquee function of this file.
* 1) Takes in orders from an orderbook (orders are already aggregated by price point), * 1) Takes in orders from an orderbook (orders are already aggregated by price point),
* 2) For each aggregated order, forms a TokenOrder entity with market data and * 2) For each aggregated order, forms a TokenOrder entity with market data and
* other information attached. * other information attached.
* @param paradexOrderbookResponse An orderbook response from the Paradex API. * @param paradexOrderbookResponse An orderbook response from the Paradex API.
* @param paradexMarket An object containing market data also directly from the API. * @param paradexMarket An object containing market data also directly from the API.
* @param observedTimestamp Time at which the orders for the market were pulled. * @param observedTimestamp Time at which the orders for the market were pulled.
* @param source The exchange where these orders are placed. In this case 'paradex'.
*/ */
export function parseParadexOrders( export function parseParadexOrders(
paradexOrderbookResponse: ParadexOrderbookResponse, paradexOrderbookResponse: ParadexOrderbookResponse,
paradexMarket: ParadexMarket, paradexMarket: ParadexMarket,
observedTimestamp: number, observedTimestamp: number,
source: string,
): TokenOrder[] { ): TokenOrder[] {
const parsedBids = paradexOrderbookResponse.bids.map(order => const parsedBids = paradexOrderbookResponse.bids.map(order =>
parseParadexOrder(paradexMarket, observedTimestamp, OrderType.Bid, source, order), parseParadexOrder(paradexMarket, observedTimestamp, OrderType.Bid, order),
); );
const parsedAsks = paradexOrderbookResponse.asks.map(order => const parsedAsks = paradexOrderbookResponse.asks.map(order =>
parseParadexOrder(paradexMarket, observedTimestamp, OrderType.Ask, source, order), parseParadexOrder(paradexMarket, observedTimestamp, OrderType.Ask, order),
); );
return parsedBids.concat(parsedAsks); return parsedBids.concat(parsedAsks);
} }
@ -36,21 +34,19 @@ export function parseParadexOrders(
* orders have been placed. * orders have been placed.
* @param observedTimestamp The time when the API response returned back to us. * @param observedTimestamp The time when the API response returned back to us.
* @param orderType 'bid' or 'ask' enum. * @param orderType 'bid' or 'ask' enum.
* @param source Exchange where these orders were placed.
* @param paradexOrder A ParadexOrder object; basically price, amount tuple. * @param paradexOrder A ParadexOrder object; basically price, amount tuple.
*/ */
export function parseParadexOrder( export function parseParadexOrder(
paradexMarket: ParadexMarket, paradexMarket: ParadexMarket,
observedTimestamp: number, observedTimestamp: number,
orderType: OrderType, orderType: OrderType,
source: string,
paradexOrder: ParadexOrder, paradexOrder: ParadexOrder,
): TokenOrder { ): TokenOrder {
const tokenOrder = new TokenOrder(); const tokenOrder = new TokenOrder();
const price = new BigNumber(paradexOrder.price); const price = new BigNumber(paradexOrder.price);
const amount = new BigNumber(paradexOrder.amount); const amount = new BigNumber(paradexOrder.amount);
tokenOrder.source = source; tokenOrder.source = PARADEX_SOURCE;
tokenOrder.observedTimestamp = observedTimestamp; tokenOrder.observedTimestamp = observedTimestamp;
tokenOrder.orderType = orderType; tokenOrder.orderType = orderType;
tokenOrder.price = price; tokenOrder.price = price;
@ -62,5 +58,6 @@ export function parseParadexOrder(
tokenOrder.quoteAssetSymbol = paradexMarket.quoteToken; tokenOrder.quoteAssetSymbol = paradexMarket.quoteToken;
tokenOrder.quoteAssetAddress = paradexMarket.quoteTokenAddress as string; tokenOrder.quoteAssetAddress = paradexMarket.quoteTokenAddress as string;
tokenOrder.quoteVolume = price.times(amount); tokenOrder.quoteVolume = price.times(amount);
tokenOrder.makerAddress = 'unknown';
return tokenOrder; return tokenOrder;
} }

View File

@ -0,0 +1,118 @@
import { ObjectMap } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { RadarBook, RadarMarket, RadarSignedOrder } from '@radarrelay/types';
import * as R from 'ramda';
import { aggregateOrders, GenericRawOrder } from '../utils';
import { RADAR_SOURCE } from '../../data_sources/radar';
import { TokenOrderbookSnapshot as TokenOrder } from '../../entities';
import { OrderType } from '../../types';
export interface AggregateOrdersByMaker {
makerAddress: string;
price: string;
amount: BigNumber;
}
/**
* Marquee function of this file.
* 1) Takes in orders from an orderbook,
* other information attached.
* @param radarOrderbook A raw orderbook that we pull from the radar API.
* @param radarMarket An object containing market data also directly from the API.
* @param observedTimestamp Time at which the orders for the market were pulled.
*/
export function parseRadarOrders(
radarOrderbook: RadarBook,
radarMarket: RadarMarket,
observedTimestamp: number,
): TokenOrder[] {
const aggregatedBids = _aggregateOrdersByMaker(radarMarket, radarOrderbook.bids);
const aggregatedAsks = _aggregateOrdersByMaker(radarMarket, radarOrderbook.asks);
const parsedBids = aggregatedBids.map(order =>
parseRadarOrder(radarMarket, observedTimestamp, OrderType.Bid, order),
);
const parsedAsks = aggregatedAsks.map(order =>
parseRadarOrder(radarMarket, observedTimestamp, OrderType.Ask, order),
);
return parsedBids.concat(parsedAsks);
}
/**
* Parse a single aggregated radar order in order to form a tokenOrder entity
* which can be saved into the database.
* @param radarMarket An object containing information about the market where these
* trades have been placed.
* @param observedTimestamp The time when the API response returned back to us.
* @param orderType 'bid' or 'ask' enum.
* @param aggregateOrder An AggregateOrdersByMaker instance which we will convert to volume-basis.
*/
export function parseRadarOrder(
radarMarket: RadarMarket,
observedTimestamp: number,
orderType: OrderType,
aggregateOrder: AggregateOrdersByMaker,
): TokenOrder {
const tokenOrder = new TokenOrder();
const price = new BigNumber(aggregateOrder.price);
const amount = aggregateOrder.amount;
const splitId = radarMarket.id.split('-');
tokenOrder.source = RADAR_SOURCE;
tokenOrder.observedTimestamp = observedTimestamp;
tokenOrder.orderType = orderType;
tokenOrder.price = price;
tokenOrder.baseAssetSymbol = splitId[0];
tokenOrder.baseAssetAddress = radarMarket.baseTokenAddress || null;
tokenOrder.baseVolume = amount;
tokenOrder.quoteAssetSymbol = splitId[1];
tokenOrder.quoteAssetAddress = radarMarket.quoteTokenAddress || null;
tokenOrder.quoteVolume = price.times(amount);
tokenOrder.makerAddress = aggregateOrder.makerAddress;
return tokenOrder;
}
function _toGeneric(radarMarket: RadarMarket, radarOrder: RadarSignedOrder): GenericRawOrder | undefined {
if (radarMarket.baseTokenDecimals === undefined) {
return undefined;
}
return {
price: radarOrder.price.toString(),
// Use the remaining fillable amount
amount: radarOrder.remainingBaseTokenAmount.toString(),
};
}
function _aggregateOrdersByMaker(radarMarket: RadarMarket, radarOrders: RadarSignedOrder[]): AggregateOrdersByMaker[] {
// group all orders by their maker
const ordersByMaker: ObjectMap<RadarSignedOrder[]> = radarOrders.reduce(
(acc: ObjectMap<RadarSignedOrder[]>, val: RadarSignedOrder) => {
const makerAddress = val.signedOrder.makerAddress;
if (acc[makerAddress]) {
acc[makerAddress].push(val);
} else {
acc[makerAddress] = [];
}
return acc;
},
{},
);
const transformToGeneric = (radarOrder: RadarSignedOrder) => _toGeneric(radarMarket, radarOrder);
const aggregationTuples: AggregateOrdersByMaker[][] = (R.keys(ordersByMaker) as string[]).map((maker: string) => {
const generalizedOrders = _removeUndefined(R.map(transformToGeneric, ordersByMaker[maker]));
const aggregatedOrders = aggregateOrders(generalizedOrders);
return aggregatedOrders.map((order: [string, BigNumber]) => ({
makerAddress: maker,
price: order[0],
amount: order[1],
}));
});
return R.unnest(aggregationTuples);
}
// tslint:disable-next-line:no-unbound-method
const _removeUndefined = R.reject(R.isNil);

View File

@ -2,7 +2,7 @@ import { logUtils } from '@0x/utils';
import * as R from 'ramda'; import * as R from 'ramda';
import { Connection, ConnectionOptions, createConnection } from 'typeorm'; import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { DDEX_SOURCE, DdexMarket, DdexSource } from '../data_sources/ddex'; import { DdexMarket, DdexSource } from '../data_sources/ddex';
import { TokenOrderbookSnapshot as TokenOrder } from '../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../entities';
import * as ormConfig from '../ormconfig'; import * as ormConfig from '../ormconfig';
import { parseDdexOrders } from '../parsers/ddex_orders'; import { parseDdexOrders } from '../parsers/ddex_orders';
@ -43,7 +43,7 @@ async function getAndSaveMarketOrderbookAsync(ddexSource: DdexSource, market: Dd
const observedTimestamp = Date.now(); const observedTimestamp = Date.now();
logUtils.log(`${market.id}: Parsing orders.`); logUtils.log(`${market.id}: Parsing orders.`);
const orders = parseDdexOrders(orderBook, market, observedTimestamp, DDEX_SOURCE); const orders = parseDdexOrders(orderBook, market, observedTimestamp);
if (orders.length > 0) { if (orders.length > 0) {
logUtils.log(`${market.id}: Saving ${orders.length} orders.`); logUtils.log(`${market.id}: Saving ${orders.length} orders.`);

View File

@ -2,7 +2,7 @@ import { logUtils } from '@0x/utils';
import * as R from 'ramda'; import * as R from 'ramda';
import { Connection, ConnectionOptions, createConnection } from 'typeorm'; import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { IDEX_SOURCE, IdexSource } from '../data_sources/idex'; import { IdexSource } from '../data_sources/idex';
import { TokenOrderbookSnapshot as TokenOrder } from '../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../entities';
import * as ormConfig from '../ormconfig'; import * as ormConfig from '../ormconfig';
import { parseIdexOrders } from '../parsers/idex_orders'; import { parseIdexOrders } from '../parsers/idex_orders';
@ -51,7 +51,7 @@ async function getAndSaveMarketOrderbookAsync(idexSource: IdexSource, marketId:
} }
logUtils.log(`${marketId}: Parsing orders.`); logUtils.log(`${marketId}: Parsing orders.`);
const orders = parseIdexOrders(orderBook, observedTimestamp, IDEX_SOURCE); const orders = parseIdexOrders(orderBook, observedTimestamp);
if (orders.length > 0) { if (orders.length > 0) {
logUtils.log(`${marketId}: Saving ${orders.length} orders.`); logUtils.log(`${marketId}: Saving ${orders.length} orders.`);

View File

@ -0,0 +1,43 @@
// tslint:disable:no-console
import 'reflect-metadata';
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { getTradesAsync, knownPublishers } from '../data_sources/nonfungible_dot_com';
import { NonfungibleDotComTrade } from '../entities';
import * as ormConfig from '../ormconfig';
import { parseNonFungibleDotComTrades } from '../parsers/nonfungible_dot_com';
import { handleError } from '../utils';
// Number of trades to save at once.
const BATCH_SAVE_SIZE = 1000;
let connection: Connection;
(async () => {
connection = await createConnection(ormConfig as ConnectionOptions);
await getAndSaveTradesAsync();
process.exit(0);
})().catch(handleError);
async function getAndSaveTradesAsync(): Promise<void> {
const tradesRepository = connection.getRepository(NonfungibleDotComTrade);
for (const publisher of knownPublishers) {
console.log(`Getting latest trades for NFT ${publisher}...`);
const tradeWithHighestBlockNumber = await tradesRepository
.createQueryBuilder('nonfungible_dot_com_trades')
.where('nonfungible_dot_com_trades.publisher = :publisher', { publisher })
.orderBy({ 'nonfungible_dot_com_trades.block_number': 'DESC' })
.getOne();
const highestExistingBlockNumber =
tradeWithHighestBlockNumber === undefined ? 0 : tradeWithHighestBlockNumber.blockNumber;
console.log(`Highest block number in existing trades: ${highestExistingBlockNumber}`);
const rawTrades = await getTradesAsync(publisher, highestExistingBlockNumber);
console.log(`Parsing ${rawTrades.length} trades...`);
const trades = parseNonFungibleDotComTrades(rawTrades, publisher);
console.log(`Saving ${rawTrades.length} trades...`);
await tradesRepository.save(trades, { chunk: Math.ceil(trades.length / BATCH_SAVE_SIZE) });
}
const newTotalTrades = await tradesRepository.count();
console.log(`Done saving trades. There are now ${newTotalTrades} total NFT trades.`);
}

View File

@ -2,7 +2,7 @@ import { logUtils } from '@0x/utils';
import * as R from 'ramda'; import * as R from 'ramda';
import { Connection, ConnectionOptions, createConnection } from 'typeorm'; import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { OASIS_SOURCE, OasisMarket, OasisSource } from '../data_sources/oasis'; import { OasisMarket, OasisSource } from '../data_sources/oasis';
import { TokenOrderbookSnapshot as TokenOrder } from '../entities'; import { TokenOrderbookSnapshot as TokenOrder } from '../entities';
import * as ormConfig from '../ormconfig'; import * as ormConfig from '../ormconfig';
import { parseOasisOrders } from '../parsers/oasis_orders'; import { parseOasisOrders } from '../parsers/oasis_orders';
@ -46,7 +46,7 @@ async function getAndSaveMarketOrderbookAsync(oasisSource: OasisSource, market:
const observedTimestamp = Date.now(); const observedTimestamp = Date.now();
logUtils.log(`${market.id}: Parsing orders.`); logUtils.log(`${market.id}: Parsing orders.`);
const orders = parseOasisOrders(orderBook, market, observedTimestamp, OASIS_SOURCE); const orders = parseOasisOrders(orderBook, market, observedTimestamp);
if (orders.length > 0) { if (orders.length > 0) {
logUtils.log(`${market.id}: Saving ${orders.length} orders.`); logUtils.log(`${market.id}: Saving ${orders.length} orders.`);

View File

@ -2,7 +2,6 @@ import { logUtils } from '@0x/utils';
import { Connection, ConnectionOptions, createConnection } from 'typeorm'; import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { import {
PARADEX_SOURCE,
ParadexActiveMarketsResponse, ParadexActiveMarketsResponse,
ParadexMarket, ParadexMarket,
ParadexSource, ParadexSource,
@ -75,7 +74,7 @@ async function getAndSaveMarketOrderbookAsync(paradexSource: ParadexSource, mark
const observedTimestamp = Date.now(); const observedTimestamp = Date.now();
logUtils.log(`${market.symbol}: Parsing orders.`); logUtils.log(`${market.symbol}: Parsing orders.`);
const orders = parseParadexOrders(paradexOrderbookResponse, market, observedTimestamp, PARADEX_SOURCE); const orders = parseParadexOrders(paradexOrderbookResponse, market, observedTimestamp);
if (orders.length > 0) { if (orders.length > 0) {
logUtils.log(`${market.symbol}: Saving ${orders.length} orders.`); logUtils.log(`${market.symbol}: Saving ${orders.length} orders.`);

View File

@ -0,0 +1,56 @@
import { logUtils } from '@0x/utils';
import { RadarMarket } from '@radarrelay/types';
import * as R from 'ramda';
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { RadarSource } from '../data_sources/radar';
import { TokenOrderbookSnapshot as TokenOrder } from '../entities';
import * as ormConfig from '../ormconfig';
import { parseRadarOrders } from '../parsers/radar_orders';
import { handleError } from '../utils';
// Number of orders to save at once.
const BATCH_SAVE_SIZE = 1000;
// Number of markets to retrieve orderbooks for at once.
const MARKET_ORDERBOOK_REQUEST_BATCH_SIZE = 50;
// Delay between market orderbook requests.
const MILLISEC_MARKET_ORDERBOOK_REQUEST_DELAY = 5000;
let connection: Connection;
(async () => {
connection = await createConnection(ormConfig as ConnectionOptions);
const radarSource = new RadarSource();
const markets = await radarSource.getActiveMarketsAsync();
for (const marketsChunk of R.splitEvery(MARKET_ORDERBOOK_REQUEST_BATCH_SIZE, markets)) {
await Promise.all(
marketsChunk.map(async (market: RadarMarket) => getAndSaveMarketOrderbookAsync(radarSource, market)),
);
await new Promise<void>(resolve => setTimeout(resolve, MILLISEC_MARKET_ORDERBOOK_REQUEST_DELAY));
}
process.exit(0);
})().catch(handleError);
/**
* Retrieve orderbook from radar API for a given market. Parse orders and insert
* them into our database.
* @param radarSource Data source which can query radar API.
* @param market Object from radar API containing market data.
*/
async function getAndSaveMarketOrderbookAsync(radarSource: RadarSource, market: RadarMarket): Promise<void> {
const orderBook = await radarSource.getMarketOrderbookAsync(market.id);
const observedTimestamp = Date.now();
logUtils.log(`${market.id}: Parsing orders.`);
const orders = parseRadarOrders(orderBook, market, observedTimestamp);
if (orders.length > 0) {
logUtils.log(`${market.id}: Saving ${orders.length} orders.`);
const TokenOrderRepository = connection.getRepository(TokenOrder);
await TokenOrderRepository.save(orders, { chunk: Math.ceil(orders.length / BATCH_SAVE_SIZE) });
} else {
logUtils.log(`${market.id}: 0 orders to save.`);
}
}

View File

@ -0,0 +1,64 @@
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
import { logUtils } from '@0x/utils';
import { EdpsSource } from '../data_sources/dex_prices';
import { CryptoCompareOHLCVSource } from '../data_sources/ohlcv_external/crypto_compare';
import { Slippage } from '../entities';
import * as ormConfig from '../ormconfig';
import { calculateSlippage } from '../transformers/slippage';
import { handleError } from '../utils';
// Number of orders to save at once.
const BATCH_SAVE_SIZE = 1000;
// Maximum requests per second to CryptoCompare
const CRYPTO_COMPARE_MAX_REQS_PER_SECOND = 60;
// USD amounts for slippage depths
// tslint:disable-next-line:custom-no-magic-numbers
const USD_AMOUNTS = [10, 100, 1000, 10000];
// TODO: fetch from database
const TOKENS = ['BAT', 'DAI', 'FUN', 'MANA', 'OMG', 'REP', 'TUSD', 'ZRX', 'MKR', 'BNB', 'USDC', 'LOOM', 'DNT', 'CVC'];
let connection: Connection;
(async () => {
connection = await createConnection(ormConfig as ConnectionOptions);
const edpsSource = new EdpsSource();
const cryptoCompareSource = new CryptoCompareOHLCVSource(CRYPTO_COMPARE_MAX_REQS_PER_SECOND);
logUtils.log('Fetching slippage records');
const nestedSlippages: Slippage[][][] = await Promise.all(
TOKENS.map(async symbol => {
const usdPrice = await cryptoCompareSource.getUsdPriceAsync(symbol);
return Promise.all(
USD_AMOUNTS.map(async usdAmount => {
const amount = usdAmount / usdPrice;
try {
const buyEdps = await edpsSource.getEdpsAsync('buy', symbol, amount);
const sellEdps = await edpsSource.getEdpsAsync('sell', symbol, amount);
return Object.keys(buyEdps).map(exchange => {
const slippage: Slippage = calculateSlippage(usdAmount, exchange, buyEdps, sellEdps);
return slippage;
});
} catch (e) {
logUtils.log(`Error getting data for symbol=${symbol}, amount=${amount}`);
logUtils.log(e);
return [new Slippage()];
}
}),
);
}),
);
const slippagesWithEmptyRecords = nestedSlippages
.reduce((acc, val) => acc.concat(val))
.reduce((acc, val) => acc.concat(val));
const slippages = slippagesWithEmptyRecords.filter(slippage => slippage.observedTimestamp);
const SlippageRepository = connection.getRepository(Slippage);
logUtils.log(`Saving ${slippages.length} records to database`);
await SlippageRepository.save(slippages, { chunk: Math.ceil(slippages.length / BATCH_SAVE_SIZE) });
logUtils.log('Done');
process.exit(0);
})().catch(handleError);

View File

@ -0,0 +1,36 @@
import { BigNumber } from '@0x/utils';
import { EdpsWrapper } from '../../data_sources/dex_prices';
import { Slippage } from '../../entities';
/**
* Calculates slippage and returns Slippage entity.
*
* @param usdAmount Amount to buy/sell in USD.
* @param exchange Exchange where we are calculating slippage.
* @param buyEdps Ethereum DEX price service object for buy side.
* @param sellEdps Ethereum DEX price service object for sell side.
*
*/
export function calculateSlippage(
usdAmount: number,
exchange: string,
buyEdps: EdpsWrapper,
sellEdps: EdpsWrapper,
): Slippage {
const b = buyEdps[exchange];
const s = sellEdps[exchange];
const slippage = new Slippage();
if (b && s && b.avgPrice && s.avgPrice) {
slippage.observedTimestamp = b.timestamp;
slippage.symbol = b.tokenSymbol;
slippage.exchange = exchange;
slippage.usdAmount = new BigNumber(usdAmount);
slippage.tokenAmount = new BigNumber(Number(b.tokenAmount)); // API returns a string
slippage.avgPriceInEthBuy = new BigNumber(b.avgPrice);
slippage.avgPriceInEthSell = new BigNumber(s.avgPrice);
slippage.slippage = new BigNumber((b.avgPrice - s.avgPrice) / b.avgPrice);
}
return slippage;
}

View File

@ -1,4 +1,4 @@
import { BigNumber } from '@0x/utils'; import { BigNumber, fetchAsync } from '@0x/utils';
export * from './transformers'; export * from './transformers';
export * from './constants'; export * from './constants';
@ -51,3 +51,16 @@ export function handleError(e: any): void {
} }
process.exit(1); process.exit(1);
} }
/**
* Does fetchAsync(), and checks the status code, throwing if it doesn't indicate success.
*/
export async function fetchSuccessfullyOrThrowAsync(url: string): Promise<any> {
const response = await fetchAsync(url);
if (!response.ok) {
throw new Error(
`Failed to fetch URL ${url}. Unsuccessful HTTP status code (${response.status}): ${response.statusText}`,
);
}
return response.json();
}

View File

@ -0,0 +1,76 @@
import { BigNumber } from '@0x/utils';
import { RadarOrderState, RadarOrderType } from '@radarrelay/types';
import * as chai from 'chai';
import 'mocha';
import { RadarSource } from '../../../src/data_sources/radar';
import { chaiSetup } from '../../utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const rawResponse = {
orderHash: '0x60bc235f7887a50801c8fc1fc18fb0625ac5f3962cdc1bd59567a6929db8b2ec',
type: 'BID',
state: 'OPEN',
baseTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
quoteTokenAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
remainingBaseTokenAmount: '9.079731811797989766',
remainingQuoteTokenAmount: '1099.999999999999999889',
price: '121.14895272244560081697',
createdDate: '2019-02-13 21:35:53',
signedOrder: {
exchangeAddress: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
senderAddress: '0x0000000000000000000000000000000000000000',
makerAddress: '0x56178a0d5f301baf6cf3e1cd53d9863437345bf9',
takerAddress: '0x0000000000000000000000000000000000000000',
makerAssetData: '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359',
takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
makerAssetAmount: '1099999999999999999889',
takerAssetAmount: '9079731811797989766',
makerFee: '0',
takerFee: '0',
expirationTimeSeconds: '1550094353',
signature:
'0x1ce161d02ad63fe7308e9cd5e97583a8873331d1b72d90e9f3863d9fcba2518cb91ab2fe7de94e4afb39742acdc820abbff2dc0622c8d3865917fade62f16322ae03',
salt: '1550093753237',
},
};
const parsedResponse = {
orderHash: '0x60bc235f7887a50801c8fc1fc18fb0625ac5f3962cdc1bd59567a6929db8b2ec',
type: 'BID' as RadarOrderType,
state: 'OPEN' as RadarOrderState,
baseTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
quoteTokenAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
remainingBaseTokenAmount: new BigNumber('9.079731811797989766'),
remainingQuoteTokenAmount: new BigNumber('1099.999999999999999889'),
price: new BigNumber('121.14895272244560081697'),
createdDate: '2019-02-13 21:35:53',
signedOrder: {
exchangeAddress: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
senderAddress: '0x0000000000000000000000000000000000000000',
makerAddress: '0x56178a0d5f301baf6cf3e1cd53d9863437345bf9',
takerAddress: '0x0000000000000000000000000000000000000000',
makerAssetData: '0xf47261b000000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359',
takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
makerAssetAmount: new BigNumber('1099999999999999999889'),
takerAssetAmount: new BigNumber('9079731811797989766'),
makerFee: new BigNumber('0'),
takerFee: new BigNumber('0'),
expirationTimeSeconds: new BigNumber('1550094353'),
signature:
'0x1ce161d02ad63fe7308e9cd5e97583a8873331d1b72d90e9f3863d9fcba2518cb91ab2fe7de94e4afb39742acdc820abbff2dc0622c8d3865917fade62f16322ae03',
salt: new BigNumber('1550093753237'),
},
};
describe('RadarSource', () => {
describe('parseRadarOrderResponse', () => {
it('Correctly parses a Radar orderbook response to a RadarBook', () => {
expect(RadarSource.parseRadarOrderResponse(rawResponse)).deep.equal(parsedResponse);
});
});
});

View File

@ -0,0 +1,48 @@
import { BigNumber } from '@0x/utils';
import 'mocha';
import 'reflect-metadata';
import { NonfungibleDotComTrade } from '../../src/entities';
import { createDbConnectionOnceAsync } from '../db_setup';
import { chaiSetup } from '../utils/chai_setup';
import { testSaveAndFindEntityAsync } from './util';
chaiSetup.configure();
const baseTrade: NonfungibleDotComTrade = {
assetDescriptor: 'Kitty #1002',
assetId: '1002',
blockNumber: 4608542,
blockTimestamp: 1543544083704,
buyerAddress: '0x316c55d1895a085c4b39a98ecb563f509301aaf7',
logIndex: 28,
marketAddress: '0xb1690C08E213a35Ed9bAb7B318DE14420FB57d8C',
meta: {
cattribute_body: 'munchkin',
cattribute_coloreyes: 'mintgreen',
cattribute_colorprimary: 'orangesoda',
cattribute_colorsecondary: 'coffee',
cattribute_colortertiary: 'kittencream',
cattribute_eyes: 'thicccbrowz',
cattribute_mouth: 'soserious',
cattribute_pattern: 'totesbasic',
generation: '0',
is_exclusive: false,
is_fancy: false,
},
sellerAddress: '0xba52c75764d6f594735dc735be7f1830cdf58ddf',
totalPrice: new BigNumber('9751388888888889'),
transactionHash: '0x468168419be7e442d5ff32d264fab24087b744bc2e37fdbac7024e1e74f4c6c8',
usdPrice: new BigNumber('3.71957'),
publisher: 'cryptokitties',
};
// tslint:disable:custom-no-magic-numbers
describe('NonfungibleDotComTrade entity', () => {
it('save/find', async () => {
const connection = await createDbConnectionOnceAsync();
const tradesRepository = connection.getRepository(NonfungibleDotComTrade);
await testSaveAndFindEntityAsync(tradesRepository, baseTrade);
});
});

View File

@ -0,0 +1,34 @@
import { BigNumber } from '@0x/utils';
import 'mocha';
import 'reflect-metadata';
import { Slippage } from '../../src/entities';
import { createDbConnectionOnceAsync } from '../db_setup';
import { chaiSetup } from '../utils/chai_setup';
import { testSaveAndFindEntityAsync } from './util';
chaiSetup.configure();
const slippage = {
observedTimestamp: 1549587475793,
symbol: 'ZRX',
exchange: 'Radar Relay',
usdAmount: new BigNumber(10),
tokenAmount: new BigNumber(25),
avgPriceInEthBuy: new BigNumber(0.0022),
avgPriceInEthSell: new BigNumber(0.002),
slippage: new BigNumber(0.01),
};
// tslint:disable:custom-no-magic-numbers
describe('Slippage entity', () => {
it('save/find', async () => {
const connection = await createDbConnectionOnceAsync();
const slippages = [slippage];
const slippageRepository = connection.getRepository(Slippage);
for (const slippageRecord of slippages) {
await testSaveAndFindEntityAsync(slippageRepository, slippageRecord);
}
});
});

View File

@ -20,6 +20,7 @@ const tokenOrderbookSnapshot: TokenOrderbookSnapshot = {
quoteAssetSymbol: 'ABC', quoteAssetSymbol: 'ABC',
quoteAssetAddress: '0x00923b9a074762b93650716333b3e1473a15048e', quoteAssetAddress: '0x00923b9a074762b93650716333b3e1473a15048e',
quoteVolume: new BigNumber(12.3234234), quoteVolume: new BigNumber(12.3234234),
makerAddress: 'unknown',
}; };
describe('TokenOrderbookSnapshot entity', () => { describe('TokenOrderbookSnapshot entity', () => {

View File

@ -31,7 +31,6 @@ describe('ddex_orders', () => {
}; };
const observedTimestamp: number = Date.now(); const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Bid; const orderType: OrderType = OrderType.Bid;
const source: string = 'ddex';
const expected = new TokenOrder(); const expected = new TokenOrder();
expected.source = 'ddex'; expected.source = 'ddex';
@ -44,8 +43,8 @@ describe('ddex_orders', () => {
expected.baseAssetSymbol = 'DEF'; expected.baseAssetSymbol = 'DEF';
expected.baseAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81'; expected.baseAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81';
expected.baseVolume = new BigNumber(10); expected.baseVolume = new BigNumber(10);
expected.makerAddress = 'unknown';
const actual = parseDdexOrder(ddexMarket, observedTimestamp, orderType, source, ddexOrder); const actual = parseDdexOrder(ddexMarket, observedTimestamp, orderType, ddexOrder);
expect(actual).deep.equal(expected); expect(actual).deep.equal(expected);
}); });
}); });

View File

@ -32,7 +32,6 @@ describe('idex_orders', () => {
}; };
const observedTimestamp: number = Date.now(); const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Bid; const orderType: OrderType = OrderType.Bid;
const source: string = 'idex';
const expected = new TokenOrder(); const expected = new TokenOrder();
expected.source = 'idex'; expected.source = 'idex';
@ -45,8 +44,8 @@ describe('idex_orders', () => {
expected.quoteAssetSymbol = 'DEF'; expected.quoteAssetSymbol = 'DEF';
expected.quoteAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81'; expected.quoteAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81';
expected.quoteVolume = new BigNumber(5); expected.quoteVolume = new BigNumber(5);
expected.makerAddress = 'unknown';
const actual = parseIdexOrder(idexOrderParam, observedTimestamp, orderType, source, idexOrder); const actual = parseIdexOrder(idexOrderParam, observedTimestamp, orderType, idexOrder);
expect(actual).deep.equal(expected); expect(actual).deep.equal(expected);
}); });
it('correctly converts ask type idexOrder to TokenOrder entity', () => { it('correctly converts ask type idexOrder to TokenOrder entity', () => {
@ -66,7 +65,6 @@ describe('idex_orders', () => {
}; };
const observedTimestamp: number = Date.now(); const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Ask; const orderType: OrderType = OrderType.Ask;
const source: string = 'idex';
const expected = new TokenOrder(); const expected = new TokenOrder();
expected.source = 'idex'; expected.source = 'idex';
@ -79,8 +77,8 @@ describe('idex_orders', () => {
expected.quoteAssetSymbol = 'DEF'; expected.quoteAssetSymbol = 'DEF';
expected.quoteAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81'; expected.quoteAssetAddress = '0xb45df06e38540a675fdb5b598abf2c0dbe9d6b81';
expected.quoteVolume = new BigNumber(5); expected.quoteVolume = new BigNumber(5);
expected.makerAddress = 'unknown';
const actual = parseIdexOrder(idexOrderParam, observedTimestamp, orderType, source, idexOrder); const actual = parseIdexOrder(idexOrderParam, observedTimestamp, orderType, idexOrder);
expect(actual).deep.equal(expected); expect(actual).deep.equal(expected);
}); });
}); });

View File

@ -0,0 +1,86 @@
// tslint:disable:custom-no-magic-numbers
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { NonfungibleDotComTradeResponse } from '../../../src/data_sources/nonfungible_dot_com';
import { NonfungibleDotComTrade } from '../../../src/entities';
import { _parseNonFungibleDotComTrade } from '../../../src/parsers/nonfungible_dot_com';
import { chaiSetup } from '../../utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const input: NonfungibleDotComTradeResponse = {
_id: '5b4cd04244abdb5ac3a8063f',
assetDescriptor: 'Kitty #1002',
assetId: '1002',
blockNumber: 4608542,
blockTimestamp: '2017-11-23T18:50:19.000Z',
buyer: '0x316c55d1895a085c4b39a98ecb563f509301aaf7',
logIndex: 28,
nftAddress: '0xb1690C08E213a35Ed9bAb7B318DE14420FB57d8C',
marketAddress: '0xb1690C08E213a35Ed9bAb7B318DE14420FB57d8C',
tokenTicker: 'eth',
meta: {
cattribute_body: 'munchkin',
cattribute_coloreyes: 'mintgreen',
cattribute_colorprimary: 'orangesoda',
cattribute_colorsecondary: 'coffee',
cattribute_colortertiary: 'kittencream',
cattribute_eyes: 'thicccbrowz',
cattribute_mouth: 'soserious',
cattribute_pattern: 'totesbasic',
generation: '0',
is_exclusive: false,
is_fancy: false,
},
seller: '0xba52c75764d6f594735dc735be7f1830cdf58ddf',
totalDecimalPrice: 0.00975138888888889,
totalPrice: '9751388888888889',
transactionHash: '0x468168419be7e442d5ff32d264fab24087b744bc2e37fdbac7024e1e74f4c6c8',
usdPrice: 3.71957,
currencyTransfer: {},
image: '',
composedOf: '',
asset_link: '',
seller_address_link: '',
buyer_address_link: '',
};
const expected: NonfungibleDotComTrade = {
assetDescriptor: 'Kitty #1002',
assetId: '1002',
blockNumber: 4608542,
blockTimestamp: 1511463019000,
buyerAddress: '0x316c55d1895a085c4b39a98ecb563f509301aaf7',
logIndex: 28,
marketAddress: '0xb1690C08E213a35Ed9bAb7B318DE14420FB57d8C',
meta: {
cattribute_body: 'munchkin',
cattribute_coloreyes: 'mintgreen',
cattribute_colorprimary: 'orangesoda',
cattribute_colorsecondary: 'coffee',
cattribute_colortertiary: 'kittencream',
cattribute_eyes: 'thicccbrowz',
cattribute_mouth: 'soserious',
cattribute_pattern: 'totesbasic',
generation: '0',
is_exclusive: false,
is_fancy: false,
},
sellerAddress: '0xba52c75764d6f594735dc735be7f1830cdf58ddf',
totalPrice: new BigNumber('9751388888888889'),
transactionHash: '0x468168419be7e442d5ff32d264fab24087b744bc2e37fdbac7024e1e74f4c6c8',
usdPrice: new BigNumber('3.71957'),
publisher: 'cryptokitties',
};
describe('nonfungible.com', () => {
describe('_parseNonFungibleDotComTrade', () => {
it(`converts NonfungibleDotComTradeResponse to NonfungibleDotComTrade entity`, () => {
const actual = _parseNonFungibleDotComTrade(expected.publisher, input);
expect(actual).deep.equal(expected);
});
});
});

View File

@ -28,7 +28,6 @@ describe('oasis_orders', () => {
}; };
const observedTimestamp: number = Date.now(); const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Bid; const orderType: OrderType = OrderType.Bid;
const source: string = 'oasis';
const expected = new TokenOrder(); const expected = new TokenOrder();
expected.source = 'oasis'; expected.source = 'oasis';
@ -41,8 +40,8 @@ describe('oasis_orders', () => {
expected.quoteAssetSymbol = 'ABC'; expected.quoteAssetSymbol = 'ABC';
expected.quoteAssetAddress = null; expected.quoteAssetAddress = null;
expected.quoteVolume = new BigNumber(5); expected.quoteVolume = new BigNumber(5);
expected.makerAddress = 'unknown';
const actual = parseOasisOrder(oasisMarket, observedTimestamp, orderType, source, oasisOrder); const actual = parseOasisOrder(oasisMarket, observedTimestamp, orderType, oasisOrder);
expect(actual).deep.equal(expected); expect(actual).deep.equal(expected);
}); });
}); });

View File

@ -33,7 +33,6 @@ describe('paradex_orders', () => {
}; };
const observedTimestamp: number = Date.now(); const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Bid; const orderType: OrderType = OrderType.Bid;
const source: string = 'paradex';
const expected = new TokenOrder(); const expected = new TokenOrder();
expected.source = 'paradex'; expected.source = 'paradex';
@ -46,8 +45,8 @@ describe('paradex_orders', () => {
expected.quoteAssetSymbol = 'ABC'; expected.quoteAssetSymbol = 'ABC';
expected.quoteAssetAddress = '0x0000000000000000000000000000000000000000'; expected.quoteAssetAddress = '0x0000000000000000000000000000000000000000';
expected.quoteVolume = new BigNumber(412 * 0.1245); expected.quoteVolume = new BigNumber(412 * 0.1245);
expected.makerAddress = 'unknown';
const actual = parseParadexOrder(paradexMarket, observedTimestamp, orderType, source, paradexOrder); const actual = parseParadexOrder(paradexMarket, observedTimestamp, orderType, paradexOrder);
expect(actual).deep.equal(expected); expect(actual).deep.equal(expected);
}); });
}); });

View File

@ -0,0 +1,55 @@
import { BigNumber } from '@0x/utils';
import { RadarMarket } from '@radarrelay/types';
import * as chai from 'chai';
import 'mocha';
import { TokenOrderbookSnapshot as TokenOrder } from '../../../src/entities';
import { AggregateOrdersByMaker, parseRadarOrder } from '../../../src/parsers/radar_orders';
import { OrderType } from '../../../src/types';
import { chaiSetup } from '../../utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
describe('radar_orders', () => {
describe('parseRadarOrder', () => {
it('converts radarOrder to TokenOrder entity', () => {
const radarOrder: AggregateOrdersByMaker = {
makerAddress: '0x6eC92694ea172ebC430C30fa31De87620967A082',
price: '0.01',
amount: new BigNumber(10000000000),
};
const radarMarket = ({
id: 'WETH-DAI',
displayName: 'WETH/DAI',
baseTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
quoteTokenAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
baseTokenDecimals: 18,
quoteTokenDecimals: 18,
quoteIncrement: 8,
minOrderSize: new BigNumber('0.00692535'),
maxOrderSize: new BigNumber('1000000000'),
score: 99.66,
// Radar types are defined using an older version of BigNumber, so need to be force cast.
} as any) as RadarMarket;
const observedTimestamp: number = Date.now();
const orderType: OrderType = OrderType.Bid;
const expected = new TokenOrder();
expected.source = 'radar';
expected.observedTimestamp = observedTimestamp;
expected.orderType = OrderType.Bid;
expected.price = new BigNumber(0.01);
expected.quoteAssetSymbol = 'DAI';
expected.quoteAssetAddress = '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359';
expected.quoteVolume = new BigNumber(100000000);
expected.baseAssetSymbol = 'WETH';
expected.baseAssetAddress = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
expected.baseVolume = new BigNumber(10000000000);
expected.makerAddress = '0x6eC92694ea172ebC430C30fa31De87620967A082';
const actual = parseRadarOrder(radarMarket, observedTimestamp, orderType, radarOrder);
expect(actual).deep.equal(expected);
});
});
});

View File

@ -0,0 +1,61 @@
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { EdpsWrapper } from '../../../src/data_sources/dex_prices';
import { Slippage } from '../../../src/entities';
import { calculateSlippage } from '../../../src/transformers/slippage';
import { chaiSetup } from '../../utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
// tslint:disable:custom-no-magic-numbers
describe('slippage', () => {
describe('calculateSlippage', () => {
it('calculates slippage correctly', () => {
const exchange = 'Radar Relay';
const ts = 1549961441473;
const symbol = 'DAI';
const amount = 1000;
const buyPrice = 10;
const sellPrice = 9;
const expectedSlippage = 0.1;
const buyEdps: EdpsWrapper = {};
buyEdps[exchange] = {
exchangeName: exchange,
totalPrice: buyPrice,
tokenAmount: amount,
tokenSymbol: symbol,
avgPrice: buyPrice / amount,
timestamp: ts,
error: '',
};
const sellEdps: EdpsWrapper = {};
sellEdps[exchange] = {
exchangeName: exchange,
totalPrice: sellPrice,
tokenAmount: amount,
tokenSymbol: symbol,
avgPrice: sellPrice / amount,
timestamp: ts,
error: '',
};
const expected = new Slippage();
expected.observedTimestamp = ts;
expected.symbol = symbol;
expected.exchange = exchange;
expected.usdAmount = new BigNumber(amount);
expected.tokenAmount = new BigNumber(amount); // API returns a string
expected.avgPriceInEthBuy = new BigNumber(buyPrice / amount);
expected.avgPriceInEthSell = new BigNumber(sellPrice / amount);
expected.slippage = new BigNumber(0.1);
const actual = calculateSlippage(amount, exchange, buyEdps, sellEdps);
const actualSlippage: BigNumber = actual.slippage ? actual.slippage : new BigNumber(0);
expect(actualSlippage.toNumber()).to.be.closeTo(expectedSlippage, 0.0001);
});
});
});

View File

@ -68,15 +68,15 @@ export class AddressTable extends React.Component<AddressTableProps, AddressTabl
} }
} }
const Wrapper = styled.div<{ marginBottom?: string }>` const Wrapper = styled.div<{ marginBottom?: string }>`
background - color: #fff; background-color: #fff;
border -radius;: 4;px; border-radius: 4px;
Margin-bottom;: $;{props => props.marginBottom || '25px';} margin-bottom: ${props => props.marginBottom || '25px'};
Padding: 10;px; 30;px; padding: 10px 30px;
Height: 230;px; height: 230px;
Overflow - y;: auto; overflow-y: auto;
`; `;
const Table = styled.table`; const Table = styled.table`
border-collapse;: collapse; border-collapse: collapse;
width: 100%; width: 100%;
`; `;

View File

@ -220,20 +220,14 @@ export class ConnectForm extends React.Component<Props, State> {
} }
public async getZrxBalanceAsync(owner: string): Promise<BigNumber> { public async getZrxBalanceAsync(owner: string): Promise<BigNumber> {
utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.');
const injectedProvider = await this._getInjectedProviderIfExistsAsync(); const contractAddresses = getContractAddressesForNetworkOrThrow(this.networkId);
const tokenAddress: string = contractAddresses.zrxToken;
if (!_.isUndefined(injectedProvider)) { try {
const contractAddresses = getContractAddressesForNetworkOrThrow(this.networkId); const amount = await this._contractWrappers.erc20Token.getBalanceAsync(tokenAddress, owner);
const tokenAddress: string = contractAddresses.zrxToken; return amount;
try { } catch (error) {
const amount = await this._contractWrappers.erc20Token.getBalanceAsync(tokenAddress, owner); return ZERO;
return amount;
} catch (error) {
return ZERO;
}
} }
return ZERO;
} }
private async _onConnectWalletClickAsync(): Promise<boolean> { private async _onConnectWalletClickAsync(): Promise<boolean> {
const shouldUseLedgerProvider = false; const shouldUseLedgerProvider = false;

View File

@ -0,0 +1,48 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { colors } from 'ts/style/colors';
interface WrapperProps {
isVisible: boolean;
}
interface LedgerSignNoteProps extends WrapperProps {
text: string;
}
export const LedgerSignNote: React.StatelessComponent<LedgerSignNoteProps> = ({ text, isVisible }) => {
return (
<Wrapper isVisible={isVisible}>
<Text>{text}</Text>
</Wrapper>
);
};
LedgerSignNote.defaultProps = {
isVisible: false,
};
const Wrapper = styled.div<WrapperProps>`
background-color: #7a7a7a;
display: flex;
align-items: center;
padding: 28px 30px;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
justify-content: center;
opacity: ${props => (props.isVisible ? 1 : 0)};
visibility: ${props => (props.isVisible ? 'visible' : 'hidden')};
`;
const Text = styled.p`
color: ${colors.white};
font-size: 1rem;
line-height: 1;
font-weight: 400;
`;

View File

@ -120,7 +120,7 @@ export class ModalVote extends React.Component<Props> {
<Confirmation isSuccessful={isSuccessful}> <Confirmation isSuccessful={isSuccessful}>
<Icon name="zeip-23" size="large" margin={[0, 0, 'default', 0]} /> <Icon name="zeip-23" size="large" margin={[0, 0, 'default', 0]} />
<Heading color={colors.textDarkPrimary} size={34} asElement="h2"> <Heading color={colors.textDarkPrimary} size={34} asElement="h2">
Vote Recieved! Vote Received!
</Heading> </Heading>
<Paragraph isMuted={true} color={colors.textDarkPrimary}> <Paragraph isMuted={true} color={colors.textDarkPrimary}>
Your vote will help to decide the future of the protocol. You will be receiving a custom Your vote will help to decide the future of the protocol. You will be receiving a custom

View File

@ -1,3 +1,4 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@ -5,7 +6,7 @@ import styled from 'styled-components';
interface VoteBarProps { interface VoteBarProps {
label: string; label: string;
color: string; color: string;
percentage: string; percentage: BigNumber;
marginBottom?: string; marginBottom?: string;
} }
@ -14,16 +15,27 @@ interface VoteColumnProps {
width: string; width: string;
} }
export const VoteBar: React.StatelessComponent<VoteBarProps> = ({ percentage, color, label, marginBottom }) => { const buildVotePercentageLabel = (percentage: BigNumber): string => {
const percentageLabel = `${percentage}%`; let percentageLabel = `${percentage.toFixed(0)}%`;
// When voting is entirely dominated it can result in showing 100% and 0%
// In this case we replace with an indication that there are some votes for
// the minority
if (percentage.isGreaterThan(99) && percentage.isLessThan(100)) {
percentageLabel = `> 99%`;
} else if (percentage.isGreaterThan(0) && percentage.isLessThan(1)) {
percentageLabel = `< 1%`;
}
return percentageLabel;
};
export const VoteBar: React.StatelessComponent<VoteBarProps> = ({ percentage, color, label, marginBottom }) => {
// TODO convert this to use a Container component // TODO convert this to use a Container component
return ( return (
<Wrapper marginBottom={marginBottom}> <Wrapper marginBottom={marginBottom}>
<VoteColumnPrefix>{label}</VoteColumnPrefix> <VoteColumnPrefix>{label}</VoteColumnPrefix>
<div style={{ display: 'flex', flex: 1, alignItems: 'center' }}> <div style={{ display: 'flex', flex: 1, alignItems: 'center' }}>
<VoteColumn color={color} width={percentage} /> <VoteColumn color={color} width={percentage.toFixed(0)} />
<VoteColumnLabel>{percentageLabel}</VoteColumnLabel> <VoteColumnLabel>{buildVotePercentageLabel(percentage)}</VoteColumnLabel>
</div> </div>
</Wrapper> </Wrapper>
); );
@ -33,7 +45,7 @@ const VoteColumn = styled.div<VoteColumnProps>`
background-color: ${props => props.color}; background-color: ${props => props.color};
width: calc(${props => props.width}% - 45px); width: calc(${props => props.width}% - 45px);
height: 13px; height: 13px;
margin-right: 10px; margin-right: 15px;
min-width: 10px; min-width: 10px;
`; `;
@ -55,4 +67,5 @@ const VoteColumnLabel = styled.span`
font-size: 1rem; font-size: 1rem;
line-height: 1; line-height: 1;
font-weight: 300; font-weight: 300;
min-width: 60px;
`; `;

View File

@ -14,6 +14,7 @@ import styled from 'styled-components';
import { Button } from 'ts/components/button'; import { Button } from 'ts/components/button';
import { Input } from 'ts/components/modals/input'; import { Input } from 'ts/components/modals/input';
import { Heading, Paragraph } from 'ts/components/text'; import { Heading, Paragraph } from 'ts/components/text';
import { LedgerSignNote } from 'ts/pages/governance/ledger_sign_note';
import { PreferenceSelecter } from 'ts/pages/governance/preference_selecter'; import { PreferenceSelecter } from 'ts/pages/governance/preference_selecter';
import { colors } from 'ts/style/colors'; import { colors } from 'ts/style/colors';
import { InjectedProvider } from 'ts/types'; import { InjectedProvider } from 'ts/types';
@ -50,6 +51,7 @@ interface State {
isWalletConnected: boolean; isWalletConnected: boolean;
isSubmitting: boolean; isSubmitting: boolean;
isSuccessful: boolean; isSuccessful: boolean;
isAwaitingLedgerSignature: boolean;
isVoted: boolean; isVoted: boolean;
selectedAddress?: string; selectedAddress?: string;
votePreference?: string; votePreference?: string;
@ -95,6 +97,7 @@ export class VoteForm extends React.Component<Props> {
public networkId: number; public networkId: number;
public state: State = { public state: State = {
isWalletConnected: false, isWalletConnected: false,
isAwaitingLedgerSignature: false,
isSubmitting: false, isSubmitting: false,
isSuccessful: false, isSuccessful: false,
isVoted: false, isVoted: false,
@ -116,7 +119,7 @@ export class VoteForm extends React.Component<Props> {
super(props); super(props);
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const { votePreference, errors, isSuccessful } = this.state; const { votePreference, errors, isSuccessful, isAwaitingLedgerSignature } = this.state;
const { currentBalance, selectedAddress } = this.props; const { currentBalance, selectedAddress } = this.props;
const bigNumberFormat = { const bigNumberFormat = {
decimalSeparator: '.', decimalSeparator: '.',
@ -185,6 +188,10 @@ export class VoteForm extends React.Component<Props> {
Back Back
</Button> </Button>
<ButtonDisabled disabled={!votePreference}>Submit</ButtonDisabled> <ButtonDisabled disabled={!votePreference}>Submit</ButtonDisabled>
<LedgerSignNote
text={'Accept or reject signature on the Ledger'}
isVisible={isAwaitingLedgerSignature}
/>
</ButtonRow> </ButtonRow>
</Form> </Form>
); );
@ -193,15 +200,19 @@ export class VoteForm extends React.Component<Props> {
e.preventDefault(); e.preventDefault();
const { zeip, votePreference, comment } = this.state; const { zeip, votePreference, comment } = this.state;
const { currentBalance, selectedAddress } = this.props; const { currentBalance, selectedAddress, isLedger } = this.props;
const makerAddress = selectedAddress; const makerAddress = selectedAddress;
if (isLedger) {
this.setState({ isAwaitingLedgerSignature: true });
}
const domainType = [{ name: 'name', type: 'string' }]; const domainType = [{ name: 'name', type: 'string' }];
const voteType = [ const voteType = [
{ name: 'preference', type: 'string' }, { name: 'preference', type: 'string' },
{ name: 'zeip', type: 'uint256' }, { name: 'zeip', type: 'uint256' },
{ name: 'from', type: 'address' }, { name: 'from', type: 'address' },
]; ];
const domainData = { const domainData = {
name: '0x Protocol Governance', name: '0x Protocol Governance',
}; };
@ -224,8 +235,14 @@ export class VoteForm extends React.Component<Props> {
const voteHashHex = `0x${voteHashBuffer.toString('hex')}`; const voteHashHex = `0x${voteHashBuffer.toString('hex')}`;
try { try {
const signedVote = await this._signVoteAsync(makerAddress, typedData); const signedVote = await this._signVoteAsync(makerAddress, typedData);
// Store the signed Order // Store the signed vote
this.setState(prevState => ({ ...prevState, signedVote, voteHash: voteHashHex, isSuccessful: true })); this.setState(prevState => ({
...prevState,
signedVote,
voteHash: voteHashHex,
isSuccessful: true,
isAwaitingLedgerSignature: false,
}));
const voteDomain = utils.isProduction() ? `https://${configs.DOMAIN_VOTE}` : 'http://localhost:3000'; const voteDomain = utils.isProduction() ? `https://${configs.DOMAIN_VOTE}` : 'http://localhost:3000';
const voteEndpoint = `${voteDomain}/v1/vote`; const voteEndpoint = `${voteDomain}/v1/vote`;
@ -248,27 +265,27 @@ export class VoteForm extends React.Component<Props> {
} else { } else {
const responseBody = await response.json(); const responseBody = await response.json();
const errorMessage = !_.isUndefined(responseBody.reason) ? responseBody.reason : 'Unknown Error'; const errorMessage = !_.isUndefined(responseBody.reason) ? responseBody.reason : 'Unknown Error';
this.props.onError this._handleError(errorMessage);
? this.props.onError(errorMessage)
: this.setState({
errors: {
signError: errorMessage,
},
isSuccessful: false,
});
} }
} catch (err) { } catch (err) {
const errorMessage = err.message; this._handleError(err.message);
this.props.onError
? this.props.onError(errorMessage)
: this.setState({
errors: {
signError: errorMessage,
},
isSuccessful: false,
});
} }
}; };
private _handleError(errorMessage: string): void {
const { onError } = this.props;
onError
? onError(errorMessage)
: this.setState({
errors: {
signError: errorMessage,
},
isSuccessful: false,
isAwaitingLedgerSignature: false,
});
this.setState({
isAwaitingLedgerSignature: false,
});
}
private async _signVoteAsync(signerAddress: string, typedData: any): Promise<SignedVote> { private async _signVoteAsync(signerAddress: string, typedData: any): Promise<SignedVote> {
const { provider: providerEngine } = this.props; const { provider: providerEngine } = this.props;
let signatureHex; let signatureHex;
@ -340,6 +357,8 @@ const InputRow = styled.div`
`; `;
const ButtonRow = styled(InputRow)` const ButtonRow = styled(InputRow)`
position: relative;
@media (max-width: 768px) { @media (max-width: 768px) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -11,6 +11,7 @@ import { constants } from 'ts/utils/constants';
interface VoteStatsProps { interface VoteStatsProps {
tally?: TallyInterface; tally?: TallyInterface;
} }
export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) => { export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) => {
const bigNumberFormat = { const bigNumberFormat = {
decimalSeparator: '.', decimalSeparator: '.',
@ -43,8 +44,8 @@ export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) =
<Heading asElement="h3" size="small" marginBottom="10px"> <Heading asElement="h3" size="small" marginBottom="10px">
Results Results
</Heading> </Heading>
<VoteBar label="Yes" color={colors.brandLight} percentage={yesPercentage.toFixed(0)} /> <VoteBar label="Yes" color={colors.brandLight} percentage={yesPercentage} />
<VoteBar label="No" color={colors.brandDark} percentage={noPercentage.toFixed(0)} marginBottom="24px" /> <VoteBar label="No" color={colors.brandDark} percentage={noPercentage} marginBottom="24px" />
<Paragraph marginBottom="24px">({totalBalanceString} ZRX total vote)</Paragraph> <Paragraph marginBottom="24px">({totalBalanceString} ZRX total vote)</Paragraph>
</> </>
); );

View File

@ -606,6 +606,14 @@
dependencies: dependencies:
npm-registry-client "7.0.9" npm-registry-client "7.0.9"
"@0xproject/types@^1.0.1-rc.3":
version "1.1.4"
resolved "https://registry.npmjs.org/@0xproject/types/-/types-1.1.4.tgz#3ffd65e670d6a21dab19ee0ffd5fad0056291b8e"
dependencies:
"@types/node" "*"
bignumber.js "~4.1.0"
ethereum-types "^1.0.11"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
version "7.0.0" version "7.0.0"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@ -1280,6 +1288,13 @@
call-me-maybe "^1.0.1" call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0" glob-to-regexp "^0.3.0"
"@radarrelay/types@^1.2.1":
version "1.2.1"
resolved "https://registry.npmjs.org/@radarrelay/types/-/types-1.2.1.tgz#d16edb43d0735a31c887b9e79ff6e53924ac8cc5"
dependencies:
"@0xproject/types" "^1.0.1-rc.3"
bignumber.js "^5.0.0"
"@reach/component-component@^0.1.1": "@reach/component-component@^0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.1.tgz#62ea2ec290da32f5e3a9872fb51f9a3ae4370cc4" resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.1.tgz#62ea2ec290da32f5e3a9872fb51f9a3ae4370cc4"
@ -3439,6 +3454,10 @@ bignumber.js@7.2.1:
version "7.2.1" version "7.2.1"
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f"
bignumber.js@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-5.0.0.tgz#fbce63f09776b3000a83185badcde525daf34833"
"bignumber.js@git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2": "bignumber.js@git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2":
version "2.0.7" version "2.0.7"
resolved "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" resolved "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2"
@ -6411,6 +6430,13 @@ ethereum-common@^0.0.18:
version "0.0.18" version "0.0.18"
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
ethereum-types@^1.0.11:
version "1.1.6"
resolved "https://registry.npmjs.org/ethereum-types/-/ethereum-types-1.1.6.tgz#14437dbf401de361e70dac6358e5f2915ad3c35d"
dependencies:
"@types/node" "*"
bignumber.js "~4.1.0"
ethereumjs-abi@0.6.5: ethereumjs-abi@0.6.5:
version "0.6.5" version "0.6.5"
resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241"
@ -7919,7 +7945,7 @@ got@^6.7.1:
graceful-fs@4.1.15, graceful-fs@^3.0.0, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~1.2.0: graceful-fs@4.1.15, graceful-fs@^3.0.0, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~1.2.0:
version "4.1.15" version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
"graceful-readlink@>= 1.0.0": "graceful-readlink@>= 1.0.0":
version "1.0.1" version "1.0.1"
@ -13472,6 +13498,15 @@ react-dom@^16.3.2:
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.0" prop-types "^15.6.0"
react-dom@^16.4.2:
version "16.8.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.8.1.tgz#ec860f98853d09d39bafd3a6f1e12389d283dbb4"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.1"
react-dom@^16.5.2: react-dom@^16.5.2:
version "16.5.2" version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
@ -13775,6 +13810,15 @@ react@^16.3.2:
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.0" prop-types "^15.6.0"
react@^16.4.2:
version "16.8.1"
resolved "https://registry.npmjs.org/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.1"
react@^16.5.2: react@^16.5.2:
version "16.5.2" version "16.5.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
@ -14646,6 +14690,13 @@ schedule@^0.5.0:
dependencies: dependencies:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler@^0.13.1:
version "0.13.1"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.13.1.tgz#1a217df1bfaabaf4f1b92a9127d5d732d85a9591"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^0.4.4: schema-utils@^0.4.4:
version "0.4.7" version "0.4.7"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"