diff --git a/apps/indexer-admin/package.json b/apps/indexer-admin/package.json index 2f86d22b8..788a101df 100644 --- a/apps/indexer-admin/package.json +++ b/apps/indexer-admin/package.json @@ -20,11 +20,11 @@ "@patternfly/react-log-viewer": "^4.43.19", "@rainbow-me/rainbowkit": "^1.2.0", "@subql/components": "3.0.1-3", - "@subql/contract-sdk": "1.3.0", + "@subql/contract-sdk": "1.7.0", "@subql/network-clients": "1.1.0", - "@subql/network-config": "1.1.1", + "@subql/network-config": "1.1.2-3", "@subql/network-query": "1.1.0", - "@subql/react-hooks": "1.1.0", + "@subql/react-hooks": "1.1.2-19", "ahooks": "^3.7.7", "animate.css": "^4.1.1", "antd": "^5.3.2", diff --git a/apps/indexer-admin/public/images/sqt.png b/apps/indexer-admin/public/images/sqt.png new file mode 100644 index 000000000..a360dd8ea Binary files /dev/null and b/apps/indexer-admin/public/images/sqt.png differ diff --git a/apps/indexer-admin/src/hooks/paygHook.ts b/apps/indexer-admin/src/hooks/paygHook.ts index fa7a64f38..65785da73 100644 --- a/apps/indexer-admin/src/hooks/paygHook.ts +++ b/apps/indexer-admin/src/hooks/paygHook.ts @@ -13,7 +13,7 @@ import { BigNumber } from 'ethers'; import { useContractSDK } from 'containers/contractSdk'; import { useCoordinatorIndexer } from 'containers/coordinatorIndexer'; import { PAYG_PRICE } from 'utils/queries'; -import { network } from 'utils/web3'; +import { network, SUPPORTED_NETWORK } from 'utils/web3'; import { useProjectDetails } from './projectHook'; @@ -42,17 +42,18 @@ export function usePAYGConfig(deploymentId: string) { useDefault: true, }; } + const usdcDecimal = STABLE_COIN_DECIMAL[SUPPORTED_NETWORK]; return { paygPrice: payg.token === sdk?.sqToken.address ? formatEther(BigNumber.from(payg.price).mul(1000)) - : formatUnits(BigNumber.from(payg.price).mul(1000), +STABLE_COIN_DECIMAL), + : formatUnits(BigNumber.from(payg.price).mul(1000), usdcDecimal), paygRatio: payg.priceRatio || 80, paygMinPrice: payg.token === sdk?.sqToken.address ? formatEther(BigNumber.from(payg.minPrice).mul(1000)) - : formatUnits(BigNumber.from(payg.minPrice).mul(1000), +STABLE_COIN_DECIMAL), + : formatUnits(BigNumber.from(payg.minPrice).mul(1000), usdcDecimal), paygExpiration: (payg.expiration ?? 0) / daySeconds, token: payg.token, useDefault: payg.useDefault, @@ -73,7 +74,7 @@ export function usePAYGConfig(deploymentId: string) { const paygPrice = token === sdk?.sqToken.address ? parseEther(minPrice) - : parseUnits(minPrice, +import.meta.env.VITE_STABLE_TOKEN_DECIMAL); + : parseUnits(minPrice, STABLE_COIN_DECIMAL[SUPPORTED_NETWORK]); await paygPriceRequest({ variables: { diff --git a/apps/indexer-admin/src/pages/global-config/GlobalConfig.tsx b/apps/indexer-admin/src/pages/global-config/GlobalConfig.tsx index 7e5868d9c..752b6bd1f 100644 --- a/apps/indexer-admin/src/pages/global-config/GlobalConfig.tsx +++ b/apps/indexer-admin/src/pages/global-config/GlobalConfig.tsx @@ -6,11 +6,14 @@ import { BsBoxArrowInUpRight } from 'react-icons/bs'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; import { useMutation, useQuery } from '@apollo/client'; import { openNotification, Spinner, Tag, Typography } from '@subql/components'; -import { formatSQT, useAsyncMemo } from '@subql/react-hooks'; +import { SQT_DECIMAL, STABLE_COIN_DECIMAL } from '@subql/network-config'; +import { formatSQT, useAsyncMemo, useStableCoin } from '@subql/react-hooks'; +import { useUpdate } from 'ahooks'; import { Button, Form, Input, Select, Switch, Tooltip } from 'antd'; import { useForm } from 'antd/es/form/Form'; import BigNumberJs from 'bignumber.js'; -import { parseEther } from 'ethers/lib/utils'; +import { STABLE_COIN_ADDRESS } from 'conf/stableCoin'; +import { formatUnits, parseEther, parseUnits } from 'ethers/lib/utils'; import styled from 'styled-components'; import { SubqlInput } from 'styles/input'; @@ -23,7 +26,7 @@ import { getIndexerSocialCredibility, SET_CONFIG, } from 'utils/queries'; -import { TOKEN_SYMBOL } from 'utils/web3'; +import { SUPPORTED_NETWORK, TOKEN_SYMBOL } from 'utils/web3'; import NodeOperatorInformation from './components/NodeOperatorInforamtion'; @@ -41,11 +44,13 @@ const Wrapper = styled.div` const GlobalConfig: FC = () => { const { indexer: account, loading } = useCoordinatorIndexer(); + const sdk = useContractSDK(); + const { transPrice } = useStableCoin(sdk, SUPPORTED_NETWORK); + const update = useUpdate(); const [config, setConfig] = useState({ autoReduceOverAllocation: false, flexEnabled: false, }); - const sdk = useContractSDK(); const configQueryData = useQuery(GET_ALL_CONFIG); const [setConfigMutation] = useMutation(SET_CONFIG); @@ -79,13 +84,26 @@ const GlobalConfig: FC = () => { const validPeriod = configQueryData.data.allConfig.find( (i) => i.key === ConfigKey.FlexValidPeriod ); + const tokenAddress = configQueryData.data.allConfig.find( + (i) => i.key === ConfigKey.FlexTokenAddress + ); + + const isSQT = tokenAddress?.value === sdk?.sqToken.address || !tokenAddress?.value; form.setFieldValue('priceRatio', BigNumberJs(priceRatio?.value || '0')); - form.setFieldValue('price', BigNumberJs(formatSQT(price?.value || '0')).multipliedBy(1000)); + form.setFieldValue('flexTokenAddress', tokenAddress?.value || sdk?.sqToken.address); + form.setFieldValue( + 'price', + BigNumberJs( + isSQT + ? formatSQT(price?.value || '0') + : formatUnits(price?.value || '0', STABLE_COIN_DECIMAL[SUPPORTED_NETWORK]) + ).multipliedBy(1000) + ); form.setFieldValue('validPeriod', BigNumberJs(validPeriod?.value || '0').dividedBy(86400)); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configQueryData.data, form]); + }, [configQueryData.data, form, sdk]); return (
{ /> + { type="number" addonAfter={ { + console.warn(e); setPaygConf({ ...paygConf, token: e, }); }} options={[ - // { - // value: STABLE_COIN_ADDRESS, - // label: ( - //
- // - // USDC - //
- // ), - // }, { - value: sdk.sqToken.address, + value: STABLE_COIN_ADDRESS, label: (
+ USDC +
+ ), + }, + { + value: sdk.sqToken.address, + label: ( +
+ {TOKEN_SYMBOL} diff --git a/apps/indexer-admin/src/pages/project-details/types.ts b/apps/indexer-admin/src/pages/project-details/types.ts index 718ac1613..90f2b8891 100644 --- a/apps/indexer-admin/src/pages/project-details/types.ts +++ b/apps/indexer-admin/src/pages/project-details/types.ts @@ -114,9 +114,11 @@ export type ProjectConfig = { priceRatio: number; token: string; useDefault: boolean; + error?: string; }; dominantPrice: { price?: string; + token?: string; lastError?: string; }; diff --git a/apps/indexer-admin/src/pages/projects/components/projectItem.tsx b/apps/indexer-admin/src/pages/projects/components/projectItem.tsx index 5c229b550..2c939e22a 100644 --- a/apps/indexer-admin/src/pages/projects/components/projectItem.tsx +++ b/apps/indexer-admin/src/pages/projects/components/projectItem.tsx @@ -4,8 +4,10 @@ import { FC, useMemo } from 'react'; import { IoWarning } from 'react-icons/io5'; import { useHistory } from 'react-router-dom'; +import { formatUnits } from '@ethersproject/units'; import { Spinner, SubqlProgress, Tag, Typography } from '@subql/components'; import { indexingProgress } from '@subql/network-clients'; +import { SQT_DECIMAL, STABLE_COIN_DECIMAL, STABLE_COIN_SYMBOLS } from '@subql/network-config'; import { formatSQT } from '@subql/react-hooks'; import { Tooltip } from 'antd'; import BigNumberJs from 'bignumber.js'; @@ -13,6 +15,7 @@ import { isNull, isUndefined } from 'lodash'; import Avatar from 'components/avatar'; import UnsafeWarn from 'components/UnsafeWarn'; +import { useContractSDK } from 'containers/contractSdk'; import { useCoordinatorIndexer } from 'containers/coordinatorIndexer'; import { useDeploymentStatus, useIsOnline } from 'hooks/projectHook'; import { useGetIfUnsafeDeployment } from 'hooks/useGetIfUnsafeDeployment'; @@ -24,7 +27,7 @@ import { } from 'pages/project-details/types'; import { cidToBytes32 } from 'utils/ipfs'; import { formatValueToFixed } from 'utils/units'; -import { TOKEN_SYMBOL } from 'utils/web3'; +import { SUPPORTED_NETWORK, TOKEN_SYMBOL } from 'utils/web3'; import { OnlineStatus, statusText } from '../constant'; import { ItemContainer, ProfileContainer, ProjectItemContainer } from '../styles'; @@ -36,7 +39,7 @@ type Props = Omit & { const ProjectItem: FC = (props) => { const { id, details, payg, metadata, projectType, metadataLoading, dominantPrice } = props; - + const sdk = useContractSDK(); const { indexer: account } = useCoordinatorIndexer(); const history = useHistory(); const { status } = useDeploymentStatus(id); @@ -122,8 +125,27 @@ const ProjectItem: FC = (props) => { {payg?.expiration ? ( - {BigNumberJs(formatSQT(payg?.price)).multipliedBy(1000).toFixed()} {TOKEN_SYMBOL} / 1000 - requests + {BigNumberJs( + formatUnits( + payg?.price, + payg.token === sdk?.sqToken.address || !payg.token + ? SQT_DECIMAL + : STABLE_COIN_DECIMAL[SUPPORTED_NETWORK] + ) + ) + .multipliedBy(1000) + .toFixed()}{' '} + {payg.token === sdk?.sqToken.address || !payg.token + ? TOKEN_SYMBOL + : STABLE_COIN_SYMBOLS[SUPPORTED_NETWORK]}{' '} + / 1000 requests + {payg.error ? ( + + + + ) : ( + '' + )} {dominantPrice.lastError ? ( = { [ConfigType.FLEX_PRICE_RATIO]: '80', [ConfigType.FLEX_VALID_PERIOD]: `${60 * 60 * 24 * 3}`, // 3 days [ConfigType.FLEX_ENABLED]: 'true', + [ConfigType.FLEX_TOKEN_ADDRESS]: `${COIN_ADDRESS[argv.network].baseSQT[0]}`, + [ConfigType.FLEX_SLIPPAGE]: '5', [ConfigType.ALLOCATION_REWARD_THRESHOLD]: '2000000000000000000000', // 2000 sqt [ConfigType.ALLOCATION_REWARD_LAST_FORCE_TIME]: '0', [ConfigType.STATE_CHANNEL_REWARD_THRESHOLD]: '2000000000000000000000', // 2000 sqt @@ -73,6 +89,8 @@ export class ConfigService { ConfigType.FLEX_PRICE_RATIO, ConfigType.FLEX_VALID_PERIOD, ConfigType.FLEX_ENABLED, + ConfigType.FLEX_TOKEN_ADDRESS, + ConfigType.FLEX_SLIPPAGE, ]; const config = await this.configRepo.find({ where: { key: In(keys) }, @@ -100,6 +118,10 @@ export class ConfigService { return values; } + getUSDC() { + return COIN_ADDRESS[argv.network].baseUSDC; + } + getDefault(key: string) { return defaultConfig[key]; } diff --git a/apps/indexer-coordinator/src/core/contract.service.ts b/apps/indexer-coordinator/src/core/contract.service.ts index c2524720c..b4b933c67 100644 --- a/apps/indexer-coordinator/src/core/contract.service.ts +++ b/apps/indexer-coordinator/src/core/contract.service.ts @@ -2,22 +2,23 @@ // SPDX-License-Identifier: Apache-2.0 import { Injectable } from '@nestjs/common'; -import { ContractSDK } from '@subql/contract-sdk'; +import { ContractSDK, PriceOracle__factory, SQContracts } from '@subql/contract-sdk'; import { cidToBytes32 } from '@subql/network-clients'; import { isValidPrivate, toBuffer } from 'ethereumjs-util'; import { BigNumber, Overrides, Wallet, providers } from 'ethers'; -import { parseEther } from 'ethers/lib/utils'; +import { parseEther, formatEther } from 'ethers/lib/utils'; import { getEthProvider } from 'src/utils/ethProvider'; import { mutexPromise } from 'src/utils/promise'; import { redisDel, redisGetObj, redisSetObj } from 'src/utils/redis'; import { argv } from 'src/yargs'; +import { ConfigService } from '../config/config.service'; import { Config } from '../configure/configure.module'; import { ChainID, initContractSDK, initProvider, networkToChainID } from '../utils/contractSDK'; import { decrypt } from '../utils/encrypt'; import { TextColor, colorText, debugLogger, getLogger } from '../utils/logger'; import { Controller } from './account.model'; import { AccountService } from './account.service'; -import { IndexerDeploymentStatus, TxFun, TxOptions, TxType } from './types'; +import { IndexerDeploymentStatus, TxOptions, TxType } from './types'; const MAX_RETRY = 3; const logger = getLogger('contract'); @@ -33,7 +34,11 @@ export class ContractService { private gasFeeLimit: BigNumber; private gasFeeLimitEnabled: boolean; - constructor(private accountService: AccountService, private config: Config) { + constructor( + private accountService: AccountService, + private config: Config, + private configService: ConfigService + ) { this.chainID = networkToChainID[config.network]; this.existentialBalance = parseEther('0.001'); this.provider = initProvider(config.networkEndpoint, this.chainID, logger); @@ -212,6 +217,26 @@ export class ContractService { } } + async convertFromUSDC(amount: string): Promise<{ error?: string; data?: string }> { + try { + const [USDC_ADDRESS] = this.configService.getUSDC(); + const priceOracleAddress = await this.sdk.settings.getContractAddress( + SQContracts.PriceOracle + ); + const priceOracleFactory = PriceOracle__factory.connect(priceOracleAddress, this.provider); + const assetPrice = await priceOracleFactory.convertPrice( + USDC_ADDRESS, + this.sdk.sqToken.address, + amount + ); + logger.debug(`convertFromUSDC amount:${amount}, asset:${formatEther(assetPrice)}`); + return { data: assetPrice.toString() }; + } catch (e) { + logger.error(`fail to convert from USDC. ${e.message}`); + return { error: e.message }; + } + } + async sendTransaction(txOptions: TxOptions) { if (this.gasFeeLimitEnabled) { switch (txOptions.type) { diff --git a/apps/indexer-coordinator/src/core/core.module.ts b/apps/indexer-coordinator/src/core/core.module.ts index b7c00ece0..cf5a57056 100644 --- a/apps/indexer-coordinator/src/core/core.module.ts +++ b/apps/indexer-coordinator/src/core/core.module.ts @@ -6,6 +6,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { NetworkService } from 'src/network/network.service'; +import { ConfigModule } from '../config/config.module'; import { SubscriptionModule } from '../subscription/subscription.module'; import { Controller, Indexer } from './account.model'; import { AccountResolver } from './account.resolver'; @@ -22,6 +23,7 @@ import { ServiceResolver } from './service.resolver'; SubscriptionModule, TypeOrmModule.forFeature([Controller, Indexer]), ScheduleModule.forRoot(), + ConfigModule, ], providers: [ ContractService, diff --git a/apps/indexer-coordinator/src/project/price.service.ts b/apps/indexer-coordinator/src/project/price.service.ts index 39ffa45d8..6782e4606 100644 --- a/apps/indexer-coordinator/src/project/price.service.ts +++ b/apps/indexer-coordinator/src/project/price.service.ts @@ -8,6 +8,7 @@ import axios from 'axios'; import { BigNumber } from 'ethers'; import _ from 'lodash'; import { ConfigService, ConfigType } from 'src/config/config.service'; +import { ContractService } from 'src/core/contract.service'; import { argv } from 'src/yargs'; import { In, Repository } from 'typeorm'; import { getLogger } from '../utils/logger'; @@ -21,7 +22,8 @@ export class PriceService { constructor( @InjectRepository(PaygEntity) private paygRepo: Repository, - private configService: ConfigService + private configService: ConfigService, + private contract: ContractService ) {} @Cron(CronExpression.EVERY_10_MINUTES) @@ -30,34 +32,116 @@ export class PriceService { await this.request(dids, true); } - async inlinePayg(paygs: Payg[]) { + async inlinePayg(paygs: Payg[], remoteExchangeRate?: string, trim: boolean = true) { const deploymentIds = paygs.map((p) => p.id); if (!deploymentIds.length) return; const dPrices = await this.getDominatePrice(deploymentIds); const defaultFlex = await this.configService.getFlexConfig(); + const [USDC_TOKEN, USDC_DECIMAL] = this.configService.getUSDC(); + const ONE_USDC = BigNumber.from(10).pow(USDC_DECIMAL).toString(); + + let effectiveExchangeRate = ''; for (const p of paygs) { - const exist = _.find(dPrices, (dp: DominantPrice) => dp.id === p.id); p.minPrice = p.price; + const exist = _.find(dPrices, (dp: DominantPrice) => dp.id === p.id); + if (p.useDefault) { p.price = defaultFlex[ConfigType.FLEX_PRICE]; p.minPrice = defaultFlex[ConfigType.FLEX_PRICE]; p.priceRatio = Number(defaultFlex[ConfigType.FLEX_PRICE_RATIO]); + p.token = defaultFlex[ConfigType.FLEX_TOKEN_ADDRESS]; + } + + if (p.token === USDC_TOKEN) { + if (!effectiveExchangeRate) { + const res = await this.checkEffectiveExchangeRate( + remoteExchangeRate, + ONE_USDC, + defaultFlex + ); + if (res.error) { + p.error = `payg: ${res.error}`; + } else { + effectiveExchangeRate = res.data; + } + } } if (exist && exist.price !== null) { + p.dominantPrice = exist.price; p.priceRatio = p.priceRatio !== null ? p.priceRatio : Number(defaultFlex[ConfigType.FLEX_PRICE_RATIO]); - const minPrice = BigNumber.from(p.price || 0); - const dominant = BigNumber.from(exist.price).mul(p.priceRatio).div(100); - p.price = minPrice.gt(dominant) ? minPrice.toString() : dominant.toString(); - p.dominantPrice = exist.price; + + if (exist.token === USDC_TOKEN) { + p.rawdominantToken = USDC_TOKEN; + + if (!effectiveExchangeRate) { + const res = await this.checkEffectiveExchangeRate( + remoteExchangeRate, + ONE_USDC, + defaultFlex + ); + if (res.error) { + p.error = `dominant: ${res.error}`; + } else { + effectiveExchangeRate = res.data; + } + } + } } } + + effectiveExchangeRate = effectiveExchangeRate || remoteExchangeRate; + + for (const p of paygs) { + p.exchangeRate = effectiveExchangeRate; + if (p.token === USDC_TOKEN) { + if (!p.exchangeRate) { + getLogger('price').error(`${p.id} fail to get payg exchange rate from usdc. ${p.error}`); + continue; + } + p.rawpaygMinPrice = p.price; + p.rawpaygToken = p.token; + + p.price = BigNumber.from(effectiveExchangeRate).mul(p.price).div(ONE_USDC).toString(); + p.minPrice = p.price; + p.token = this.contract.getSdk().sqToken.address; + } + + if (!p.dominantPrice) continue; + + if (p.rawdominantToken === USDC_TOKEN) { + if (!p.exchangeRate) { + getLogger('price').error( + `${p.id} fail to get dominant price exchange rate for usdc. ${p.error}` + ); + continue; + } + p.rawdominantPrice = p.dominantPrice; + + p.dominantPrice = BigNumber.from(effectiveExchangeRate) + .mul(p.dominantPrice) + .div(ONE_USDC) + .toString(); + } + + const minPrice = BigNumber.from(p.price || 0); + const dominant = BigNumber.from(p.dominantPrice).mul(p.priceRatio).div(100); + + p.price = minPrice.gt(dominant) ? minPrice.toString() : dominant.toString(); + } + + if (trim) { + _.remove(paygs, function (p) { + return !p.exchangeRate && (p.token === USDC_TOKEN || p.rawdominantToken === USDC_TOKEN); + }); + } } - async fillPaygAndDominatePrice(projects: Project[]) { + // eslint-disable-next-line complexity + async fillPaygAndDominatePrice(projects: Project[], convert: boolean = false) { const deploymentIds = projects.map((p) => p.id); if (deploymentIds.length) { const [paygRes, priceRes] = await Promise.allSettled([ @@ -83,6 +167,12 @@ export class PriceService { const defaultFlex = await this.configService.getFlexConfig(); + const [USDC_TOKEN, USDC_DECIMAL] = this.configService.getUSDC(); + const ONE_USDC = BigNumber.from(10).pow(USDC_DECIMAL).toString(); + const SQT_TOKEN = this.contract.getSdk().sqToken.address; + + let exchangeRate = ''; + for (const p of projects) { p.payg = _.find(paygs, (payg: PaygEntity) => payg.id === p.id); p.dominantPrice = _.find(prices, (pri: DominantPrice) => pri.id === p.id); @@ -94,6 +184,7 @@ export class PriceService { p.payg.price = defaultFlex[ConfigType.FLEX_PRICE]; p.payg.minPrice = defaultFlex[ConfigType.FLEX_PRICE]; p.payg.priceRatio = Number(defaultFlex[ConfigType.FLEX_PRICE_RATIO]); + p.payg.token = defaultFlex[ConfigType.FLEX_TOKEN_ADDRESS]; } else { p.payg.minPrice = p.payg.price; p.payg.priceRatio = @@ -102,13 +193,111 @@ export class PriceService { : Number(defaultFlex[ConfigType.FLEX_PRICE_RATIO]); } + if (convert && p.payg.token === USDC_TOKEN) { + if (!exchangeRate) { + const transferRes = await this.contract.convertFromUSDC(ONE_USDC); + if (transferRes.error) { + p.payg.error = transferRes.error; + } else { + exchangeRate = transferRes.data; + } + } + } + // no dominant price - if (!p.dominantPrice?.price) continue; + if (!p.dominantPrice?.price) { + continue; + } - const minPrice = BigNumber.from(p.payg.price || 0); - const dominant = BigNumber.from(p.dominantPrice.price).mul(p.payg.priceRatio).div(100); - p.payg.price = minPrice.gt(dominant) ? minPrice.toString() : dominant.toString(); + if (convert && p.dominantPrice.token === USDC_TOKEN) { + if (!exchangeRate) { + const transferRes = await this.contract.convertFromUSDC(ONE_USDC); + if (transferRes.error) { + p.payg.error = transferRes.error; + } else { + exchangeRate = transferRes.data; + } + } + } } + + for (const p of projects) { + if (convert && p.payg.token === USDC_TOKEN) { + if (!exchangeRate) continue; + + p.payg.error = null; + p.payg.rawpaygMinPrice = p.payg.price; + p.payg.rawpaygToken = p.payg.token; + + p.payg.price = BigNumber.from(exchangeRate).mul(p.payg.price).div(ONE_USDC).toString(); + p.payg.minPrice = p.payg.price; + p.payg.token = SQT_TOKEN; + p.payg.exchangeRate = exchangeRate; + } + + if (!p.dominantPrice?.price) { + continue; + } + + if (convert && p.dominantPrice?.token === USDC_TOKEN) { + if (!exchangeRate) continue; + + p.payg.error = null; + p.dominantPrice.rawToken = p.dominantPrice.token; + p.dominantPrice.rawPrice = p.dominantPrice.price; + + p.dominantPrice.token = SQT_TOKEN; + p.dominantPrice.price = BigNumber.from(exchangeRate) + .mul(p.dominantPrice.price) + .div(ONE_USDC) + .toString(); + } + + if (convert) { + const minPrice = BigNumber.from(p.payg.price || 0); + const dominant = BigNumber.from(p.dominantPrice.price).mul(p.payg.priceRatio).div(100); + p.payg.price = minPrice.gt(dominant) ? minPrice.toString() : dominant.toString(); + } + } + } + } + + private async checkEffectiveExchangeRate( + remoteExchangeRate: string, + ONE_USDC: string, + defaultFlex: Record + ): Promise<{ error?: string; data?: string }> { + const transferRes = await this.contract.convertFromUSDC(ONE_USDC); + if (transferRes.error) { + // p.error = transferRes.error; + return { error: transferRes.error }; + } else { + const curChainExchangeRateBig = BigNumber.from(transferRes.data); + + if (!remoteExchangeRate) { + return { data: curChainExchangeRateBig.toString() }; + } + + const slippage = Number(defaultFlex[ConfigType.FLEX_SLIPPAGE]); + const rerBig = BigNumber.from(remoteExchangeRate); + + let smaller = rerBig; + let bigger = curChainExchangeRateBig; + + if (smaller.gte(bigger)) { + smaller = curChainExchangeRateBig; + bigger = rerBig; + } + + const upperbound = BigNumber.from(smaller) + .mul(100 + slippage) + .div(100); + + const effectiveExchangeRate = bigger.lte(upperbound) + ? remoteExchangeRate + : curChainExchangeRateBig.toString(); + + return { data: effectiveExchangeRate }; } } @@ -118,7 +307,7 @@ export class PriceService { for (const did of deploymentIds) { const c = this.cache.get(did); if (c) { - res.push(_.pick(c, ['id', 'price', 'lastError'])); + res.push(_.pick(c, ['id', 'price', 'token', 'lastError'])); continue; } nocacheIds.push(did); @@ -144,17 +333,18 @@ export class PriceService { if (r.data.error) { throw new Error(r.data.error); } - for (const { deployment: id, price } of r.data) { + for (const { deployment: id, price, token_address: token } of r.data) { const info = { id, price, + token, retrieveCount: 1, failCount: 0, }; if (setCache && price !== null) { this.cache.set(id, info); } - res.push(_.pick(info, ['id', 'price', 'lastError'])); + res.push(_.pick(info, ['id', 'price', 'token', 'lastError'])); } } catch (e) { getLogger('price').error(`fail to request price, error: ${e.message}`); diff --git a/apps/indexer-coordinator/src/project/project.model.ts b/apps/indexer-coordinator/src/project/project.model.ts index 4662e058b..c472a103c 100644 --- a/apps/indexer-coordinator/src/project/project.model.ts +++ b/apps/indexer-coordinator/src/project/project.model.ts @@ -49,6 +49,12 @@ export class DominantPrice { @Field({ nullable: true }) price?: string; @Field({ nullable: true }) + token?: string; + @Field({ nullable: true }) + rawPrice?: string; + @Field({ nullable: true }) + rawToken?: string; + @Field({ nullable: true }) retrieveCount?: number; @Field({ nullable: true }) failCount?: number; @@ -461,6 +467,24 @@ export class Payg extends PaygEntity { @Field({ nullable: true }) dominantPrice?: string; + + @Field({ nullable: true }) + exchangeRate?: string; + + @Field({ nullable: true }) + rawdominantPrice?: string; + + @Field({ nullable: true }) + rawdominantToken?: string; + + @Field({ nullable: true }) + rawpaygMinPrice?: string; + + @Field({ nullable: true }) + rawpaygToken?: string; + + @Field({ nullable: true }) + error?: string; } @ObjectType('Project') @@ -488,5 +512,4 @@ export class ProjectDetails extends ProjectEntity { @Field(() => DominantPrice, { nullable: true }) dominantPrice?: DominantPrice; - } diff --git a/apps/indexer-coordinator/src/project/project.resolver.ts b/apps/indexer-coordinator/src/project/project.resolver.ts index 6b3997f53..03ae1e683 100644 --- a/apps/indexer-coordinator/src/project/project.resolver.ts +++ b/apps/indexer-coordinator/src/project/project.resolver.ts @@ -191,8 +191,8 @@ export class ProjectResolver { } @Query(() => [Payg]) - getAlivePaygs() { - return this.projectService.getAlivePaygs(); + getAlivePaygs(@Args('exchangeRate', { nullable: true }) exchangeRate: string) { + return this.projectService.getAlivePaygs(exchangeRate); } @Query(() => AggregatedManifest) diff --git a/apps/indexer-coordinator/src/project/project.service.ts b/apps/indexer-coordinator/src/project/project.service.ts index 39054ba78..0597f2000 100644 --- a/apps/indexer-coordinator/src/project/project.service.ts +++ b/apps/indexer-coordinator/src/project/project.service.ts @@ -163,7 +163,7 @@ export class ProjectService { const projects = (await this.projectRepo.find()) as Project[]; if (!['monitor', 'metadata'].includes(source)) { - await this.priceService.fillPaygAndDominatePrice(projects); + await this.priceService.fillPaygAndDominatePrice(projects, true); } return projects.sort((a, b) => { @@ -177,11 +177,12 @@ export class ProjectService { }); } - async getAlivePaygs(): Promise { + async getAlivePaygs(remoteExchangeRate?: string): Promise { // return this.paygRepo.find({ where: { price: Not('') } }); // FIXME remove this + getLogger('project').debug(`remoteExchangeRate: ${remoteExchangeRate}`); const paygs = await this.paygRepo.find({ where: { price: Not('') } }); - await this.priceService.inlinePayg(paygs); + await this.priceService.inlinePayg(paygs, remoteExchangeRate); for (const payg of paygs) { payg.overflow = 10000; } @@ -275,14 +276,14 @@ export class ProjectService { const flexConfig = await this.configService.getFlexConfig(); let paygConfig = {}; - if (flexConfig.flex_enabled === 'true') { + if (flexConfig[ConfigType.FLEX_ENABLED] === 'true') { paygConfig = { id: id.trim(), - price: flexConfig.flex_price, + price: flexConfig[ConfigType.FLEX_PRICE], expiration: Number(this.configService.getDefault(ConfigType.FLEX_VALID_PERIOD)) || 259200, // default: 3 days threshold: 10, overflow: 10, - token: this.contract.getSdk().sqToken.address, + token: flexConfig[ConfigType.FLEX_TOKEN_ADDRESS], } as PaygEntity; } diff --git a/ttt.js b/ttt.js new file mode 100644 index 000000000..151deb5ec --- /dev/null +++ b/ttt.js @@ -0,0 +1,17 @@ +const target = Object.defineProperties({}, { + foo: { + value: 123, + writable: false, + configurable: false + }, +}); + +const handler = { + get(target, propKey) { + return 123; + } +}; + +const proxy = new Proxy(target, handler); + +proxy.xx