diff --git a/package.json b/package.json index 9d137dc0..9c226111 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@hiveio/dhive": "^1.2.7", - "axios": "^0.21.1", + "axios": "^1.7.3", "cors": "^2.8.5", "express": "^4.17.1", "hivesigner": "^3.2.5", diff --git a/src/server/handlers/hive-explorer.ts b/src/server/handlers/hive-explorer.ts index ba45cef7..0207be11 100644 --- a/src/server/handlers/hive-explorer.ts +++ b/src/server/handlers/hive-explorer.ts @@ -6,20 +6,15 @@ const BASE_URL = 'https://hivexplorer.com/api'; export const fetchGlobalProps = async () => { let globalDynamic; - let medianHistory; try { - const _globalPropsUrl = `${BASE_URL}/get_dynamic_global_properties`; - const _mediaHistoryUrl = `${BASE_URL}/get_current_median_history_price`; const globalDynamicResp = await baseApiRequest(_globalPropsUrl, 'GET'); - const medianHistoryResp = await baseApiRequest(_mediaHistoryUrl, 'GET'); globalDynamic = globalDynamicResp.data; - medianHistory = medianHistoryResp.data; - - if (!globalDynamic || !medianHistory) { + + if (!globalDynamic) { throw new Error("Invalid global props data") } @@ -40,14 +35,8 @@ export const fetchGlobalProps = async () => { (_totalFunds / _totalShares) * 1e6; - - const base = parseAsset(medianHistory.base).amount; - const quote = parseAsset(medianHistory.quote).amount; - const globalProps = { hivePerMVests, - base, - quote, }; return globalProps; diff --git a/src/server/handlers/wallet-api.ts b/src/server/handlers/wallet-api.ts index 6a7b2bf4..42d60365 100644 --- a/src/server/handlers/wallet-api.ts +++ b/src/server/handlers/wallet-api.ts @@ -3,12 +3,30 @@ import express from "express"; import { baseApiRequest, pipe } from "../util"; import { fetchGlobalProps, getAccount } from "./hive-explorer"; import { apiRequest } from "../helper"; -import { EngineContracts, EngineIds, EngineMetric, EngineRequestPayload, EngineTables, JSON_RPC, Methods, Token, TokenBalance } from "../../models/hiveEngine.types"; -import { convertEngineToken } from "../../models/converters"; -import config from "../../config"; + +import { EngineContracts, EngineIds, EngineMetric, EngineRequestPayload, EngineTables, JSON_RPC, Methods, Token, TokenBalance, TokenStatus } from "../../models/hiveEngine.types"; +import { convertEngineToken, convertRewardsStatus } from "../../models/converters"; + //docs: https://hive-engine.github.io/engine-docs/ -const BASE_ENGINE_URL = 'https://api2.hive-engine.com';//'https://api2.hive-engine.com'; +//available nodes: https://beacon.peakd.com/ select tab 'Hive Engine' +const ENGINE_NODES = [ + "https://engine.rishipanthee.com", + "https://herpc.dtools.dev", + "https://api.hive-engine.com/rpc", + "https://ha.herpc.dtools.dev", + "https://herpc.kanibot.com", + "https://he.sourov.dev", + "https://herpc.actifit.io", + "https://api2.hive-engine.com/rpc" + ]; + +// min and max included +const randomIntFromInterval = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +let BASE_ENGINE_URL = `${ENGINE_NODES[randomIntFromInterval(0,6)]}/contracts`; const BASE_SPK_URL = 'https://spk.good-karma.xyz'; const ENGINE_REWARDS_URL = 'https://scot-api.hive-engine.com/'; @@ -17,25 +35,20 @@ const ENGINE_CHART_URL = 'https://info-api.tribaldex.com/market/ohlcv'; //docs: https://github.com/hive-engine/ssc_tokens_history/tree/hive#api-usage const ENGINE_ACCOUNT_HISTORY_URL = 'https://history.hive-engine.com/accountHistory'; -const PATH_RPC = 'rpc'; export const PATH_CONTRACTS = 'contracts'; //client engine api requests export const eapi = async (req: express.Request, res: express.Response) => { const data = req.body; - pipe(engineContractsRequest(data), res); + pipe(engineContractsRequest(data, BASE_ENGINE_URL), res); } export const erewardapi = async (req: express.Request, res: express.Response) => { const { username } = req.params; const params = req.query; - - const url = `${ENGINE_REWARDS_URL}@${username}`; - const headers = { 'Content-type': 'application/json', 'User-Agent': 'Ecency' }; - - pipe(baseApiRequest(url, "GET", headers, undefined, params), res); + pipe(engineRewardsRequest(username, params), res); } export const echartapi = async (req: express.Request, res: express.Response) => { @@ -51,21 +64,26 @@ export const engineAccountHistory = (req: express.Request, res: express.Response const params = req.query; const url = `${ENGINE_ACCOUNT_HISTORY_URL}`; - const headers = { 'Content-type': 'application/json', 'User-Agent': 'Ecency Apps' }; + const headers = { 'Content-type': 'application/json', 'User-Agent': 'Ecency' }; pipe(baseApiRequest(url, "GET", headers, undefined, params), res); } -//raw engine api calls -const engineContractsRequest = (data: EngineRequestPayload) => { - const url = `${BASE_ENGINE_URL}/${PATH_RPC}/${PATH_CONTRACTS}`; +//raw engine api call +const engineContractsRequest = (data: EngineRequestPayload, url: string) => { const headers = { 'Content-type': 'application/json', 'User-Agent': 'Ecency' }; return baseApiRequest(url, "POST", headers, data) } +//raw engine rewards api call +const engineRewardsRequest = (username:string, params:any) => { + const url = `${ENGINE_REWARDS_URL}/@${username}`; + const headers = { 'Content-type': 'application/json', 'User-Agent': 'Ecency' }; + return baseApiRequest(url, "GET", headers, undefined, params) +} //engine contracts methods @@ -82,38 +100,59 @@ export const fetchEngineBalances = async (account: string): Promise => { const data: EngineRequestPayload = { - jsonrpc: JSON_RPC.RPC_2, - method: Methods.FIND, - params: { - contract: EngineContracts.TOKENS, - table: EngineTables.TOKENS, - query: { - symbol: { $in: tokens }, + jsonrpc: JSON_RPC.RPC_2, + method: Methods.FIND, + params: { + contract: EngineContracts.TOKENS, + table: EngineTables.TOKENS, + query: { + symbol: { $in: tokens }, + }, }, - }, - id: EngineIds.ONE, + id: EngineIds.ONE, }; + try { + const response = await engineContractsRequest(data, BASE_ENGINE_URL); + + if (!response.data?.result) { + throw new Error("Failed to get engine tokens data") + } - const response = await engineContractsRequest(data); + return response.data.result; + } catch(e) { + BASE_ENGINE_URL = `${ENGINE_NODES[randomIntFromInterval(0,6)]}/contracts`; + const response = await engineContractsRequest(data, BASE_ENGINE_URL); - if (!response.data?.result) { - throw new Error("Failed to get engine tokens data") - } + if (!response.data?.result) { + throw new Error("Failed to get engine tokens data") + } - return response.data.result; + return response.data.result; + } } @@ -122,22 +161,52 @@ export const fetchEngineMetics = async (tokens: string[]): Promise => { + try { + const response = await engineRewardsRequest(username, {hive : 1}); - return response.data.result; + const rawData:TokenStatus[] = Object.values(response.data); + if (!rawData || rawData.length === 0) { + throw new Error('No rewards data returned'); + } + + const data = rawData.map(convertRewardsStatus); + const filteredData = data.filter((item) => item && item.pendingToken > 0); + + console.log('unclaimed engine rewards data', filteredData); + return filteredData; + } catch (err) { + console.warn('failed ot get unclaimed engine rewards', err); + return []; + } } @@ -147,23 +216,31 @@ const fetchEngineTokensWithBalance = async (username: string) => { try { const balances = await fetchEngineBalances(username); + + if (!balances) { + throw new Error("failed to fetch engine balances"); + } + const symbols = balances.map((t) => t.symbol); - const tokens = await fetchEngineTokens(symbols); - const metrices = await fetchEngineMetics(symbols); - // const unclaimed = await fetchUnclaimedRewards(username); //TODO: handle rewards later + const promiseTokens = fetchEngineTokens(symbols); + const promiseMmetrices = fetchEngineMetics(symbols); + const promiseUnclaimed = fetchEngineRewards(username) + + const [tokens, metrices, unclaimed] = + await Promise.all([promiseTokens, promiseMmetrices, promiseUnclaimed]) return balances.map((balance: any) => { const token = tokens.find((t: any) => t.symbol == balance.symbol); const metrics = metrices.find((t: any) => t.symbol == balance.symbol); - // const pendingRewards = unclaimed.find((t: any) => t.symbol == balance.symbol); //TODO: handle rewards later - return convertEngineToken(balance, token, metrics); + const pendingRewards = unclaimed.find((t: any) => t.symbol == balance.symbol); + return convertEngineToken(balance, token, metrics, pendingRewards); }); } catch (err) { - console.warn("Spk data fetch failed", err); - //TODO: instead of throwing error, handle to skip spk data addition - return; + console.warn("Engine data fetch failed", err); + // instead of throwing error, handle to skip engine data addition + return null; } } @@ -181,151 +258,54 @@ const fetchSpkData = async (username: string) => { } catch (err) { console.warn("Spk data fetch failed", err); - //TODO: instead of throwing error, handle to skip spk data addition + //instead of throwing error, handle to skip spk data addition + return null; + } +} + + +const apiRequestData = async (endpoint: string) => { + const resp = await apiRequest(endpoint, "GET"); + + if (!resp.data) { + throw new Error("failed to get data"); } + return resp.data; } export const portfolio = async (req: express.Request, res: express.Response) => { try { + const respObj: { [key: string]: any } = {}; const { username } = req.body; //fetch basic hive data - const _globalProps = await fetchGlobalProps(); - const _userdata = await getAccount(username); - - //fetch points data - //TODO: put back api request - // const _marketData = await apiRequest(`market-data/latest`, "GET"); - const _marketData = await dummyMarketData() - - //TODO: put back api request - // const _pointsData =await apiRequest(`users/${username}`, "GET"); - const _pointsData = await dummyPointSummary() + const globalProps = fetchGlobalProps(); + const accountData = getAccount(username); - //TODO: fetch engine assets - const _engineData = await fetchEngineTokensWithBalance(username) + //fetch market and points data + const marketData = apiRequestData(`market-data/latest`); + const pointsData = apiRequestData(`users/${username}`); + //fetch engine assets + const engineData = fetchEngineTokensWithBalance(username); - //TODO: fetch spk assets - const _spkData = await fetchSpkData(username); + //fetch spk assets + const spkData = fetchSpkData(username); - res.send({ - globalProps: _globalProps, - marketData: _marketData, - accountData: _userdata, - pointsData: _pointsData, - engineData: _engineData, - spkData: _spkData, + const responses = await Promise.all([globalProps, marketData, accountData, pointsData, engineData, spkData]); + const responseKeys = ["globalProps", "marketData", "accountData", "pointsData", "engineData", "spkData"] + responses.forEach((response, index) => { + if (response) { + respObj[responseKeys[index]] = response; + } }) + return res.send(respObj) + } catch (err: any) { console.warn("failed to compile portfolio", err); - res.status(500).send(err.message) + return res.status(500).send(err.message) } } - - - - -//TODO: remove before merging -const dummyPointSummary = async () => { - return { - "username": "${username}", - "points": "5523.704", - "points_by_type": { - "10": "6750.500", - "20": "100.000", - "30": "7930.000", - "100": "862.500", - "110": "1075.105", - "120": "190.200", - "130": "70.400", - "150": "5089.637" - }, - "unclaimed_points": "276.167", - "unclaimed_points_by_type": { - "10": "96.500", - "30": "160.000", - "110": "19.667" - } - } -} - -//TOOD: remove before merging -const dummyMarketData = async () => { - return { - "btc": { - "quotes": { - "btc": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0, - "price": 1 - }, - "usd": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0.0060308, - "price": 0.0001638804887141081 - } - } - }, - "estm": { - "quotes": { - "btc": { - "last_updated": "2022-08-12T04:57:00.000Z", - "percent_change": 0, - "price": 8e-8 - }, - "usd": { - "last_updated": "2022-08-12T04:57:00.000Z", - "percent_change": 0, - "price": 0.002 - } - } - }, - "eth": { - "quotes": { - "btc": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0, - "price": 1.0797900018640657e-7 - }, - "usd": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0, - "price": 0.007013003078153161 - } - } - }, - "hbd": { - "quotes": { - "btc": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0.28189758, - "price": 0.00001531436662541532 - }, - "usd": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0.19563211, - "price": 0.9947278858468801 - } - } - }, - "hive": { - "quotes": { - "btc": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0.50376444, - "price": 0.0000034331098014452116 - }, - "usd": { - "last_updated": "2024-07-18T06:43:00.000Z", - "percent_change": 0.55258548, - "price": 0.22308583979341057 - } - } - } - } -} - diff --git a/yarn.lock b/yarn.lock index 3ce40d84..16479cc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2060,12 +2060,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== -axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== +axios@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85" + integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw== dependencies: - follow-redirects "^1.10.0" + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" babel-code-frame@^6.22.0: version "6.26.0" @@ -2991,7 +2993,7 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -4367,11 +4369,16 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.10.0: +follow-redirects@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4396,6 +4403,15 @@ fork-ts-checker-webpack-plugin@3.1.1, fork-ts-checker-webpack-plugin@^3.1.1: tapable "^1.0.0" worker-rpc "^0.1.0" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -8409,6 +8425,11 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"