Add connect to monorepo
This commit is contained in:
parent
5bd8e172c9
commit
655b0636fa
1
packages/connect/README.md
Normal file
1
packages/connect/README.md
Normal file
@ -0,0 +1 @@
|
||||
This repository contains a Javascript library that makes it easy to interact with Relayers that conform to the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api)
|
68
packages/connect/package.json
Normal file
68
packages/connect/package.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@0xproject/connect",
|
||||
"version": "0.0.0",
|
||||
"description": "A javascript library for interacting with the standard relayer api",
|
||||
"keywords": [
|
||||
"0x-connect",
|
||||
"0xproject",
|
||||
"ethereum",
|
||||
"tokens",
|
||||
"exchange"
|
||||
],
|
||||
"main": "lib/src/index.js",
|
||||
"types": "lib/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "shx rm -rf _bundles lib test_temp",
|
||||
"copy_test_fixtures": "copyfiles -u 2 './test/fixtures/**/*.json' ./lib/test/fixtures",
|
||||
"lint": "tslint src/**/*.ts test/**/*.ts",
|
||||
"prepublishOnly": "run-p build",
|
||||
"run_mocha": "mocha lib/test/**/*_test.js",
|
||||
"test": "run-s clean build copy_test_fixtures run_mocha",
|
||||
"test:circleci": "yarn test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/0xProject/0x.js.git"
|
||||
},
|
||||
"author": "Brandon Millman",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/0xProject/0x.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/0xProject/0x.js/packages/connect/README.md",
|
||||
"dependencies": {
|
||||
"@0xproject/assert": "0.0.4",
|
||||
"@0xproject/json-schemas": "0.6.7",
|
||||
"0x.js": "~0.25.1",
|
||||
"bignumber.js": "~4.1.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"query-string": "^5.0.1",
|
||||
"websocket": "^1.0.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@0xproject/tslint-config": "0.1.0",
|
||||
"@types/fetch-mock": "^5.12.1",
|
||||
"@types/lodash": "^4.14.77",
|
||||
"@types/mocha": "^2.2.42",
|
||||
"@types/query-string": "^5.0.1",
|
||||
"@types/websocket": "^0.0.34",
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-as-promised-typescript-typings": "0.0.3",
|
||||
"chai-typescript-typings": "^0.0.1",
|
||||
"copyfiles": "^1.2.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"fetch-mock": "^5.13.1",
|
||||
"mocha": "^4.0.0",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"shx": "^0.2.2",
|
||||
"tslint": "5.8.0",
|
||||
"typescript": "~2.6.1",
|
||||
"web3-typescript-typings": "^0.7.1"
|
||||
}
|
||||
}
|
6
packages/connect/src/globals.d.ts
vendored
Normal file
6
packages/connect/src/globals.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module 'dirty-chai';
|
||||
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
171
packages/connect/src/http_client.ts
Normal file
171
packages/connect/src/http_client.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import 'isomorphic-fetch';
|
||||
import * as _ from 'lodash';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import * as queryString from 'query-string';
|
||||
import {assert} from '@0xproject/assert';
|
||||
import {schemas} from '@0xproject/json-schemas';
|
||||
import {SignedOrder} from '0x.js';
|
||||
import {
|
||||
Client,
|
||||
FeesRequest,
|
||||
FeesResponse,
|
||||
OrderbookRequest,
|
||||
OrderbookResponse,
|
||||
OrdersRequest,
|
||||
TokenPairsItem,
|
||||
TokenPairsRequest,
|
||||
} from './types';
|
||||
import {schemas as clientSchemas} from './schemas/schemas';
|
||||
import {typeConverters} from './utils/type_converters';
|
||||
|
||||
interface RequestOptions {
|
||||
params?: object;
|
||||
payload?: object;
|
||||
}
|
||||
|
||||
enum RequestType {
|
||||
Get = 'GET',
|
||||
Post = 'POST',
|
||||
}
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to interacting with a set of HTTP endpoints
|
||||
* that implement the standard relayer API v0
|
||||
*/
|
||||
export class HttpClient implements Client {
|
||||
private apiEndpointUrl: string;
|
||||
/**
|
||||
* Instantiates a new HttpClient instance
|
||||
* @param url The base url for making API calls
|
||||
* @return An instance of HttpClient
|
||||
*/
|
||||
constructor(url: string) {
|
||||
assert.isHttpUrl('url', url);
|
||||
this.apiEndpointUrl = url;
|
||||
}
|
||||
/**
|
||||
* Retrieve token pair info from the API
|
||||
* @param request A TokenPairsRequest instance describing specific token information
|
||||
* to retrieve
|
||||
* @return The resulting TokenPairsItems that match the request
|
||||
*/
|
||||
public async getTokenPairsAsync(request?: TokenPairsRequest): Promise<TokenPairsItem[]> {
|
||||
if (!_.isUndefined(request)) {
|
||||
assert.doesConformToSchema('request', request, clientSchemas.relayerTokenPairsRequestSchema);
|
||||
}
|
||||
const requestOpts = {
|
||||
params: request,
|
||||
};
|
||||
const tokenPairs = await this._requestAsync('/token_pairs', RequestType.Get, requestOpts);
|
||||
assert.doesConformToSchema(
|
||||
'tokenPairs', tokenPairs, schemas.relayerApiTokenPairsResponseSchema);
|
||||
_.each(tokenPairs, (tokenPair: object) => {
|
||||
typeConverters.convertStringsFieldsToBigNumbers(tokenPair, [
|
||||
'tokenA.minAmount',
|
||||
'tokenA.maxAmount',
|
||||
'tokenB.minAmount',
|
||||
'tokenB.maxAmount',
|
||||
]);
|
||||
});
|
||||
return tokenPairs;
|
||||
}
|
||||
/**
|
||||
* Retrieve orders from the API
|
||||
* @param request An OrdersRequest instance describing specific orders to retrieve
|
||||
* @return The resulting SignedOrders that match the request
|
||||
*/
|
||||
public async getOrdersAsync(request?: OrdersRequest): Promise<SignedOrder[]> {
|
||||
if (!_.isUndefined(request)) {
|
||||
assert.doesConformToSchema('request', request, clientSchemas.relayerOrdersRequestSchema);
|
||||
}
|
||||
const requestOpts = {
|
||||
params: request,
|
||||
};
|
||||
const orders = await this._requestAsync(`/orders`, RequestType.Get, requestOpts);
|
||||
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
|
||||
_.each(orders, (order: object) => typeConverters.convertOrderStringFieldsToBigNumber(order));
|
||||
return orders;
|
||||
}
|
||||
/**
|
||||
* Retrieve a specific order from the API
|
||||
* @param orderHash An orderHash generated from the desired order
|
||||
* @return The SignedOrder that matches the supplied orderHash
|
||||
*/
|
||||
public async getOrderAsync(orderHash: string): Promise<SignedOrder> {
|
||||
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
|
||||
const order = await this._requestAsync(`/order/${orderHash}`, RequestType.Get);
|
||||
assert.doesConformToSchema('order', order, schemas.signedOrderSchema);
|
||||
typeConverters.convertOrderStringFieldsToBigNumber(order);
|
||||
return order;
|
||||
}
|
||||
/**
|
||||
* Retrieve an orderbook from the API
|
||||
* @param request An OrderbookRequest instance describing the specific orderbook to retrieve
|
||||
* @return The resulting OrderbookResponse that matches the request
|
||||
*/
|
||||
public async getOrderbookAsync(request: OrderbookRequest): Promise<OrderbookResponse> {
|
||||
assert.doesConformToSchema('request', request, clientSchemas.relayerOrderBookRequestSchema);
|
||||
const requestOpts = {
|
||||
params: request,
|
||||
};
|
||||
const orderBook = await this._requestAsync('/orderbook', RequestType.Get, requestOpts);
|
||||
assert.doesConformToSchema('orderBook', orderBook, schemas.relayerApiOrderBookResponseSchema);
|
||||
typeConverters.convertOrderbookStringFieldsToBigNumber(orderBook);
|
||||
return orderBook;
|
||||
}
|
||||
/**
|
||||
* Retrieve fee information from the API
|
||||
* @param request A FeesRequest instance describing the specific fees to retrieve
|
||||
* @return The resulting FeesResponse that matches the request
|
||||
*/
|
||||
public async getFeesAsync(request: FeesRequest): Promise<FeesResponse> {
|
||||
assert.doesConformToSchema('request', request, schemas.relayerApiFeesPayloadSchema);
|
||||
typeConverters.convertBigNumberFieldsToStrings(request, [
|
||||
'makerTokenAmount',
|
||||
'takerTokenAmount',
|
||||
'expirationUnixTimestampSec',
|
||||
'salt',
|
||||
]);
|
||||
const requestOpts = {
|
||||
payload: request,
|
||||
};
|
||||
const fees = await this._requestAsync('/fees', RequestType.Post, requestOpts);
|
||||
assert.doesConformToSchema('fees', fees, schemas.relayerApiFeesResponseSchema);
|
||||
typeConverters.convertStringsFieldsToBigNumbers(fees, ['makerFee', 'takerFee']);
|
||||
return fees;
|
||||
}
|
||||
/**
|
||||
* Submit a signed order to the API
|
||||
* @param signedOrder A SignedOrder instance to submit
|
||||
*/
|
||||
public async submitOrderAsync(signedOrder: SignedOrder): Promise<void> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
const requestOpts = {
|
||||
payload: signedOrder,
|
||||
};
|
||||
await this._requestAsync('/order', RequestType.Post, requestOpts);
|
||||
}
|
||||
private async _requestAsync(path: string, requestType: RequestType, requestOptions?: RequestOptions): Promise<any> {
|
||||
const params = _.get(requestOptions, 'params');
|
||||
const payload = _.get(requestOptions, 'payload');
|
||||
let query = '';
|
||||
if (!_.isUndefined(params) && !_.isEmpty(params)) {
|
||||
const stringifiedParams = queryString.stringify(params);
|
||||
query = `?${stringifiedParams}`;
|
||||
}
|
||||
const url = `${this.apiEndpointUrl}/v0${path}${query}`;
|
||||
const headers = new Headers({
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: requestType,
|
||||
body: payload,
|
||||
headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
}
|
15
packages/connect/src/index.ts
Normal file
15
packages/connect/src/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export {HttpClient} from './http_client';
|
||||
export {WebSocketOrderbookChannel} from './ws_orderbook_channel';
|
||||
export {
|
||||
Client,
|
||||
FeesRequest,
|
||||
FeesResponse,
|
||||
OrderbookChannel,
|
||||
OrderbookChannelHandler,
|
||||
OrderbookChannelSubscriptionOpts,
|
||||
OrderbookRequest,
|
||||
OrderbookResponse,
|
||||
OrdersRequest,
|
||||
TokenPairsItem,
|
||||
TokenPairsRequest,
|
||||
} from './types';
|
@ -0,0 +1,8 @@
|
||||
export const relayerOrderBookRequestSchema = {
|
||||
id: '/RelayerOrderBookRequest',
|
||||
type: 'object',
|
||||
properties: {
|
||||
baseTokenAddress: {$ref: '/Address'},
|
||||
quoteTokenAddress: {$ref: '/Address'},
|
||||
},
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
export const relayerOrderBookRequestSchema = {
|
||||
id: '/RelayerOrderBookRequest',
|
||||
type: 'object',
|
||||
properties: {
|
||||
baseTokenAddress: {$ref: '/Address'},
|
||||
quoteTokenAddress: {$ref: '/Address'},
|
||||
},
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
export const relayerOrdersRequestSchema = {
|
||||
id: '/RelayerOrdersRequest',
|
||||
type: 'object',
|
||||
properties: {
|
||||
exchangeContractAddress: {$ref: '/Address'},
|
||||
tokenAddress: {$ref: '/Address'},
|
||||
makerTokenAddress: {$ref: '/Address'},
|
||||
takerTokenAddress: {$ref: '/Address'},
|
||||
tokenA: {$ref: '/Address'},
|
||||
tokenB: {$ref: '/Address'},
|
||||
maker: {$ref: '/Address'},
|
||||
taker: {$ref: '/Address'},
|
||||
trader: {$ref: '/Address'},
|
||||
feeRecipient: {$ref: '/Address'},
|
||||
},
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
export const relayerTokenPairsRequestSchema = {
|
||||
id: '/RelayerTokenPairsRequest',
|
||||
type: 'object',
|
||||
properties: {
|
||||
tokenA: {$ref: '/Address'},
|
||||
tokenB: {$ref: '/Address'},
|
||||
},
|
||||
};
|
15
packages/connect/src/schemas/schemas.ts
Normal file
15
packages/connect/src/schemas/schemas.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {
|
||||
relayerOrderBookRequestSchema,
|
||||
} from './relayer_orderbook_request_schema';
|
||||
import {
|
||||
relayerOrdersRequestSchema,
|
||||
} from './relayer_orders_request_schema';
|
||||
import {
|
||||
relayerTokenPairsRequestSchema,
|
||||
} from './relayer_token_pairs_request_schema';
|
||||
|
||||
export const schemas = {
|
||||
relayerOrderBookRequestSchema,
|
||||
relayerOrdersRequestSchema,
|
||||
relayerTokenPairsRequestSchema,
|
||||
};
|
120
packages/connect/src/types.ts
Normal file
120
packages/connect/src/types.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import {SignedOrder} from '0x.js';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
|
||||
export interface Client {
|
||||
getTokenPairsAsync: (request?: TokenPairsRequest) => Promise<TokenPairsItem[]>;
|
||||
getOrdersAsync: (request?: OrdersRequest) => Promise<SignedOrder[]>;
|
||||
getOrderAsync: (orderHash: string) => Promise<SignedOrder>;
|
||||
getOrderbookAsync: (request: OrderbookRequest) => Promise<OrderbookResponse>;
|
||||
getFeesAsync: (request: FeesRequest) => Promise<FeesResponse>;
|
||||
submitOrderAsync: (signedOrder: SignedOrder) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface OrderbookChannel {
|
||||
subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface OrderbookChannelHandler {
|
||||
onSnapshot: (channel: OrderbookChannel, snapshot: OrderbookResponse) => void;
|
||||
onUpdate: (channel: OrderbookChannel, order: SignedOrder) => void;
|
||||
onError: (channel: OrderbookChannel, err: Error) => void;
|
||||
onClose: (channel: OrderbookChannel) => void;
|
||||
}
|
||||
|
||||
export type OrderbookChannelMessage =
|
||||
SnapshotOrderbookChannelMessage |
|
||||
UpdateOrderbookChannelMessage |
|
||||
UnknownOrderbookChannelMessage;
|
||||
|
||||
export enum OrderbookChannelMessageTypes {
|
||||
Snapshot = 'snapshot',
|
||||
Update = 'update',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export interface SnapshotOrderbookChannelMessage {
|
||||
type: OrderbookChannelMessageTypes.Snapshot;
|
||||
payload: OrderbookResponse;
|
||||
}
|
||||
|
||||
export interface UpdateOrderbookChannelMessage {
|
||||
type: OrderbookChannelMessageTypes.Update;
|
||||
payload: SignedOrder;
|
||||
}
|
||||
|
||||
export interface UnknownOrderbookChannelMessage {
|
||||
type: OrderbookChannelMessageTypes.Unknown;
|
||||
payload: undefined;
|
||||
}
|
||||
|
||||
/*
|
||||
* baseTokenAddress: The address of token designated as the baseToken in the currency pair calculation of price
|
||||
* quoteTokenAddress: The address of token designated as the quoteToken in the currency pair calculation of price
|
||||
* snapshot: If true, a snapshot of the orderbook will be sent before the updates to the orderbook
|
||||
* limit: Maximum number of bids and asks in orderbook snapshot
|
||||
*/
|
||||
export interface OrderbookChannelSubscriptionOpts {
|
||||
baseTokenAddress: string;
|
||||
quoteTokenAddress: string;
|
||||
snapshot: boolean;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface TokenPairsRequest {
|
||||
tokenA?: string;
|
||||
tokenB?: string;
|
||||
}
|
||||
|
||||
export interface TokenPairsItem {
|
||||
tokenA: TokenTradeInfo;
|
||||
tokenB: TokenTradeInfo;
|
||||
}
|
||||
|
||||
export interface TokenTradeInfo {
|
||||
address: string;
|
||||
minAmount: BigNumber;
|
||||
maxAmount: BigNumber;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
export interface OrdersRequest {
|
||||
exchangeContractAddress?: string;
|
||||
tokenAddress?: string;
|
||||
makerTokenAddress?: string;
|
||||
takerTokenAddress?: string;
|
||||
tokenA?: string;
|
||||
tokenB?: string;
|
||||
maker?: string;
|
||||
taker?: string;
|
||||
trader?: string;
|
||||
feeRecipient?: string;
|
||||
}
|
||||
|
||||
export interface OrderbookRequest {
|
||||
baseTokenAddress: string;
|
||||
quoteTokenAddress: string;
|
||||
}
|
||||
|
||||
export interface OrderbookResponse {
|
||||
bids: SignedOrder[];
|
||||
asks: SignedOrder[];
|
||||
}
|
||||
|
||||
export interface FeesRequest {
|
||||
exchangeContractAddress: string;
|
||||
maker: string;
|
||||
taker: string;
|
||||
makerTokenAddress: string;
|
||||
takerTokenAddress: string;
|
||||
makerTokenAmount: BigNumber;
|
||||
takerTokenAmount: BigNumber;
|
||||
expirationUnixTimestampSec: BigNumber;
|
||||
salt: BigNumber;
|
||||
}
|
||||
|
||||
export interface FeesResponse {
|
||||
feeRecipient: string;
|
||||
makerFee: BigNumber;
|
||||
takerFee: BigNumber;
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import * as _ from 'lodash';
|
||||
import {SignedOrder} from '0x.js';
|
||||
import {assert} from '@0xproject/assert';
|
||||
import {schemas} from '@0xproject/json-schemas';
|
||||
import {
|
||||
OrderbookChannelMessage,
|
||||
OrderbookChannelMessageTypes,
|
||||
} from '../types';
|
||||
import {typeConverters} from './type_converters';
|
||||
|
||||
export const orderbookChannelMessageParsers = {
|
||||
parser(utf8Data: string): OrderbookChannelMessage {
|
||||
const messageObj = JSON.parse(utf8Data);
|
||||
const type: string = _.get(messageObj, 'type');
|
||||
assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`);
|
||||
switch (type) {
|
||||
case (OrderbookChannelMessageTypes.Snapshot): {
|
||||
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema);
|
||||
const orderbook = messageObj.payload;
|
||||
typeConverters.convertOrderbookStringFieldsToBigNumber(orderbook);
|
||||
return {
|
||||
type,
|
||||
payload: orderbook,
|
||||
};
|
||||
}
|
||||
case (OrderbookChannelMessageTypes.Update): {
|
||||
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelUpdateSchema);
|
||||
const order = messageObj.payload;
|
||||
typeConverters.convertOrderStringFieldsToBigNumber(order);
|
||||
return {
|
||||
type,
|
||||
payload: order,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
type: OrderbookChannelMessageTypes.Unknown,
|
||||
payload: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
31
packages/connect/src/utils/type_converters.ts
Normal file
31
packages/connect/src/utils/type_converters.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as _ from 'lodash';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
|
||||
// TODO: convert all of these to non-mutating, pure functions
|
||||
export const typeConverters = {
|
||||
convertOrderbookStringFieldsToBigNumber(orderbook: object): void {
|
||||
_.each(orderbook, (orders: object[]) => {
|
||||
_.each(orders, (order: object) => this.convertOrderStringFieldsToBigNumber(order));
|
||||
});
|
||||
},
|
||||
convertOrderStringFieldsToBigNumber(order: object): void {
|
||||
this.convertStringsFieldsToBigNumbers(order, [
|
||||
'makerTokenAmount',
|
||||
'takerTokenAmount',
|
||||
'makerFee',
|
||||
'takerFee',
|
||||
'expirationUnixTimestampSec',
|
||||
'salt',
|
||||
]);
|
||||
},
|
||||
convertBigNumberFieldsToStrings(obj: object, fields: string[]): void {
|
||||
_.each(fields, field => {
|
||||
_.update(obj, field, (value: BigNumber) => value.toString());
|
||||
});
|
||||
},
|
||||
convertStringsFieldsToBigNumbers(obj: object, fields: string[]): void {
|
||||
_.each(fields, field => {
|
||||
_.update(obj, field, (value: string) => new BigNumber(value));
|
||||
});
|
||||
},
|
||||
};
|
127
packages/connect/src/ws_orderbook_channel.ts
Normal file
127
packages/connect/src/ws_orderbook_channel.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as WebSocket from 'websocket';
|
||||
import {assert} from '@0xproject/assert';
|
||||
import {schemas} from '@0xproject/json-schemas';
|
||||
import {SignedOrder} from '0x.js';
|
||||
import {
|
||||
OrderbookChannel,
|
||||
OrderbookChannelHandler,
|
||||
OrderbookChannelMessageTypes,
|
||||
OrderbookChannelSubscriptionOpts,
|
||||
} from './types';
|
||||
import {orderbookChannelMessageParsers} from './utils/orderbook_channel_message_parsers';
|
||||
|
||||
enum ConnectionEventType {
|
||||
Close = 'close',
|
||||
Error = 'error',
|
||||
Message = 'message',
|
||||
}
|
||||
|
||||
enum ClientEventType {
|
||||
Connect = 'connect',
|
||||
ConnectFailed = 'connectFailed',
|
||||
}
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to interacting with a websocket endpoint
|
||||
* that implements the standard relayer API v0
|
||||
*/
|
||||
export class WebSocketOrderbookChannel implements OrderbookChannel {
|
||||
private apiEndpointUrl: string;
|
||||
private client: WebSocket.client;
|
||||
private connectionIfExists?: WebSocket.connection;
|
||||
/**
|
||||
* Instantiates a new WebSocketOrderbookChannel instance
|
||||
* @param url The base url for making API calls
|
||||
* @return An instance of WebSocketOrderbookChannel
|
||||
*/
|
||||
constructor(url: string) {
|
||||
assert.isUri('url', url);
|
||||
this.apiEndpointUrl = url;
|
||||
this.client = new WebSocket.client();
|
||||
}
|
||||
/**
|
||||
* Subscribe to orderbook snapshots and updates from the websocket
|
||||
* @param subscriptionOpts An OrderbookChannelSubscriptionOpts instance describing which
|
||||
* token pair to subscribe to
|
||||
* @param handler An OrderbookChannelHandler instance that responds to various
|
||||
* channel updates
|
||||
*/
|
||||
public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void {
|
||||
assert.doesConformToSchema(
|
||||
'subscriptionOpts', subscriptionOpts, schemas.relayerApiOrderbookChannelSubscribePayload);
|
||||
assert.isFunction('handler.onSnapshot', _.get(handler, 'onSnapshot'));
|
||||
assert.isFunction('handler.onUpdate', _.get(handler, 'onUpdate'));
|
||||
assert.isFunction('handler.onError', _.get(handler, 'onError'));
|
||||
assert.isFunction('handler.onClose', _.get(handler, 'onClose'));
|
||||
const subscribeMessage = {
|
||||
type: 'subscribe',
|
||||
channel: 'orderbook',
|
||||
payload: subscriptionOpts,
|
||||
};
|
||||
this._getConnection((error, connection) => {
|
||||
if (!_.isUndefined(error)) {
|
||||
handler.onError(this, error);
|
||||
} else if (!_.isUndefined(connection) && connection.connected) {
|
||||
connection.on(ConnectionEventType.Error, wsError => {
|
||||
handler.onError(this, wsError);
|
||||
});
|
||||
connection.on(ConnectionEventType.Close, () => {
|
||||
handler.onClose(this);
|
||||
});
|
||||
connection.on(ConnectionEventType.Message, message => {
|
||||
this._handleWebSocketMessage(message, handler);
|
||||
});
|
||||
connection.sendUTF(JSON.stringify(subscribeMessage));
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Close the websocket and stop receiving updates
|
||||
*/
|
||||
public close() {
|
||||
if (!_.isUndefined(this.connectionIfExists)) {
|
||||
this.connectionIfExists.close();
|
||||
}
|
||||
}
|
||||
private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void) {
|
||||
if (!_.isUndefined(this.connectionIfExists) && this.connectionIfExists.connected) {
|
||||
callback(undefined, this.connectionIfExists);
|
||||
} else {
|
||||
this.client.on(ClientEventType.Connect, connection => {
|
||||
this.connectionIfExists = connection;
|
||||
callback(undefined, this.connectionIfExists);
|
||||
});
|
||||
this.client.on(ClientEventType.ConnectFailed, error => {
|
||||
callback(error, undefined);
|
||||
});
|
||||
this.client.connect(this.apiEndpointUrl);
|
||||
}
|
||||
}
|
||||
private _handleWebSocketMessage(message: WebSocket.IMessage, handler: OrderbookChannelHandler): void {
|
||||
if (!_.isUndefined(message.utf8Data)) {
|
||||
try {
|
||||
const utf8Data = message.utf8Data;
|
||||
const parserResult = orderbookChannelMessageParsers.parser(utf8Data);
|
||||
const type = parserResult.type;
|
||||
switch (parserResult.type) {
|
||||
case (OrderbookChannelMessageTypes.Snapshot): {
|
||||
handler.onSnapshot(this, parserResult.payload);
|
||||
break;
|
||||
}
|
||||
case (OrderbookChannelMessageTypes.Update): {
|
||||
handler.onUpdate(this, parserResult.payload);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
handler.onError(this, new Error(`Message has missing a type parameter: ${utf8Data}`));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handler.onError(this, error);
|
||||
}
|
||||
} else {
|
||||
handler.onError(this, new Error(`Message does not contain utf8Data`));
|
||||
}
|
||||
}
|
||||
}
|
5
packages/connect/test/fixtures/standard_relayer_api/fees.json
vendored
Normal file
5
packages/connect/test/fixtures/standard_relayer_api/fees.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"feeRecipient": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"makerFee": "10000000000000000",
|
||||
"takerFee": "30000000000000000"
|
||||
}
|
8
packages/connect/test/fixtures/standard_relayer_api/fees.ts
vendored
Normal file
8
packages/connect/test/fixtures/standard_relayer_api/fees.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {FeesResponse} from '../../../src/types';
|
||||
|
||||
export const feesResponse: FeesResponse = {
|
||||
feeRecipient: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
makerFee: new BigNumber('10000000000000000'),
|
||||
takerFee: new BigNumber('30000000000000000'),
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32",
|
||||
"makerFee": "100000000000000",
|
||||
"takerFee": "200000000000000",
|
||||
"makerTokenAmount": "10000000000000000",
|
||||
"takerTokenAmount": "20000000000000000",
|
||||
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
|
||||
"salt": "256",
|
||||
"feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da",
|
||||
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
|
||||
"expirationUnixTimestampSec": "42",
|
||||
"ecSignature": {
|
||||
"v": 27,
|
||||
"r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33",
|
||||
"s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254"
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
|
||||
export const orderResponse = {
|
||||
maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
makerFee: new BigNumber('100000000000000'),
|
||||
takerFee: new BigNumber('200000000000000'),
|
||||
makerTokenAmount: new BigNumber('10000000000000000'),
|
||||
takerTokenAmount: new BigNumber('20000000000000000'),
|
||||
makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
salt: new BigNumber('256'),
|
||||
feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da',
|
||||
exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093',
|
||||
expirationUnixTimestampSec: new BigNumber('42'),
|
||||
ecSignature: {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
},
|
||||
};
|
44
packages/connect/test/fixtures/standard_relayer_api/orderbook.json
vendored
Normal file
44
packages/connect/test/fixtures/standard_relayer_api/orderbook.json
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"bids": [
|
||||
{
|
||||
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32",
|
||||
"makerFee": "100000000000000",
|
||||
"takerFee": "200000000000000",
|
||||
"makerTokenAmount": "10000000000000000",
|
||||
"takerTokenAmount": "20000000000000000",
|
||||
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
|
||||
"salt": "256",
|
||||
"feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da",
|
||||
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
|
||||
"expirationUnixTimestampSec": "42",
|
||||
"ecSignature": {
|
||||
"v": 27,
|
||||
"r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33",
|
||||
"s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254"
|
||||
}
|
||||
}
|
||||
],
|
||||
"asks": [
|
||||
{
|
||||
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32",
|
||||
"makerFee": "100000000000000",
|
||||
"takerFee": "200000000000000",
|
||||
"makerTokenAmount": "10000000000000000",
|
||||
"takerTokenAmount": "20000000000000000",
|
||||
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
|
||||
"salt": "256",
|
||||
"feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da",
|
||||
"exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093",
|
||||
"expirationUnixTimestampSec": "42",
|
||||
"ecSignature": {
|
||||
"v": 27,
|
||||
"r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33",
|
||||
"s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
46
packages/connect/test/fixtures/standard_relayer_api/orderbook.ts
vendored
Normal file
46
packages/connect/test/fixtures/standard_relayer_api/orderbook.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
|
||||
export const orderbookResponse = {
|
||||
bids: [
|
||||
{
|
||||
maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
makerFee: new BigNumber('100000000000000'),
|
||||
takerFee: new BigNumber('200000000000000'),
|
||||
makerTokenAmount: new BigNumber('10000000000000000'),
|
||||
takerTokenAmount: new BigNumber('20000000000000000'),
|
||||
makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
salt: new BigNumber('256'),
|
||||
feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da',
|
||||
exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093',
|
||||
expirationUnixTimestampSec: new BigNumber('42'),
|
||||
ecSignature: {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
},
|
||||
},
|
||||
],
|
||||
asks: [
|
||||
{
|
||||
maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
makerFee: new BigNumber('100000000000000'),
|
||||
takerFee: new BigNumber('200000000000000'),
|
||||
makerTokenAmount: new BigNumber('10000000000000000'),
|
||||
takerTokenAmount: new BigNumber('20000000000000000'),
|
||||
makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
salt: new BigNumber('256'),
|
||||
feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da',
|
||||
exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093',
|
||||
expirationUnixTimestampSec: new BigNumber('42'),
|
||||
ecSignature: {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
21
packages/connect/test/fixtures/standard_relayer_api/orders.json
vendored
Normal file
21
packages/connect/test/fixtures/standard_relayer_api/orders.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32",
|
||||
"makerFee": "100000000000000",
|
||||
"takerFee": "200000000000000",
|
||||
"makerTokenAmount": "10000000000000000",
|
||||
"takerTokenAmount": "20000000000000000",
|
||||
"makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990",
|
||||
"salt": "256",
|
||||
"feeRecipient": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"exchangeContractAddress": "0x9e56625509c2f60af937f23b7b532600390e8c8b",
|
||||
"expirationUnixTimestampSec": "42",
|
||||
"ecSignature": {
|
||||
"v": 27,
|
||||
"r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33",
|
||||
"s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254"
|
||||
}
|
||||
}
|
||||
]
|
23
packages/connect/test/fixtures/standard_relayer_api/orders.ts
vendored
Normal file
23
packages/connect/test/fixtures/standard_relayer_api/orders.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
|
||||
export const ordersResponse = [
|
||||
{
|
||||
maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
makerFee: new BigNumber('100000000000000'),
|
||||
takerFee: new BigNumber('200000000000000'),
|
||||
makerTokenAmount: new BigNumber('10000000000000000'),
|
||||
takerTokenAmount: new BigNumber('20000000000000000'),
|
||||
makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
salt: new BigNumber('256'),
|
||||
feeRecipient: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
exchangeContractAddress: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
expirationUnixTimestampSec: new BigNumber('42'),
|
||||
ecSignature: {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
},
|
||||
},
|
||||
];
|
17
packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts
vendored
Normal file
17
packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import * as orderbookJSON from './orderbook.json';
|
||||
|
||||
const orderbookJsonString = JSON.stringify(orderbookJSON);
|
||||
|
||||
export const snapshotOrderbookChannelMessage = `{
|
||||
"type": "snapshot",
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": ${orderbookJsonString}
|
||||
}`;
|
||||
|
||||
export const malformedSnapshotOrderbookChannelMessage = `{
|
||||
"type": "snapshot",
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": {}
|
||||
}`;
|
16
packages/connect/test/fixtures/standard_relayer_api/token_pairs.json
vendored
Normal file
16
packages/connect/test/fixtures/standard_relayer_api/token_pairs.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"tokenA": {
|
||||
"address": "0x323b5d4c32345ced77393b3530b1eed0f346429d",
|
||||
"minAmount": "0",
|
||||
"maxAmount": "10000000000000000000",
|
||||
"precision": 5
|
||||
},
|
||||
"tokenB": {
|
||||
"address": "0xef7fff64389b814a946f3e92105513705ca6b990",
|
||||
"minAmount": "0",
|
||||
"maxAmount": "50000000000000000000",
|
||||
"precision": 5
|
||||
}
|
||||
}
|
||||
]
|
19
packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts
vendored
Normal file
19
packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {TokenPairsItem} from '../../../src/types';
|
||||
|
||||
export const tokenPairsResponse: TokenPairsItem[] = [
|
||||
{
|
||||
tokenA: {
|
||||
address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
minAmount: new BigNumber(0),
|
||||
maxAmount: new BigNumber('10000000000000000000'),
|
||||
precision: 5,
|
||||
},
|
||||
tokenB: {
|
||||
address: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
minAmount: new BigNumber(0),
|
||||
maxAmount: new BigNumber('50000000000000000000'),
|
||||
precision: 5,
|
||||
},
|
||||
},
|
||||
];
|
10
packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts
vendored
Normal file
10
packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import * as orderResponseJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json';
|
||||
|
||||
const orderJSONString = JSON.stringify(orderResponseJSON);
|
||||
|
||||
export const unknownOrderbookChannelMessage = `{
|
||||
"type": "superGoodUpdate",
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": ${orderJSONString}
|
||||
}`;
|
17
packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts
vendored
Normal file
17
packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import * as orderResponseJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json';
|
||||
|
||||
const orderJSONString = JSON.stringify(orderResponseJSON);
|
||||
|
||||
export const updateOrderbookChannelMessage = `{
|
||||
"type": "update",
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": ${orderJSONString}
|
||||
}`;
|
||||
|
||||
export const malformedUpdateOrderbookChannelMessage = `{
|
||||
"type": "update",
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": {}
|
||||
}`;
|
130
packages/connect/test/http_client_test.ts
Normal file
130
packages/connect/test/http_client_test.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import 'mocha';
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
import * as chai from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import * as fetchMock from 'fetch-mock';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {HttpClient} from '../src/index';
|
||||
import {feesResponse} from './fixtures/standard_relayer_api/fees';
|
||||
import {
|
||||
orderResponse,
|
||||
} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f';
|
||||
import {ordersResponse} from './fixtures/standard_relayer_api/orders';
|
||||
import {tokenPairsResponse} from './fixtures/standard_relayer_api/token_pairs';
|
||||
import {orderbookResponse} from './fixtures/standard_relayer_api/orderbook';
|
||||
import * as feesResponseJSON from './fixtures/standard_relayer_api/fees.json';
|
||||
// tslint:disable-next-line:max-line-length
|
||||
import * as orderResponseJSON from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json';
|
||||
import * as ordersResponseJSON from './fixtures/standard_relayer_api/orders.json';
|
||||
import * as tokenPairsResponseJSON from './fixtures/standard_relayer_api/token_pairs.json';
|
||||
import * as orderbookJSON from './fixtures/standard_relayer_api/orderbook.json';
|
||||
|
||||
chai.config.includeStack = true;
|
||||
chai.use(dirtyChai);
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('HttpClient', () => {
|
||||
const relayUrl = 'https://example.com';
|
||||
const relayerClient = new HttpClient(relayUrl);
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
describe('#getTokenPairsAsync', () => {
|
||||
const url = `${relayUrl}/v0/token_pairs`;
|
||||
it('gets token pairs', async () => {
|
||||
fetchMock.get(url, tokenPairsResponseJSON);
|
||||
const tokenPairs = await relayerClient.getTokenPairsAsync();
|
||||
expect(tokenPairs).to.be.deep.equal(tokenPairsResponse);
|
||||
});
|
||||
it('gets specfic token pairs for request', async () => {
|
||||
const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d';
|
||||
const tokenPairsRequest = {
|
||||
tokenA: tokenAddress,
|
||||
};
|
||||
const urlWithQuery = `${url}?tokenA=${tokenAddress}`;
|
||||
fetchMock.get(urlWithQuery, tokenPairsResponseJSON);
|
||||
const tokenPairs = await relayerClient.getTokenPairsAsync(tokenPairsRequest);
|
||||
expect(tokenPairs).to.be.deep.equal(tokenPairsResponse);
|
||||
});
|
||||
it('throws an error for invalid JSON response', async () => {
|
||||
fetchMock.get(url, {test: 'dummy'});
|
||||
expect(relayerClient.getTokenPairsAsync()).to.be.rejected();
|
||||
});
|
||||
});
|
||||
describe('#getOrdersAsync', () => {
|
||||
const url = `${relayUrl}/v0/orders`;
|
||||
it('gets orders', async () => {
|
||||
fetchMock.get(url, ordersResponseJSON);
|
||||
const orders = await relayerClient.getOrdersAsync();
|
||||
expect(orders).to.be.deep.equal(ordersResponse);
|
||||
});
|
||||
it('gets specfic orders for request', async () => {
|
||||
const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d';
|
||||
const ordersRequest = {
|
||||
tokenA: tokenAddress,
|
||||
};
|
||||
const urlWithQuery = `${url}?tokenA=${tokenAddress}`;
|
||||
fetchMock.get(urlWithQuery, ordersResponseJSON);
|
||||
const orders = await relayerClient.getOrdersAsync(ordersRequest);
|
||||
expect(orders).to.be.deep.equal(ordersResponse);
|
||||
});
|
||||
it('throws an error for invalid JSON response', async () => {
|
||||
fetchMock.get(url, {test: 'dummy'});
|
||||
expect(relayerClient.getOrdersAsync()).to.be.rejected();
|
||||
});
|
||||
});
|
||||
describe('#getOrderAsync', () => {
|
||||
const orderHash = '0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f';
|
||||
const url = `${relayUrl}/v0/order/${orderHash}`;
|
||||
it('gets order', async () => {
|
||||
fetchMock.get(url, orderResponseJSON);
|
||||
const order = await relayerClient.getOrderAsync(orderHash);
|
||||
expect(order).to.be.deep.equal(orderResponse);
|
||||
});
|
||||
it('throws an error for invalid JSON response', async () => {
|
||||
fetchMock.get(url, {test: 'dummy'});
|
||||
expect(relayerClient.getOrderAsync(orderHash)).to.be.rejected();
|
||||
});
|
||||
});
|
||||
describe('#getOrderBookAsync', () => {
|
||||
const request = {
|
||||
baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
quoteTokenAddress: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
};
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const url = `${relayUrl}/v0/orderbook?baseTokenAddress=${request.baseTokenAddress}"eTokenAddress=${request.quoteTokenAddress}`;
|
||||
it('gets order book', async () => {
|
||||
fetchMock.get(url, orderbookJSON);
|
||||
const orderbook = await relayerClient.getOrderbookAsync(request);
|
||||
expect(orderbook).to.be.deep.equal(orderbookResponse);
|
||||
});
|
||||
it('throws an error for invalid JSON response', async () => {
|
||||
fetchMock.get(url, {test: 'dummy'});
|
||||
expect(relayerClient.getOrderbookAsync(request)).to.be.rejected();
|
||||
});
|
||||
});
|
||||
describe('#getFeesAsync', () => {
|
||||
const request = {
|
||||
exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093',
|
||||
maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b',
|
||||
taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32',
|
||||
makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
makerTokenAmount: new BigNumber('10000000000000000000'),
|
||||
takerTokenAmount: new BigNumber('30000000000000000000'),
|
||||
salt: new BigNumber('256'),
|
||||
expirationUnixTimestampSec: new BigNumber('42'),
|
||||
};
|
||||
const url = `${relayUrl}/v0/fees`;
|
||||
it('gets fees', async () => {
|
||||
fetchMock.post(url, feesResponseJSON);
|
||||
const fees = await relayerClient.getFeesAsync(request);
|
||||
expect(fees).to.be.deep.equal(feesResponse);
|
||||
});
|
||||
it('throws an error for invalid JSON response', async () => {
|
||||
fetchMock.post(url, {test: 'dummy'});
|
||||
expect(relayerClient.getFeesAsync(request)).to.be.rejected();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,66 @@
|
||||
import 'mocha';
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
import * as chai from 'chai';
|
||||
import {orderbookChannelMessageParsers} from '../src/utils/orderbook_channel_message_parsers';
|
||||
import {
|
||||
snapshotOrderbookChannelMessage,
|
||||
malformedSnapshotOrderbookChannelMessage,
|
||||
} from './fixtures/standard_relayer_api/snapshot_orderbook_channel_message';
|
||||
import {
|
||||
updateOrderbookChannelMessage,
|
||||
malformedUpdateOrderbookChannelMessage,
|
||||
} from './fixtures/standard_relayer_api/update_orderbook_channel_message';
|
||||
import {unknownOrderbookChannelMessage} from './fixtures/standard_relayer_api/unknown_orderbook_channel_message';
|
||||
import {orderbookResponse} from './fixtures/standard_relayer_api/orderbook';
|
||||
// tslint:disable-next-line:max-line-length
|
||||
import {orderResponse} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f';
|
||||
|
||||
chai.config.includeStack = true;
|
||||
chai.use(dirtyChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('orderbookChannelMessageParsers', () => {
|
||||
describe('#parser', () => {
|
||||
it('parses snapshot messages', () => {
|
||||
const snapshotMessage = orderbookChannelMessageParsers.parser(snapshotOrderbookChannelMessage);
|
||||
expect(snapshotMessage.type).to.be.equal('snapshot');
|
||||
expect(snapshotMessage.payload).to.be.deep.equal(orderbookResponse);
|
||||
});
|
||||
it('parses update messages', () => {
|
||||
const updateMessage = orderbookChannelMessageParsers.parser(updateOrderbookChannelMessage);
|
||||
expect(updateMessage.type).to.be.equal('update');
|
||||
expect(updateMessage.payload).to.be.deep.equal(orderResponse);
|
||||
});
|
||||
it('returns unknown message for messages with unsupported types', () => {
|
||||
const unknownMessage = orderbookChannelMessageParsers.parser(unknownOrderbookChannelMessage);
|
||||
expect(unknownMessage.type).to.be.equal('unknown');
|
||||
expect(unknownMessage.payload).to.be.undefined();
|
||||
});
|
||||
it('throws when message does not include a type', () => {
|
||||
const typelessMessage = `{
|
||||
"channel": "orderbook",
|
||||
"channelId": 1,
|
||||
"payload": {}
|
||||
}`;
|
||||
const badCall = () => orderbookChannelMessageParsers.parser(typelessMessage);
|
||||
expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`);
|
||||
});
|
||||
it('throws when snapshot message has malformed payload', () => {
|
||||
const badCall = () =>
|
||||
orderbookChannelMessageParsers.parser(malformedSnapshotOrderbookChannelMessage);
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const errMsg = 'Validation errors: instance.payload requires property "bids", instance.payload requires property "asks"';
|
||||
expect(badCall).throws(errMsg);
|
||||
});
|
||||
it('throws when update message has malformed payload', () => {
|
||||
const badCall = () =>
|
||||
orderbookChannelMessageParsers.parser(malformedUpdateOrderbookChannelMessage);
|
||||
expect(badCall).throws(/^Expected message to conform to schema/);
|
||||
});
|
||||
it('throws when input message is not valid JSON', () => {
|
||||
const nonJsonString = 'h93b{sdfs9fsd f';
|
||||
const badCall = () => orderbookChannelMessageParsers.parser(nonJsonString);
|
||||
expect(badCall).throws('Unexpected token h in JSON at position 0');
|
||||
});
|
||||
});
|
||||
});
|
46
packages/connect/test/ws_orderbook_channel_test.ts
Normal file
46
packages/connect/test/ws_orderbook_channel_test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import 'mocha';
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
import * as chai from 'chai';
|
||||
import {
|
||||
WebSocketOrderbookChannel,
|
||||
} from '../src/index';
|
||||
|
||||
chai.config.includeStack = true;
|
||||
chai.use(dirtyChai);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('WebSocketOrderbookChannel', () => {
|
||||
const websocketUrl = 'ws://localhost:8080';
|
||||
const orderbookChannel = new WebSocketOrderbookChannel(websocketUrl);
|
||||
const subscriptionOpts = {
|
||||
baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
|
||||
quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
|
||||
snapshot: true,
|
||||
limit: 100,
|
||||
};
|
||||
const emptyOrderbookChannelHandler = {
|
||||
onSnapshot: () => { return; },
|
||||
onUpdate: () => { return; },
|
||||
onError: () => { return; },
|
||||
onClose: () => { return; },
|
||||
};
|
||||
describe('#subscribe', () => {
|
||||
it('throws when subscriptionOpts does not conform to schema', () => {
|
||||
const badSubscribeCall = orderbookChannel.subscribe.bind(
|
||||
orderbookChannel, {}, emptyOrderbookChannelHandler);
|
||||
// tslint:disable-next-line:max-line-length
|
||||
expect(badSubscribeCall)
|
||||
.throws('Expected subscriptionOpts to conform to schema /RelayerApiOrderbookChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseTokenAddress", instance requires property "quoteTokenAddress"');
|
||||
});
|
||||
it('throws when handler has the incorrect members', () => {
|
||||
const badSubscribeCall = orderbookChannel.subscribe.bind(orderbookChannel, subscriptionOpts, {});
|
||||
expect(badSubscribeCall)
|
||||
.throws('Expected handler.onSnapshot to be of type function, encountered: undefined');
|
||||
});
|
||||
it('does not throw when inputs are of correct types', () => {
|
||||
const goodSubscribeCall = orderbookChannel.subscribe.bind(
|
||||
orderbookChannel, subscriptionOpts, emptyOrderbookChannelHandler);
|
||||
expect(goodSubscribeCall).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
19
packages/connect/tsconfig.json
Normal file
19
packages/connect/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./test/**/*",
|
||||
"../../node_modules/chai-as-promised-typescript-typings/index.d.ts",
|
||||
"../../node_modules/chai-typescript-typings/index.d.ts",
|
||||
"../../node_modules/web3-typescript-typings/index.d.ts"
|
||||
]
|
||||
}
|
5
packages/connect/tslint.json
Normal file
5
packages/connect/tslint.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"@0xproject/tslint-config"
|
||||
]
|
||||
}
|
67
yarn.lock
67
yarn.lock
@ -2,6 +2,10 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/fetch-mock@^5.12.1":
|
||||
version "5.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-5.12.2.tgz#8c96517ff74303031c65c5da2d99858e34c844d2"
|
||||
|
||||
"@types/fs-extra@^4.0.0":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.4.tgz#72947e108f2cbeda5ab288a927399fdf6d02bd42"
|
||||
@ -41,7 +45,7 @@
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.78":
|
||||
"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.77", "@types/lodash@^4.14.78":
|
||||
version "4.14.85"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.85.tgz#a16fbf942422f6eca5622b6910492c496c35069b"
|
||||
|
||||
@ -61,6 +65,10 @@
|
||||
version "8.0.51"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb"
|
||||
|
||||
"@types/query-string@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-5.0.1.tgz#6cb41c724cb1644d56c2d1dae7c7b204e706b39e"
|
||||
|
||||
"@types/shelljs@^0.7.0":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.5.tgz#5834fb7385d1137bd2be5842f2c278ac36a117f4"
|
||||
@ -82,6 +90,12 @@
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45"
|
||||
|
||||
"@types/websocket@^0.0.34":
|
||||
version "0.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.34.tgz#25596764cec885eda070fdb6d19cd76fe582747c"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
JSONStream@^1.0.4:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
|
||||
@ -2281,6 +2295,14 @@ fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
|
||||
fetch-mock@^5.13.1:
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-5.13.1.tgz#955794a77f3d972f1644b9ace65a0fdfd60f1df7"
|
||||
dependencies:
|
||||
glob-to-regexp "^0.3.0"
|
||||
node-fetch "^1.3.3"
|
||||
path-to-regexp "^1.7.0"
|
||||
|
||||
fetch-ponyfill@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893"
|
||||
@ -2599,6 +2621,10 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-to-regexp@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||
|
||||
glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
@ -3140,7 +3166,7 @@ is-text-path@^1.0.0:
|
||||
dependencies:
|
||||
text-extensions "^1.0.0"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
||||
@ -3174,7 +3200,7 @@ isobject@^3.0.0, isobject@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
|
||||
isomorphic-fetch@^2.2.0:
|
||||
isomorphic-fetch@^2.2.0, isomorphic-fetch@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
dependencies:
|
||||
@ -3881,7 +3907,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
|
||||
nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0:
|
||||
nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
|
||||
@ -3917,7 +3943,7 @@ node-abi@^2.1.1:
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-fetch@^1.0.1, node-fetch@~1.7.1:
|
||||
node-fetch@^1.0.1, node-fetch@^1.3.3, node-fetch@~1.7.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
dependencies:
|
||||
@ -4519,6 +4545,14 @@ qs@~6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
query-string@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88"
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
@ -5279,6 +5313,10 @@ stream-http@^2.3.1:
|
||||
to-arraybuffer "^1.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
strict-uri-encode@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
|
||||
string-editor@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-editor/-/string-editor-0.1.2.tgz#f5ff1b5ac4aed7ac6c2fb8de236d1551b20f61d0"
|
||||
@ -5667,6 +5705,12 @@ type-detect@^4.0.0:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2"
|
||||
|
||||
typedarray-to-buffer@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz#1017b32d984ff556eba100f501589aba1ace2e04"
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
@ -5978,6 +6022,15 @@ webpack@^3.0.0, webpack@^3.1.0:
|
||||
webpack-sources "^1.0.1"
|
||||
yargs "^8.0.2"
|
||||
|
||||
websocket@^1.0.25:
|
||||
version "1.0.25"
|
||||
resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.25.tgz#998ec790f0a3eacb8b08b50a4350026692a11958"
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
nan "^2.3.3"
|
||||
typedarray-to-buffer "^3.1.2"
|
||||
yaeti "^0.0.6"
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
|
||||
@ -6094,6 +6147,10 @@ y18n@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
|
||||
yaeti@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
|
Loading…
x
Reference in New Issue
Block a user