From da69222bb0c26a4068b44ca57d228d11253b54b3 Mon Sep 17 00:00:00 2001 From: iGroza Date: Fri, 23 Aug 2024 17:01:16 +0700 Subject: [PATCH] refactor(HW-630): refactor explorer urls --- src/components/account-detail.tsx | 4 +- .../settings-account-detail.tsx | 4 +- src/components/swap/swap.tsx | 5 +- src/components/ui/copy-menu.tsx | 4 +- src/event-actions/on-provider-changed.ts | 2 +- src/hooks/nft/use-show-nft.tsx | 6 +- src/models/provider/provider-config-model.ts | 77 +++++++++++++++++++ src/models/provider/provider-config.ts | 65 ++++++++++------ src/models/provider/provider.ts | 38 ++++++++- .../HomeStack/NftStack/nft-item-details.tsx | 7 +- src/screens/SwapStack/swap-screen.tsx | 24 +++--- src/services/indexer/indexer.types.ts | 5 ++ src/types.ts | 2 +- 13 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 src/models/provider/provider-config-model.ts diff --git a/src/components/account-detail.tsx b/src/components/account-detail.tsx index e91dd754d..cdf07bd43 100644 --- a/src/components/account-detail.tsx +++ b/src/components/account-detail.tsx @@ -21,9 +21,9 @@ import { InfoBlock, Text, } from '@app/components/ui'; +import {app} from '@app/contexts'; import {createTheme} from '@app/helpers'; import {I18N} from '@app/i18n'; -import {RemoteProviderConfig} from '@app/models/provider'; import {Wallet} from '@app/models/wallet'; import {sendNotification} from '@app/services'; import {GRADIENT_END, GRADIENT_START} from '@app/variables/common'; @@ -76,7 +76,7 @@ export const AccountDetail = observer( title={I18N.evmTitle} component={null} /> - {RemoteProviderConfig.isBech32Enabled && ( + {app.provider.config.isBech32Enabled && ( {wallet?.address} - {RemoteProviderConfig.isBech32Enabled && ( + {app.provider.config.isBech32Enabled && ( <> - {RemoteProviderConfig.isBech32Enabled && ( + {app.provider.config.isBech32Enabled && ( <> diff --git a/src/event-actions/on-provider-changed.ts b/src/event-actions/on-provider-changed.ts index fad6ad996..7302d23a5 100644 --- a/src/event-actions/on-provider-changed.ts +++ b/src/event-actions/on-provider-changed.ts @@ -28,7 +28,7 @@ export const onProviderChanged = createAsyncTask(async () => { await Transaction.fetchLatestTransactions(Wallet.addressList(), true); await Currencies.fetchCurrencies(); - if (RemoteProviderConfig.isNftEnabled) { + if (app.provider.config.isNftEnabled) { await Nft.fetchNft(); } Provider.fetchProviders(); diff --git a/src/hooks/nft/use-show-nft.tsx b/src/hooks/nft/use-show-nft.tsx index 9df8431c9..be70f6f5d 100644 --- a/src/hooks/nft/use-show-nft.tsx +++ b/src/hooks/nft/use-show-nft.tsx @@ -2,14 +2,14 @@ import {useEffect, useState} from 'react'; import {autorun} from 'mobx'; -import {RemoteProviderConfig} from '@app/models/provider'; +import {app} from '@app/contexts'; export const useShowNft = () => { - const [showNft, setShowNft] = useState(RemoteProviderConfig.isNftEnabled); + const [showNft, setShowNft] = useState(app.provider.config.isNftEnabled); useEffect(() => { const disposer = autorun(() => { - setShowNft(RemoteProviderConfig.isNftEnabled); + setShowNft(app.provider.config.isNftEnabled); }); return () => { diff --git a/src/models/provider/provider-config-model.ts b/src/models/provider/provider-config-model.ts new file mode 100644 index 000000000..9555d561c --- /dev/null +++ b/src/models/provider/provider-config-model.ts @@ -0,0 +1,77 @@ +import {ProviderConfig} from '@app/services/indexer'; + +export class ProviderConfigModel { + constructor(public config: ProviderConfig) {} + + get isNftEnabled() { + return Boolean(this.config?.nft_exists); + } + + get isBech32Enabled() { + return Boolean(this.config?.bech32_exists); + } + + get swapEnabled() { + return Boolean(this.config?.swap_enabled); + } + get swapRouterV3() { + return this.config?.swap_router_v3 ?? ''; + } + get wethAddress() { + return this.config?.weth_address ?? ''; + } + + get wethSymbol() { + return this.config?.weth_symbol ?? ''; + } + + /** + * Get the EVM explorer URL template for an address. + * @returns {string} The explorer address URL template or an empty string if not configured. + * Usage: Replace {{address}} with the actual address. + * @example "https://explorer.haqq.network/address/{{address}}" + */ + get explorerAddressUrl() { + return this.config?.explorer_address_url ?? ''; + } + + /** + * Get the explorer URL template for a Cosmos transaction. + * @returns {string} The explorer Cosmos transaction URL template or an empty string if not configured. + * Usage: Replace {{tx_hash}} with the actual transaction hash. + * @example "https://ping.pub/haqq/tx/{{tx_hash}}" + */ + get explorerCosmosTxUrl() { + return this.config?.explorer_cosmos_tx_url ?? ''; + } + + /** + * Get the EVM explorer URL template for a token. + * @returns {string} The explorer token URL template or an empty string if not configured. + * Usage: Replace {{address}} with the actual token address. + * @example "https://explorer.haqq.network/token/{{address}}" + */ + get explorerTokenUrl() { + return this.config?.explorer_token_url ?? ''; + } + + /** + * Get the EVM explorer URL template for a transaction. + * @returns {string} The explorer transaction URL template or an empty string if not configured. + * Usage: Replace {{tx_hash}} with the actual transaction hash. + * @example "https://explorer.haqq.network/tx/{{tx_hash}}" + */ + get explorerTxUrl() { + return this.config?.explorer_tx_url ?? ''; + } + + /** + * Get the EVM explorer URL template for a token ID. + * @returns {string} The explorer token ID URL template or an empty string if not configured. + * Usage: Replace {{address}} with the token contract address and {{token_id}} with the token ID. + * @example "https://explorer.haqq.network/token/{{address}}/instance/{{token_id}}" + */ + get explorerTokenIdUrl() { + return this.config?.explorer_token_id_url ?? ''; + } +} diff --git a/src/models/provider/provider-config.ts b/src/models/provider/provider-config.ts index 92a12cff1..ba009a7fc 100644 --- a/src/models/provider/provider-config.ts +++ b/src/models/provider/provider-config.ts @@ -1,49 +1,68 @@ import {makeAutoObservable, runInAction} from 'mobx'; import {makePersistable} from 'mobx-persist-store'; +import {app} from '@app/contexts'; import {Indexer} from '@app/services/indexer'; import {ProviderConfig} from '@app/services/indexer/indexer.types'; import {storage} from '@app/services/mmkv'; +import {ChainId} from '@app/types'; + +import {Provider} from './provider'; +import {ProviderConfigModel} from './provider-config-model'; class ProviderConfigStore { - config: ProviderConfig | null = null; + private _data: Record = {}; + constructor() { makeAutoObservable(this); makePersistable(this, { name: this.constructor.name, - properties: ['config'], + properties: ['_data'] as (keyof this)[], storage: storage, }); } init = async () => { - const newConfig = await Indexer.instance.getProviderConfig(); - runInAction(() => { - this.config = newConfig; - }); + try { + const config = await Indexer.instance.getProviderConfig(); + runInAction(() => { + this._data[app.provider.ethChainId] = config; + }); + this.lazyLoadOtherConfig(); + return Promise.resolve(); + } catch (error) { + Logger.captureException(error, 'ProviderConfigStore:init'); + } }; - get isNftEnabled() { - return Boolean(this.config?.nft_exists); + get data() { + return this._data; } - get isBech32Enabled() { - return Boolean(this.config?.bech32_exists); - } + lazyLoadOtherConfig = async () => { + const providers = Provider.getAll().filter( + p => p.ethChainId !== app.provider.ethChainId, + ); - get swapEnabled() { - return Boolean(this.config?.swap_enabled); - } - get swapRouterV3() { - return this.config?.swap_router_v3 ?? ''; - } - get wethAddress() { - return this.config?.weth_address ?? ''; - } + for await (const p of providers) { + try { + if (this._data[p.ethChainId]) { + continue; + } + const indexer = new Indexer(p.ethChainId); + const c = await indexer.getProviderConfig(); + runInAction(() => { + this._data[p.ethChainId] = c; + }); + } catch (error) { + Logger.captureException(error, 'failed to initialize provider config:'); + } + } + }; - get wethSymbol() { - return this.config?.weth_symbol ?? ''; - } + getConfig = (chainId: ChainId) => { + return new ProviderConfigModel(this.data[chainId]); + }; } const instance = new ProviderConfigStore(); diff --git a/src/models/provider/provider.ts b/src/models/provider/provider.ts index dfa74d457..5466e0998 100644 --- a/src/models/provider/provider.ts +++ b/src/models/provider/provider.ts @@ -12,11 +12,17 @@ import {storage} from '@app/services/mmkv'; import {createAsyncTask, sleep} from '@app/utils'; import {DEFAULT_PROVIDERS, ISLM_DENOM} from '@app/variables/common'; +import {RemoteProviderConfig} from './provider-config'; import {ProviderID} from './provider.types'; import {VariablesString} from '../variables-string'; const HAQQ_BENCH_32_PREFIX = 'haqq'; +const EXPLORER_URL_TEMPLATES = { + ADDRESS: '{{address}}', + TOKEN_ID: '{{token_id}}', + TX: '{{tx_hash}}', +}; const logger = Logger.create('NetworkProvider:store', { stringifyJson: true, @@ -240,6 +246,10 @@ export class Provider { return this.model.coin_name; } + get config() { + return RemoteProviderConfig.getConfig(this.ethChainId); + } + toJSON() { return { ethChainIdHex: this.ethChainIdHex, @@ -261,14 +271,36 @@ export class Provider { } if (txHash.startsWith('0x') || txHash.startsWith('0X')) { - return `${this.explorer}/tx/${txHash}`; + return this.config.explorerTxUrl.replace( + EXPLORER_URL_TEMPLATES.TX, + txHash, + ); } - return `${this.cosmosExplorer}/${this.bench32Prefix}/tx/${txHash}`; + return this.config.explorerCosmosTxUrl.replace( + EXPLORER_URL_TEMPLATES.TX, + txHash, + ); } getAddressExplorerUrl(address: string) { - return `${this.explorer}/address/${AddressUtils.toEth(address)}`; + return this.config.explorerAddressUrl.replace( + EXPLORER_URL_TEMPLATES.ADDRESS, + AddressUtils.toEth(address), + ); + } + + getCollectionExplorerUrl(address: string) { + return this.config.explorerTokenUrl.replace( + EXPLORER_URL_TEMPLATES.ADDRESS, + AddressUtils.toEth(address), + ); + } + + getTokenExplorerUrl(address: string, tokenId: string | number) { + return this.config.explorerTokenIdUrl + .replace(EXPLORER_URL_TEMPLATES.ADDRESS, AddressUtils.toEth(address)) + .replace(EXPLORER_URL_TEMPLATES.TOKEN_ID, tokenId.toString()); } } diff --git a/src/screens/HomeStack/NftStack/nft-item-details.tsx b/src/screens/HomeStack/NftStack/nft-item-details.tsx index f4712a6c6..513fc5ce2 100644 --- a/src/screens/HomeStack/NftStack/nft-item-details.tsx +++ b/src/screens/HomeStack/NftStack/nft-item-details.tsx @@ -1,7 +1,6 @@ import React, {memo, useCallback} from 'react'; import {app} from '@app/contexts'; -import {AddressUtils} from '@app/helpers/address-utils'; import {useTypedNavigation, useTypedRoute} from '@app/hooks'; import { HomeStackRoutes, @@ -29,10 +28,10 @@ export const NftItemDetailsScreen = memo(() => { }, [params.item, navigation]); const onPressExplorer = useCallback(() => { - // https://explorer.haqq.network/token/0xe5C15B68cfE3182c106f60230A1bE377ceaad483/instance/2998 - const url = `${app.provider.explorer}/token/${AddressUtils.toEth( + const url = app.provider.getTokenExplorerUrl( params.item.contract, - )}/instance/${params.item.tokenId}`; + params.item.tokenId, + ); return openInAppBrowser(url); }, [params.item]); diff --git a/src/screens/SwapStack/swap-screen.tsx b/src/screens/SwapStack/swap-screen.tsx index 698596da9..746a87152 100644 --- a/src/screens/SwapStack/swap-screen.tsx +++ b/src/screens/SwapStack/swap-screen.tsx @@ -27,7 +27,7 @@ import {getRpcProvider} from '@app/helpers/get-rpc-provider'; import {useSumAmount, useTypedRoute} from '@app/hooks'; import {I18N, getText} from '@app/i18n'; import {Currencies} from '@app/models/currencies'; -import {Provider, RemoteProviderConfig} from '@app/models/provider'; +import {Provider} from '@app/models/provider'; import {Token} from '@app/models/tokens'; import {Wallet} from '@app/models/wallet'; import {navigator} from '@app/navigator'; @@ -174,13 +174,13 @@ export const SwapScreen = observer(() => { () => tokenIn?.symbol?.toLowerCase() === app.provider.denom?.toLowerCase() && tokenOut?.symbol?.toLowerCase() === - RemoteProviderConfig.wethSymbol?.toLowerCase(), + app.provider.config.wethSymbol?.toLowerCase(), [tokenIn, tokenOut, app.provider.denom], ); const isUnwrapTx = useMemo( () => tokenIn?.symbol?.toLowerCase() === - RemoteProviderConfig.wethSymbol?.toLowerCase() && + app.provider.config.wethSymbol?.toLowerCase() && tokenOut?.symbol?.toLowerCase() === app.provider.denom?.toLowerCase(), [tokenIn, tokenOut, app.provider.denom], ); @@ -660,7 +660,7 @@ export const SwapScreen = observer(() => { setSwapInProgress(() => true); const swapRouter = new ethers.Contract( - RemoteProviderConfig.swapRouterV3, + app.provider.config.swapRouterV3, V3SWAPROUTER_ABI, ); @@ -697,7 +697,7 @@ export const SwapScreen = observer(() => { params: [ { from: currentWallet.address, - to: RemoteProviderConfig.swapRouterV3, + to: app.provider.config.swapRouterV3, value: tokenInIsISLM ? estimateData.amount_in : '0x0', data: encodedTxData, }, @@ -767,7 +767,7 @@ export const SwapScreen = observer(() => { ); const data = erc20Token.interface.encodeFunctionData('approve', [ - RemoteProviderConfig.swapRouterV3, + app.provider.config.swapRouterV3, amountBN._hex, ]); @@ -814,7 +814,7 @@ export const SwapScreen = observer(() => { setSwapInProgress(() => true); const provider = await getRpcProvider(app.provider); const WETH = new ethers.Contract( - AddressUtils.toEth(RemoteProviderConfig.wethAddress), + AddressUtils.toEth(app.provider.config.wethAddress), WETH_ABI, provider, ); @@ -828,7 +828,7 @@ export const SwapScreen = observer(() => { params: [ { from: currentWallet.address, - to: AddressUtils.toEth(RemoteProviderConfig.wethAddress), + to: AddressUtils.toEth(app.provider.config.wethAddress), value: t0Current.toHex(), data: txData, }, @@ -885,7 +885,7 @@ export const SwapScreen = observer(() => { setSwapInProgress(() => true); const provider = await getRpcProvider(app.provider); const WETH = new ethers.Contract( - AddressUtils.toEth(RemoteProviderConfig.wethAddress), + AddressUtils.toEth(app.provider.config.wethAddress), WETH_ABI, provider, ); @@ -906,7 +906,7 @@ export const SwapScreen = observer(() => { params: [ { from: currentWallet.address, - to: AddressUtils.toEth(RemoteProviderConfig.wethAddress), + to: AddressUtils.toEth(app.provider.config.wethAddress), value: '0x0', data: txData, }, @@ -1025,7 +1025,7 @@ export const SwapScreen = observer(() => { }, [tokenIn, tokenOut, currentWallet, currentRoute, amountsIn.amount]); useEffect(() => { - if (!RemoteProviderConfig.swapEnabled) { + if (!app.provider.config.swapEnabled) { return navigator.goBack(); } const fetchData = () => { @@ -1090,7 +1090,7 @@ export const SwapScreen = observer(() => { setCurrentRoute( () => data.routes.find(r => - AddressUtils.equals(r.token0, RemoteProviderConfig.wethAddress), + AddressUtils.equals(r.token0, app.provider.config.wethAddress), ) || data.routes[1], ); if (!data.pools?.length || !data?.routes?.length) { diff --git a/src/services/indexer/indexer.types.ts b/src/services/indexer/indexer.types.ts index 11dd12496..ead5050b3 100644 --- a/src/services/indexer/indexer.types.ts +++ b/src/services/indexer/indexer.types.ts @@ -61,6 +61,11 @@ export type ProviderConfig = { swap_router_v3: string; weth_address: string; weth_symbol: string; + explorer_address_url: string; + explorer_cosmos_tx_url?: string; + explorer_token_url: string; + explorer_tx_url: string; + explorer_token_id_url: string; }; export type VerifyContractRequest = { diff --git a/src/types.ts b/src/types.ts index 8600a61a7..87832424c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1851,7 +1851,7 @@ export type IndexerTransactionResponse = { txs: IndexerTransaction[]; }; -export type ChainId = string; +export type ChainId = string | number; export type IndexerTxParsedTokenInfo = { name: string;