aboutsummaryrefslogblamecommitdiffstats
path: root/packages/asset-buyer/src/asset_buyer.ts
blob: 76b96427e92adf0e2f4faaa220141e985d83cf40 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                

                                                                     





                                                      

                                                                                                     






                                       
 



                                                                                         





                                                   
                                                         

                                                                                          









                                                                                                                                                                                                 




                                                         
                                                                                     













                                                                                                                














                                                                                                                                      
                                                                                     



















                                                                                                                                      




                                                         
                                                                                     






                                                                            






                                                                

                          














                                                                                                                                                                                             
                                                                                     

























                                                                                                                         
                                                                 





























                                                                                                                              
                                                  














































                                                                                                                                                          
                                                                                                               

































                                                                                                          
                                                                                          



































                                                                                                         
import { ContractWrappers } from '@0xproject/contract-wrappers';
import { schemas } from '@0xproject/json-schemas';
import { assetDataUtils, SignedOrder } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';

import { constants } from './constants';
import { ProvidedOrderFetcher } from './order_fetchers/provided_order_fetcher';
import { StandardRelayerAPIOrderFetcher } from './order_fetchers/standard_relayer_api_order_fetcher';
import {
    AssetBuyerError,
    AssetBuyerOrdersAndFillableAmounts,
    BuyQuote,
    OrderFetcher,
    OrderFetcherResponse,
} from './types';

import { assert } from './utils/assert';
import { buyQuoteCalculator } from './utils/buy_quote_calculator';
import { orderFetcherResponseProcessor } from './utils/order_fetcher_response_processor';

