Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rates providers #79

Merged
merged 19 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 16 additions & 1 deletion src/atomex/atomex.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import BigNumber from 'bignumber.js';

import type { AuthorizationManager } from '../authorization/index';
import type { WalletsManager } from '../blockchain/index';
import type { AtomexService, Currency } from '../common/index';
import type { ExchangeManager } from '../exchange/exchangeManager';
import type { Swap, SwapManager } from '../swaps/index';
import { toFixedBigNumber } from '../utils/converters';
import type { AtomexContext } from './atomexContext';
import {
SwapOperationCompleteStage, AtomexOptions,
Expand Down Expand Up @@ -66,7 +69,7 @@ export class Atomex implements AtomexService {
this.atomexContext.providers.blockchainProvider.addBlockchain(blockchain, blockchainOptions);
}

getCurrency(currencyId: Currency['id']) {
getCurrency(currencyId: Currency['id']): Currency | undefined {
return this.atomexContext.providers.currenciesProvider.getCurrency(currencyId);
}

Expand All @@ -93,4 +96,16 @@ export class Atomex implements AtomexService {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return swaps.length === 1 ? swaps[0]! : (swaps as readonly Swap[]);
}

async convertCurrency(fromAmount: BigNumber.Value, fromCurrency: Currency['id'], toCurrency: Currency['id']): Promise<BigNumber | undefined> {
const price = await this.atomexContext.managers.priceManager.getAveragePrice({ baseCurrency: fromCurrency, quoteCurrency: toCurrency });
if (!price)
return undefined;

const inAmountBigNumber = BigNumber.isBigNumber(fromAmount) ? fromAmount : new BigNumber(fromAmount);
const outAmount = inAmountBigNumber.multipliedBy(price);
const toCurrencyInfo = this.getCurrency(toCurrency);

return toCurrencyInfo ? toFixedBigNumber(outAmount, toCurrencyInfo.decimals) : outAmount;
}
}
14 changes: 13 additions & 1 deletion src/atomex/atomexContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AuthorizationManager } from '../authorization/index';
import type { WalletsManager, AtomexBlockchainProvider } from '../blockchain/index';
import type { AtomexNetwork, CurrenciesProvider } from '../common/index';
import type { ExchangeManager, ExchangeService, ManagedExchangeSymbolsProvider, ManagedOrderBookProvider } from '../exchange/index';
import type { ExchangeManager, ExchangeService, ManagedExchangeSymbolsProvider, ManagedOrderBookProvider, PriceManager } from '../exchange/index';
import type { SwapManager, SwapService } from '../swaps/index';

