diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 0ba1b4975d..eb5086f07f 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -41,6 +41,10 @@ { "note": "Add DFB support + refactor swap quote calculator utils", "pr": 2536 + }, + { + "note": "Add support for RFQ-T, querying maker-hosted endpoints for quotes to be submitted by the taker", + "pr": 2541 } ] }, diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 3df0a94cb7..2ad638cda4 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -11,7 +11,8 @@ "build": "yarn tsc -b", "watch": "tsc -w -p tsconfig.json", "build:ci": "yarn build", - "lint": "tslint --format stylish --project .", + "lint": "tslint --format stylish --project . && yarn prettier", + "prettier": "prettier --check 'src/**/*.{ts,tsx,json,md}' --config ../../.prettierrc", "fix": "tslint --fix --format stylish --project .", "test": "yarn run_mocha", "rebuild_and_test": "run-s clean build test", @@ -53,6 +54,8 @@ "@0x/orderbook": "^2.2.5", "@0x/utils": "^5.4.1", "@0x/web3-wrapper": "^7.0.7", + "axios": "^0.19.2", + "axios-mock-adapter": "^1.18.1", "heartbeats": "^5.0.1", "lodash": "^4.17.11" }, diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index f9b94b4ffb..cadd6d03f2 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -5,6 +5,7 @@ import { ForwarderExtensionContractOpts, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, + RfqtFirmQuoteRequestOpts, SwapQuoteExecutionOpts, SwapQuoteGetOutputOpts, SwapQuoteRequestOpts, @@ -43,6 +44,10 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { }, ...DEFAULT_ORDER_PRUNER_OPTS, samplerGasLimit: 250e6, + rfqt: { + takerApiKeyWhitelist: [], + makerEndpoints: [], + }, }; const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = { @@ -61,6 +66,10 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, }; +const DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS: RfqtFirmQuoteRequestOpts = { + makerEndpointMaxResponseTimeMs: 1000, +}; + export const constants = { ETH_GAS_STATION_API_BASE_URL, PROTOCOL_FEE_MULTIPLIER, @@ -77,6 +86,7 @@ export const constants = { DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, DEFAULT_SWAP_QUOTE_REQUEST_OPTS, DEFAULT_PER_PAGE, + DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS, NULL_ERC20_ASSET_DATA, PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index d1d918c563..9ea63d5b63 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -42,7 +42,10 @@ export { GetExtensionContractTypeOpts, LiquidityForTakerMakerAssetDataPair, MarketBuySwapQuote, + MarketOperation, MarketSellSwapQuote, + MockedRfqtFirmQuoteResponse, + RfqtFirmQuoteRequestOpts, SwapQuote, SwapQuoteConsumerBase, SwapQuoteConsumerOpts, @@ -64,3 +67,5 @@ export { } from './utils/market_operation_utils/types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; +export { QuoteRequestor } from './utils/quote_requestor'; +export { rfqtMocker } from './utils/rfqt_mocker'; diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index d87bef6a5b..25e913b822 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -27,6 +27,7 @@ import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; +import { QuoteRequestor } from './utils/quote_requestor'; import { sortingUtils } from './utils/sorting_utils'; import { SwapQuoteCalculator } from './utils/swap_quote_calculator'; @@ -42,6 +43,8 @@ export class SwapQuoter { private readonly _devUtilsContract: DevUtilsContract; private readonly _marketOperationUtils: MarketOperationUtils; private readonly _orderStateUtils: OrderStateUtils; + private readonly _quoteRequestor: QuoteRequestor; + private readonly _rfqtTakerApiKeyWhitelist: string[]; /** * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. @@ -161,10 +164,13 @@ export class SwapQuoter { this.orderbook = orderbook; this.expiryBufferMs = expiryBufferMs; this.permittedOrderFeeTypes = permittedOrderFeeTypes; + this._rfqtTakerApiKeyWhitelist = options.rfqt ? options.rfqt.takerApiKeyWhitelist || [] : []; this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); + this._quoteRequestor = + options.quoteRequestor || new QuoteRequestor(options.rfqt ? options.rfqt.makerEndpoints || [] : []); const sampler = new DexOrderSampler( new IERC20BridgeSamplerContract(this._contractAddresses.erc20BridgeSampler, this.provider, { gas: samplerGasLimit, @@ -498,8 +504,7 @@ export class SwapQuoter { this.permittedOrderFeeTypes, this.expiryBufferMs, ); - const sortedPrunedOrders = sortingUtils.sortOrders(prunedOrders); - return sortedPrunedOrders; + return prunedOrders; } /** @@ -512,40 +517,68 @@ export class SwapQuoter { marketOperation: MarketOperation, options: Partial, ): Promise { - const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options); + const opts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options); assert.isString('makerAssetData', makerAssetData); assert.isString('takerAssetData', takerAssetData); let gasPrice: BigNumber; - if (!!options.gasPrice) { - gasPrice = options.gasPrice; + if (!!opts.gasPrice) { + gasPrice = opts.gasPrice; assert.isBigNumber('gasPrice', gasPrice); } else { gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync(); } - // get the relevant orders for the makerAsset - let prunedOrders = await this._getSignedOrdersAsync(makerAssetData, takerAssetData); + // get batches of orders from different sources, awaiting sources in parallel + const orderBatchPromises: Array> = []; + orderBatchPromises.push(this._getSignedOrdersAsync(makerAssetData, takerAssetData)); // order book + if ( + opts.rfqt && + opts.rfqt.intentOnFilling && + opts.apiKey && + this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) + ) { + if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { + throw new Error('RFQ-T requests must specify a taker address'); + } + orderBatchPromises.push( + this._quoteRequestor.requestRfqtFirmQuotesAsync( + makerAssetData, + takerAssetData, + assetFillAmount, + marketOperation, + opts.apiKey, + opts.rfqt.takerAddress, + ), + ); + } + + const orderBatches: SignedOrder[][] = await Promise.all(orderBatchPromises); + + const unsortedOrders: SignedOrder[] = orderBatches.reduce((_orders, batch) => _orders.concat(...batch)); + + const orders = sortingUtils.sortOrders(unsortedOrders); + // if no native orders, pass in a dummy order for the sampler to have required metadata for sampling - if (prunedOrders.length === 0) { - prunedOrders = [ + if (orders.length === 0) { + orders.push( createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge), - ]; + ); } let swapQuote: SwapQuote; if (marketOperation === MarketOperation.Buy) { swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - prunedOrders, + orders, assetFillAmount, gasPrice, - calculateSwapQuoteOpts, + opts, ); } else { swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - prunedOrders, + orders, assetFillAmount, gasPrice, - calculateSwapQuoteOpts, + opts, ); } diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index d809c1b061..a93e8ede40 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { GetMarketOrdersOpts } from './utils/market_operation_utils/types'; +import { QuoteRequestor } from './utils/quote_requestor'; /** * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). @@ -192,6 +193,11 @@ export interface SwapQuoteOrdersBreakdown { */ export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts { gasPrice?: BigNumber; + apiKey?: string; + rfqt?: { + takerAddress: string; + intentOnFilling: boolean; + }; } /** @@ -213,6 +219,11 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { contractAddresses?: ContractAddresses; samplerGasLimit?: number; liquidityProviderRegistryAddress?: string; + rfqt?: { + takerApiKeyWhitelist: string[]; + makerEndpoints: string[]; + }; + quoteRequestor?: QuoteRequestor; } /** @@ -262,3 +273,20 @@ export enum OrderPrunerPermittedFeeTypes { MakerDenominatedTakerFee = 'MAKER_DENOMINATED_TAKER_FEE', TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE', } + +export interface RfqtFirmQuoteRequestOpts { + makerEndpointMaxResponseTimeMs?: number; +} + +/** + * Represents a mocked RFQT maker responses. + */ +export interface MockedRfqtFirmQuoteResponse { + endpoint: string; + requestApiKey: string; + requestParams: { + [key: string]: string | undefined; + }; + responseData: any; + responseCode: number; +} diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts new file mode 100644 index 0000000000..db73ab0038 --- /dev/null +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -0,0 +1,116 @@ +import { schemas, SchemaValidator } from '@0x/json-schemas'; +import { assetDataUtils, SignedOrder } from '@0x/order-utils'; +import { ERC20AssetData } from '@0x/types'; +import { BigNumber, logUtils } from '@0x/utils'; +import Axios, { AxiosResponse } from 'axios'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { MarketOperation, RfqtFirmQuoteRequestOpts } from '../types'; + +/** + * Request quotes from RFQ-T providers + */ + +function getTokenAddressOrThrow(assetData: string): string { + const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); + if (decodedAssetData.hasOwnProperty('tokenAddress')) { + // type cast necessary here as decodeAssetDataOrThrow returns + // an AssetData object, which doesn't necessarily contain a + // token address. (it could possibly be a StaticCallAssetData, + // which lacks an address.) so we'll just assume it's a token + // here. should be safe, with the enclosing guard condition + // and subsequent error. + // tslint:disable-next-line:no-unnecessary-type-assertion + return (decodedAssetData as ERC20AssetData).tokenAddress; + } + throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`); +} + +export class QuoteRequestor { + private readonly _rfqtMakerEndpoints: string[]; + private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); + + constructor(rfqtMakerEndpoints: string[]) { + this._rfqtMakerEndpoints = rfqtMakerEndpoints; + } + + public async requestRfqtFirmQuotesAsync( + makerAssetData: string, + takerAssetData: string, + assetFillAmount: BigNumber, + marketOperation: MarketOperation, + takerApiKey: string, + takerAddress: string, + options?: Partial, + ): Promise { + const { makerEndpointMaxResponseTimeMs } = _.merge({}, constants.DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS, options); + + const buyToken = getTokenAddressOrThrow(makerAssetData); + const sellToken = getTokenAddressOrThrow(takerAssetData); + + // create an array of promises for quote responses, using "undefined" + // as a placeholder for failed requests. + const responsesIfDefined: Array> = await Promise.all( + this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => { + try { + return await Axios.get(`${rfqtMakerEndpoint}/quote`, { + headers: { '0x-api-key': takerApiKey }, + params: { + sellToken, + buyToken, + buyAmount: marketOperation === MarketOperation.Buy ? assetFillAmount.toString() : undefined, + sellAmount: + marketOperation === MarketOperation.Sell ? assetFillAmount.toString() : undefined, + takerAddress, + }, + timeout: makerEndpointMaxResponseTimeMs, + }); + } catch (err) { + logUtils.warn( + `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${takerApiKey} for taker address ${takerAddress}`, + ); + logUtils.warn(err); + return undefined; + } + }), + ); + + const responses = responsesIfDefined.filter( + (respIfDefd): respIfDefd is AxiosResponse => respIfDefd !== undefined, + ); + + const ordersWithStringInts = responses.map(response => response.data); // not yet BigNumber + + const validatedOrdersWithStringInts = ordersWithStringInts.filter(order => { + const hasValidSchema = this._schemaValidator.isValid(order, schemas.signedOrderSchema); + if (!hasValidSchema) { + logUtils.warn(`Invalid RFQ-t order received, filtering out: ${JSON.stringify(order)}`); + return false; + } + + const hasExpectedMakerAssetData = order.makerAssetData.toLowerCase() === makerAssetData.toLowerCase(); + const hasExpectedTakerAssetData = order.takerAssetData.toLowerCase() === takerAssetData.toLowerCase(); + if (!hasExpectedMakerAssetData || !hasExpectedTakerAssetData) { + logUtils.warn(`Unexpected asset data in RFQ-T order, filtering out: ${JSON.stringify(order)}`); + return false; + } + + return true; + }); + + const orders: SignedOrder[] = validatedOrdersWithStringInts.map(orderWithStringInts => { + return { + ...orderWithStringInts, + makerAssetAmount: new BigNumber(orderWithStringInts.makerAssetAmount), + takerAssetAmount: new BigNumber(orderWithStringInts.takerAssetAmount), + makerFee: new BigNumber(orderWithStringInts.makerFee), + takerFee: new BigNumber(orderWithStringInts.takerFee), + expirationTimeSeconds: new BigNumber(orderWithStringInts.expirationTimeSeconds), + salt: new BigNumber(orderWithStringInts.salt), + }; + }); + + return orders; + } +} diff --git a/packages/asset-swapper/src/utils/rfqt_mocker.ts b/packages/asset-swapper/src/utils/rfqt_mocker.ts new file mode 100644 index 0000000000..dfb8426f67 --- /dev/null +++ b/packages/asset-swapper/src/utils/rfqt_mocker.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; +import AxiosMockAdapter from 'axios-mock-adapter'; + +import { MockedRfqtFirmQuoteResponse } from '../types'; + +/** + * A helper utility for testing which mocks out + * requests to RFQ-t providers + */ +export const rfqtMocker = { + /** + * Stubs out responses from RFQ-T providers by mocking out + * HTTP calls via axios. Always restores the mock adapter + * after executing the `performFn`. + */ + withMockedRfqtFirmQuotes: async ( + mockedResponses: MockedRfqtFirmQuoteResponse[], + performFn: () => Promise, + ) => { + const mockedAxios = new AxiosMockAdapter(axios); + try { + // Mock out RFQT responses + for (const mockedResponse of mockedResponses) { + const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; + const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; + mockedAxios + .onGet(`${endpoint}/quote`, { params: requestParams }, requestHeaders) + .replyOnce(responseCode, responseData); + } + + await performFn(); + } finally { + // Ensure we always restore axios afterwards + mockedAxios.restore(); + } + }, +}; diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts new file mode 100644 index 0000000000..1062ae4a03 --- /dev/null +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -0,0 +1,138 @@ +import { tokenUtils } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { StatusCodes } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { MarketOperation, MockedRfqtFirmQuoteResponse } from '../src/types'; +import { QuoteRequestor } from '../src/utils/quote_requestor'; +import { rfqtMocker } from '../src/utils/rfqt_mocker'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('QuoteRequestor', async () => { + const [makerToken, takerToken, otherToken1] = tokenUtils.getDummyERC20TokenAddresses(); + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken); + + describe('requestRfqtFirmQuotesAsync', async () => { + it('should return successful RFQT requests', async () => { + const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; + const takerApiKey = 'my-ko0l-api-key'; + + // Set up RFQT responses + // tslint:disable-next-line:array-type + const mockedRequests: MockedRfqtFirmQuoteResponse[] = []; + const expectedParams = { + sellToken: takerToken, + buyToken: makerToken, + sellAmount: '10000', + buyAmount: undefined, + takerAddress, + }; + // Successful response + const successfulOrder1 = testOrderFactory.generateTestSignedOrder({ + makerAssetData, + takerAssetData, + feeRecipientAddress: '0x0000000000000000000000000000000000000001', + }); + mockedRequests.push({ + endpoint: 'https://1337.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: successfulOrder1, + responseCode: StatusCodes.Success, + }); + // Test out a bad response code, ensure it doesnt cause throw + mockedRequests.push({ + endpoint: 'https://420.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: { error: 'bad request' }, + responseCode: StatusCodes.InternalError, + }); + // Test out a successful response code but an invalid order + mockedRequests.push({ + endpoint: 'https://421.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: { makerAssetData: '123' }, + responseCode: StatusCodes.Success, + }); + // A successful response code and valid order, but for wrong maker asset data + const wrongMakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ + makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), + takerAssetData, + }); + mockedRequests.push({ + endpoint: 'https://422.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: wrongMakerAssetDataOrder, + responseCode: StatusCodes.Success, + }); + // A successful response code and valid order, but for wrong taker asset data + const wrongTakerAssetDataOrder = testOrderFactory.generateTestSignedOrder({ + makerAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1), + }); + mockedRequests.push({ + endpoint: 'https://423.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: wrongTakerAssetDataOrder, + responseCode: StatusCodes.Success, + }); + // A successful response code and good order but its unsigned + const unsignedOrder = testOrderFactory.generateTestSignedOrder({ + makerAssetData, + takerAssetData, + feeRecipientAddress: '0x0000000000000000000000000000000000000002', + }); + delete unsignedOrder.signature; + mockedRequests.push({ + endpoint: 'https://424.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: unsignedOrder, + responseCode: StatusCodes.Success, + }); + + // Another Successful response + const successfulOrder2 = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData }); + mockedRequests.push({ + endpoint: 'https://37.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: successfulOrder2, + responseCode: StatusCodes.Success, + }); + + return rfqtMocker.withMockedRfqtFirmQuotes(mockedRequests, async () => { + const qr = new QuoteRequestor([ + 'https://1337.0.0.1', + 'https://420.0.0.1', + 'https://421.0.0.1', + 'https://422.0.0.1', + 'https://423.0.0.1', + 'https://424.0.0.1', + 'https://37.0.0.1', + ]); + const resp = await qr.requestRfqtFirmQuotesAsync( + makerAssetData, + takerAssetData, + new BigNumber(10000), + MarketOperation.Sell, + takerApiKey, + takerAddress, + ); + expect(resp.sort()).to.eql([successfulOrder1, successfulOrder2].sort()); + }); + }); + }); +}); diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index f19125053b..4cc23820ae 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -37,6 +37,10 @@ { "note": "Revert to older Curve Bridge (without Gas Tokens)", "pr": 2536 + }, + { + "note": "Updated Ganache's ERC20BridgeSampler address", + "pr": 2541 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 273b87f4f5..07c7af5e8c 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -153,7 +153,7 @@ "stakingProxy": "0x59adefa01843c627ba5d6aa350292b4b7ccae67a", "uniswapBridge": "0x0000000000000000000000000000000000000000", "eth2DaiBridge": "0x0000000000000000000000000000000000000000", - "erc20BridgeSampler": "0x0000000000000000000000000000000000000000", + "erc20BridgeSampler": "0x2c530e4ecc573f11bd72cf5fdf580d134d25f15f", "kyberBridge": "0x0000000000000000000000000000000000000000", "chaiBridge": "0x0000000000000000000000000000000000000000", "dydxBridge": "0x0000000000000000000000000000000000000000", diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index b2e44cd245..728ec259a5 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add `dexForwarderBridge` to migration output", "pr": 2525 + }, + { + "note": "Add `ERC20BridgeSampler` deployment", + "pr": 2541 } ] }, diff --git a/packages/migrations/package.json b/packages/migrations/package.json index d09d403b55..0d6377128a 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -11,7 +11,8 @@ "build": "tsc -b", "build:ci": "yarn build", "clean": "shx rm -rf lib ${npm_package_config_snapshot_name} ${npm_package_config_snapshot_name}-*.zip", - "lint": "tslint --format stylish --project .", + "prettier": "prettier --check 'src/**/*.{ts,tsx,json,md}' --config ../../.prettierrc", + "lint": "tslint --format stylish --project . && yarn prettier", "fix": "tslint --fix --format stylish --project .", "test": "yarn run_mocha", "test:circleci": "yarn test", @@ -28,7 +29,8 @@ "build:snapshot:docker": "docker build --tag ${npm_package_config_docker_snapshot_name}:${npm_package_version} --tag ${npm_package_config_docker_snapshot_name}:latest .", "publish:snapshot": "aws s3 cp ${npm_package_config_snapshot_name}-${npm_package_version}.zip ${npm_package_config_s3_snapshot_bucket} && aws s3 cp ${npm_package_config_s3_snapshot_bucket}/${npm_package_config_snapshot_name}-${npm_package_version}.zip ${npm_package_config_s3_snapshot_bucket}/${npm_package_config_snapshot_name}-latest.zip", "publish:snapshot:docker": "docker push ${npm_package_config_docker_snapshot_name}:latest && docker push ${npm_package_config_docker_snapshot_name}:${npm_package_version}", - "test_contract_configs": "node ./lib/test_contract_configs.js" + "test_contract_configs": "node ./lib/test_contract_configs.js", + "publish:private": "yarn build && gitpkg publish" }, "config": { "s3_snapshot_bucket": "s3://ganache-snapshots.0x.org", @@ -42,6 +44,9 @@ "0x-migrate": "bin/0x-migrate.js" }, "license": "Apache-2.0", + "gitpkg": { + "registry": "git@github.com:0xProject/gitpkg-registry.git" + }, "devDependencies": { "@0x/dev-utils": "^3.2.1", "@0x/ts-doc-gen": "^0.0.22", @@ -50,6 +55,7 @@ "@types/yargs": "^11.0.0", "chai": "^4.0.1", "dirty-chai": "^2.0.1", + "gitpkg": "https://github.com/0xProject/gitpkg.git", "make-promises-safe": "^1.1.0", "mocha": "^6.2.0", "npm-run-all": "^4.1.2", @@ -69,6 +75,7 @@ "@0x/contracts-dev-utils": "^1.3.3", "@0x/contracts-erc1155": "^2.1.5", "@0x/contracts-erc20": "^3.1.5", + "@0x/contracts-erc20-bridge-sampler": "^1.5.1", "@0x/contracts-erc721": "^3.1.5", "@0x/contracts-exchange": "^3.2.5", "@0x/contracts-exchange-forwarder": "^4.2.5", diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index ecd27803da..8b25d6ab3e 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -16,6 +16,10 @@ import { import { artifacts as devUtilsArtifacts, DevUtilsContract } from '@0x/contracts-dev-utils'; import { artifacts as erc1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155'; import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; +import { + artifacts as erc20BridgeSamplerArtifacts, + ERC20BridgeSamplerContract, +} from '@0x/contracts-erc20-bridge-sampler'; import { artifacts as erc721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721'; import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange'; import { artifacts as forwarderArtifacts, ForwarderContract } from '@0x/contracts-exchange-forwarder'; @@ -42,6 +46,7 @@ const allArtifacts = { ...exchangeArtifacts, ...forwarderArtifacts, ...stakingArtifacts, + ...erc20BridgeSamplerArtifacts, }; /** @@ -277,6 +282,14 @@ export async function runMigrationsAsync( etherToken.address, ); + const erc20BridgeSampler = await ERC20BridgeSamplerContract.deployFrom0xArtifactAsync( + erc20BridgeSamplerArtifacts.ERC20BridgeSampler, + provider, + txDefaults, + allArtifacts, + devUtils.address, + ); + const contractAddresses = { erc20Proxy: erc20Proxy.address, erc721Proxy: erc721Proxy.address, @@ -300,7 +313,7 @@ export async function runMigrationsAsync( uniswapBridge: constants.NULL_ADDRESS, eth2DaiBridge: constants.NULL_ADDRESS, kyberBridge: constants.NULL_ADDRESS, - erc20BridgeSampler: constants.NULL_ADDRESS, + erc20BridgeSampler: erc20BridgeSampler.address, chaiBridge: constants.NULL_ADDRESS, dydxBridge: constants.NULL_ADDRESS, curveBridge: constants.NULL_ADDRESS, diff --git a/yarn.lock b/yarn.lock index ea9130eae9..2e29c3acc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,6 +3069,13 @@ aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" +axios-mock-adapter@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.18.1.tgz#a2ba2638ef513d954793f96bde3e26bd4a1b7940" + dependencies: + fast-deep-equal "^3.1.1" + is-buffer "^2.0.3" + axios@^0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" @@ -3076,6 +3083,12 @@ axios@^0.18.0: follow-redirects "^1.3.0" is-buffer "^1.1.5" +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + dependencies: + follow-redirects "1.5.10" + babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -7353,6 +7366,10 @@ fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -7659,6 +7676,12 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.1" readable-stream "^2.0.4" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + dependencies: + debug "=3.1.0" + follow-redirects@^1.3.0: version "1.5.8" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.8.tgz#1dbfe13e45ad969f813e86c00e5296f525c885a1" @@ -9133,6 +9156,10 @@ is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" +is-buffer@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + is-buffer@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"