Skip to content

Commit

Permalink
Merge branch 'develop' into feat/atm-356-swap-preview
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/atomex/atomex.ts
#	src/atomex/atomexContext.ts
#	src/atomexBuilder/atomexBuilder.ts
#	src/atomexBuilder/controlledAtomexContext.ts
  • Loading branch information
maxima-net committed Aug 25, 2022
2 parents c470d82 + 29cce26 commit f178b5d
Show file tree
Hide file tree
Showing 20 changed files with 329 additions and 4 deletions.
15 changes: 14 additions & 1 deletion src/atomex/atomex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AtomexService, Currency } from '../common/index';
import type { Mutable } from '../core';
import { NewOrderRequest, ExchangeManager, symbolsHelper, OrderPreview, NormalizedOrderPreviewParameters } from '../exchange/index';
import type { Swap, SwapManager } from '../swaps/index';
import { toFixedBigNumber } from '../utils/converters';
import type { AtomexContext } from './atomexContext';
import { isNormalizedSwapPreviewParameters, normalizeSwapPreviewParameters } from './helpers';
import {
Expand Down Expand Up @@ -74,7 +75,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 Down Expand Up @@ -254,6 +255,18 @@ export class Atomex implements AtomexService {
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;
}

protected normalizeSwapPreviewParametersIfNeeded(swapPreviewParameters: SwapPreviewParameters | NormalizedSwapPreviewParameters): NormalizedSwapPreviewParameters {
return isNormalizedSwapPreviewParameters(swapPreviewParameters)
? swapPreviewParameters
Expand Down
14 changes: 13 additions & 1 deletion src/atomex/atomexContext.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 { BalanceManager } from '../blockchain/balanceManager';
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 @@ -28,6 +28,7 @@ class AtomexContextManagersSection {
private _authorizationManager: AuthorizationManager | undefined;
private _exchangeManager: ExchangeManager | undefined;
private _swapManager: SwapManager | undefined;
private _priceManager: PriceManager | undefined;
private _balanceManager: BalanceManager | undefined;

constructor(readonly context: AtomexContext) {
Expand Down Expand Up @@ -77,6 +78,17 @@ class AtomexContextManagersSection {
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;
}

get balanceManager(): BalanceManager {
if (!this._balanceManager)
throw new AtomexComponentNotResolvedError('managers.balanceManager');
Expand Down
14 changes: 13 additions & 1 deletion src/atomexBuilder/atomexBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { CachedBalanceManager } from '../blockchain/balanceManager';
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 @@ -58,6 +61,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();
this.controlledAtomexContext.managers.balanceManager = this.createBalanceManager();
const blockchains = this.createDefaultBlockchainOptions();

Expand Down Expand Up @@ -126,4 +130,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 @@ -3,7 +3,7 @@ import type { AtomexBlockchainProvider } from '../blockchain/atomexBlockchainPro
import type { BalanceManager } from '../blockchain/balanceManager';
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 @@ -28,6 +28,9 @@ interface ControlledAtomexContextManagersSection {
get swapManager(): SwapManager;
set swapManager(value: SwapManager);

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

get balanceManager(): BalanceManager;
set balanceManager(value: BalanceManager);
}
Expand Down
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

0 comments on commit f178b5d

Please sign in to comment.