Compare commits

..

7 Commits

Author SHA1 Message Date
Github Actions
9ce090c8cd Publish
- @0x/asset-swapper@16.65.0
2022-08-01 22:10:47 +00:00
Github Actions
980d60deb8 Updated CHANGELOGS & MD docs 2022-08-01 22:10:42 +00:00
Kyu
d6d79e51e7 Use 0x gas api instead of eth gas station api [TKR-502] (#532)
* Use 0x gas api instead of eth gas station api

* Add integration test for `ProtocolFeeUtils`

* Update CHANGELOG.json
2022-08-01 14:42:45 -07:00
Kyu
3ef5de93bb Remove getBidAskLiquidityForMakerTakerAssetPairAsync (#528) 2022-07-28 09:26:07 -07:00
Kyu
ab7dc33ca4 Refactor PoolsCache (part 2) [TKR-500] (#526)
* Introduce NoOpPoolsCache and use it in unsupported chains for BeethovenX

* Use `NoOpPoolsCache` for `CreamPoolsCache` and `BalancerPoolsCache` on unsupported chains
2022-07-28 09:12:02 -07:00
Kyu
14dcee5bb6 Refactor PoolsCache (part 1) [TKR-500] (#525)
* Make _refreshPoolCacheIfRequiredAsync type-safe and remove Promise.all

* Factor out PoolsCache key logic into a function

* Use Map instead of object in PoolsCache and increase the default timeout

* Clean up PoolsCache and simplify its public interface
2022-07-28 09:04:42 -07:00
Pavel
9856e78609 Update reference.mdx (#531)
Align with `DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE` value from c74e31c219/src/constants.ts (L26)
2022-07-27 12:35:16 -07:00
24 changed files with 1008 additions and 249 deletions

View File

@@ -1,4 +1,14 @@
[ [
{
"version": "16.65.0",
"changes": [
{
"note": "Use 0x gas api instead of eth gas station api",
"pr": 532
}
],
"timestamp": 1659391840
},
{ {
"version": "16.64.0", "version": "16.64.0",
"changes": [ "changes": [

View File

@@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG CHANGELOG
## v16.65.0 - _August 1, 2022_
* Use 0x gas api instead of eth gas station api (#532)
## v16.64.0 - _July 27, 2022_ ## v16.64.0 - _July 27, 2022_
* Refactor `TokenAdjacency` and `TokenAdjacencyBuilder` (#517) * Refactor `TokenAdjacency` and `TokenAdjacencyBuilder` (#517)

View File

@@ -1897,7 +1897,7 @@ ___
# Interface: SwapQuoteRequestOpts # Interface: SwapQuoteRequestOpts
slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%). slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.01 (1%).
gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount

View File

@@ -1,6 +1,6 @@
{ {
"name": "@0x/asset-swapper", "name": "@0x/asset-swapper",
"version": "16.64.0", "version": "16.65.0",
"engines": { "engines": {
"node": ">=6.12" "node": ">=6.12"
}, },
@@ -109,6 +109,7 @@
"dirty-chai": "^2.0.1", "dirty-chai": "^2.0.1",
"gitpkg": "https://github.com/0xProject/gitpkg.git", "gitpkg": "https://github.com/0xProject/gitpkg.git",
"mocha": "^6.2.0", "mocha": "^6.2.0",
"msw": "^0.44.2",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"nyc": "^11.0.1", "nyc": "^11.0.1",
"shx": "^0.2.2", "shx": "^0.2.2",

View File

@@ -19,7 +19,7 @@ import {
DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID, DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID,
} from './utils/market_operation_utils/constants'; } from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json'; const ZERO_EX_GAS_API_URL = 'https://gas.api.0x.org/source/median';
const NULL_BYTES = '0x'; const NULL_BYTES = '0x';
const NULL_ERC20_ASSET_DATA = '0xf47261b00000000000000000000000000000000000000000000000000000000000000000'; const NULL_ERC20_ASSET_DATA = '0xf47261b00000000000000000000000000000000000000000000000000000000000000000';
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
@@ -48,7 +48,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
orderRefreshIntervalMs: 10000, // 10 seconds orderRefreshIntervalMs: 10000, // 10 seconds
...DEFAULT_ORDER_PRUNER_OPTS, ...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 500e6, samplerGasLimit: 500e6,
ethGasStationUrl: ETH_GAS_STATION_API_URL, zeroExGasApiUrl: ZERO_EX_GAS_API_URL,
rfqt: { rfqt: {
integratorsWhitelist: [], integratorsWhitelist: [],
makerAssetOfferings: {}, makerAssetOfferings: {},
@@ -99,7 +99,7 @@ export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(30000);
export const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS; export const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS;
export const constants = { export const constants = {
ETH_GAS_STATION_API_URL, ZERO_EX_GAS_API_URL,
PROTOCOL_FEE_MULTIPLIER, PROTOCOL_FEE_MULTIPLIER,
POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS,
NULL_BYTES, NULL_BYTES,

View File

@@ -157,8 +157,6 @@ export {
GetMarketOrdersRfqOpts, GetMarketOrdersRfqOpts,
LiquidityProviderFillData, LiquidityProviderFillData,
LiquidityProviderRegistry, LiquidityProviderRegistry,
MarketDepth,
MarketDepthSide,
MooniswapFillData, MooniswapFillData,
MultiHopFillData, MultiHopFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,

View File

@@ -36,9 +36,6 @@ import {
FillData, FillData,
GasSchedule, GasSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketDepth,
MarketDepthSide,
MarketSideLiquidity,
OptimizedMarketOrder, OptimizedMarketOrder,
OptimizerResultWithReport, OptimizerResultWithReport,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
@@ -112,7 +109,7 @@ export class SwapQuoter {
}; };
this._protocolFeeUtils = ProtocolFeeUtils.getInstance( this._protocolFeeUtils = ProtocolFeeUtils.getInstance(
constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
options.ethGasStationUrl, options.zeroExGasApiUrl,
); );
// Allow the sampler bytecode to be overwritten using geths override functionality // Allow the sampler bytecode to be overwritten using geths override functionality
const samplerBytecode = _.get(artifacts.ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object'); const samplerBytecode = _.get(artifacts.ERC20BridgeSampler, 'compilerOutput.evm.deployedBytecode.object');
@@ -228,67 +225,6 @@ export class SwapQuoter {
return batchSwapQuotes.filter(x => x !== undefined) as MarketBuySwapQuote[]; return batchSwapQuotes.filter(x => x !== undefined) as MarketBuySwapQuote[];
} }
/**
* Returns the bids and asks liquidity for the entire market.
* For certain sources (like AMM's) it is recommended to provide a practical maximum takerAssetAmount.
* @param makerTokenAddress The address of the maker asset
* @param takerTokenAddress The address of the taker asset
* @param takerAssetAmount The amount to sell and buy for the bids and asks.
*
* @return An object that conforms to MarketDepth that contains all of the samples and liquidity
* information for the source.
*/
public async getBidAskLiquidityForMakerTakerAssetPairAsync(
makerToken: string,
takerToken: string,
takerAssetAmount: BigNumber,
options: Partial<SwapQuoteRequestOpts> = {},
): Promise<MarketDepth> {
assert.isString('makerToken', makerToken);
assert.isString('takerToken', takerToken);
const sourceFilters = new SourceFilters([], options.excludedSources, options.includedSources);
let [sellOrders, buyOrders] = !sourceFilters.isAllowed(ERC20BridgeSource.Native)
? [[], []]
: await Promise.all([
this.orderbook.getOrdersAsync(makerToken, takerToken),
this.orderbook.getOrdersAsync(takerToken, makerToken),
]);
if (!sellOrders || sellOrders.length === 0) {
sellOrders = [createDummyOrder(makerToken, takerToken)];
}
if (!buyOrders || buyOrders.length === 0) {
buyOrders = [createDummyOrder(takerToken, makerToken)];
}
const getMarketDepthSide = (marketSideLiquidity: MarketSideLiquidity): MarketDepthSide => {
const { dexQuotes, nativeOrders } = marketSideLiquidity.quotes;
const { side } = marketSideLiquidity;
return [
...dexQuotes,
nativeOrders.map(o => {
return {
input: side === MarketOperation.Sell ? o.fillableTakerAmount : o.fillableMakerAmount,
output: side === MarketOperation.Sell ? o.fillableMakerAmount : o.fillableTakerAmount,
fillData: o,
source: ERC20BridgeSource.Native,
};
}),
];
};
const [bids, asks] = await Promise.all([
this._marketOperationUtils.getMarketBuyLiquidityAsync(buyOrders, takerAssetAmount, options),
this._marketOperationUtils.getMarketSellLiquidityAsync(sellOrders, takerAssetAmount, options),
]);
return {
bids: getMarketDepthSide(bids),
asks: getMarketDepthSide(asks),
makerTokenDecimals: asks.makerTokenDecimals,
takerTokenDecimals: asks.takerTokenDecimals,
};
}
/** /**
* Returns the recommended gas price for a fast transaction * Returns the recommended gas price for a fast transaction
*/ */

View File

@@ -337,7 +337,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
contractAddresses?: AssetSwapperContractAddresses; contractAddresses?: AssetSwapperContractAddresses;
samplerGasLimit?: number; samplerGasLimit?: number;
multiBridgeAddress?: string; multiBridgeAddress?: string;
ethGasStationUrl?: string; zeroExGasApiUrl?: string;
rfqt?: SwapQuoterRfqOpts; rfqt?: SwapQuoterRfqOpts;
samplerOverrides?: SamplerOverrides; samplerOverrides?: SamplerOverrides;
tokenAdjacencyGraph?: TokenAdjacencyGraph; tokenAdjacencyGraph?: TokenAdjacencyGraph;

View File

@@ -2176,7 +2176,6 @@ export const LIDO_INFO_BY_CHAIN = valueByChainId<LidoInfo>(
}, },
); );
export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
export const BALANCER_TOP_POOLS_FETCHED = 250; export const BALANCER_TOP_POOLS_FETCHED = 250;
export const BALANCER_MAX_POOLS_FETCHED = 3; export const BALANCER_MAX_POOLS_FETCHED = 3;

View File

@@ -862,14 +862,9 @@ export class MarketOperationUtils {
} }
private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> { private async _refreshPoolCacheIfRequiredAsync(takerToken: string, makerToken: string): Promise<void> {
void Promise.all( _.values(this._sampler.poolsCaches)
Object.values(this._sampler.poolsCaches).map(async cache => { .filter(cache => cache !== undefined && !cache.isFresh(takerToken, makerToken))
if (!cache || cache.isFresh(takerToken, makerToken)) { .forEach(cache => cache?.getFreshPoolsForPairAsync(takerToken, makerToken));
return Promise.resolve([]);
}
return cache.getFreshPoolsForPairAsync(takerToken, makerToken);
}),
);
} }
} }

View File

@@ -1,16 +1,21 @@
import { ChainId } from '@0x/contract-addresses';
import { getPoolsWithTokens, parsePoolData } from 'balancer-labs-sor-v1'; import { getPoolsWithTokens, parsePoolData } from 'balancer-labs-sor-v1';
import { Pool } from 'balancer-labs-sor-v1/dist/types'; import { Pool } from 'balancer-labs-sor-v1/dist/types';
import { gql, request } from 'graphql-request'; import { gql, request } from 'graphql-request';
import { DEFAULT_WARNING_LOGGER } from '../../../constants'; import { DEFAULT_WARNING_LOGGER } from '../../../constants';
import { LogFunction } from '../../../types'; import { LogFunction } from '../../../types';
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from '../constants'; import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants';
import { CacheValue, PoolsCache } from './pools_cache'; import { NoOpPoolsCache } from './no_op_pools_cache';
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
const ONE_DAY_MS = 24 * 60 * 60 * 1000; const ONE_DAY_MS = 24 * 60 * 60 * 1000;
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
// tslint:disable: member-ordering
const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
interface BalancerPoolResponse { interface BalancerPoolResponse {
id: string; id: string;
@@ -20,10 +25,18 @@ interface BalancerPoolResponse {
totalWeight: string; totalWeight: string;
} }
export class BalancerPoolsCache extends PoolsCache { export class BalancerPoolsCache extends AbstractPoolsCache {
constructor( public static create(chainId: ChainId): PoolsCache {
if (chainId !== ChainId.Mainnet) {
return new NoOpPoolsCache();
}
return new BalancerPoolsCache();
}
private constructor(
private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL, private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL,
cache: { [key: string]: CacheValue } = {}, cache: Map<string, CacheValue> = new Map(),
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED, private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER, private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER,

View File

@@ -9,7 +9,10 @@ import { LogFunction } from '../../../types';
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants'; import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants';
import { parsePoolData } from './balancer_sor_v2'; import { parsePoolData } from './balancer_sor_v2';
import { CacheValue, PoolsCache } from './pools_cache'; import { NoOpPoolsCache } from './no_op_pools_cache';
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
// tslint:disable: member-ordering
const BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN = new Map<ChainId, string>([ const BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN = new Map<ChainId, string>([
[ChainId.Fantom, 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx'], [ChainId.Fantom, 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx'],
@@ -28,11 +31,11 @@ interface BalancerPoolResponse {
amp: string | null; amp: string | null;
} }
export class BalancerV2PoolsCache extends PoolsCache { export class BalancerV2PoolsCache extends AbstractPoolsCache {
public static createBeethovenXPoolCache(chainId: ChainId): BalancerV2PoolsCache | undefined { public static createBeethovenXPoolCache(chainId: ChainId): PoolsCache {
const subgraphUrl = BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN.get(chainId); const subgraphUrl = BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN.get(chainId);
if (subgraphUrl === undefined) { if (subgraphUrl === undefined) {
return undefined; return new NoOpPoolsCache();
} }
return new BalancerV2PoolsCache(subgraphUrl); return new BalancerV2PoolsCache(subgraphUrl);
@@ -58,12 +61,12 @@ export class BalancerV2PoolsCache extends PoolsCache {
}; };
} }
constructor( private constructor(
private readonly subgraphUrl: string, private readonly subgraphUrl: string,
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED, private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER, private readonly _warningLogger: LogFunction = DEFAULT_WARNING_LOGGER,
cache: { [key: string]: CacheValue } = {}, cache: Map<string, CacheValue> = new Map(),
) { ) {
super(cache); super(cache);
void this._loadTopPoolsAsync(); void this._loadTopPoolsAsync();

View File

@@ -1,18 +1,20 @@
import { ChainId } from '@0x/contract-addresses';
import { Pool } from 'balancer-labs-sor-v1/dist/types'; import { Pool } from 'balancer-labs-sor-v1/dist/types';
import { getPoolsWithTokens, parsePoolData } from 'cream-sor'; import { getPoolsWithTokens, parsePoolData } from 'cream-sor';
import { BALANCER_MAX_POOLS_FETCHED } from '../constants'; import { BALANCER_MAX_POOLS_FETCHED } from '../constants';
import { CacheValue, PoolsCache } from './pools_cache'; import { NoOpPoolsCache } from './no_op_pools_cache';
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
export class CreamPoolsCache extends PoolsCache { export class CreamPoolsCache extends AbstractPoolsCache {
constructor( public static create(chainId: ChainId): PoolsCache {
_cache: { [key: string]: CacheValue } = {}, if (chainId !== ChainId.Mainnet) {
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED, return new NoOpPoolsCache();
) { }
super(_cache);
return new CreamPoolsCache();
} }
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> { protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
try { try {
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools; const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
@@ -25,4 +27,10 @@ export class CreamPoolsCache extends PoolsCache {
return []; return [];
} }
} }
private constructor(
_cache: Map<string, CacheValue> = new Map(),
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
) {
super(_cache);
}
} }

View File

@@ -1,4 +1,4 @@
export { BalancerPoolsCache } from './balancer_utils'; export { BalancerPoolsCache } from './balancer_utils';
export { BalancerV2PoolsCache } from './balancer_v2_utils'; export { BalancerV2PoolsCache } from './balancer_v2_utils';
export { CreamPoolsCache } from './cream_utils'; export { CreamPoolsCache } from './cream_utils';
export { PoolsCache } from './pools_cache'; export { AbstractPoolsCache, PoolsCache } from './pools_cache';

View File

@@ -0,0 +1,21 @@
import { Pool, PoolsCache } from './pools_cache';
// tslint:disable:prefer-function-over-method
export class NoOpPoolsCache implements PoolsCache {
public async getFreshPoolsForPairAsync(
_takerToken: string,
_makerToken: string,
_timeoutMs?: number | undefined,
): Promise<Pool[]> {
return [];
}
public getPoolAddressesForPair(_takerToken: string, _makerToken: string): string[] {
return [];
}
public isFresh(_takerToken: string, _makerToken: string): boolean {
return true;
}
}

View File

@@ -10,15 +10,29 @@ export interface CacheValue {
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
// Cache results for 30mins // Cache results for 30mins
const DEFAULT_CACHE_TIME_MS = (ONE_HOUR_IN_SECONDS / 2) * ONE_SECOND_MS; const DEFAULT_CACHE_TIME_MS = (ONE_HOUR_IN_SECONDS / 2) * ONE_SECOND_MS;
const DEFAULT_TIMEOUT_MS = 1000; const DEFAULT_TIMEOUT_MS = 3000;
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
export abstract class PoolsCache { export interface PoolsCache {
protected static _isExpired(value: CacheValue): boolean { getFreshPoolsForPairAsync(takerToken: string, makerToken: string, timeoutMs?: number): Promise<Pool[]>;
getPoolAddressesForPair(takerToken: string, makerToken: string): string[];
isFresh(takerToken: string, makerToken: string): boolean;
}
export abstract class AbstractPoolsCache implements PoolsCache {
protected static _getKey(takerToken: string, makerToken: string): string {
return `${takerToken}-${makerToken}`;
}
protected static _isExpired(value: CacheValue | undefined): boolean {
if (value === undefined) {
return true;
}
return Date.now() >= value.expiresAt; return Date.now() >= value.expiresAt;
} }
constructor( constructor(
protected readonly _cache: { [key: string]: CacheValue }, protected readonly _cache: Map<string, CacheValue>,
protected readonly _cacheTimeMs: number = DEFAULT_CACHE_TIME_MS, protected readonly _cacheTimeMs: number = DEFAULT_CACHE_TIME_MS,
) {} ) {}
@@ -31,47 +45,42 @@ export abstract class PoolsCache {
return Promise.race([this._getAndSaveFreshPoolsForPairAsync(takerToken, makerToken), timeout]); return Promise.race([this._getAndSaveFreshPoolsForPairAsync(takerToken, makerToken), timeout]);
} }
public getCachedPoolAddressesForPair( /**
takerToken: string, * Returns pool addresses (can be stale) for a pair.
makerToken: string, *
ignoreExpired: boolean = true, * An empty array will be returned if cache does not exist.
): string[] | undefined { */
const key = JSON.stringify([takerToken, makerToken]); public getPoolAddressesForPair(takerToken: string, makerToken: string): string[] {
const value = this._cache[key]; const value = this._getValue(takerToken, makerToken);
if (ignoreExpired) { return value === undefined ? [] : value.pools.map(pool => pool.id);
return value === undefined ? [] : value.pools.map(pool => pool.id);
}
if (!value) {
return undefined;
}
if (PoolsCache._isExpired(value)) {
return undefined;
}
return (value || []).pools.map(pool => pool.id);
} }
public isFresh(takerToken: string, makerToken: string): boolean { public isFresh(takerToken: string, makerToken: string): boolean {
const cached = this.getCachedPoolAddressesForPair(takerToken, makerToken, false); const value = this._getValue(takerToken, makerToken);
return cached !== undefined; return !AbstractPoolsCache._isExpired(value);
}
protected _getValue(takerToken: string, makerToken: string): CacheValue | undefined {
const key = AbstractPoolsCache._getKey(takerToken, makerToken);
return this._cache.get(key);
} }
protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> { protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
const key = JSON.stringify([takerToken, makerToken]); const key = AbstractPoolsCache._getKey(takerToken, makerToken);
const value = this._cache[key]; const value = this._cache.get(key);
if (value === undefined || value.expiresAt >= Date.now()) { if (!AbstractPoolsCache._isExpired(value)) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken); return value!.pools;
const expiresAt = Date.now() + this._cacheTimeMs;
this._cachePoolsForPair(takerToken, makerToken, pools, expiresAt);
} }
return this._cache[key].pools;
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const expiresAt = Date.now() + this._cacheTimeMs;
this._cachePoolsForPair(takerToken, makerToken, pools, expiresAt);
return pools;
} }
protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void { protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void {
const key = JSON.stringify([takerToken, makerToken]); const key = AbstractPoolsCache._getKey(takerToken, makerToken);
this._cache[key] = { this._cache.set(key, { pools, expiresAt });
pools,
expiresAt,
};
} }
protected abstract _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]>; protected abstract _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]>;

View File

@@ -56,7 +56,7 @@ import {
} from './constants'; } from './constants';
import { getGeistInfoForPair } from './geist_utils'; import { getGeistInfoForPair } from './geist_utils';
import { getLiquidityProvidersForPair } from './liquidity_provider_utils'; import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache } from './pools_cache'; import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new'; import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
import { SamplerContractOperation } from './sampler_contract_operation'; import { SamplerContractOperation } from './sampler_contract_operation';
import { SamplerNoOperation } from './sampler_no_operation'; import { SamplerNoOperation } from './sampler_no_operation';
@@ -114,10 +114,10 @@ export const TWO_HOP_SOURCE_FILTERS = SourceFilters.all().exclude([
export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]); export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]);
export interface PoolsCacheMap { export interface PoolsCacheMap {
[ERC20BridgeSource.Balancer]: BalancerPoolsCache; [ERC20BridgeSource.Balancer]: PoolsCache;
[ERC20BridgeSource.BalancerV2]: BalancerV2SwapInfoCache | undefined; [ERC20BridgeSource.BalancerV2]: BalancerV2SwapInfoCache | undefined;
[ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache | undefined; [ERC20BridgeSource.Beethovenx]: PoolsCache;
[ERC20BridgeSource.Cream]: CreamPoolsCache; [ERC20BridgeSource.Cream]: PoolsCache;
} }
// tslint:disable:no-inferred-empty-object-type no-unbound-method // tslint:disable:no-inferred-empty-object-type no-unbound-method
@@ -156,8 +156,8 @@ export class SamplerOperations {
? poolsCaches ? poolsCaches
: { : {
[ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache.createBeethovenXPoolCache(chainId), [ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache.createBeethovenXPoolCache(chainId),
[ERC20BridgeSource.Balancer]: new BalancerPoolsCache(), [ERC20BridgeSource.Balancer]: BalancerPoolsCache.create(chainId),
[ERC20BridgeSource.Cream]: new CreamPoolsCache(), [ERC20BridgeSource.Cream]: CreamPoolsCache.create(chainId),
[ERC20BridgeSource.BalancerV2]: [ERC20BridgeSource.BalancerV2]:
BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[chainId] === NULL_ADDRESS BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[chainId] === NULL_ADDRESS
? undefined ? undefined
@@ -1592,20 +1592,17 @@ export class SamplerOperations {
), ),
]; ];
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
return ( return this.poolsCaches[ERC20BridgeSource.Balancer]
this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair( .getPoolAddressesForPair(takerToken, makerToken)
takerToken, .map(balancerPool =>
makerToken, this.getBalancerSellQuotes(
) || [] balancerPool,
).map(balancerPool => makerToken,
this.getBalancerSellQuotes( takerToken,
balancerPool, takerFillAmounts,
makerToken, ERC20BridgeSource.Balancer,
takerToken, ),
takerFillAmounts, );
ERC20BridgeSource.Balancer,
),
);
case ERC20BridgeSource.BalancerV2: { case ERC20BridgeSource.BalancerV2: {
const cache = this.poolsCaches[source]; const cache = this.poolsCaches[source];
if (!cache) { if (!cache) {
@@ -1624,18 +1621,14 @@ export class SamplerOperations {
} }
case ERC20BridgeSource.Beethovenx: { case ERC20BridgeSource.Beethovenx: {
const cache = this.poolsCaches[source]; const cache = this.poolsCaches[source];
if (cache === undefined) { const poolAddresses = cache.getPoolAddressesForPair(takerToken, makerToken);
return [];
}
const poolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken) || [];
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId]; const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
if (vault === NULL_ADDRESS) { if (vault === NULL_ADDRESS) {
return []; return [];
} }
return poolIds.map(poolId => return poolAddresses.map(poolAddress =>
this.getBalancerV2SellQuotes( this.getBalancerV2SellQuotes(
{ poolId, vault }, { poolId: poolAddress, vault },
makerToken, makerToken,
takerToken, takerToken,
takerFillAmounts, takerFillAmounts,
@@ -1644,20 +1637,17 @@ export class SamplerOperations {
); );
} }
case ERC20BridgeSource.Cream: case ERC20BridgeSource.Cream:
return ( return this.poolsCaches[ERC20BridgeSource.Cream]
this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair( .getPoolAddressesForPair(takerToken, makerToken)
takerToken, .map(creamPool =>
makerToken, this.getBalancerSellQuotes(
) || [] creamPool,
).map(creamPool => makerToken,
this.getBalancerSellQuotes( takerToken,
creamPool, takerFillAmounts,
makerToken, ERC20BridgeSource.Cream,
takerToken, ),
takerFillAmounts, );
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo: case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return []; return [];
@@ -1948,20 +1938,17 @@ export class SamplerOperations {
), ),
]; ];
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
return ( return this.poolsCaches[ERC20BridgeSource.Balancer]
this.poolsCaches[ERC20BridgeSource.Balancer].getCachedPoolAddressesForPair( .getPoolAddressesForPair(takerToken, makerToken)
takerToken, .map(poolAddress =>
makerToken, this.getBalancerBuyQuotes(
) || [] poolAddress,
).map(poolAddress => makerToken,
this.getBalancerBuyQuotes( takerToken,
poolAddress, makerFillAmounts,
makerToken, ERC20BridgeSource.Balancer,
takerToken, ),
makerFillAmounts, );
ERC20BridgeSource.Balancer,
),
);
case ERC20BridgeSource.BalancerV2: { case ERC20BridgeSource.BalancerV2: {
const cache = this.poolsCaches[source]; const cache = this.poolsCaches[source];
if (!cache) { if (!cache) {
@@ -1986,11 +1973,7 @@ export class SamplerOperations {
} }
case ERC20BridgeSource.Beethovenx: { case ERC20BridgeSource.Beethovenx: {
const cache = this.poolsCaches[source]; const cache = this.poolsCaches[source];
if (cache === undefined) { const poolIds = cache.getPoolAddressesForPair(takerToken, makerToken) || [];
return [];
}
const poolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken) || [];
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId]; const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
if (vault === NULL_ADDRESS) { if (vault === NULL_ADDRESS) {
return []; return [];
@@ -2006,20 +1989,17 @@ export class SamplerOperations {
); );
} }
case ERC20BridgeSource.Cream: case ERC20BridgeSource.Cream:
return ( return this.poolsCaches[ERC20BridgeSource.Cream]
this.poolsCaches[ERC20BridgeSource.Cream].getCachedPoolAddressesForPair( .getPoolAddressesForPair(takerToken, makerToken)
takerToken, .map(poolAddress =>
makerToken, this.getBalancerBuyQuotes(
) || [] poolAddress,
).map(poolAddress => makerToken,
this.getBalancerBuyQuotes( takerToken,
poolAddress, makerFillAmounts,
makerToken, ERC20BridgeSource.Cream,
takerToken, ),
makerFillAmounts, );
ERC20BridgeSource.Cream,
),
);
case ERC20BridgeSource.Dodo: case ERC20BridgeSource.Dodo:
if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) { if (!isValidAddress(DODOV1_CONFIG_BY_CHAIN_ID[this.chainId].registry)) {
return []; return [];

View File

@@ -617,15 +617,6 @@ export interface OptimizerResultWithReport extends OptimizerResult {
priceComparisonsReport?: PriceComparisonsReport; priceComparisonsReport?: PriceComparisonsReport;
} }
export type MarketDepthSide = Array<Array<DexSample<FillData>>>;
export interface MarketDepth {
bids: MarketDepthSide;
asks: MarketDepthSide;
makerTokenDecimals: number;
takerTokenDecimals: number;
}
export interface MarketSideLiquidity { export interface MarketSideLiquidity {
side: MarketOperation; side: MarketOperation;
inputAmount: BigNumber; inputAmount: BigNumber;

View File

@@ -6,28 +6,36 @@ import { SwapQuoterError } from '../types';
const MAX_ERROR_COUNT = 5; const MAX_ERROR_COUNT = 5;
interface GasOracleResponse {
result: {
// gas price in wei
fast: number;
};
}
export class ProtocolFeeUtils { export class ProtocolFeeUtils {
private static _instance: ProtocolFeeUtils; private static _instance: ProtocolFeeUtils;
private readonly _ethGasStationUrl!: string; private readonly _zeroExGasApiUrl: string;
private readonly _gasPriceHeart: any; private readonly _gasPriceHeart: any;
private _gasPriceEstimation: BigNumber = constants.ZERO_AMOUNT; private _gasPriceEstimation: BigNumber = constants.ZERO_AMOUNT;
private _errorCount: number = 0; private _errorCount: number = 0;
public static getInstance( public static getInstance(
gasPricePollingIntervalInMs: number, gasPricePollingIntervalInMs: number,
ethGasStationUrl: string = constants.ETH_GAS_STATION_API_URL, zeroExGasApiUrl: string = constants.ZERO_EX_GAS_API_URL,
initialGasPrice: BigNumber = constants.ZERO_AMOUNT, initialGasPrice: BigNumber = constants.ZERO_AMOUNT,
): ProtocolFeeUtils { ): ProtocolFeeUtils {
if (!ProtocolFeeUtils._instance) { if (!ProtocolFeeUtils._instance) {
ProtocolFeeUtils._instance = new ProtocolFeeUtils( ProtocolFeeUtils._instance = new ProtocolFeeUtils(
gasPricePollingIntervalInMs, gasPricePollingIntervalInMs,
ethGasStationUrl, zeroExGasApiUrl,
initialGasPrice, initialGasPrice,
); );
} }
return ProtocolFeeUtils._instance; return ProtocolFeeUtils._instance;
} }
/** @returns gas price (in wei) */
public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> { public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> {
if (this._gasPriceEstimation.eq(constants.ZERO_AMOUNT)) { if (this._gasPriceEstimation.eq(constants.ZERO_AMOUNT)) {
return this._getGasPriceFromGasStationOrThrowAsync(); return this._getGasPriceFromGasStationOrThrowAsync();
@@ -48,27 +56,21 @@ export class ProtocolFeeUtils {
private constructor( private constructor(
gasPricePollingIntervalInMs: number, gasPricePollingIntervalInMs: number,
ethGasStationUrl: string = constants.ETH_GAS_STATION_API_URL, zeroExGasApiUrl: string = constants.ZERO_EX_GAS_API_URL,
initialGasPrice: BigNumber = constants.ZERO_AMOUNT, initialGasPrice: BigNumber = constants.ZERO_AMOUNT,
) { ) {
this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs); this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs);
this._gasPriceEstimation = initialGasPrice; this._gasPriceEstimation = initialGasPrice;
this._ethGasStationUrl = ethGasStationUrl; this._zeroExGasApiUrl = zeroExGasApiUrl;
this._initializeHeartBeat(); this._initializeHeartBeat();
} }
// tslint:disable-next-line: prefer-function-over-method // tslint:disable-next-line: prefer-function-over-method
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> { private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
try { try {
const res = await fetch(this._ethGasStationUrl); const res = await fetch(this._zeroExGasApiUrl);
const gasInfo = await res.json(); const gasInfo: GasOracleResponse = await res.json();
// Eth Gas Station result is gwei * 10 const gasPriceWei = new BigNumber(gasInfo.result.fast);
// tslint:disable-next-line:custom-no-magic-numbers
const BASE_TEN = 10;
const gasPriceGwei = new BigNumber(gasInfo.fast / BASE_TEN);
// tslint:disable-next-line:custom-no-magic-numbers
const unit = new BigNumber(BASE_TEN).pow(9);
const gasPriceWei = unit.times(gasPriceGwei);
// Reset the error count to 0 once we have a successful response // Reset the error count to 0 once we have a successful response
this._errorCount = 0; this._errorCount = 0;
return gasPriceWei; return gasPriceWei;

View File

@@ -24,7 +24,7 @@ import {
SOURCE_FLAGS, SOURCE_FLAGS,
ZERO_AMOUNT, ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants'; } from '../src/utils/market_operation_utils/constants';
import { PoolsCache } from '../src/utils/market_operation_utils/pools_cache'; import { AbstractPoolsCache } from '../src/utils/market_operation_utils/pools_cache';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import { SourceFilters } from '../src/utils/market_operation_utils/source_filters'; import { SourceFilters } from '../src/utils/market_operation_utils/source_filters';
@@ -98,9 +98,9 @@ async function getMarketBuyOrdersAsync(
return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts); return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts);
} }
class MockPoolsCache extends PoolsCache { class MockPoolsCache extends AbstractPoolsCache {
constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) { constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) {
super({}); super(new Map());
} }
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> { protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
return this._handler(takerToken, makerToken); return this._handler(takerToken, makerToken);

View File

@@ -28,12 +28,12 @@ describe('Pools Caches for Balancer-based sampling', () => {
expect(pools.length).greaterThan(0, `Failed to find any pools for ${takerToken} and ${makerToken}`); expect(pools.length).greaterThan(0, `Failed to find any pools for ${takerToken} and ${makerToken}`);
expect(pools[0]).not.undefined(); expect(pools[0]).not.undefined();
expect(Object.keys(pools[0])).to.include.members(poolKeys); expect(Object.keys(pools[0])).to.include.members(poolKeys);
const cachedPoolIds = cache.getCachedPoolAddressesForPair(takerToken, makerToken); const cachedPoolIds = cache.getPoolAddressesForPair(takerToken, makerToken);
expect(cachedPoolIds).to.deep.equal(pools.map(p => p.id)); expect(cachedPoolIds).to.deep.equal(pools.map(p => p.id));
} }
describe('BalancerPoolsCache', () => { describe('BalancerPoolsCache', () => {
const cache = new BalancerPoolsCache(); const cache = BalancerPoolsCache.create(ChainId.Mainnet);
it('fetches pools', async () => { it('fetches pools', async () => {
const pairs = [ const pairs = [
[usdcAddress, daiAddress], [usdcAddress, daiAddress],
@@ -58,15 +58,14 @@ describe('Pools Caches for Balancer-based sampling', () => {
[wftmAddress, fantomWethAddress], [wftmAddress, fantomWethAddress],
]; ];
expect(cache).not.null();
await Promise.all( await Promise.all(
pairs.map(async ([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache!, takerToken, makerToken)), pairs.map(async ([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache, takerToken, makerToken)),
); );
}); });
}); });
describe('CreamPoolsCache', () => { describe('CreamPoolsCache', () => {
const cache = new CreamPoolsCache(); const cache = CreamPoolsCache.create(ChainId.Mainnet);
it('fetches pools', async () => { it('fetches pools', async () => {
const pairs = [ const pairs = [
[usdcAddress, creamAddress], [usdcAddress, creamAddress],

View File

@@ -0,0 +1,45 @@
import * as chai from 'chai';
import 'mocha';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ProtocolFeeUtils } from '../src';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const server = setupServer(
rest.get('https://mock-0x-gas-api.org/median', (_req, res, ctx) => {
return res(
ctx.json({
result: {
source: 'MEDIAN',
timestamp: 1659386474,
instant: 22000000000,
fast: 18848500000,
standard: 14765010000,
low: 13265000000,
},
}),
);
}),
);
describe('ProtocolFeeUtils', () => {
describe('getGasPriceEstimationOrThrowAsync', () => {
beforeEach(() => {
server.listen();
});
afterEach(() => {
server.close();
});
it('parses fast gas price response correctly', async () => {
const utils = ProtocolFeeUtils.getInstance(420000, 'https://mock-0x-gas-api.org/median');
const gasPrice = await utils.getGasPriceEstimationOrThrowAsync();
expect(gasPrice.toNumber()).to.eq(18848500000);
});
});
});

View File

@@ -1,6 +1,7 @@
{ {
"extends": ["@0x/tslint-config"], "extends": ["@0x/tslint-config"],
"rules": { "rules": {
"custom-no-magic-numbers": false,
"max-file-line-count": false, "max-file-line-count": false,
"binary-expression-operand-order": false "binary-expression-operand-order": false
}, },

758
yarn.lock

File diff suppressed because it is too large Load Diff