Integrate one-time dump and API for nonfungible.com (#1603)
* Add script for pulling NFT trade data from nonfungible.com * corrections for current state of API * change data model to match data source * change primary key * pull data from initial dump first, then API * pull all supported NFT's, not just cryptokitties * disable problematic data sources * rename function to satisfy linter * Rename table to nonfungible_dot_com_trades * rename parser module to nonfungible_dot_com from non_fungible_dot_com, for consistency * correct mistaken reference to Bloxy * rename NonfungibleDotComTrade to ...TradeResponse * `NftTrade` -> `NonfungibleDotComTrade` * rename files to match prior object renaming * use fetchAsync instead of axios * improve fetchAsync error message: include URL * avoid non-null contraints in API trades too, not just for trades from the one-time dump * disable mythereum publisher
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
220
packages/pipeline/src/data_sources/nonfungible_dot_com/index.ts
Normal file
220
packages/pipeline/src/data_sources/nonfungible_dot_com/index.ts
Normal 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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ 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 { SraOrder } from './sra_order';
|
export { SraOrder } from './sra_order';
|
||||||
|
|||||||
35
packages/pipeline/src/entities/nonfungible_dot_com_trade.ts
Normal file
35
packages/pipeline/src/entities/nonfungible_dot_com_trade.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ExchangeCancelEvent,
|
ExchangeCancelEvent,
|
||||||
ExchangeCancelUpToEvent,
|
ExchangeCancelUpToEvent,
|
||||||
ExchangeFillEvent,
|
ExchangeFillEvent,
|
||||||
|
NonfungibleDotComTrade,
|
||||||
OHLCVExternal,
|
OHLCVExternal,
|
||||||
Relayer,
|
Relayer,
|
||||||
SraOrder,
|
SraOrder,
|
||||||
@@ -33,6 +34,7 @@ const entities = [
|
|||||||
ExchangeCancelUpToEvent,
|
ExchangeCancelUpToEvent,
|
||||||
ExchangeFillEvent,
|
ExchangeFillEvent,
|
||||||
ERC20ApprovalEvent,
|
ERC20ApprovalEvent,
|
||||||
|
NonfungibleDotComTrade,
|
||||||
OHLCVExternal,
|
OHLCVExternal,
|
||||||
Relayer,
|
Relayer,
|
||||||
SraOrder,
|
SraOrder,
|
||||||
|
|||||||
42
packages/pipeline/src/parsers/nonfungible_dot_com/index.ts
Normal file
42
packages/pipeline/src/parsers/nonfungible_dot_com/index.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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.`);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
48
packages/pipeline/test/entities/nft_trades_test.ts
Normal file
48
packages/pipeline/test/entities/nft_trades_test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user