From 991513777dd2cdef85b097f7dd1d9222e131064b Mon Sep 17 00:00:00 2001 From: Maxim Kucherov Date: Thu, 18 Aug 2022 00:07:25 +0300 Subject: [PATCH] Implement tzkt balance provider --- .vscode/settings.json | 7 +-- src/clients/rest/index.ts | 1 - src/clients/rest/restAtomexClient.ts | 2 +- src/{clients/rest => core}/httpClient.ts | 13 +++-- src/core/index.ts | 1 + src/tezos/balancesProviders/index.ts | 2 +- .../tezosBalancesProvider.ts | 18 ------- .../balancesProviders/tzktBalancesProvider.ts | 54 +++++++++++++++++++ src/tezos/config/defaultOptions.ts | 6 +-- src/tezos/index.ts | 2 +- .../atomexBlockchainProvider.test.ts | 6 +-- 11 files changed, 76 insertions(+), 36 deletions(-) rename src/{clients/rest => core}/httpClient.ts (75%) delete mode 100644 src/tezos/balancesProviders/tezosBalancesProvider.ts create mode 100644 src/tezos/balancesProviders/tzktBalancesProvider.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fa89e8e..62418de4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,9 +20,10 @@ "cSpell.language": "en", "cSpell.words": [ "atomex", - "tezos", - "outdir", "esbuild", - "taquito" + "outdir", + "taquito", + "tezos", + "Tzkt" ] } diff --git a/src/clients/rest/index.ts b/src/clients/rest/index.ts index 4a16d6e2..a31152d0 100644 --- a/src/clients/rest/index.ts +++ b/src/clients/rest/index.ts @@ -1,2 +1 @@ -export { HttpClient, type RequestOptions } from './httpClient'; export { RestAtomexClient, type RestAtomexClientOptions } from './restAtomexClient'; diff --git a/src/clients/rest/restAtomexClient.ts b/src/clients/rest/restAtomexClient.ts index 3082346f..c8b8c57c 100644 --- a/src/clients/rest/restAtomexClient.ts +++ b/src/clients/rest/restAtomexClient.ts @@ -4,6 +4,7 @@ import type { AuthorizationManager } from '../../authorization/index'; import type { Transaction } from '../../blockchain/index'; import type { AtomexNetwork, CancelAllSide, CurrenciesProvider, Side } from '../../common/index'; import { EventEmitter } from '../../core'; +import { HttpClient } from '../../core/index'; import { Order, OrderBook, Quote, ExchangeSymbol, NewOrderRequest, OrdersSelector, CancelOrderRequest, @@ -17,7 +18,6 @@ import { mapOrderBookDtoToOrderBook, mapOrderDtosToOrders, mapOrderDtoToOrder, mapQuoteDtosToQuotes, mapSwapDtosToSwaps, mapSwapDtoToSwap, mapSymbolDtosToSymbols } from '../helpers'; -import { HttpClient } from './httpClient'; export interface RestAtomexClientOptions { atomexNetwork: AtomexNetwork; diff --git a/src/clients/rest/httpClient.ts b/src/core/httpClient.ts similarity index 75% rename from src/clients/rest/httpClient.ts rename to src/core/httpClient.ts index 415206a0..e6c86127 100644 --- a/src/clients/rest/httpClient.ts +++ b/src/core/httpClient.ts @@ -1,5 +1,3 @@ -import type { DeepRequired } from '../../core/index'; - type QueryParams = { [key: string]: string | number | boolean | null | undefined }; type Payload = { [key: string]: unknown }; @@ -12,11 +10,16 @@ export interface RequestOptions { } export class HttpClient { + private static readonly defaultUndefinedResponseStatuses = [404]; + constructor( protected readonly baseUrl: string ) { } - async request(options: RequestOptions): Promise { + async request(options: RequestOptions): Promise; + async request(options: RequestOptions, returnUndefinedOn404: true): Promise; + async request(options: RequestOptions, returnUndefinedOn404: false): Promise; + async request(options: RequestOptions, returnUndefinedOn404 = true): Promise { const url = new URL(options.urlPath, this.baseUrl); if (options.params) @@ -28,7 +31,7 @@ export class HttpClient { body: options.payload ? JSON.stringify(options.payload) : undefined }); - if (response.status === 404) + if (returnUndefinedOn404 && response.status === 404) return undefined; if (!response.ok) { @@ -39,7 +42,7 @@ export class HttpClient { return await response.json(); } - private setSearchParams(url: URL, params: DeepRequired) { + private setSearchParams(url: URL, params: RequestOptions['params']) { for (const key in params) { const value = params[key]; if (value !== null && value !== undefined) diff --git a/src/core/index.ts b/src/core/index.ts index 73931000..d8de7c44 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,6 @@ export { EventEmitter } from './eventEmitter'; export { DeferredEventEmitter } from './deferredEventEmitter'; +export { HttpClient, type RequestOptions } from './httpClient'; export type { Result, SuccessResult, ErrorResult } from './result'; export type { PublicEventEmitter, ToEventEmitter, ToEventEmitters } from './eventEmitter'; diff --git a/src/tezos/balancesProviders/index.ts b/src/tezos/balancesProviders/index.ts index e053ca0d..a84d8b67 100644 --- a/src/tezos/balancesProviders/index.ts +++ b/src/tezos/balancesProviders/index.ts @@ -1 +1 @@ -export { TezosBalancesProvider } from './tezosBalancesProvider'; +export { TzktBalancesProvider } from './tzktBalancesProvider'; diff --git a/src/tezos/balancesProviders/tezosBalancesProvider.ts b/src/tezos/balancesProviders/tezosBalancesProvider.ts deleted file mode 100644 index 4eb39283..00000000 --- a/src/tezos/balancesProviders/tezosBalancesProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type BigNumber from 'bignumber.js'; - -import type { BalancesProvider } from '../../blockchain/index'; -import type { Currency } from '../../index'; -import { isTezosCurrency } from '../utils'; - -export class TezosBalancesProvider implements BalancesProvider { - constructor( - private readonly baseUrl: string - ) { } - - getBalance(_address: string, currency: Currency): Promise { - if (!isTezosCurrency(currency)) - throw new Error('Not tezos blockchain currency provided'); - - throw new Error('Not implemented'); - } -} diff --git a/src/tezos/balancesProviders/tzktBalancesProvider.ts b/src/tezos/balancesProviders/tzktBalancesProvider.ts new file mode 100644 index 00000000..09fe8267 --- /dev/null +++ b/src/tezos/balancesProviders/tzktBalancesProvider.ts @@ -0,0 +1,54 @@ +import type BigNumber from 'bignumber.js'; + +import type { BalancesProvider } from '../../blockchain/index'; +import type { Currency } from '../../common/index'; +import { HttpClient } from '../../core/index'; +import { numberToTokensAmount } from '../../utils/converters'; +import type { FA12TezosCurrency, FA2TezosCurrency, NativeTezosCurrency } from '../models/index'; +import { isTezosCurrency } from '../utils/index'; + +export class TzktBalancesProvider implements BalancesProvider { + private readonly httpClient: HttpClient; + + constructor(baseUrl: string) { + this.httpClient = new HttpClient(baseUrl); + } + + async getBalance(address: string, currency: Currency): Promise { + if (!isTezosCurrency(currency)) + throw new Error('Not tezos blockchain currency provided'); + + switch (currency.type) { + case 'native': + return await this.getNativeTokenBalance(address, currency); + + case 'fa1.2': + case 'fa2': + return await this.getTokenBalance(address, currency); + } + } + + private async getNativeTokenBalance(address: string, currency: NativeTezosCurrency): Promise { + const urlPath = `/v1/accounts/${address}/balance`; + const balance = await this.httpClient.request({ urlPath }, false); + + return numberToTokensAmount(balance, currency.decimals); + } + + private async getTokenBalance(address: string, currency: FA12TezosCurrency | FA2TezosCurrency): Promise { + const urlPath = '/v1/tokens/balances'; + const params = { + 'account': address, + 'token.contract': currency.contractAddress, + 'token.tokenId': currency.type === 'fa1.2' ? 0 : currency.tokenId, + 'select': 'balance' + }; + + const balances = await this.httpClient.request({ urlPath, params }, false); + const balance = balances[0]; + if (balance === undefined) + throw new Error('Invalid response'); + + return numberToTokensAmount(balance, currency.decimals); + } +} diff --git a/src/tezos/config/defaultOptions.ts b/src/tezos/config/defaultOptions.ts index 5b8696cb..5b2e78cb 100644 --- a/src/tezos/config/defaultOptions.ts +++ b/src/tezos/config/defaultOptions.ts @@ -1,6 +1,6 @@ import type { AtomexBlockchainNetworkOptions, AtomexContext, AtomexCurrencyOptions } from '../../atomex/index'; import { FA12TezosTaquitoAtomexProtocolV1, FA2TezosTaquitoAtomexProtocolV1, TezosTaquitoAtomexProtocolV1 } from '../atomexProtocol'; -import { TezosBalancesProvider } from '../balancesProviders/index'; +import { TzktBalancesProvider } from '../balancesProviders/index'; import { TaquitoBlockchainToolkitProvider } from '../blockchainToolkitProviders/index'; import type { TezosCurrency } from '../models'; import { TezosSwapTransactionsProvider } from '../swapTransactionsProviders/index'; @@ -81,7 +81,7 @@ export const createDefaultTezosBlockchainOptions = (atomexContext: AtomexContext currencies: tezosMainnetCurrencies, currencyOptions: createCurrencyOptions(atomexContext, tezosMainnetCurrencies, mainnetTezosTaquitoAtomexProtocolV1Options), blockchainToolkitProvider: new TaquitoBlockchainToolkitProvider(mainnetRpcUrl), - balancesProvider: new TezosBalancesProvider('https://api.mainnet.tzkt.io/'), + balancesProvider: new TzktBalancesProvider('https://api.mainnet.tzkt.io/'), swapTransactionsProvider, } : { @@ -89,7 +89,7 @@ export const createDefaultTezosBlockchainOptions = (atomexContext: AtomexContext currencies: tezosTestnetCurrencies, currencyOptions: createCurrencyOptions(atomexContext, tezosTestnetCurrencies, testnetTezosTaquitoAtomexProtocolV1Options), blockchainToolkitProvider: new TaquitoBlockchainToolkitProvider(testNetRpcUrl), - balancesProvider: new TezosBalancesProvider('https://api.ghostnet.tzkt.io/'), + balancesProvider: new TzktBalancesProvider('https://api.ghostnet.tzkt.io/'), swapTransactionsProvider, }; diff --git a/src/tezos/index.ts b/src/tezos/index.ts index 4f73dbb1..7a75e6f6 100644 --- a/src/tezos/index.ts +++ b/src/tezos/index.ts @@ -1,6 +1,6 @@ export { TaquitoBlockchainWallet } from './wallets/index'; export { TezosTaquitoAtomexProtocolV1, FA12TezosTaquitoAtomexProtocolV1, FA2TezosTaquitoAtomexProtocolV1 } from './atomexProtocol/index'; -export { TezosBalancesProvider } from './balancesProviders'; +export { TzktBalancesProvider } from './balancesProviders'; export { TezosSwapTransactionsProvider } from './swapTransactionsProviders'; export { TaquitoBlockchainToolkitProvider } from './blockchainToolkitProviders'; export { tezosMainnetCurrencies, tezosTestnetCurrencies, createDefaultTezosBlockchainOptions } from './config/index'; diff --git a/tests/blockchain/atomexBlockchainProvider.test.ts b/tests/blockchain/atomexBlockchainProvider.test.ts index 85ad54d1..f6eed793 100644 --- a/tests/blockchain/atomexBlockchainProvider.test.ts +++ b/tests/blockchain/atomexBlockchainProvider.test.ts @@ -6,7 +6,7 @@ import { } from '../../src/ethereum/index'; import { Web3BlockchainToolkitProvider } from '../../src/evm/index'; import { - TezosBalancesProvider, TaquitoBlockchainToolkitProvider, TezosCurrency, TezosSwapTransactionsProvider + TzktBalancesProvider, TaquitoBlockchainToolkitProvider, TezosCurrency, TezosSwapTransactionsProvider } from '../../src/tezos/index'; describe('Atomex Blockchain Provider', () => { @@ -41,7 +41,7 @@ describe('Atomex Blockchain Provider', () => { test('applies blockchain options and returns them', () => { const tezosNetworkOptions: AtomexBlockchainNetworkOptions = { rpcUrl: '', - balancesProvider: new TezosBalancesProvider(), + balancesProvider: new TzktBalancesProvider(''), blockchainToolkitProvider: new TaquitoBlockchainToolkitProvider(''), swapTransactionsProvider: new TezosSwapTransactionsProvider(), currencies: [tezosNativeCurrency], @@ -79,7 +79,7 @@ describe('Atomex Blockchain Provider', () => { try { const networkOptions: AtomexBlockchainNetworkOptions = { rpcUrl: '', - balancesProvider: new TezosBalancesProvider(), + balancesProvider: new TzktBalancesProvider(''), blockchainToolkitProvider: new TaquitoBlockchainToolkitProvider(''), swapTransactionsProvider: new TezosSwapTransactionsProvider(), currencies: [tezosNativeCurrency],