export class AtomexContext {
Expand All @@ -27,6 +27,7 @@ class AtomexContextManagersSection {
private _authorizationManager: AuthorizationManager | undefined;
private _exchangeManager: ExchangeManager | undefined;
private _swapManager: SwapManager | undefined;
private _priceManager: PriceManager | undefined;

constructor(readonly context: AtomexContext) {
}
Expand Down Expand Up @@ -74,6 +75,17 @@ class AtomexContextManagersSection {
private set swapManager(swapManager: SwapManager) {
this._swapManager = swapManager;
}

get priceManager(): PriceManager {
if (!this._priceManager)
throw new AtomexComponentNotResolvedError('managers.priceManager');

return this._priceManager;
}

private set priceManager(priceManager: PriceManager) {
this._priceManager = priceManager;
}
}

class AtomexContextServicesSection {
Expand Down
14 changes: 13 additions & 1 deletion src/atomexBuilder/atomexBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type { AuthorizationManager } from '../authorization/index';
import { AtomexBlockchainProvider, WalletsManager } from '../blockchain/index';
import type { DeepReadonly } from '../core/index';
import { createDefaultEthereumBlockchainOptions } from '../ethereum/index';
import { ExchangeManager, InMemoryExchangeSymbolsProvider, InMemoryOrderBookProvider } from '../exchange/index';
import {
AtomexPriceProvider, BinancePriceProvider, ExchangeManager, InMemoryExchangeSymbolsProvider,
InMemoryOrderBookProvider, KrakenPriceProvider, PriceManager, MixedPriceManager, PriceProvider
} from '../exchange/index';
import { SwapManager } from '../swaps/swapManager';
import { createDefaultTezosBlockchainOptions } from '../tezos/index';
import type { AtomexBuilderOptions } from './atomexBuilderOptions';
Expand Down Expand Up @@ -57,6 +60,7 @@ export class AtomexBuilder {
this.controlledAtomexContext.services.swapService = atomexClient;
this.controlledAtomexContext.managers.exchangeManager = this.createExchangeManager();
this.controlledAtomexContext.managers.swapManager = this.createSwapManager();
this.controlledAtomexContext.managers.priceManager = this.createPriceManager();
const blockchains = this.createDefaultBlockchainOptions();

return new Atomex({
Expand Down Expand Up @@ -119,4 +123,12 @@ export class AtomexBuilder {
ethereum: createDefaultEthereumBlockchainOptions(this.atomexContext)
};
}

protected createPriceManager(): PriceManager {
return new MixedPriceManager(new Map<string, PriceProvider>([
['binance', new BinancePriceProvider()],
['kraken', new KrakenPriceProvider()],
['atomex', new AtomexPriceProvider(this.atomexContext.services.exchangeService)]
]));
}
}
5 changes: 4 additions & 1 deletion src/atomexBuilder/controlledAtomexContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AuthorizationManager } from '../authorization/index';
import type { AtomexBlockchainProvider } from '../blockchain/atomexBlockchainProvider';
import type { WalletsManager } from '../blockchain/index';
import type { AtomexNetwork, CurrenciesProvider } from '../common/index';
import type { ExchangeManager, ExchangeService, ManagedExchangeSymbolsProvider, ManagedOrderBookProvider } from '../exchange/index';
import type { ExchangeManager, ExchangeService, ManagedExchangeSymbolsProvider, ManagedOrderBookProvider, PriceManager } from '../exchange/index';
import type { SwapManager, SwapService } from '../swaps/index';

export interface ControlledAtomexContext {
Expand All @@ -26,6 +26,9 @@ interface ControlledAtomexContextManagersSection {

get swapManager(): SwapManager;
set swapManager(value: SwapManager);

get priceManager(): PriceManager;
set priceManager(value: PriceManager);
}

interface ControlledAtomexContextServicesSection {
Expand Down
3 changes: 3 additions & 0 deletions src/common/disposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Disposable {
dispose(): Promise<void>;
}
1 change: 1 addition & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { InMemoryCurrenciesProvider } from './inMemoryCurrenciesProvider';

export type { AtomexService } from './atomexService';
export type { CurrenciesProvider } from './currenciesProvider';
export type { Disposable } from './disposable';
3 changes: 3 additions & 0 deletions src/exchange/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export { InMemoryOrderBookProvider } from './orderBookProvider/index';
export type { ExchangeSymbolsProvider, ManagedExchangeSymbolsProvider } from './exchangeSymbolsProvider/index';
export type { OrderBookProvider, ManagedOrderBookProvider } from './orderBookProvider/index';
export type { ExchangeService, ExchangeServiceEvents } from './exchangeService';

export { type PriceManager, MixedPriceManager } from './priceManager/index';
export { type PriceProvider, AtomexPriceProvider, BinancePriceProvider, KrakenPriceProvider } from './priceProvider/index';
2 changes: 2 additions & 0 deletions src/exchange/priceManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { PriceManager, GetPriceParameters, GetAveragePriceParameters } from './priceManager';
export { MixedPriceManager } from './mixedPriceManager/index';
1 change: 1 addition & 0 deletions src/exchange/priceManager/mixedPriceManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MixedPriceManager } from './mixedPriceManager';
66 changes: 66 additions & 0 deletions src/exchange/priceManager/mixedPriceManager/mixedPriceManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import BigNumber from 'bignumber.js';

import { Currency, DataSource } from '../../../common';
import type { PriceProvider } from '../../priceProvider/index';
import type { GetAveragePriceParameters, GetPriceParameters, PriceManager } from '../priceManager';

export class MixedPriceManager implements PriceManager {
constructor(
private readonly providersMap: Map<string, PriceProvider>
) { }

async getAveragePrice({ baseCurrency, quoteCurrency, dataSource = DataSource.All }: GetAveragePriceParameters): Promise<BigNumber | undefined> {
const providers = this.getAvailableProviders();
const pricePromises = providers.map(provider => this.getPrice({ baseCurrency, quoteCurrency, provider }));
const pricePromiseResults = await Promise.allSettled(pricePromises);

const prices: BigNumber[] = [];
for (const result of pricePromiseResults)
if (result.status === 'fulfilled' && result.value !== undefined)
prices.push(result.value);

return prices.length ? BigNumber.sum(...prices).div(prices.length) : undefined;
}

async getPrice({ baseCurrency, quoteCurrency, provider, dataSource = DataSource.All }: GetPriceParameters): Promise<BigNumber | undefined> {
let price = await this.getPriceCore(baseCurrency, quoteCurrency, provider);
if (!price) {
const reversedPrice = await this.getPriceCore(quoteCurrency, baseCurrency, provider);
if (reversedPrice)
price = reversedPrice.pow(-1);
}

return price;
}

getAvailableProviders(): string[] {
return [...this.providersMap.keys()];
}

dispose(): Promise<void> {
throw new Error('Method not implemented.');
}

private async getPriceCore(baseCurrency: Currency['id'], quoteCurrency: Currency['id'], provider?: string): Promise<BigNumber | undefined> {
const providers = this.getSelectedProviders(provider);
const pricePromises = providers.map(provider => provider.getPrice(baseCurrency, quoteCurrency));
const pricePromiseResults = await Promise.allSettled(pricePromises);

for (const result of pricePromiseResults)
if (result.status === 'fulfilled' && result.value !== undefined)
return result.value;

return undefined;
}

private getSelectedProviders(provider?: string): PriceProvider[] {
if (!provider)
return [...this.providersMap.values()];

const selectedProvider = this.providersMap.get(provider);
if (!selectedProvider)
throw new Error(`Provider not found for key: ${provider}`);

return [selectedProvider];
}
}
22 changes: 22 additions & 0 deletions src/exchange/priceManager/priceManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type BigNumber from 'bignumber.js';

import type { Currency, DataSource, Disposable } from '../../common/index';

export interface GetPriceParameters {
baseCurrency: Currency['id'];
quoteCurrency: Currency['id'];
provider?: string;
dataSource?: DataSource;
}

export interface GetAveragePriceParameters {
baseCurrency: Currency['id'];
quoteCurrency: Currency['id'];
dataSource?: DataSource;
}

export interface PriceManager extends Disposable {
getPrice(parameters: GetPriceParameters): Promise<BigNumber | undefined>;
getAveragePrice(parameters: GetAveragePriceParameters): Promise<BigNumber | undefined>;
getAvailableProviders(): string[];
}
23 changes: 23 additions & 0 deletions src/exchange/priceProvider/atomex/atomexPriceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type BigNumber from 'bignumber.js';

import type { Currency } from '../../../common/index';
import type { ExchangeService } from '../../exchangeService';
import type { Quote } from '../../models/index';
import type { PriceProvider } from '../priceProvider';

export class AtomexPriceProvider implements PriceProvider {
constructor(
private readonly exchangeService: ExchangeService
) { }

async getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined> {
const symbol = `${baseCurrency}/${quoteCurrency}`;
const quote = (await this.exchangeService.getTopOfBook([{ from: baseCurrency, to: quoteCurrency }]))?.[0];

return quote && quote.symbol == symbol ? this.getMiddlePrice(quote) : undefined;
}

private getMiddlePrice(quote: Quote): BigNumber {
return quote.ask.plus(quote.bid).div(2);
}
}
1 change: 1 addition & 0 deletions src/exchange/priceProvider/atomex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AtomexPriceProvider } from './atomexPriceProvider';
52 changes: 52 additions & 0 deletions src/exchange/priceProvider/binance/binancePriceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import BigNumber from 'bignumber.js';

import type { Currency } from '../../../common';
import { HttpClient } from '../../../core';
import type { PriceProvider } from '../priceProvider';
import type { BinanceErrorDto, BinanceRatesDto } from './dtos';
import { isErrorDto } from './utils';

export class BinancePriceProvider implements PriceProvider {
private static readonly baseUrl = 'https://www.binance.com';
private static readonly priceUrlPath = '/api/v3/ticker/price';

private readonly httpClient: HttpClient;
private _allSymbols: Set<string> | undefined;

constructor() {
this.httpClient = new HttpClient(BinancePriceProvider.baseUrl);
}

async getPrice(baseCurrency: Currency['id'], quoteCurrency: Currency['id']): Promise<BigNumber | undefined> {
const symbol = `${baseCurrency}${quoteCurrency}`;
const allSymbols = await this.getAllSymbols();
if (!allSymbols.has(symbol))
return undefined;

const urlPath = `${BinancePriceProvider.priceUrlPath}?symbol=${symbol}`;
const responseDto = await this.httpClient.request<BinanceRatesDto | BinanceErrorDto>({ urlPath }, false);

return this.mapRatesDtoToPrice(responseDto);
}

private mapRatesDtoToPrice(dto: BinanceRatesDto | BinanceErrorDto): BigNumber | undefined {
if (isErrorDto(dto))
return undefined;

return new BigNumber(dto.price);
}

private async getAllSymbols(): Promise<Set<string>> {
if (!this._allSymbols)
this._allSymbols = new Set(await this.requestAllSymbols());

return this._allSymbols;
}

private async requestAllSymbols(): Promise<string[]> {
const urlPath = BinancePriceProvider.priceUrlPath;
const responseDto = await this.httpClient.request<BinanceRatesDto[]>({ urlPath }, false);

return responseDto.map(dto => dto.symbol);
}
}
9 changes: 9 additions & 0 deletions src/exchange/priceProvider/binance/dtos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BinanceRatesDto {
symbol: string;
price: string;
}

export interface BinanceErrorDto {
code: number;
msg: string;
}
1 change: 1 addition & 0 deletions src/exchange/priceProvider/binance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BinancePriceProvider } from './binancePriceProvider';
6 changes: 6 additions & 0 deletions src/exchange/priceProvider/binance/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { BinanceErrorDto } from './dtos';

export const isErrorDto = (dto: unknown): dto is BinanceErrorDto => {
const errorDto = dto as BinanceErrorDto;
return typeof errorDto.code === 'number' && typeof errorDto.msg === 'string';
};
4 changes: 4 additions & 0 deletions src/exchange/priceProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type { PriceProvider } from './priceProvider';
export { AtomexPriceProvider } from './atomex/index';
export { BinancePriceProvider } from './binance/index';
export { KrakenPriceProvider } from './kraken/index';
51 changes: 51 additions & 0 deletions src/exchange/priceProvider/kraken/dtos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export interface KrakenRatesDto {
error: string[];
result: Record<string, KrakenTickerInfo>;
}

export interface KrakenTickerInfo {
/**
* Ask
*/
a: [price: string, wholeLotVolume: string, lotVolume: string];

/**
* Bid
*/
b: [price: string, wholeLotVolume: string, lotVolume: string];

/**
* Last trade closed
*/
c: [price: string, lotVolume: string];

/**
* Volume
*/
v: [today: string, last24Hours: string];

/**
* Volume weighted average price
*/
p: [today: string, last24Hours: string];

/**
* Number of trades
*/
t: [today: number, last24Hours: number];

/**
* Low
*/
l: [today: string, last24Hours: string];

/**
* High
*/
h: [today: string, last24Hours: string];

/**
* Today's opening price
*/
o: string;
}
1 change: 1 addition & 0 deletions src/exchange/priceProvider/kraken/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { KrakenPriceProvider } from './krakenPriceProvider';
Loading