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

Commit 965dedb

Browse files
authored
feat: Rfqt included (#293)
* added initial implementation and unit tests * completed implementation * Upgrades Asset Swapper * fixed typos * remove test filter * linting and prettifying * started addressing feedback * refactor if-else * type changes * inting fixing * Added some RFQ tests * fixes * trailing comma
1 parent 0e44de9 commit 965dedb

9 files changed

Lines changed: 308 additions & 19 deletions

File tree

src/config.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
DEFAULT_LOCAL_POSTGRES_URI,
2424
DEFAULT_LOGGER_INCLUDE_TIMESTAMP,
2525
DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE,
26-
DEFAULT_RFQT_SKIP_BUY_REQUESTS,
2726
NULL_ADDRESS,
2827
NULL_BYTES,
2928
QUOTE_ORDER_EXPIRATION_BUFFER_MS,
@@ -192,10 +191,6 @@ export const RFQT_MAKER_ASSET_OFFERINGS: RfqtMakerAssetOfferings = _.isEmpty(pro
192191
);
193192

194193
// tslint:disable-next-line:boolean-naming
195-
export const RFQT_SKIP_BUY_REQUESTS: boolean = _.isEmpty(process.env.RFQT_SKIP_BUY_REQUESTS)
196-
? DEFAULT_RFQT_SKIP_BUY_REQUESTS
197-
: assertEnvVarType('RFQT_SKIP_BUY_REQUESTS', process.env.RFQT_SKIP_BUY_REQUESTS, EnvVarType.Boolean);
198-
199194
export const RFQT_REQUEST_MAX_RESPONSE_MS = 600;
200195

201196
// Whitelisted 0x API keys that can use the meta-txn /submit endpoint

src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export const PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS = 6000;
4040
export const UNWRAP_QUOTE_GAS = new BigNumber(60000);
4141
export const WRAP_QUOTE_GAS = UNWRAP_QUOTE_GAS;
4242
export const ONE_GWEI = new BigNumber(1000000000);
43-
export const DEFAULT_RFQT_SKIP_BUY_REQUESTS = false;
4443

4544
// API namespaces
4645
export const SRA_PATH = '/sra/v3';

src/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,16 @@ export enum ValidationErrorCodes {
131131
InvalidOrder = 1007,
132132
InternalError = 1008,
133133
TokenNotSupported = 1009,
134+
FieldInvalid = 1010,
134135
}
135136

136137
export enum ValidationErrorReasons {
137138
PercentageOutOfRange = 'MUST_BE_LESS_THAN_OR_EQUAL_TO_ONE',
139+
ConflictingFilteringArguments = 'CONFLICTING_FILTERING_ARGUMENTS',
140+
ArgumentNotYetSupported = 'ARGUMENT_NOT_YET_SUPPORTED',
141+
InvalidApiKey = 'INVALID_API_KEY',
142+
TakerAddressInvalid = 'TAKER_ADDRESS_INVALID',
143+
RequiresIntentOnFilling = 'REQUIRES_INTENT_ON_FILLING',
138144
}
139145
export abstract class AlertError {
140146
public abstract message: string;

src/handlers/swap_handlers.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { SwapQuoterError } from '@0x/asset-swapper';
1+
import { RfqtRequestOpts, SwapQuoterError } from '@0x/asset-swapper';
22
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
33
import * as express from 'express';
44
import * as HttpStatus from 'http-status-codes';
55

6-
import { CHAIN_ID } from '../config';
6+
import { CHAIN_ID, RFQT_API_KEY_WHITELIST } from '../config';
77
import {
88
DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE,
99
MARKET_DEPTH_DEFAULT_DISTRIBUTION,
@@ -220,6 +220,7 @@ export class SwapHandlers {
220220
: {
221221
intentOnFilling: rfqt.intentOnFilling,
222222
isIndicative: rfqt.isIndicative,
223+
nativeExclusivelyRFQT: rfqt.nativeExclusivelyRFQT,
223224
},
224225
skipValidation,
225226
swapVersion,
@@ -295,17 +296,28 @@ const parseGetSwapQuoteRequestParams = (
295296
},
296297
]);
297298
}
298-
const excludedSources =
299-
req.query.excludedSources === undefined
300-
? undefined
301-
: parseUtils.parseStringArrForERC20BridgeSources((req.query.excludedSources as string).split(','));
302-
const affiliateAddress = req.query.affiliateAddress as string;
299+
303300
const apiKey = req.header('0x-api-key');
304-
const rfqt =
301+
// tslint:disable-next-line: boolean-naming
302+
const { excludedSources, nativeExclusivelyRFQT } = parseUtils.parseRequestForExcludedSources(
303+
{
304+
excludedSources: req.query.excludedSources as string | undefined,
305+
includedSources: req.query.includedSources as string | undefined,
306+
intentOnFilling: req.query.intentOnFilling as string | undefined,
307+
takerAddress,
308+
apiKey,
309+
},
310+
RFQT_API_KEY_WHITELIST,
311+
endpoint,
312+
);
313+
314+
const affiliateAddress = req.query.affiliateAddress as string;
315+
const rfqt: Pick<RfqtRequestOpts, 'intentOnFilling' | 'isIndicative' | 'nativeExclusivelyRFQT'> =
305316
takerAddress && apiKey
306317
? {
307318
intentOnFilling: endpoint === 'quote' && req.query.intentOnFilling === 'true',
308319
isIndicative: endpoint === 'price',
320+
nativeExclusivelyRFQT,
309321
}
310322
: undefined;
311323
// tslint:disable-next-line:boolean-naming

src/schemas/swap_quote_request_schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"excludedSources": {
2323
"type": "string"
2424
},
25-
"apiKey": {
25+
"includedSources": {
2626
"type": "string"
2727
},
2828
"intentOnFilling": {

src/types.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,10 +496,7 @@ export interface GetSwapQuoteRequestParams {
496496
gasPrice?: BigNumber;
497497
excludedSources?: ERC20BridgeSource[];
498498
affiliateAddress?: string;
499-
rfqt?: {
500-
intentOnFilling?: boolean;
501-
isIndicative?: boolean;
502-
};
499+
rfqt?: Pick<RfqtRequestOpts, 'intentOnFilling' | 'isIndicative' | 'nativeExclusivelyRFQT'>;
503500
skipValidation: boolean;
504501
apiKey?: string;
505502
}

src/utils/parse_utils.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert } from '@0x/assert';
22
import { ERC20BridgeSource } from '@0x/asset-swapper';
33

4+
import { ValidationError, ValidationErrorCodes, ValidationErrorReasons } from '../errors';
45
import {
56
MetaTransactionDailyLimiterConfig,
67
MetaTransactionRateLimitConfig,
@@ -10,7 +11,124 @@ import {
1011

1112
import { AvailableRateLimiter, DatabaseKeysUsedForRateLimiter, RollingLimiterIntervalUnit } from './rate-limiters';
1213

14+
interface ParseRequestForExcludedSourcesParams {
15+
takerAddress?: string;
16+
excludedSources?: string;
17+
includedSources?: string;
18+
intentOnFilling?: string;
19+
apiKey?: string;
20+
}
21+
22+
/**
23+
* This constant contains, as keys, all ERC20BridgeSource types except from `Native`.
24+
* As we add more bridge sources to AssetSwapper, we want to keep ourselves accountable to add
25+
* them to this constant. Since there isn't a good way to enumerate over enums, we use a obect type.
26+
* The type has been defined in a way that the code won't compile if a new ERC20BridgeSource is added.
27+
*/
28+
const ALL_EXCEPT_NATIVE: { [key in Exclude<ERC20BridgeSource, ERC20BridgeSource.Native>]: boolean } = {
29+
Uniswap: true,
30+
Balancer: true,
31+
Curve: true,
32+
Eth2Dai: true,
33+
Kyber: true,
34+
LiquidityProvider: true,
35+
MultiBridge: true,
36+
Uniswap_V2: true,
37+
};
38+
1339
export const parseUtils = {
40+
parseRequestForExcludedSources(
41+
request: ParseRequestForExcludedSourcesParams,
42+
validApiKeys: string[],
43+
endpoint: 'price' | 'quote',
44+
): { excludedSources: ERC20BridgeSource[]; nativeExclusivelyRFQT: boolean } {
45+
// Ensure that both filtering arguments cannot be present.
46+
if (request.excludedSources !== undefined && request.includedSources !== undefined) {
47+
throw new ValidationError([
48+
{
49+
field: 'excludedSources',
50+
code: ValidationErrorCodes.IncorrectFormat,
51+
reason: ValidationErrorReasons.ConflictingFilteringArguments,
52+
},
53+
{
54+
field: 'includedSources',
55+
code: ValidationErrorCodes.IncorrectFormat,
56+
reason: ValidationErrorReasons.ConflictingFilteringArguments,
57+
},
58+
]);
59+
}
60+
61+
// If excludedSources is present, parse the string array and return
62+
if (request.excludedSources !== undefined) {
63+
return {
64+
excludedSources: parseUtils.parseStringArrForERC20BridgeSources(request.excludedSources.split(',')),
65+
nativeExclusivelyRFQT: false,
66+
};
67+
}
68+
69+
if (request.includedSources !== undefined) {
70+
// Only RFQT is eligible as of now
71+
if (request.includedSources === 'RFQT') {
72+
// We assume that if a `takerAddress` key is present, it's value was already validated by the JSON
73+
// schema.
74+
if (request.takerAddress === undefined) {
75+
throw new ValidationError([
76+
{
77+
field: 'takerAddress',
78+
code: ValidationErrorCodes.RequiredField,
79+
reason: ValidationErrorReasons.TakerAddressInvalid,
80+
},
81+
]);
82+
}
83+
84+
// We enforce a valid API key - we don't want to fail silently.
85+
if (request.apiKey === undefined) {
86+
throw new ValidationError([
87+
{
88+
field: '0x-api-key',
89+
code: ValidationErrorCodes.RequiredField,
90+
reason: ValidationErrorReasons.InvalidApiKey,
91+
},
92+
]);
93+
}
94+
if (!validApiKeys.includes(request.apiKey)) {
95+
throw new ValidationError([
96+
{
97+
field: '0x-api-key',
98+
code: ValidationErrorCodes.FieldInvalid,
99+
reason: ValidationErrorReasons.InvalidApiKey,
100+
},
101+
]);
102+
}
103+
104+
// If the user is requesting a firm quote, we want to make sure that `intentOnFilling` is set to "true".
105+
if (endpoint === 'quote' && request.intentOnFilling !== 'true') {
106+
throw new ValidationError([
107+
{
108+
field: 'intentOnFilling',
109+
code: ValidationErrorCodes.RequiredField,
110+
reason: ValidationErrorReasons.RequiresIntentOnFilling,
111+
},
112+
]);
113+
}
114+
115+
return {
116+
nativeExclusivelyRFQT: true,
117+
excludedSources: Object.keys(ALL_EXCEPT_NATIVE) as ERC20BridgeSource[],
118+
};
119+
} else {
120+
throw new ValidationError([
121+
{
122+
field: 'includedSources',
123+
code: ValidationErrorCodes.IncorrectFormat,
124+
reason: ValidationErrorReasons.ArgumentNotYetSupported,
125+
},
126+
]);
127+
}
128+
}
129+
130+
return { excludedSources: [], nativeExclusivelyRFQT: false };
131+
},
14132
parseStringArrForERC20BridgeSources(excludedSources: string[]): ERC20BridgeSource[] {
15133
// Need to compare value of the enum instead of the key, as values are used by asset-swapper
16134
// CurveUsdcDaiUsdt = 'Curve_USDC_DAI_USDT' is excludedSources=Curve_USDC_DAI_USDT

test/parse_utils_test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { ERC20BridgeSource } from '@0x/asset-swapper';
2+
import { expect } from '@0x/contracts-test-utils';
3+
import { NULL_ADDRESS } from '@0x/utils';
4+
import 'mocha';
5+
6+
import { parseUtils } from '../src/utils/parse_utils';
7+
8+
const SUITE_NAME = 'parseUtils';
9+
10+
describe(SUITE_NAME, () => {
11+
it('raises a ValidationError if includedSources is anything else than RFQT', async () => {
12+
expect(() => {
13+
parseUtils.parseRequestForExcludedSources(
14+
{
15+
includedSources: 'Uniswap',
16+
},
17+
[],
18+
'price',
19+
);
20+
}).throws();
21+
});
22+
23+
it('raises a ValidationError if includedSources is RFQT and a taker is not specified', async () => {
24+
expect(() => {
25+
parseUtils.parseRequestForExcludedSources(
26+
{
27+
includedSources: 'RFQT',
28+
},
29+
[],
30+
'price',
31+
);
32+
}).throws();
33+
});
34+
35+
it('raises a ValidationError if API keys are not present or valid', async () => {
36+
expect(() => {
37+
parseUtils.parseRequestForExcludedSources(
38+
{
39+
includedSources: 'RFQT',
40+
takerAddress: NULL_ADDRESS,
41+
apiKey: 'foo',
42+
},
43+
['lorem', 'ipsum'],
44+
'price',
45+
);
46+
}).throws();
47+
});
48+
49+
it('returns excludedSources correctly when excludedSources is present', async () => {
50+
// tslint:disable-next-line: boolean-naming
51+
const { excludedSources, nativeExclusivelyRFQT } = parseUtils.parseRequestForExcludedSources(
52+
{
53+
excludedSources: 'Uniswap,Kyber',
54+
},
55+
[],
56+
'price',
57+
);
58+
expect(excludedSources[0]).to.eql(ERC20BridgeSource.Uniswap);
59+
expect(excludedSources[1]).to.eql(ERC20BridgeSource.Kyber);
60+
expect(nativeExclusivelyRFQT).to.eql(false);
61+
});
62+
63+
it('returns empty array if no includedSources and excludedSources are present', async () => {
64+
// tslint:disable-next-line: boolean-naming
65+
const { excludedSources, nativeExclusivelyRFQT } = parseUtils.parseRequestForExcludedSources({}, [], 'price');
66+
expect(excludedSources.length).to.eql(0);
67+
expect(nativeExclusivelyRFQT).to.eql(false);
68+
});
69+
70+
it('returns excludedSources correctly when includedSources=RFQT', async () => {
71+
// tslint:disable-next-line: boolean-naming
72+
const { excludedSources, nativeExclusivelyRFQT } = parseUtils.parseRequestForExcludedSources(
73+
{
74+
includedSources: 'RFQT',
75+
takerAddress: NULL_ADDRESS,
76+
apiKey: 'ipsum',
77+
},
78+
['lorem', 'ipsum'],
79+
'price',
80+
);
81+
expect(nativeExclusivelyRFQT).to.eql(true);
82+
83+
// Ensure that all sources of liquidity are excluded aside from `Native`.
84+
const allPossibleSources: Set<ERC20BridgeSource> = new Set(
85+
Object.keys(ERC20BridgeSource).map(s => ERC20BridgeSource[s]),
86+
);
87+
for (const source of excludedSources) {
88+
allPossibleSources.delete(source);
89+
}
90+
const allPossibleSourcesArray = Array.from(allPossibleSources);
91+
expect(allPossibleSourcesArray.length).to.eql(1);
92+
expect(allPossibleSourcesArray[0]).to.eql(ERC20BridgeSource.Native);
93+
});
94+
95+
it('raises a ValidationError if includedSources and excludedSources are both present', async () => {
96+
expect(() => {
97+
parseUtils.parseRequestForExcludedSources(
98+
{
99+
excludedSources: 'Native',
100+
includedSources: 'RFQT',
101+
},
102+
[],
103+
'price',
104+
);
105+
}).throws();
106+
});
107+
108+
it('raises a ValidationError if a firm quote is requested and "intentOnFilling" is not set to "true"', async () => {
109+
expect(() => {
110+
parseUtils.parseRequestForExcludedSources(
111+
{
112+
includedSources: 'RFQT',
113+
takerAddress: NULL_ADDRESS,
114+
apiKey: 'ipsum',
115+
},
116+
['lorem', 'ipsum'],
117+
'quote',
118+
);
119+
}).throws();
120+
});
121+
});

0 commit comments

Comments
 (0)