From 883bcf928ffd99ada35c5fca7b0286dd5aa97f46 Mon Sep 17 00:00:00 2001 From: Aleksandr Kraiz Date: Tue, 10 May 2022 11:44:09 +0400 Subject: [PATCH] New fetchWithValidation / introducing simpleFetch --- .eslintrc.js | 4 +- README.md | 49 ++++++- package-lock.json | 11 ++ package.json | 3 +- src/OrionUnit/Exchange/deposit.ts | 5 +- src/OrionUnit/Exchange/swapMarket.ts | 18 +-- src/OrionUnit/Exchange/withdraw.ts | 5 +- src/OrionUnit/FarmingManager/index.ts | 9 +- src/fetchWithValidation.ts | 184 ++++++++++++++++++++------ src/index.ts | 3 +- src/services/OrionAggregator/index.ts | 38 ++++-- src/services/OrionBlockchain/index.ts | 95 ++++++++----- src/services/PriceFeed/index.ts | 6 +- src/simpleFetch.ts | 28 ++++ src/utils/getBalance.ts | 7 +- src/utils/index.ts | 2 +- 16 files changed, 350 insertions(+), 117 deletions(-) create mode 100644 src/simpleFetch.ts diff --git a/.eslintrc.js b/.eslintrc.js index dd765e6..66b8648 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,11 +15,11 @@ module.exports = { ], parser: '@typescript-eslint/parser', parserOptions: { - tsconfigRootDir: "./", + tsconfigRootDir: __dirname, project: [ "./tsconfig.json" ], - ecmaVersion: 12, + ecmaVersion: 2021, sourceType: 'module', }, plugins: [ diff --git a/README.md b/README.md index 988f4d9..5b0ba67 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ const chain = "0x61"; // bsc-testnet const env = "testing"; const startApp = async (provider: BaseProvider) => { - const web3provider = new providers.Web3Provider(provider); + const web3Provider = new providers.Web3Provider(provider); await web3Provider.ready; const signer = web3Provider.getSigner(); // ready to go const orionUnit = initOrionUnit(chain, env); // ready to go @@ -128,7 +128,9 @@ orionUnit.farmingManager.removeAllLiquidity({ ### Get historical price ```ts -const candles = await orionUnit.priceFeed.getCandles( +import { simpleFetch } from "@orionprotocol/sdk"; + +const candles = await simpleFetch(orionUnit.priceFeed.getCandles)( "ORN-USDT", 1650287678, // interval start 1650374078, // interval end @@ -163,13 +165,16 @@ const orionVoting = contracts.OrionVoting__factory.connect( ### Get tradable pairs ```ts -const pairsList = await orionUnit.orionAggregator.getPairsList(); +import { simpleFetch } from "@orionprotocol/sdk"; +const pairsList = await simpleFetch(orionUnit.orionAggregator.getPairsList)(); ``` ### Get swap info ```ts -const swapInfo = await orionUnit.orionAggregator.getSwapInfo( +import { simpleFetch } from '@orionprotocol/sdk'; + +const swapInfo = await simpleFetch(orionUnit.orionAggregator.getSwapInfo)( // Use 'exactSpend' when 'amount' is how much you want spend. Use 'exactReceive' otherwise type: 'exactSpend', assetIn: 'ORN', @@ -181,7 +186,12 @@ const swapInfo = await orionUnit.orionAggregator.getSwapInfo( ### Place order in Orion Aggregator ```ts -const { orderId } = await orionUnit.orionAggregator.placeOrder( +import { simpleFetch } from "@orionprotocol/sdk"; + +// You can yse simpleFetch or "default" (vebose) fetch +// Simple fetch + +const { orderId } = await simpleFetch(orionUnit.orionAggregator.placeOrder)( { senderAddress: "0x61eed69c0d112c690fd6f44bb621357b89fbe67f", matcherAddress: "0xfbcad2c3a90fbd94c335fbdf8e22573456da7f68", @@ -198,6 +208,35 @@ const { orderId } = await orionUnit.orionAggregator.placeOrder( }, false // Place in internal orderbook ); + +// Default ("verbose") fetch + +const placeOrderFetchResult = await simpleFetch( + orionUnit.orionAggregator.placeOrder +)(); +// Same params as above + +if (placeOrderFetchResult.isErr()) { + // You can handle fetching errors here + // You can access error text, statuses + const { error } = placeOrderFetchResult; + switch (error.type) { + case "fetchError": // (no network, connection refused, connection break) + console.error(error.message); + break; + case "unknownFetchError": // Instance of Error + console.error(`URL: ${error.url}, Error: ${error.message}`); + break; + case "unknownFetchThrow": + console.error("Something wrong happened furing fetching", error.error); + break; + // ... and 8 errors types more + // see src/fetchWithValidation.ts for details + } +} else { + // Succes result + const { orderId } = placeOrderFetchResult.value; +} ``` ### Orion Aggregator WebSocket diff --git a/package-lock.json b/package-lock.json index f5865fc..c7067d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "ethers": "^5.6.2", "isomorphic-ws": "^4.0.1", "just-clone": "^5.0.1", + "neverthrow": "^4.3.1", "node-fetch": "^2.6.7", "socket.io-client": "2.4.0", "stream-browserify": "^3.0.0", @@ -9135,6 +9136,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/neverthrow": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-4.3.1.tgz", + "integrity": "sha512-+vxjSaiDWjAj6kR6KKW0YDuV6O4UCNWGAO8m8ITjFKPWcTmU1GVnL+J5TAUTKpPnUAHCKDxXpOHVaERid223Ww==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -17497,6 +17503,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "neverthrow": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-4.3.1.tgz", + "integrity": "sha512-+vxjSaiDWjAj6kR6KKW0YDuV6O4UCNWGAO8m8ITjFKPWcTmU1GVnL+J5TAUTKpPnUAHCKDxXpOHVaERid223Ww==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", diff --git a/package.json b/package.json index 69c9a9f..b9d4fc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orionprotocol/sdk", - "version": "0.3.4", + "version": "0.4.0", "description": "Orion Protocol SDK", "main": "./lib/esm/index.js", "module": "./lib/esm/index.js", @@ -70,6 +70,7 @@ "ethers": "^5.6.2", "isomorphic-ws": "^4.0.1", "just-clone": "^5.0.1", + "neverthrow": "^4.3.1", "node-fetch": "^2.6.7", "socket.io-client": "2.4.0", "stream-browserify": "^3.0.0", diff --git a/src/OrionUnit/Exchange/deposit.ts b/src/OrionUnit/Exchange/deposit.ts index dd6e86e..0de7ed2 100644 --- a/src/OrionUnit/Exchange/deposit.ts +++ b/src/OrionUnit/Exchange/deposit.ts @@ -10,6 +10,7 @@ import { } from '../../constants'; import { normalizeNumber } from '../../utils'; import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency'; +import simpleFetch from '../../simpleFetch'; export type DepositParams = { asset: string, @@ -38,12 +39,12 @@ export default async function deposit({ const { exchangeContractAddress, assetToAddress, - } = await orionBlockchain.getInfo(); + } = await simpleFetch(orionBlockchain.getInfo)(); const nativeCryptocurrency = getNativeCryptocurrency(assetToAddress); const exchangeContract = contracts.Exchange__factory.connect(exchangeContractAddress, provider); - const gasPriceWei = await orionBlockchain.getGasPriceWei(); + const gasPriceWei = await simpleFetch(orionBlockchain.getGasPriceWei)(); const assetAddress = assetToAddress[asset]; if (!assetAddress) throw new Error(`Asset '${asset}' not found`); diff --git a/src/OrionUnit/Exchange/swapMarket.ts b/src/OrionUnit/Exchange/swapMarket.ts index 4fe36e9..facc9e0 100644 --- a/src/OrionUnit/Exchange/swapMarket.ts +++ b/src/OrionUnit/Exchange/swapMarket.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable max-len */ import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; @@ -8,6 +9,7 @@ import OrionUnit from '..'; import { contracts, crypt, utils } from '../..'; import { INTERNAL_ORION_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants'; import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency'; +import simpleFetch from '../../simpleFetch'; export type SwapMarketParams = { type: 'exactSpend' | 'exactReceive', @@ -74,13 +76,13 @@ export default async function swapMarket({ exchangeContractAddress, matcherAddress, assetToAddress, - } = await orionBlockchain.getInfo(); - const nativeCryptocurrency = getNativeCryptocurrency(assetToAddress); + } = await simpleFetch(orionBlockchain.getInfo)(); + const nativeCryptocurrency = getNativeCryptocurrency(assetToAddress); const exchangeContract = contracts.Exchange__factory.connect(exchangeContractAddress, provider); - const feeAssets = await orionBlockchain.getTokensFee(); - const pricesInOrn = await orionBlockchain.getPrices(); - const gasPriceWei = await orionBlockchain.getGasPriceWei(); + const feeAssets = await simpleFetch(orionBlockchain.getTokensFee)(); + const pricesInOrn = await simpleFetch(orionBlockchain.getPrices)(); + const gasPriceWei = await simpleFetch(orionBlockchain.getGasPriceWei)(); const gasPriceGwei = ethers.utils.formatUnits(gasPriceWei, 'gwei').toString(); @@ -111,7 +113,7 @@ export default async function swapMarket({ signer, ); - const swapInfo = await orionAggregator.getSwapInfo(type, assetIn, assetOut, amount.toString()); + const swapInfo = await simpleFetch(orionAggregator.getSwapInfo)(type, assetIn, assetOut, amount.toString()); if (swapInfo.type === 'exactReceive' && amountBN.lt(swapInfo.minAmountOut)) { throw new Error(`Amount is too low. Min amountOut is ${swapInfo.minAmountOut} ${assetOut}`); @@ -240,7 +242,7 @@ export default async function swapMarket({ .toString(); const [baseAssetName, quoteAssetName] = swapInfo.orderInfo.assetPair.split('-'); - const pairConfig = await orionAggregator.getPairConfig(`${baseAssetName}-${quoteAssetName}`); + const pairConfig = await simpleFetch(orionAggregator.getPairConfig)(`${baseAssetName}-${quoteAssetName}`); if (!pairConfig) throw new Error(`Pair config ${baseAssetName}-${quoteAssetName} not found`); const baseAssetAddress = assetToAddress[baseAssetName]; @@ -336,7 +338,7 @@ export default async function swapMarket({ const orderIsOk = await exchangeContract.validateOrder(signedOrder); if (!orderIsOk) throw new Error('Order is not valid'); - const { orderId } = await orionAggregator.placeOrder(signedOrder, false); + const { orderId } = await simpleFetch(orionAggregator.placeOrder)(signedOrder, false); return { through: 'aggregator', id: orderId, diff --git a/src/OrionUnit/Exchange/withdraw.ts b/src/OrionUnit/Exchange/withdraw.ts index 64a968d..64bc6bd 100644 --- a/src/OrionUnit/Exchange/withdraw.ts +++ b/src/OrionUnit/Exchange/withdraw.ts @@ -10,6 +10,7 @@ import { } from '../../constants'; import { normalizeNumber } from '../../utils'; import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency'; +import simpleFetch from '../../simpleFetch'; export type WithdrawParams = { asset: string, @@ -38,11 +39,11 @@ export default async function withdraw({ const { exchangeContractAddress, assetToAddress, - } = await orionBlockchain.getInfo(); + } = await simpleFetch(orionBlockchain.getInfo)(); const nativeCryptocurrency = getNativeCryptocurrency(assetToAddress); const exchangeContract = contracts.Exchange__factory.connect(exchangeContractAddress, provider); - const gasPriceWei = await orionBlockchain.getGasPriceWei(); + const gasPriceWei = await simpleFetch(orionBlockchain.getGasPriceWei)(); const assetAddress = assetToAddress[asset]; if (!assetAddress) throw new Error(`Asset '${asset}' not found`); diff --git a/src/OrionUnit/FarmingManager/index.ts b/src/OrionUnit/FarmingManager/index.ts index b105548..013937f 100644 --- a/src/OrionUnit/FarmingManager/index.ts +++ b/src/OrionUnit/FarmingManager/index.ts @@ -4,6 +4,7 @@ import OrionUnit from '..'; import { contracts } from '../..'; import BalanceGuard from '../../BalanceGuard'; import { ADD_LIQUIDITY_GAS_LIMIT, INTERNAL_ORION_PRECISION, NATIVE_CURRENCY_PRECISION } from '../../constants'; +import simpleFetch from '../../simpleFetch'; import { denormalizeNumber, normalizeNumber } from '../../utils'; import getBalances from '../../utils/getBalances'; import getNativeCryptocurrency from '../../utils/getNativeCryptocurrency'; @@ -46,7 +47,7 @@ export default class FarmingManager { exchangeContractAddress, assetToAddress, assetToDecimals, - } = await this.orionUnit.orionBlockchain.getInfo(); + } = await simpleFetch(this.orionUnit.orionBlockchain.getInfo)(); const walletAddress = await signer.getAddress(); @@ -85,7 +86,7 @@ export default class FarmingManager { signer, ); - const poolsConfig = await this.orionUnit.orionBlockchain.getPoolsConfig(); + const poolsConfig = await simpleFetch(this.orionUnit.orionBlockchain.getPoolsConfig)(); const pool = poolsConfig.pools[poolName]; if (!pool) throw new Error(`Pool ${poolName} not found`); @@ -223,7 +224,7 @@ export default class FarmingManager { assetToAddress, assetToDecimals, exchangeContractAddress, - } = await this.orionUnit.orionBlockchain.getInfo(); + } = await simpleFetch(this.orionUnit.orionBlockchain.getInfo)(); const assetAAddress = assetToAddress[assetA]; if (!assetAAddress) throw new Error(`Asset '${assetA}' not found`); @@ -235,7 +236,7 @@ export default class FarmingManager { const assetBDecimals = assetToDecimals[assetB]; if (!assetBDecimals) throw new Error(`Decimals for asset '${assetB}' not found`); - const poolsConfig = await this.orionUnit.orionBlockchain.getPoolsConfig(); + const poolsConfig = await simpleFetch(this.orionUnit.orionBlockchain.getPoolsConfig)(); const pool = poolsConfig.pools[poolName]; if (!pool) throw new Error(`Pool ${poolName} not found`); diff --git a/src/fetchWithValidation.ts b/src/fetchWithValidation.ts index 628e6c9..207d9e0 100644 --- a/src/fetchWithValidation.ts +++ b/src/fetchWithValidation.ts @@ -1,55 +1,155 @@ import { Schema, z } from 'zod'; -import fetch, { RequestInit } from 'node-fetch'; -import { isWithError, isWithReason, HttpError } from './utils'; +import fetch, { FetchError, RequestInit } from 'node-fetch'; +import { + err, fromPromise, fromThrowable, ok, +} from 'neverthrow'; -export class ExtendedError extends Error { - public url: string; - - public status: number | null; - - constructor(url: string, status: number | null, message: string) { - super(); - this.url = url; - this.status = status; - this.message = message; - } -} - -export const fetchJsonWithValidation = async ( +export default async function fetchWithValidation( url: string, schema: Schema, options?: RequestInit, errorSchema?: Schema, -) => { - const response = await fetch(url, { +) { + // Cases: + // 1. fetchError (no network, connection refused, connection break) + // 2. unknownFetchError + // 3. unknownFetchThrow + // 4. unknownGetTextError + // 5. unknownGetTextUnknownError + // 6. serverError + // 7. jsonParseError + // 8. jsonParseUnknownError + // 9. clientErrorWithResponsePayload + // 10. clientErrorPayloadParseError + // 11. clientError + // 12. payloadParseError + // 13. payload + + const fetchResult = await fromPromise(fetch(url, { ...options || {}, headers: { 'Cache-Control': 'no-store, max-age=0', ...(options ? options.headers : {}), }, - }); - const text = await response.text(); - - // The ok read-only property of the Response interface contains a Boolean - // stating whether the response was successful (status in the range 200 - 299) or not. - - if (!response.ok) { - throw new HttpError(response.status, text, 'HTTP', response.statusText); - } - - const payload: unknown = JSON.parse(text); - - try { - const data = schema.parse(payload); - return data; - } catch (e) { - if (errorSchema) { - const errorObj = errorSchema.parse(payload); - if (isWithError(errorObj) && isWithReason(errorObj.error)) { - throw new ExtendedError(url, response.status, errorObj.error.reason); - } + }), (e) => { + if (e instanceof FetchError) { + return err({ + type: 'fetchError' as const, + url, + message: `${e.message} (${e.type})`, + error: e, + }); + } if (e instanceof Error) { + return err({ + type: 'unknownFetchError' as const, + url, + message: e.message, + error: e, + }); } - if (e instanceof Error) throw new ExtendedError(url, response.status, e.message); - throw e; + return err({ + type: 'unknownFetchThrow' as const, + url, + message: 'Unknown fetch error', + error: e, + }); + }); + + if (fetchResult.isErr()) return fetchResult.error; + + const response = fetchResult.value; + + const textResult = await fromPromise(response.text(), (e) => { + if (e instanceof Error) { + return err({ + type: 'unknownGetTextError' as const, + url, + message: `Can't get response content: ${e.message}`, + error: e, + }); + } + return err({ + type: 'unknownGetTextUnknownError' as const, + url, + message: "Can't get response content: unknown error", + error: e, + }); + }); + + if (textResult.isErr()) return textResult.error; + + const text = textResult.value; + + if (response.status >= 500) { // Server error + return err({ + type: 'serverError' as const, + url, + message: `Server error: ${response.status} ${response.statusText}`, + status: response.status, + text, + }); } -}; + + const safeParseJson = fromThrowable(JSON.parse, (e) => { + if (e instanceof Error) { + return err({ + type: 'jsonParseError' as const, + url, + message: e.message, + error: e, + }); + } + return err({ + type: 'jsonParseUnknownError' as const, + url, + message: 'Unknown JSON parse error', + error: e, + }); + }); + + const jsonResult = safeParseJson(text); + + if (jsonResult.isErr()) return jsonResult.error; + + const json: unknown = jsonResult.value; + + if (response.status >= 400) { // Client error + if (errorSchema) { + const serverError = errorSchema.safeParse(json); + if (serverError.success) { + return err({ + type: 'clientErrorWithResponsePayload' as const, + url, + message: `Client error: ${response.status} ${response.statusText}`, + status: response.status, + payload: serverError.data, + }); + } + return err({ + type: 'clientErrorPayloadParseError' as const, + message: 'Can\'t recognize error message', + status: response.status, + text, + error: serverError.error, + }); + } + return err({ + type: 'clientError' as const, + url, + message: `Error: ${response.status} ${response.statusText}`, + status: response.status, + text, + }); + } + + const payload = schema.safeParse(json); + if (!payload.success) { + return err({ + type: 'payloadParseError' as const, + url, + message: 'Can\'t recognize response payload', + error: payload.error, + }); + } + return ok(payload.data); +} diff --git a/src/index.ts b/src/index.ts index 8f75e6b..214962d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,8 @@ export * as config from './config'; // export * from './entities'; export { default as OrionUnit } from './OrionUnit'; export { default as initOrionUnit } from './initOrionUnit'; -export * from './fetchWithValidation'; +export { default as fetchWithValidation } from './fetchWithValidation'; +export { default as simpleFetch } from './simpleFetch'; export * as utils from './utils'; export * as services from './services'; export * as contracts from './artifacts/contracts'; diff --git a/src/services/OrionAggregator/index.ts b/src/services/OrionAggregator/index.ts index 806e7d9..8ab758d 100644 --- a/src/services/OrionAggregator/index.ts +++ b/src/services/OrionAggregator/index.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js'; import { z } from 'zod'; -import { fetchJsonWithValidation } from '../../fetchWithValidation'; +import fetchWithValidation from '../../fetchWithValidation'; import swapInfoSchema from './schemas/swapInfoSchema'; import exchangeInfoSchema from './schemas/exchangeInfoSchema'; import cancelOrderSchema from './schemas/cancelOrderSchema'; @@ -8,7 +8,7 @@ import orderBenefitsSchema from './schemas/orderBenefitsSchema'; import errorSchema from './errorSchema'; import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema'; import { OrionAggregatorWS } from './ws'; -import atomicSwapHistorySchema from './schemas/atomicSwapHistorySchema'; +import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema'; import { SignedCancelOrderRequest, SignedOrder, SupportedChainId } from '../../types'; import { pairConfigSchema } from './schemas'; @@ -20,6 +20,18 @@ class OrionAggregator { constructor(apiUrl: string, chainId: SupportedChainId) { this.apiUrl = apiUrl; this.ws = new OrionAggregatorWS(this.aggregatorWSUrl, chainId); + + this.getHistoryAtomicSwaps = this.getHistoryAtomicSwaps.bind(this); + this.getPairConfig = this.getPairConfig.bind(this); + this.getPairConfigs = this.getPairConfigs.bind(this); + this.getPairsList = this.getPairsList.bind(this); + this.getSwapInfo = this.getSwapInfo.bind(this); + this.getTradeProfits = this.getTradeProfits.bind(this); + this.placeAtomicSwap = this.placeAtomicSwap.bind(this); + this.placeOrder = this.placeOrder.bind(this); + this.cancelOrder = this.cancelOrder.bind(this); + this.checkWhitelisted = this.checkWhitelisted.bind(this); + this.getLockedBalance = this.getLockedBalance.bind(this); } get aggregatorWSUrl() { return `wss://${this.apiUrl}/v1`; } @@ -29,14 +41,14 @@ class OrionAggregator { } getPairsList() { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/pairs/list`, z.array(z.string()), ); } getPairConfigs() { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/pairs/exchangeInfo`, exchangeInfoSchema, undefined, @@ -45,7 +57,7 @@ class OrionAggregator { } getPairConfig(assetPair: string) { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/pairs/exchangeInfo/${assetPair}`, pairConfigSchema, undefined, @@ -54,7 +66,7 @@ class OrionAggregator { } checkWhitelisted(address: string) { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/whitelist/check?address=${address}`, z.boolean(), undefined, @@ -73,7 +85,7 @@ class OrionAggregator { ...partnerId && { 'X-Partner-Id': partnerId }, }; - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/order/${isCreateInternalOrder ? 'internal' : ''}`, z.object({ orderId: z.string(), @@ -95,7 +107,7 @@ class OrionAggregator { } cancelOrder(signedCancelOrderRequest: SignedCancelOrderRequest) { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/order`, cancelOrderSchema, { @@ -128,7 +140,7 @@ class OrionAggregator { url.searchParams.append('amountOut', amount); } - return fetchJsonWithValidation( + return fetchWithValidation( url.toString(), swapInfoSchema, undefined, @@ -139,7 +151,7 @@ class OrionAggregator { getLockedBalance(address: string, currency: string) { const url = new URL(`${this.aggregatorUrl}/api/v1/address/balance/reserved/${currency}`); url.searchParams.append('address', address); - return fetchJsonWithValidation( + return fetchWithValidation( url.toString(), z.object({ [currency]: z.number(), @@ -159,7 +171,7 @@ class OrionAggregator { url.searchParams.append('amount', amount.toString()); url.searchParams.append('side', isBuy ? 'buy' : 'sell'); - return fetchJsonWithValidation( + return fetchWithValidation( url.toString(), orderBenefitsSchema, undefined, @@ -177,7 +189,7 @@ class OrionAggregator { secretHash: string, sourceNetworkCode: string, ) { - return fetchJsonWithValidation( + return fetchWithValidation( `${this.aggregatorUrl}/api/v1/atomic-swap`, placeAtomicSwapSchema, { @@ -204,7 +216,7 @@ class OrionAggregator { const url = new URL(`${this.aggregatorUrl}/api/v1/atomic-swap/history/all`); url.searchParams.append('sender', sender); url.searchParams.append('limit', limit.toString()); - return fetchJsonWithValidation(url.toString(), atomicSwapHistorySchema); + return fetchWithValidation(url.toString(), atomicSwapHistorySchema); } } export * as schemas from './schemas'; diff --git a/src/services/OrionBlockchain/index.ts b/src/services/OrionBlockchain/index.ts index 8b3ef95..9fa0b82 100644 --- a/src/services/OrionBlockchain/index.ts +++ b/src/services/OrionBlockchain/index.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { fetchJsonWithValidation } from '../../fetchWithValidation'; +import fetchWithValidation from '../../fetchWithValidation'; import { PairStatusEnum, pairStatusSchema } from './schemas/adminPoolsListSchema'; import { IDOSchema, atomicHistorySchema, @@ -56,6 +56,32 @@ class OrionBlockchain { constructor(apiUrl: string) { this.apiUrl = apiUrl; this.ws = new OrionBlockchainSocketIO(`https://${apiUrl}/`); + + this.getAtomicSwapAssets = this.getAtomicSwapAssets.bind(this); + this.getAtomicSwapHistory = this.getAtomicSwapHistory.bind(this); + this.getAuthToken = this.getAuthToken.bind(this); + this.getCirculatingSupply = this.getCirculatingSupply.bind(this); + this.getInfo = this.getInfo.bind(this); + this.getPoolsConfig = this.getPoolsConfig.bind(this); + this.getPoolsInfo = this.getPoolsInfo.bind(this); + this.getHistory = this.getHistory.bind(this); + this.getPrices = this.getPrices.bind(this); + this.getTokensFee = this.getTokensFee.bind(this); + this.getGasPriceWei = this.getGasPriceWei.bind(this); + this.checkFreeRedeemAvailable = this.checkFreeRedeemAvailable.bind(this); + this.redeemAtomicSwap = this.redeemAtomicSwap.bind(this); + this.redeem2AtomicSwaps = this.redeem2AtomicSwaps.bind(this); + this.checkRedeem = this.checkRedeem.bind(this); + this.checkRedeem2Atomics = this.checkRedeem2Atomics.bind(this); + this.getIDOInfo = this.getIDOInfo.bind(this); + this.checkAuth = this.checkAuth.bind(this); + this.addPool = this.addPool.bind(this); + this.editPool = this.editPool.bind(this); + this.getPoolsList = this.getPoolsList.bind(this); + this.getSourceAtomicSwapHistory = this.getSourceAtomicSwapHistory.bind(this); + this.getTargetAtomicSwapHistory = this.getTargetAtomicSwapHistory.bind(this); + this.checkPoolInformation = this.checkPoolInformation.bind(this); + this.checkIfHashUsed = this.checkIfHashUsed.bind(this); } get orionBlockchainWsUrl() { @@ -63,43 +89,43 @@ class OrionBlockchain { } getAuthToken() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/auth/token`, z.object({ token: z.string() })); + return fetchWithValidation(`https://${this.apiUrl}/api/auth/token`, z.object({ token: z.string() })); } getCirculatingSupply() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/circulating-supply`, z.number()); + return fetchWithValidation(`https://${this.apiUrl}/api/circulating-supply`, z.number()); } getInfo() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/info`, infoSchema); + return fetchWithValidation(`https://${this.apiUrl}/api/info`, infoSchema); } getPoolsConfig() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/pools/config`, poolsConfigSchema); + return fetchWithValidation(`https://${this.apiUrl}/api/pools/config`, poolsConfigSchema); } getPoolsInfo() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/pools/info`, poolsInfoSchema); + return fetchWithValidation(`https://${this.apiUrl}/api/pools/info`, poolsInfoSchema); } getHistory(address: string) { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/history/${address}`, historySchema); + return fetchWithValidation(`https://${this.apiUrl}/api/history/${address}`, historySchema); } getPrices() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/prices`, z.record(z.string()).transform(utils.makePartial)); + return fetchWithValidation(`https://${this.apiUrl}/api/prices`, z.record(z.string()).transform(utils.makePartial)); } getTokensFee() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/tokensFee`, z.record(z.string()).transform(utils.makePartial)); + return fetchWithValidation(`https://${this.apiUrl}/api/tokensFee`, z.record(z.string()).transform(utils.makePartial)); } getGasPriceWei() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/gasPrice`, z.string()); + return fetchWithValidation(`https://${this.apiUrl}/api/gasPrice`, z.string()); } checkFreeRedeemAvailable(walletAddress: string) { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/atomic/has-free-redeem/${walletAddress}`, z.boolean()); + return fetchWithValidation(`https://${this.apiUrl}/api/atomic/has-free-redeem/${walletAddress}`, z.boolean()); } redeemAtomicSwap( @@ -107,7 +133,7 @@ class OrionBlockchain { secret: string, sourceNetwork: string, ) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/atomic/matcher-redeem`, z.string(), { @@ -131,7 +157,7 @@ class OrionBlockchain { secret2: string, sourceNetwork: string, ) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/atomic/matcher-redeem2atomics`, z.string(), { @@ -151,31 +177,31 @@ class OrionBlockchain { } checkRedeem(secretHash: string) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/atomic/matcher-redeem/${secretHash}`, z.enum(['OK', 'FAIL']).nullable(), ); } checkRedeem2Atomics(firstSecretHash: string, secondSecretHash: string) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/atomic/matcher-redeem/${firstSecretHash}-${secondSecretHash}`, z.enum(['OK', 'FAIL']).nullable(), ); } getIDOInfo() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/solarflare`, IDOSchema); + return fetchWithValidation(`https://${this.apiUrl}/api/solarflare`, IDOSchema); } checkAuth(headers: IAdminAuthHeaders) { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/auth/check`, z.object({ + return fetchWithValidation(`https://${this.apiUrl}/api/auth/check`, z.object({ auth: z.boolean(), }), { headers }); } getPoolsList(headers: IAdminAuthHeaders) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/pools/list`, adminPoolsListSchema, { headers }, @@ -183,7 +209,7 @@ class OrionBlockchain { } editPool(address: string, data: IEditPool, headers: IAdminAuthHeaders) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/pools/edit/${address}`, pairStatusSchema, { @@ -198,22 +224,27 @@ class OrionBlockchain { } addPool(data: z.infer) { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/pools/add`, z.number(), { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', + return fetchWithValidation( + `https://${this.apiUrl}/api/pools/add`, + z.number(), + { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, }, - }); + z.string(), + ); } checkPoolInformation(poolAddress: string) { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/pools/check/${poolAddress}`, pairStatusSchema); + return fetchWithValidation(`https://${this.apiUrl}/api/pools/check/${poolAddress}`, pairStatusSchema); } getAtomicSwapAssets() { - return fetchJsonWithValidation(`https://${this.apiUrl}/api/atomic/swap-assets`, z.array(z.string())); + return fetchWithValidation(`https://${this.apiUrl}/api/atomic/swap-assets`, z.array(z.string())); } /** @@ -226,7 +257,7 @@ class OrionBlockchain { Object.entries(query) .forEach(([key, value]) => url.searchParams.append(key, value.toString())); - return fetchJsonWithValidation(url.toString(), atomicHistorySchema); + return fetchWithValidation(url.toString(), atomicHistorySchema); } getSourceAtomicSwapHistory(query: AtomicSwapHistorySourceQuery) { @@ -237,7 +268,7 @@ class OrionBlockchain { if (!query.type) url.searchParams.append('type', 'source'); - return fetchJsonWithValidation(url.toString(), sourceAtomicHistorySchema); + return fetchWithValidation(url.toString(), sourceAtomicHistorySchema); } getTargetAtomicSwapHistory(query: AtomicSwapHistoryTargetQuery) { @@ -248,11 +279,11 @@ class OrionBlockchain { if (!query.type) url.searchParams.append('type', 'target'); - return fetchJsonWithValidation(url.toString(), targetAtomicHistorySchema); + return fetchWithValidation(url.toString(), targetAtomicHistorySchema); } checkIfHashUsed(secretHashes: string[]) { - return fetchJsonWithValidation( + return fetchWithValidation( `https://${this.apiUrl}/api/atomic/is-hash-used`, z.record(z.boolean()).transform(utils.makePartial), { diff --git a/src/services/PriceFeed/index.ts b/src/services/PriceFeed/index.ts index 9443a2d..3dca9b4 100644 --- a/src/services/PriceFeed/index.ts +++ b/src/services/PriceFeed/index.ts @@ -1,4 +1,4 @@ -import { fetchJsonWithValidation } from '../../fetchWithValidation'; +import fetchWithValidation from '../../fetchWithValidation'; import candlesSchema from './schemas/candlesSchema'; class PriceFeed { @@ -6,6 +6,8 @@ class PriceFeed { constructor(apiUrl: string) { this.apiUrl = apiUrl; + + this.getCandles = this.getCandles.bind(this); } getCandles( @@ -22,7 +24,7 @@ class PriceFeed { url.searchParams.append('interval', interval); url.searchParams.append('exchange', exchange); - return fetchJsonWithValidation( + return fetchWithValidation( url.toString(), candlesSchema, ); diff --git a/src/simpleFetch.ts b/src/simpleFetch.ts new file mode 100644 index 0000000..494ecc1 --- /dev/null +++ b/src/simpleFetch.ts @@ -0,0 +1,28 @@ +import { Schema, z } from 'zod'; +import { RequestInit } from 'node-fetch'; +import fetchWithValidation from './fetchWithValidation'; + +// https://stackoverflow.com/a/64919133 +class Wrapper { + // eslint-disable-next-line class-methods-use-this + wrapped( + url: string, + schema: Schema, + options?: RequestInit, + errorSchema?: Schema, + ) { + return fetchWithValidation(url, schema, options, errorSchema); + } +} + +type FetchWithValidationInternalType = ReturnType['wrapped']> + +export default function simpleFetch( + f: (...params: P) => FetchWithValidationInternalType, +) { + return async (...params: Parameters) => { + const result = await f(...params); + if (result.isErr()) throw new Error(result.error.message); + return result.value; + }; +} diff --git a/src/utils/getBalance.ts b/src/utils/getBalance.ts index 7d87f10..4d9df63 100644 --- a/src/utils/getBalance.ts +++ b/src/utils/getBalance.ts @@ -30,10 +30,13 @@ export default async function getBalance( } const assetContractBalance = await exchangeContract.getBalance(assetAddress, walletAddress); const denormalizedAssetInContractBalance = utils.denormalizeNumber(assetContractBalance, INTERNAL_ORION_PRECISION); - const denormalizedAssetLockedBalance = await orionAggregator.getLockedBalance(walletAddress, asset); + const denormalizedAssetLockedBalanceResult = await orionAggregator.getLockedBalance(walletAddress, asset); + if (denormalizedAssetLockedBalanceResult.isErr()) { + throw new Error(denormalizedAssetLockedBalanceResult.error.message); + } return { - exchange: denormalizedAssetInContractBalance.minus(denormalizedAssetLockedBalance[asset] ?? 0), + exchange: denormalizedAssetInContractBalance.minus(denormalizedAssetLockedBalanceResult.value[asset] ?? 0), wallet: denormalizedAssetInWalletBalance, }; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 763927f..9bef250 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -10,6 +10,6 @@ export { default as normalizeNumber } from './normalizeNumber'; export { default as getSwapPair } from './getSwapPair'; export { default as getSwapSide } from './getSwapSide'; -export { default as HttpError } from './httpError'; +// export { default as HttpError } from './httpError'; export * from './typeHelpers';