Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

RFQ-T Firm Quotes #162

Merged
merged 37 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d3cfe8e
test: Stop running contract migrations
feuGeneA Apr 6, 2020
05ca1b6
Add test of /swap/v0/quote endpoint
feuGeneA Mar 11, 2020
e026de2
Add and use RFQ-T configuration types
feuGeneA Mar 13, 2020
0a0de81
yarn clean: separate docker from ts
feuGeneA Mar 31, 2020
89ff735
Only run prettier on ./src
feuGeneA Apr 7, 2020
61cbd5c
Include prettier in lint
feuGeneA Apr 7, 2020
268bff8
yarn add --dev make-promises-safe
feuGeneA Apr 7, 2020
2578a79
Adapt to new RFQ-T namespace in SwapQuoterOpts
feuGeneA Apr 9, 2020
6b21a39
Adapt to new RFQ-T namespace in SwapQuoterReqOpts
feuGeneA Apr 9, 2020
d11b850
Revert addition of env var defs to `yarn dev`
feuGeneA Apr 9, 2020
b126316
Don't enforce presence of taker address
feuGeneA Apr 9, 2020
cbecfc4
Enforce a specific value for intentOnFilling
feuGeneA Apr 9, 2020
902b15d
Remove unused apiKey parameter
feuGeneA Apr 9, 2020
bdc7bae
Add `rfqt:` namespace for RFQ-T specific options
feuGeneA Apr 9, 2020
47227ad
Parse RFQT environment variables as CSV, not JSON
feuGeneA Apr 10, 2020
0255b25
Set RFQT taker address to Forwarder for ETH sales
feuGeneA Apr 10, 2020
29d6215
Restore accidentally deleted line in recent commit.
feuGeneA Apr 10, 2020
b0488ef
Add metadata for dummy kovan tokens
feuGeneA Apr 10, 2020
e50fc98
introduce new test using rfqtMocker
Apr 10, 2020
e150eec
HACK: fix ganache-core bug
feuGeneA Apr 7, 2020
7c448d3
TEMPORARY: use gene's ganache snapshot for tests
feuGeneA Apr 6, 2020
08f4d31
TEMPORARY: Update gitpkg monorepo references
feuGeneA Apr 6, 2020
f325f42
Enable prettier for {.,test}/**/*.{ts,tsx,json,md}
feuGeneA Apr 15, 2020
ea8c8e9
Add SwapQuoteRequestParam `skipValidation`
feuGeneA Apr 15, 2020
0633aca
Add API Key and `intentOnFilling` to API schema
feuGeneA Apr 15, 2020
556a45b
Parse present but empty intentOnFilling as true
feuGeneA Apr 15, 2020
e1693bb
Fix bug: env var csv split was wrongly applied
feuGeneA Apr 15, 2020
11cb5a8
RFQT: More tests (#170)
Apr 16, 2020
bb2814a
Validate bool query params as enum, not presence
feuGeneA Apr 16, 2020
74d3ea7
Add comments around using Forwarder as Taker
feuGeneA Apr 16, 2020
6a0aed8
Test no quote provided when taker address absent
feuGeneA Apr 16, 2020
fb176a5
Revert "TEMPORARY: Update gitpkg monorepo references"
feuGeneA Apr 16, 2020
2e7c7bb
Revert "TEMPORARY: use gene's ganache snapshot for tests"
feuGeneA Apr 16, 2020
3ed896f
Pin updated monorepo deps to development revision
feuGeneA Apr 16, 2020
c772d43
Pin tests' ganache snapshot to monorepo commit
feuGeneA Apr 16, 2020
735fe3e
Simplify expressions parsing boolean query params
feuGeneA Apr 16, 2020
bf835d0
Merge branch 'master' into rfqt
feuGeneA Apr 16, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ jobs:
WS_RPC_ADDR: '0.0.0.0:60557'
HTTP_RPC_ADDR: '0.0.0.0:60556'
ETHEREUM_RPC_MAX_REQUESTS_PER_24_HR_UTC: '150000'
- image: 883408475785.dkr.ecr.us-east-1.amazonaws.com/neptune/quoter:402d78919b45fe257cc1f9f5c956c0ee2d95a254
aws_auth:
aws_access_key_id: $AWS_ACCESS_KEY_ID
aws_secret_access_key: $AWS_SECRET_ACCESS_KEY
environment:
ALLOWED_API_KEYS: 'koolApiKey1,koolApikey2'
SELL_TOKEN_ADDRESS: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082'
BUY_TOKEN_ADDRESS: '0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c'
QUOTE_STRATEGY: 'demo_one_to_one'
ORDER_EXPIRY: 10
ETH_FROM: '0x5409ED021D9299bf6814279A6A1411A7e866A631'
CHAIN_ID: 1337
RPC_URL: 'http://0.0.0.0:8545'
AUTH_METHOD: 'other'
PRIVATE_KEY: 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d'
POSTGRES_CONNECTION_STRING: 'whatever. quoter calls rebalancer now, which demands this.'
REDIS_URL: 'whatever. not used'
working_directory: ~/repo
environment:
ETHEREUM_RPC_URL: http://0.0.0.0:8545
Expand Down
22 changes: 22 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,25 @@ services:
ETHEREUM_RPC_URL: '${ETHEREUM_RPC_URL}'
CHAIN_ID: '${CHAIN_ID}'
POSTGRES_URI: 'postgresql://api:api@postgres/api'
quoter:
image: 883408475785.dkr.ecr.us-east-1.amazonaws.com/neptune/quoter:402d78919b45fe257cc1f9f5c956c0ee2d95a254
depends_on:
- ganache
restart: on-failure
environment:
ALLOWED_API_KEYS: 'koolApiKey1,koolApikey2'
SELL_TOKEN_ADDRESS: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082'
BUY_TOKEN_ADDRESS: '0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c'
QUOTE_STRATEGY: 'demo_one_to_one'
ORDER_EXPIRY: 10
ETH_FROM: '0x5409ED021D9299bf6814279A6A1411A7e866A631'
CHAIN_ID: 1337
RPC_URL: 'http://ganache:8545'
AUTH_METHOD: 'other'
PRIVATE_KEY: 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d'
POSTGRES_CONNECTION_STRING: 'whatever. quoter calls rebalancer now, which demands this.'
REDIS_URL: 'whatever. not used'
ports:
- '4000:4000'
command: |
sh -c "waitForGanache () { until curl -s -d '{\"method\":\"net_listening\"}' http://ganache:8545 | grep true; do continue; done }; waitForGanache && cd quoter; yarn start"
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"scripts": {
"clean": "shx rm -rf lib 0x_mesh/db",
"build": "tsc -p tsconfig.json",
"test": "ETHEREUM_RPC_URL=http://localhost:8545 mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 100000 --exit",
"dev": "nodemon -r dotenv/config src/index.ts | pino-pretty",
"test": "ETHEREUM_RPC_URL=http://localhost:8545 CHAIN_ID=1337 RFQT_API_KEY_WHITELIST='[\"koolApiKey1\"]' RFQT_MAKER_ENDPOINTS='[\"http://localhost:4000\"]' mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 100000 --exit",
"dev": "ETHEREUM_RPC_URL=http://localhost:8545 CHAIN_ID=1337 RFQT_API_KEY_WHITELIST='[\"koolApiKey1\"]' RFQT_MAKER_ENDPOINTS='[\"http://localhost:4000\"]' nodemon -r dotenv/config src/index.ts | pino-pretty",
"dev:service:http": "nodemon -r dotenv/config src/runners/http_service_runner.ts | pino-pretty",
"dev:service:sra_http": "nodemon -r dotenv/config src/runners/http_sra_service_runner.ts | pino-pretty",
"dev:service:staking_http": "nodemon -r dotenv/config src/runners/http_staking_service_runner.ts | pino-pretty",
Expand All @@ -24,15 +24,15 @@
"start:service:staking_http": "node -r dotenv/config lib/src/runners/http_staking_service_runner.js",
"start:service:swap_http": "node -r dotenv/config lib/src/runners/http_swap_service_runner.js",
"start:service:order_watcher": "node -r dotenv/config lib/src/runners/order_watcher_service_runner.js",
"lint": "tslint --project . --format stylish"
"lint": "tslint --project . --format stylish",
"docker:login": "eval $(aws ecr get-login --no-include-email --region \"us-east-1\") # to pull private quoter repo. see https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login.html"
},
"resolutions": {
"@0x/contract-addresses": "0xProject/gitpkg-registry#0x-contract-addresses-v4.9.0-58b6c25d5",
"@0x/contract-wrappers": "0xProject/gitpkg-registry#0x-contract-wrappers-v13.6.3-e3f3bb670"
},
"devDependencies": {
"@0x/dev-utils": "^3.1.1",
"@0x/migrations": "^5.0.2",
"@0x/tslint-config": "^4.0.0",
"@0x/types": "^3.1.1",
"@0x/typescript-typings": "^5.0.1",
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export const LIQUIDITY_POOL_REGISTRY_ADDRESS: string | undefined = _.isEmpty(
EnvVarType.ETHAddressHex,
);

export const RFQT_API_KEY_WHITELIST: string[] = JSON.parse(process.env.RFQT_API_KEY_WHITELIST || '[]');

export const RFQT_MAKER_ENDPOINTS: string[] = JSON.parse(process.env.RFQT_MAKER_ENDPOINTS || '[]');

// Max number of entities per page
export const MAX_PER_PAGE = 1000;
// Default ERC20 token precision
Expand Down
6 changes: 6 additions & 0 deletions src/handlers/swap_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class SwapHandlers {
gasPrice,
excludedSources,
affiliateAddress,
intentOnFilling,
} = parseGetSwapQuoteRequestParams(req);
const isETHSell = isETHSymbol(sellToken);
const sellTokenAddress = findTokenAddressOrThrowApiError(sellToken, 'sellToken', CHAIN_ID);
Expand Down Expand Up @@ -63,6 +64,8 @@ export class SwapHandlers {
gasPrice,
excludedSources,
affiliateAddress,
apiKey: takerAddress && takerAddress !== NULL_ADDRESS ? req.header('0x-api-key') : undefined,
intentOnFilling,
});
res.status(HttpStatus.OK).send(swapQuote);
} catch (e) {
Expand Down Expand Up @@ -166,6 +169,8 @@ const parseGetSwapQuoteRequestParams = (req: express.Request): GetSwapQuoteReque
? undefined
: parseStringArrForERC20BridgeSources(req.query.excludedSources.split(','));
const affiliateAddress = req.query.affiliateAddress;
// tslint:disable-next-line:boolean-naming
const intentOnFilling = req.query.intentOnFilling === undefined ? false : true;
return {
takerAddress,
sellToken,
Expand All @@ -176,5 +181,6 @@ const parseGetSwapQuoteRequestParams = (req: express.Request): GetSwapQuoteReque
gasPrice,
excludedSources,
affiliateAddress,
intentOnFilling,
};
};
9 changes: 9 additions & 0 deletions src/services/swap_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
CHAIN_ID,
FEE_RECIPIENT_ADDRESS,
LIQUIDITY_POOL_REGISTRY_ADDRESS,
RFQT_API_KEY_WHITELIST,
RFQT_MAKER_ENDPOINTS,
} from '../config';
import {
DEFAULT_TOKEN_DECIMALS,
Expand Down Expand Up @@ -51,6 +53,8 @@ export class SwapService {
chainId: CHAIN_ID,
expiryBufferMs: QUOTE_ORDER_EXPIRATION_BUFFER_MS,
liquidityProviderRegistryAddress: LIQUIDITY_POOL_REGISTRY_ADDRESS,
rfqtTakerApiKeyWhitelist: RFQT_API_KEY_WHITELIST,
rfqtMakerEndpoints: RFQT_MAKER_ENDPOINTS,
};
this._swapQuoter = new SwapQuoter(this._provider, orderbook, swapQuoterOpts);
this._swapQuoteConsumer = new SwapQuoteConsumer(this._provider, swapQuoterOpts);
Expand All @@ -69,13 +73,18 @@ export class SwapService {
from,
excludedSources,
affiliateAddress,
apiKey,
intentOnFilling,
} = params;
const assetSwapperOpts = {
...ASSET_SWAPPER_MARKET_ORDERS_OPTS,
slippagePercentage,
bridgeSlippage: slippagePercentage,
gasPrice: providedGasPrice,
excludedSources, // TODO(dave4506): overrides the excluded sources selected by chainId
apiKey,
intentOnFilling,
takerAddress: from,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider not conflating from and takerAddress. As you discovered, this is sometimes not often the case. I.e Via forwarder takerAddress is the Forwarder, from is the end user.

What happens when the Forwarder is used? the takerAddress in the order MUST be the Forwarder contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to account for this here: 0255b25. Does that address the issue?

};
if (sellAmount !== undefined) {
swapQuote = await this._swapQuoter.getMarketSellSwapQuoteAsync(
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ export interface GetSwapQuoteRequestParams {
gasPrice?: BigNumber;
excludedSources?: ERC20BridgeSource[];
affiliateAddress?: string;
apiKey?: string;
intentOnFilling?: boolean;
}

export interface CalculateSwapQuoteParams {
Expand All @@ -355,6 +357,8 @@ export interface CalculateSwapQuoteParams {
gasPrice?: BigNumber;
excludedSources?: ERC20BridgeSource[];
affiliateAddress?: string;
apiKey?: string;
intentOnFilling?: boolean;
}

export interface GetSwapQuoteResponseLiquiditySource {
Expand Down
66 changes: 62 additions & 4 deletions test/app_test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { ERC20TokenContract, WETH9Contract } from '@0x/contract-wrappers';
import { BlockchainLifecycle, web3Factory } from '@0x/dev-utils';
import { runMigrationsOnceAsync } from '@0x/migrations';
import { Web3ProviderEngine } from '@0x/subproviders';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as HttpStatus from 'http-status-codes';
import 'mocha';
import * as request from 'supertest';

import { getAppAsync, getDefaultAppDependenciesAsync } from '../src/app';
import * as config from '../src/config';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH, SWAP_PATH } from '../src/constants';

import { expect } from './utils/expect';

Expand All @@ -32,10 +34,9 @@ describe('app test', () => {
blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
await blockchainLifecycle.startAsync();
accounts = await web3Wrapper.getAvailableAddressesAsync();
const owner = accounts[0];
await runMigrationsOnceAsync(provider, { from: owner });

const dependencies = await getDefaultAppDependenciesAsync(provider, config);

// start the 0x-api app
app = await getAppAsync({ ...dependencies }, config);
});
Expand All @@ -54,4 +55,61 @@ describe('app test', () => {
expect(response.body.records).to.deep.equal([]);
});
});
describe('should respond to GET /swap/quote', () => {
it("with INSUFFICIENT_ASSET_LIQUIDITY when there's no liquidity (empty orderbook, sampling excluded, no RFQ)", async () => {
await request(app)
.get(
`${SWAP_PATH}/quote?buyToken=DAI&sellToken=WETH&buyAmount=100000000000000000&excludedSources=Uniswap,Eth2Dai,Kyber,LiquidityProvider`,
)
.expect(HttpStatus.BAD_REQUEST)
.expect('Content-Type', /json/)
.then(response => {
const responseJson = JSON.parse(response.text);
expect(responseJson.reason).to.equal('Validation Failed');
expect(responseJson.validationErrors.length).to.equal(1);
expect(responseJson.validationErrors[0].field).to.equal('buyAmount');
expect(responseJson.validationErrors[0].reason).to.equal('INSUFFICIENT_ASSET_LIQUIDITY');
});
});
});
describe('should hit RFQ-T when apropriate', () => {
it('should get a quote from an RFQ-T provider', async () => {
// the 0xorg/test-quoter is running and serving RFQT quotes, using accounts[0] as its maker address.
// the API will exclude unfillable RFQ-T orders, so we need to set the maker's balances and allowances.
const [makerAddress, takerAddress] = accounts;
const sellAmount = new BigNumber(100000000000000000);

const contractAddresses: ContractAddresses = getContractAddressesForChainOrThrow(
parseInt(process.env.CHAIN_ID || '1337', 10),
);

const wethContract = new WETH9Contract(contractAddresses.etherToken, provider);
await wethContract.deposit().sendTransactionAsync({ value: sellAmount, from: takerAddress });
await wethContract
.approve(contractAddresses.erc20Proxy, sellAmount)
.sendTransactionAsync({ from: takerAddress });

const zrxToken = new ERC20TokenContract(contractAddresses.zrxToken, provider);
await zrxToken.approve(contractAddresses.erc20Proxy, sellAmount).sendTransactionAsync(
// using buyAmount based on assumption that the RFQ-T provider will be using a "one-to-one" strategy.
{ from: makerAddress },
);
// done setting balances and allowances

await request(app)
.get(
`${SWAP_PATH}/quote?buyToken=ZRX&sellToken=WETH&sellAmount=${sellAmount.toString()}&takerAddress=${takerAddress}&intentOnFilling=true&excludedSources=Uniswap,Eth2Dai,Kyber,LiquidityProvider`,
)
.set('0x-api-key', 'koolApiKey1')
.expect(HttpStatus.OK)
.expect('Content-Type', /json/)
.then(response => {
const responseJson = JSON.parse(response.text);
expect(responseJson.orders.length).to.equal(1); // the one from 0xorg/test-quoter
expect(responseJson.orders[0].takerAddress.toLowerCase()).to.equal(takerAddress);
expect(responseJson.orders[0].makerAddress.toLowerCase()).to.equal(makerAddress);
expect(responseJson.orders[0].takerAssetAmount).to.equal(sellAmount.toString());
});
});
});
});