diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6e4a2d4..368a922 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -212,5 +212,20 @@ "Unfavorite": "Unfavorite", "share link": "Share Link", "Mulitsig Transaction Details": "Mulitsig Transaction Details", - "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving." + "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.", + "api_key": { + "title": "Set up API key", + "button": "Advanced settings", + "desc_storage": "Your API key is stored only in this browser (not uploaded to Subscan servers).", + "desc_security": "Don't save it on public/shared devices. Keep it secure.", + "desc_features": "After setup, you can enable features like History and auto-fetching Call Data.", + "desc_get_key": "Get an API key:", + "label": "API Key", + "placeholder": "Enter your Subscan API key", + "current_set": "API key is currently set", + "saved": "API key saved", + "deleted": "API key deleted", + "invalid_key": "Invalid API key. Please check and try again.", + "invalid_key_or_network": "Invalid API key or unsupported network. Please check and try again." + } } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index c718a2e..9bd70c6 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -210,5 +210,20 @@ "unfavorite": "取消收藏", "share link": "分享链接", "Mulitsig Transaction Details": "多签交易详情", - "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "在批准前,请务必仔细检查多签交易进度和参数。" + "Please be sure to double-check the mulitsig transaction progress and parameters carefully before approving.": "在批准前,请务必仔细检查多签交易进度和参数。", + "api_key": { + "title": "配置 API Key", + "button": "高级设置", + "desc_storage": "API Key 仅保存在本地浏览器中(不上传 Subscan 服务器)。", + "desc_security": "请勿在公共设备上保存,并妥善保管以防泄露。", + "desc_features": "配置后可启用:历史记录、自动获取 Call Data 等功能。", + "desc_get_key": "获取 API Key:", + "label": "API Key", + "placeholder": "请输入 Subscan API Key", + "current_set": "当前已设置 API Key", + "saved": "API Key 已保存", + "deleted": "API Key 已删除", + "invalid_key": "API Key 无效,请检查后重试。", + "invalid_key_or_network": "API Key 无效或网络不受支持,请检查后重试。" + } } diff --git a/src/App.tsx b/src/App.tsx index f70764d..a08be1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import subscanLogo from 'src/assets/images/subscan_logo.png'; import { Footer } from './components/Footer'; import { HeadAccounts } from './components/HeadAccounts'; import { DownIcon } from './components/icons'; +import { ApiKeyModal } from './components/modals/ApiKeyModal'; import { SelectNetworkModal } from './components/modals/SelectNetworkModal'; import Status from './components/Status'; import { chains } from './config/chains'; @@ -54,6 +55,7 @@ function App() { }, [rpc]); const [selectNetworkModalVisible, setSelectNetworkModalVisible] = useState(false); + const [apiKeyModalVisible, setApiKeyModalVisible] = useState(false); const openExplorer = () => { if (networkConfig?.explorerHostName) { @@ -89,6 +91,14 @@ function App() { {networkStatus === 'success' && } + + + + {currentKey && ( + + )} + + + + ); +}; diff --git a/src/config/chains/assethub_kusama.json b/src/config/chains/assethub_kusama.json index e30e0dc..209413c 100644 --- a/src/config/chains/assethub_kusama.json +++ b/src/config/chains/assethub_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-kusama.webapi.subscan.io" + "subscan": "https://assethub-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/assethub-kusama.png", diff --git a/src/config/chains/assethub_paseo.json b/src/config/chains/assethub_paseo.json index 921d501..89e10cb 100644 --- a/src/config/chains/assethub_paseo.json +++ b/src/config/chains/assethub_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-paseo-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-paseo.webapi.subscan.io" + "subscan": "https://assethub-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/assethub-paseo.png", diff --git a/src/config/chains/assethub_polkadot.json b/src/config/chains/assethub_polkadot.json index 5a0a34e..5edcbcf 100644 --- a/src/config/chains/assethub_polkadot.json +++ b/src/config/chains/assethub_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://asset-hub-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://assethub-polkadot.webapi.subscan.io" + "subscan": "https://assethub-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/assethub-polkadot.png", diff --git a/src/config/chains/astar.json b/src/config/chains/astar.json index a583c90..d05ae96 100644 --- a/src/config/chains/astar.json +++ b/src/config/chains/astar.json @@ -4,7 +4,7 @@ "rpc": "wss://astar-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://astar.webapi.subscan.io" + "subscan": "https://astar.api.subscan.io" }, "category": "polkadot", "logo": "/image/astar.png", diff --git a/src/config/chains/bifrost.json b/src/config/chains/bifrost.json index a744207..a8b69b6 100644 --- a/src/config/chains/bifrost.json +++ b/src/config/chains/bifrost.json @@ -4,7 +4,7 @@ "rpc": "wss://hk.p.bifrost-rpc.liebi.com/ws", "api": { "subql": "", - "subscan": "https://bifrost.webapi.subscan.io" + "subscan": "https://bifrost.api.subscan.io" }, "category": "polkadot", "logo": "/image/bifrost.png", diff --git a/src/config/chains/coretime_kusama.json b/src/config/chains/coretime_kusama.json index dca4f62..ef8d676 100644 --- a/src/config/chains/coretime_kusama.json +++ b/src/config/chains/coretime_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://coretime-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://coretime-kusama.webapi.subscan.io" + "subscan": "https://coretime-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/coretime-kusama.png", diff --git a/src/config/chains/coretime_paseo.json b/src/config/chains/coretime_paseo.json index 3293de5..d0d4835 100644 --- a/src/config/chains/coretime_paseo.json +++ b/src/config/chains/coretime_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://sys.ibp.network/coretime-paseo", "api": { "subql": "", - "subscan": "https://coretime-paseo.webapi.subscan.io" + "subscan": "https://coretime-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/coretime-paseo.png", diff --git a/src/config/chains/coretime_polkadot.json b/src/config/chains/coretime_polkadot.json index b3091df..a866121 100644 --- a/src/config/chains/coretime_polkadot.json +++ b/src/config/chains/coretime_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://coretime-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://coretime-polkadot.webapi.subscan.io" + "subscan": "https://coretime-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/coretime-polkadot.png", diff --git a/src/config/chains/hydration.json b/src/config/chains/hydration.json index 6cb6f8a..74d5f55 100644 --- a/src/config/chains/hydration.json +++ b/src/config/chains/hydration.json @@ -4,7 +4,7 @@ "rpc": "wss://hydration-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://hydration.webapi.subscan.io" + "subscan": "https://hydration.api.subscan.io" }, "category": "polkadot", "logo": "/image/hydration.png", diff --git a/src/config/chains/pendulum.json b/src/config/chains/pendulum.json index 3d5f006..7c38419 100644 --- a/src/config/chains/pendulum.json +++ b/src/config/chains/pendulum.json @@ -4,7 +4,7 @@ "rpc": "wss://rpc-pendulum.prd.pendulumchain.tech", "api": { "subql": "", - "subscan": "https://pendulum.webapi.subscan.io" + "subscan": "https://pendulum.api.subscan.io" }, "category": "polkadot", "logo": "/image/pendulum.png", diff --git a/src/config/chains/people_kusama.json b/src/config/chains/people_kusama.json index b28f135..25f65fc 100644 --- a/src/config/chains/people_kusama.json +++ b/src/config/chains/people_kusama.json @@ -4,7 +4,7 @@ "rpc": "wss://people-kusama-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://people-kusama.webapi.subscan.io" + "subscan": "https://people-kusama.api.subscan.io" }, "category": "kusama", "logo": "/image/people-kusama.png", diff --git a/src/config/chains/people_paseo.json b/src/config/chains/people_paseo.json index 1563cdc..a08e24f 100644 --- a/src/config/chains/people_paseo.json +++ b/src/config/chains/people_paseo.json @@ -4,7 +4,7 @@ "rpc": "wss://sys.ibp.network/people-paseo", "api": { "subql": "", - "subscan": "https://people-paseo.webapi.subscan.io" + "subscan": "https://people-paseo.api.subscan.io" }, "category": "paseo", "logo": "/image/people-paseo.png", diff --git a/src/config/chains/people_polkadot.json b/src/config/chains/people_polkadot.json index 6f7de55..0ad7f12 100644 --- a/src/config/chains/people_polkadot.json +++ b/src/config/chains/people_polkadot.json @@ -4,7 +4,7 @@ "rpc": "wss://people-polkadot-rpc.n.dwellir.com", "api": { "subql": "", - "subscan": "https://people-polkadot.webapi.subscan.io" + "subscan": "https://people-polkadot.api.subscan.io" }, "category": "polkadot", "logo": "/image/people-polkadot.png", diff --git a/src/hooks/AxiosRequest.ts b/src/hooks/AxiosRequest.ts index f5702bc..0e93dbb 100644 --- a/src/hooks/AxiosRequest.ts +++ b/src/hooks/AxiosRequest.ts @@ -15,6 +15,7 @@ export class Request { this.instance = axios.create(Object.assign(this.baseConfig, config)); this.instance.interceptors.request.use( + // eslint-disable-next-line complexity (inConfig) => { const token = localStorage.getItem('token') as string; const organizationId = localStorage.getItem('organization:id') as string; @@ -27,6 +28,15 @@ export class Request { inConfig.headers!['OPEN-PLATFORM-ORGANIZATION'] = organizationId; } + try { + const storage = JSON.parse(localStorage.getItem('multisig') || '{}'); + if (storage.subscanApiKey) { + inConfig.headers!['X-API-Key'] = storage.subscanApiKey; + } + } catch { + // ignore storage parse errors + } + return inConfig; }, (err: any) => { diff --git a/src/hooks/combineQuery.ts b/src/hooks/combineQuery.ts index a7e3333..e44c806 100644 --- a/src/hooks/combineQuery.ts +++ b/src/hooks/combineQuery.ts @@ -11,6 +11,7 @@ import { useMultisigAccountDetail as useSubscanMultisigAccountDetail, useMultisigRecordCount as usSubscanMultisigRecordCount, useMultisigRecords as useSubscanMultisigRecords, + useResourceCount as useSubscanResourceCount, constants as subscanConstants, } from './subscan'; @@ -22,6 +23,12 @@ export interface MultisigRecordCountResult { multisigRecords: { totalCount: number }; } +export interface MultisigCountsResult { + approvalCount: number | undefined; + confirmedCount: number | undefined; + cancelledCount: number | undefined; +} + export function useMultisigAccountDetail(network: NetConfigV2 | undefined) { const subquery = useSubqueryMultisigAccountDetail(network); const subscan = useSubscanMultisigAccountDetail(network); @@ -97,6 +104,42 @@ export function useMultisigRecords( return { fetchData, data, loading }; } +// Fetch counts for all three statuses in a single request (subscan) or two fallback calls (subquery). +export function useMultisigResourceCount(network: NetConfigV2 | undefined) { + const subscan = useSubscanResourceCount(network); + const confirmedSubquery = usSubqueryMultisigRecordCount(network); + const cancelledSubquery = usSubqueryMultisigRecordCount(network); + + const useSubscan = !!network?.api?.subscan; + + const fetchData = useCallback( + (account: string) => { + if (!account) return; + if (useSubscan) { + subscan.fetchData(account); + } else { + confirmedSubquery.fetchData(account, 'confirmed'); + cancelledSubquery.fetchData(account, 'cancelled'); + } + }, + [useSubscan, subscan.fetchData, confirmedSubquery.fetchData, cancelledSubquery.fetchData] + ); + + if (useSubscan) { + return { fetchData, data: subscan.data, loading: subscan.loading }; + } + + return { + fetchData, + data: { + approvalCount: undefined, + confirmedCount: confirmedSubquery.data?.multisigRecords.totalCount, + cancelledCount: cancelledSubquery.data?.multisigRecords.totalCount, + } as MultisigCountsResult, + loading: confirmedSubquery.loading || cancelledSubquery.loading, + }; +} + export function useDataSourceTools(network: NetConfigV2 | undefined) { return { constants: network?.api?.subscan ? subscanConstants : subqueryConstants, diff --git a/src/hooks/subscan.ts b/src/hooks/subscan.ts index c4f14a5..337db8d 100644 --- a/src/hooks/subscan.ts +++ b/src/hooks/subscan.ts @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { MultisigRecordsQueryRes } from 'src/components/ExtrinsicRecords'; import { NetConfigV2 } from 'src/model'; import axiosRequest from './AxiosRequest'; -import { MultisigAccountDetailResult, MultisigRecordCountResult } from './combineQuery'; +import { MultisigAccountDetailResult, MultisigCountsResult, MultisigRecordCountResult } from './combineQuery'; function subscanRecordsStatusConverter(status: string) { switch (status) { @@ -195,6 +195,49 @@ export const useMultisigRecords = (network: NetConfigV2 | undefined) => { return { fetchData, data: userInfo, loading }; }; +export const useResourceCount = (network: NetConfigV2 | undefined) => { + const [counts, setCounts] = useState({ + approvalCount: undefined, + confirmedCount: undefined, + cancelledCount: undefined, + }); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback( + async (account: string) => { + if (!network || !account) return; + setLoading(true); + try { + const { data } = await axiosRequest.post<{ + list: { resource: string; count: number }[]; + }>(`${network.api?.subscan}/api/scan/resource_count`, { + resource: ['Multisig'], + extra: { + Multisig: { + status: ['Approval', 'Executed', 'Cancelled'], + }, + }, + account, + }); + const list = data.data.list || []; + const find = (name: string) => list.find((item) => item.resource === name)?.count; + setCounts({ + approvalCount: find('MultisigApproval'), + confirmedCount: find('MultisigExecuted'), + cancelledCount: find('MultisigCancelled'), + }); + } catch (error) { + // keep previous counts on error + } finally { + setLoading(false); + } + }, + [network] + ); + + return { fetchData, data: counts, loading }; +}; + export const constants = { approveType_initialize: 'Initialize', approveType_executed: 'Executed', diff --git a/src/model/storage.ts b/src/model/storage.ts index 8db95dd..56d2097 100644 --- a/src/model/storage.ts +++ b/src/model/storage.ts @@ -7,4 +7,5 @@ export interface StorageInfo { customNetwork?: NetConfigV2; addedCustomNetworks?: NetConfigV2[]; selectedRpc?: string; + subscanApiKey?: string; } diff --git a/src/providers/multisig-provider.tsx b/src/providers/multisig-provider.tsx index 9f9a769..718312e 100644 --- a/src/providers/multisig-provider.tsx +++ b/src/providers/multisig-provider.tsx @@ -2,7 +2,7 @@ import { KeyringAddress } from '@polkadot/ui-keyring/types'; import { Spin } from 'antd'; import { createContext, useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useApi, useMultisig, useMultisigRecordCount } from '../hooks'; +import { useApi, useMultisig, useMultisigResourceCount } from '../hooks'; import { Entry } from '../model'; import { empty } from '../utils'; @@ -13,8 +13,7 @@ export const MultisigContext = createContext<{ cancelledAccount: number | undefined; setMultisigAccount: React.Dispatch> | null; queryInProgress: (silent?: boolean) => Promise; - refreshConfirmedAccount: () => void; - refreshCancelledAccount: () => void; + refreshCounts: () => void; setIsPageLock: (lock: boolean) => void; loadingInProgress: boolean; fetchInProgress: any; @@ -26,8 +25,7 @@ export const MultisigContext = createContext<{ cancelledAccount: 0, queryInProgress: () => Promise.resolve(), setIsPageLock: empty, - refreshConfirmedAccount: empty, - refreshCancelledAccount: empty, + refreshCounts: empty, loadingInProgress: false, // eslint-disable-next-line @typescript-eslint/no-empty-function fetchInProgress: () => {}, @@ -44,32 +42,24 @@ export const EntriesProvider = ({ children }: React.PropsWithChildren) fetchInProgress(); }, [fetchInProgress]); - const { fetchData, data } = useMultisigRecordCount(networkConfig); + const { fetchData: fetchCounts, data: countsData } = useMultisigResourceCount(networkConfig); - const refreshConfirmedAccount = useCallback(() => { - fetchData(account, 'confirmed'); - }, [account, fetchData]); - - const { fetchData: fetchCancelledData, data: cancelledData } = useMultisigRecordCount(networkConfig); - - const refreshCancelledAccount = useCallback(() => { - fetchCancelledData(account, 'cancelled'); - }, [account, fetchCancelledData]); + const refreshCounts = useCallback(() => { + fetchCounts(account); + }, [account, fetchCounts]); useEffect(() => { - refreshConfirmedAccount(); - refreshCancelledAccount(); - }, [refreshConfirmedAccount, refreshCancelledAccount]); + fetchCounts(account); + }, [account, fetchCounts]); return (