export class AssetBuyer {
    public readonly provider: Provider;
    public readonly assetData: string;
    public readonly orderFetcher: OrderFetcher;
    public readonly networkId: number;
    public readonly orderRefreshIntervalMs: number;
    private readonly _contractWrappers: ContractWrappers;
    private _lastRefreshTimeIfExists?: number;
    private _currentOrdersAndFillableAmountsIfExists?: AssetBuyerOrdersAndFillableAmounts;
    /**
     * Instantiates a new AssetBuyer instance
     * @param   provider                The Provider instance you would like to use for interacting with the Ethereum network.
     * @param   orders                  A non-empty array of objects that conform to SignedOrder. All orders must have the same makerAssetData and takerAssetData (WETH).
     * @param   feeOrders               A array of objects that conform to SignedOrder. All orders must have the same makerAssetData (ZRX) and takerAssetData (WETH). Defaults to an empty array.
     * @param   networkId               The ethereum network id. Defaults to 1 (mainnet).
     * @param   orderRefreshIntervalMs  The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states.
     *                                  Defaults to 10000ms (10s).
     * @return  An instance of AssetBuyer
     */
    public static getAssetBuyerForProvidedOrders(
        provider: Provider,
        orders: SignedOrder[],
        feeOrders: SignedOrder[] = [],
        networkId: number = constants.MAINNET_NETWORK_ID,
        orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS,
    ): AssetBuyer {
        assert.isWeb3Provider('provider', provider);
        assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema);
        assert.doesConformToSchema('feeOrders', feeOrders, schemas.signedOrdersSchema);
        assert.isNumber('networkId', networkId);
        assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
        assert.areValidProvidedOrders('orders', orders);
        assert.areValidProvidedOrders('feeOrders', feeOrders);
        assert.assert(orders.length !== 0, `Expected orders to contain at least one order`);
        const assetData = orders[0].makerAssetData;
        const orderFetcher = new ProvidedOrderFetcher(_.concat(orders, feeOrders));
        const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs);
        return assetBuyer;
    }
    /**
     * Instantiates a new AssetBuyer instance
     * @param   provider                The Provider instance you would like to use for interacting with the Ethereum network.
     * @param   assetData               The assetData that identifies the desired asset to buy.
     * @param   sraApiUrl               The standard relayer API base HTTP url you would like to source orders from.
     * @param   networkId               The ethereum network id. Defaults to 1 (mainnet).
     * @param   orderRefreshIntervalMs  The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states.
     *                                  Defaults to 10000ms (10s).
     * @return  An instance of AssetBuyer
     */
    public static getAssetBuyerForAssetData(
        provider: Provider,
        assetData: string,
        sraApiUrl: string,
        networkId: number = constants.MAINNET_NETWORK_ID,
        orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS,
    ): AssetBuyer {
        assert.isWeb3Provider('provider', provider);
        assert.isHexString('assetData', assetData);
        assert.isWebUri('sraApiUrl', sraApiUrl);
        assert.isNumber('networkId', networkId);
        assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
        const orderFetcher = new StandardRelayerAPIOrderFetcher(sraApiUrl);
        const assetBuyer = new AssetBuyer(provider, assetData, orderFetcher, networkId, orderRefreshIntervalMs);
        return assetBuyer;
    }
    /**
     * Instantiates a new AssetBuyer instance
     * @param   provider                The Provider instance you would like to use for interacting with the Ethereum network.
     * @param   tokenAddress            The ERC20 token address that identifies the desired asset to buy.
     * @param   sraApiUrl               The standard relayer API base HTTP url you would like to source orders from.
     * @param   networkId               The ethereum network id. Defaults to 1 (mainnet).
     * @param   orderRefreshIntervalMs  The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states.
     *                                  Defaults to 10000ms (10s).
     * @return  An instance of AssetBuyer
     */
    public static getAssetBuyerForERC20TokenAddress(
        provider: Provider,
        tokenAddress: string,
        sraApiUrl: string,
        networkId: number = constants.MAINNET_NETWORK_ID,
        orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS,
    ): AssetBuyer {
        assert.isWeb3Provider('provider', provider);
        assert.isETHAddressHex('tokenAddress', tokenAddress);
        assert.isWebUri('sraApiUrl', sraApiUrl);
        assert.isNumber('networkId', networkId);
        assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
        const assetData = assetDataUtils.encodeERC20AssetData(tokenAddress);
        const assetBuyer = AssetBuyer.getAssetBuyerForAssetData(
            provider,
            assetData,
            sraApiUrl,
            networkId,
            orderRefreshIntervalMs,
        );
        return assetBuyer;
    }
    /**
     * Instantiates a new AssetBuyer instance
     * @param   provider                The Provider instance you would like to use for interacting with the Ethereum network.
     * @param   assetData               The assetData of the desired asset to buy (for more info: https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
     * @param   orderFetcher            An object that conforms to OrderFetcher, see type for definition.
     * @param   networkId               The ethereum network id. Defaults to 1 (mainnet).
     * @param   orderRefreshIntervalMs  The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states.
     *                                  Defaults to 10000ms (10s).
     * @return  An instance of AssetBuyer
     */
    constructor(
        provider: Provider,
        assetData: string,
        orderFetcher: OrderFetcher,
        networkId: number = constants.MAINNET_NETWORK_ID,
        orderRefreshIntervalMs: number = constants.DEFAULT_ORDER_REFRESH_INTERVAL_MS,
    ) {
        assert.isWeb3Provider('provider', provider);
        assert.isString('assetData', assetData);
        assert.isValidOrderFetcher('orderFetcher', orderFetcher);
        assert.isNumber('networkId', networkId);
        assert.isNumber('orderRefreshIntervalMs', orderRefreshIntervalMs);
        this.provider = provider;
        this.assetData = assetData;
        this.orderFetcher = orderFetcher;
        this.networkId = networkId;
        this.orderRefreshIntervalMs = orderRefreshIntervalMs;
        this._contractWrappers = new ContractWrappers(this.provider, {
            networkId,
        });
    }
    /**
     * Get a BuyQuote containing all information relevant to fulfilling a buy.
     * Pass the BuyQuote to executeBuyQuoteAsync to execute the buy.
     * @param   assetBuyAmount      The amount of asset to buy.
     * @param   feePercentage       The affiliate fee percentage. Defaults to 0.
     * @param   forceOrderRefresh   If set to true, new orders and state will be fetched instead of waiting for
     *                              the next orderRefreshIntervalMs. Defaults to false.
     * @return  An object that conforms to BuyQuote that satisfies the request. See type definition for more information.
     */
    public async getBuyQuoteAsync(
        assetBuyAmount: BigNumber,
        feePercentage: number = constants.DEFAULT_FEE_PERCENTAGE,
        forceOrderRefresh: boolean = false,
    ): Promise<BuyQuote> {
        assert.isBigNumber('assetBuyAmount', assetBuyAmount);
        assert.isNumber('feePercentage', feePercentage);
        assert.isBoolean('forceOrderRefresh', forceOrderRefresh);
        // we should refresh if:
        // we do not have any orders OR
        // we are forced to OR
        // we have some last refresh time AND that time was sufficiently long ago
        const shouldRefresh =
            _.isUndefined(this._currentOrdersAndFillableAmountsIfExists) ||
            forceOrderRefresh ||
            (!_.isUndefined(this._lastRefreshTimeIfExists) &&
                this._lastRefreshTimeIfExists + this.orderRefreshIntervalMs < Date.now());
        let ordersAndFillableAmounts: AssetBuyerOrdersAndFillableAmounts;
        if (shouldRefresh) {
            ordersAndFillableAmounts = await this._getLatestOrdersAndFillableAmountsAsync();
            this._lastRefreshTimeIfExists = Date.now();
            this._currentOrdersAndFillableAmountsIfExists = ordersAndFillableAmounts;
        } else {
            // it is safe to cast to AssetBuyerOrdersAndFillableAmounts because shouldRefresh catches the undefined case above
            ordersAndFillableAmounts = this
                ._currentOrdersAndFillableAmountsIfExists as AssetBuyerOrdersAndFillableAmounts;
        }
        // TODO: optimization
        // make the slippage percentage customizable by integrator
        const buyQuote = buyQuoteCalculator.calculate(
            ordersAndFillableAmounts,
            assetBuyAmount,
            feePercentage,
            constants.DEFAULT_SLIPPAGE_PERCENTAGE,
        );
        return buyQuote;
    }
    /**
     * Given a BuyQuote and desired rate, attempt to execute the buy.
     * @param   buyQuote        An object that conforms to BuyQuote. See type definition for more information.
     * @param   rate            The desired rate to execute the buy at. Affects the amount of ETH sent with the transaction, defaults to buyQuote.maxRate.
     * @param   takerAddress    The address to perform the buy. Defaults to the first available address from the provider.
     * @param   feeRecipient    The address where affiliate fees are sent. Defaults to null address (0x000...000).
     * @return  A promise of the txHash.
     */
    public async executeBuyQuoteAsync(
        buyQuote: BuyQuote,
        rate?: BigNumber,
        takerAddress?: string,
        feeRecipient: string = constants.NULL_ADDRESS,
    ): Promise<string> {
        assert.isValidBuyQuote('buyQuote', buyQuote);
        if (!_.isUndefined(rate)) {
            assert.isBigNumber('rate', rate);
        }
        if (!_.isUndefined(takerAddress)) {
            assert.isETHAddressHex('takerAddress', takerAddress);
        }
        assert.isETHAddressHex('feeRecipient', feeRecipient);
        const { orders, feeOrders, feePercentage, assetBuyAmount, maxRate } = buyQuote;
        // if no takerAddress is provided, try to get one from the provider
        let finalTakerAddress;
        if (!_.isUndefined(takerAddress)) {
            finalTakerAddress = takerAddress;
        } else {
            const web3Wrapper = new Web3Wrapper(this.provider);
            const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
            const firstAvailableAddress = _.head(availableAddresses);
            if (!_.isUndefined(firstAvailableAddress)) {
                finalTakerAddress = firstAvailableAddress;
            } else {
                throw new Error(AssetBuyerError.NoAddressAvailable);
            }
        }
        // if no rate is provided, default to the maxRate from buyQuote
        const desiredRate = rate || maxRate;
        // calculate how much eth is required to buy assetBuyAmount at the desired rate
        const ethAmount = assetBuyAmount.dividedToIntegerBy(desiredRate);
        // TODO: critical
        // update the forwarder wrapper to take in feePercentage as a number instead of a BigNumber, verify with Amir that this is being done correctly
        const feePercentageBigNumber = !_.isUndefined(feePercentage)
            ? Web3Wrapper.toBaseUnitAmount(new BigNumber(1), constants.ETHER_TOKEN_DECIMALS).mul(feePercentage)
            : constants.ZERO_AMOUNT;
        const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
            orders,
            assetBuyAmount,
            finalTakerAddress,
            ethAmount,
            feeOrders,
            feePercentageBigNumber,
            feeRecipient,
        );
        return txHash;
    }
    /**
     * Ask the order fetcher for orders and process them.
     */
    private async _getLatestOrdersAndFillableAmountsAsync(): Promise<AssetBuyerOrdersAndFillableAmounts> {
        // find ether token asset data
        const etherTokenAssetData = this._getEtherTokenAssetData();
        // find zrx token asset data
        const zrxTokenAssetData = this._getZrxTokenAssetData();
        // construct order fetcher requests
        const targetOrderFetcherRequest = {
            makerAssetData: this.assetData,
            takerAssetData: etherTokenAssetData,
            networkId: this.networkId,
        };
        const feeOrderFetcherRequest = {
            makerAssetData: zrxTokenAssetData,
            takerAssetData: etherTokenAssetData,
            networkId: this.networkId,
        };
        const requests = [targetOrderFetcherRequest, feeOrderFetcherRequest];
        // fetch orders and possible fillable amounts
        const [targetOrderFetcherResponse, feeOrderFetcherResponse] = await Promise.all(
            _.map(requests, async request => this.orderFetcher.fetchOrdersAsync(request)),
        );
        // process the responses into one object
        const ordersAndFillableAmounts = await orderFetcherResponseProcessor.processAsync(
            targetOrderFetcherResponse,
            feeOrderFetcherResponse,
            zrxTokenAssetData,
            this._contractWrappers.orderValidator,
        );
        return ordersAndFillableAmounts;
    }
    /**
     * Get the assetData that represents the WETH token.
     * Will throw if WETH does not exist for the current network.
     */
    private _getEtherTokenAssetData(): string {
        const etherTokenAddressIfExists = this._contractWrappers.etherToken.getContractAddressIfExists();
        if (_.isUndefined(etherTokenAddressIfExists)) {
            throw new Error(AssetBuyerError.NoEtherTokenContractFound);
        }
        const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddressIfExists);
        return etherTokenAssetData;
    }
    /**
     * Get the assetData that represents the ZRX token.
     * Will throw if ZRX does not exist for the current network.
     */
    private _getZrxTokenAssetData(): string {
        let zrxTokenAssetData: string;
        try {
            zrxTokenAssetData = this._contractWrappers.exchange.getZRXAssetData();
        } catch (err) {
            throw new Error(AssetBuyerError.NoZrxTokenContractFound);
        }
        return zrxTokenAssetData;
    }
}