|
|
@@ -1,117 +1,80 @@
|
|
|
-import React, { ChangeEventHandler, useEffect, useState } from 'react';
|
|
|
-import { PageProps } from "gatsby"
|
|
|
-import { Typography, Grid, Form, Input, Button, Radio } from 'antd';
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
+import { Link, PageProps } from "gatsby"
|
|
|
+import { Typography, Grid, Button, } from 'antd';
|
|
|
const { Title } = Typography;
|
|
|
-const { TextArea } = Input
|
|
|
const { useBreakpoint } = Grid
|
|
|
-import { SearchOutlined } from '@ant-design/icons';
|
|
|
-import { injectIntl, WrappedComponentProps, FormattedMessage } from 'gatsby-plugin-intl';
|
|
|
+
|
|
|
+import { FormattedMessage, useIntl } from 'gatsby-plugin-intl';
|
|
|
|
|
|
import { Layout } from '~/components/Layout';
|
|
|
import { SEO } from '~/components/SEO';
|
|
|
-import { ExplorerQuery } from '~/components/ExplorerQuery'
|
|
|
+
|
|
|
+import { ExplorerStats } from '~/components/ExplorerStats'
|
|
|
+import { contractNameFormatter } from '~/components/ExplorerStats/utils';
|
|
|
import { titleStyles } from '~/styles';
|
|
|
+import { WithNetwork, NetworkSelect } from '~/components/NetworkSelect'
|
|
|
+import { ExplorerSearchForm, ExplorerTxForm } from '~/components/App/ExplorerSearch';
|
|
|
+import { ChainID } from '~/utils/misc/constants';
|
|
|
+import { OutboundLink } from 'gatsby-plugin-google-gtag';
|
|
|
+import { nativeExplorerUri } from '~/components/ExplorerStats/utils';
|
|
|
+import { CloseOutlined } from '@ant-design/icons';
|
|
|
|
|
|
|
|
|
// form props
|
|
|
-interface ExplorerFormValues {
|
|
|
+interface ExplorerQueryValues {
|
|
|
emitterChain: number,
|
|
|
emitterAddress: string,
|
|
|
sequence: string
|
|
|
+ txId: string
|
|
|
}
|
|
|
-const formFields = ['emitterChain', 'emitterAddress', 'sequence']
|
|
|
-const emitterChains = [
|
|
|
- { label: 'Solana', value: 1 },
|
|
|
- { label: 'Ethereum', value: 2 },
|
|
|
- { label: 'Terra', value: 3 },
|
|
|
- { label: 'Binance Smart Chain', value: 4 },
|
|
|
-
|
|
|
-]
|
|
|
-
|
|
|
-interface ExplorerProps extends PageProps, WrappedComponentProps<'intl'> { }
|
|
|
-const Explorer = ({ location, intl, navigate }: ExplorerProps) => {
|
|
|
|
|
|
+interface ExplorerProps extends PageProps { }
|
|
|
+const Explorer: React.FC<ExplorerProps> = ({ location, navigate }) => {
|
|
|
+ const intl = useIntl()
|
|
|
const screens = useBreakpoint()
|
|
|
- const [, forceUpdate] = useState({});
|
|
|
- const [form] = Form.useForm<ExplorerFormValues>();
|
|
|
- const [emitterChain, setEmitterChain] = useState<ExplorerFormValues["emitterChain"]>()
|
|
|
- const [emitterAddress, setEmitterAddress] = useState<ExplorerFormValues["emitterAddress"]>()
|
|
|
- const [sequence, setSequence] = useState<ExplorerFormValues["sequence"]>()
|
|
|
+ const [emitterChain, setEmitterChain] = useState<ExplorerQueryValues["emitterChain"]>()
|
|
|
+ const [emitterAddress, setEmitterAddress] = useState<ExplorerQueryValues["emitterAddress"]>()
|
|
|
+ const [sequence, setSequence] = useState<ExplorerQueryValues["sequence"]>()
|
|
|
+ const [txId, setTxId] = useState<ExplorerQueryValues["txId"]>()
|
|
|
+ const [showQueryForm, setShowQueryForm] = useState<boolean>(false)
|
|
|
|
|
|
useEffect(() => {
|
|
|
- // To disable submit button on first load.
|
|
|
- forceUpdate({});
|
|
|
- }, [])
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
-
|
|
|
if (location.search) {
|
|
|
// take searchparams from the URL and set the values in the form
|
|
|
const searchParams = new URLSearchParams(location.search);
|
|
|
|
|
|
const chain = searchParams.get('emitterChain')
|
|
|
const address = searchParams.get('emitterAddress')
|
|
|
- const sequence = searchParams.get('sequence')
|
|
|
-
|
|
|
+ const seq = searchParams.get('sequence')
|
|
|
+ const tx = searchParams.get('txId')
|
|
|
|
|
|
- // get the current values from the form fields
|
|
|
- const { emitterChain, emitterAddress, sequence: seq } = form.getFieldsValue(true)
|
|
|
|
|
|
- // if the search params are different form values, update the form.
|
|
|
- if (chain) {
|
|
|
- if (Number(chain) !== emitterChain) {
|
|
|
- form.setFieldsValue({ emitterChain: Number(chain) })
|
|
|
- }
|
|
|
- setEmitterChain(Number(chain))
|
|
|
+ // if the search params are different form values, update state
|
|
|
+ if (Number(chain) !== emitterChain) {
|
|
|
+ setEmitterChain(Number(chain) || undefined)
|
|
|
}
|
|
|
- if (address) {
|
|
|
- if (address !== emitterAddress) {
|
|
|
- form.setFieldsValue({ emitterAddress: address })
|
|
|
- }
|
|
|
- setEmitterAddress(address)
|
|
|
+ if (address !== emitterAddress) {
|
|
|
+ setEmitterAddress(address || undefined)
|
|
|
}
|
|
|
- if (sequence) {
|
|
|
- if (sequence !== seq) {
|
|
|
- form.setFieldsValue({ sequence: sequence })
|
|
|
- }
|
|
|
- setSequence(sequence)
|
|
|
+ if (seq !== sequence) {
|
|
|
+ setSequence(seq || undefined)
|
|
|
}
|
|
|
+ if (tx !== txId) {
|
|
|
+ setTxId(tx || undefined)
|
|
|
+ }
|
|
|
+ if (!tx && (chain && address && seq)) {
|
|
|
+ setShowQueryForm(true)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // clear state
|
|
|
+ setEmitterChain(undefined)
|
|
|
+ setEmitterAddress(undefined)
|
|
|
+ setSequence(undefined)
|
|
|
+ setTxId(undefined)
|
|
|
+ setShowQueryForm(false)
|
|
|
}
|
|
|
}, [location.search])
|
|
|
|
|
|
-
|
|
|
-
|
|
|
- const onFinish = ({ emitterChain, emitterAddress, sequence }: ExplorerFormValues) => {
|
|
|
- // pushing to the history stack will cause the component to get new props, and useEffect will run.
|
|
|
- navigate(`/${intl.locale}/explorer/?emitterChain=${emitterChain}&emitterAddress=${emitterAddress}&sequence=${sequence}`)
|
|
|
- };
|
|
|
-
|
|
|
- const onAddress: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
|
|
- if (e.currentTarget.value) {
|
|
|
- // trim whitespace
|
|
|
- form.setFieldsValue({ emitterAddress: e.currentTarget.value.replace(/\s/g, "") })
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
- const onSequence: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
|
- if (e.currentTarget.value) {
|
|
|
- // remove everything except numbers
|
|
|
- form.setFieldsValue({ sequence: e.currentTarget.value.replace(/\D/g, '') })
|
|
|
- }
|
|
|
- }
|
|
|
- const formatLabel = (textKey: string) => (
|
|
|
- <span style={{ fontSize: 16 }}>
|
|
|
- <FormattedMessage id={textKey} />
|
|
|
- </span>
|
|
|
-
|
|
|
- )
|
|
|
- const formatHelp = (textKey: string) => (
|
|
|
- <span style={{ fontSize: 14 }}>
|
|
|
- <FormattedMessage id={textKey} />
|
|
|
- </span>
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
return (
|
|
|
<Layout>
|
|
|
<SEO
|
|
|
@@ -123,106 +86,88 @@ const Explorer = ({ location, intl, navigate }: ExplorerProps) => {
|
|
|
style={{ paddingTop: screens.md === false ? 24 : 100 }}
|
|
|
>
|
|
|
<div
|
|
|
- className="responsive-padding max-content-width"
|
|
|
+ className="wider-responsive-padding max-content-width"
|
|
|
style={{ width: '100%' }}
|
|
|
>
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 40 }}>
|
|
|
+ <Title level={1} style={titleStyles}>{intl.formatMessage({ id: 'explorer.title' })}</Title>
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginRight: !screens.md ? 0 : 80 }}>
|
|
|
+ <div><FormattedMessage id="networks.network" /></div>
|
|
|
+ <NetworkSelect />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style={{ width: "100%", display: 'flex', justifyContent: 'flex-start', alignItems: 'center', flexDirection: 'column', marginBottom: 40 }}>
|
|
|
+ <div style={{ width: '100%', maxWidth: 960, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
|
|
+ <Title level={3} style={titleStyles}>{intl.formatMessage({ id: 'explorer.lookupPrompt' })}</Title>
|
|
|
+ <div style={{ marginRight: !screens.md ? 0 : 80 }}>
|
|
|
+ {showQueryForm && <a onClick={() => setShowQueryForm(false)}><FormattedMessage id="explorer.queryByTxId" /></a>}
|
|
|
+ {!showQueryForm && <a onClick={() => setShowQueryForm(true)}><FormattedMessage id="explorer.queryByMessageId" /></a>}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style={{ width: '100%', maxWidth: 900 }}>
|
|
|
+ {showQueryForm ? (
|
|
|
+ <ExplorerSearchForm location={location} navigate={navigate} />
|
|
|
+ ) : (
|
|
|
+ <ExplorerTxForm location={location} navigate={navigate} />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
|
|
|
- <Title level={1} style={titleStyles}>{intl.formatMessage({ id: 'explorer.title' })}</Title>
|
|
|
-
|
|
|
- <div>
|
|
|
- <Form
|
|
|
- layout="vertical"
|
|
|
- form={form}
|
|
|
- name="explorer-query"
|
|
|
- onFinish={onFinish}
|
|
|
- size="large"
|
|
|
- style={{ width: '90%', maxWidth: 800, marginBlockEnd: 60, fontSize: 14 }}
|
|
|
- colon={false}
|
|
|
- requiredMark={false}
|
|
|
- validateMessages={{ required: "'${label}' is required", }}
|
|
|
- >
|
|
|
- <Form.Item
|
|
|
- name="emitterAddress"
|
|
|
- label={formatLabel("explorer.emitterAddress")}
|
|
|
- help={formatHelp("explorer.emitterAddressHelp")}
|
|
|
- rules={[{ required: true }]}
|
|
|
- >
|
|
|
- <TextArea onChange={onAddress} allowClear autoSize />
|
|
|
- </Form.Item>
|
|
|
-
|
|
|
- <Form.Item
|
|
|
- name="emitterChain"
|
|
|
- label={formatLabel("explorer.emitterChain")}
|
|
|
- help={formatHelp("explorer.emitterChainHelp")}
|
|
|
- rules={[{ required: true }]}
|
|
|
- style={
|
|
|
- screens.md === false ? {
|
|
|
- display: 'block', width: '100%'
|
|
|
- } : {
|
|
|
- display: 'inline-block', width: '50%'
|
|
|
- }}
|
|
|
- >
|
|
|
- <Radio.Group
|
|
|
- optionType="button"
|
|
|
- options={emitterChains}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
-
|
|
|
- <Form.Item shouldUpdate
|
|
|
- style={
|
|
|
- screens.md === false ? {
|
|
|
- display: 'block', width: '100%'
|
|
|
- } : {
|
|
|
- display: 'inline-block', width: '50%'
|
|
|
- }}
|
|
|
+ </div>
|
|
|
+ {!(emitterChain && emitterAddress && sequence) && !txId ? (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ display: 'flex',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ marginBottom: 40
|
|
|
+ }}
|
|
|
>
|
|
|
- {() => (
|
|
|
-
|
|
|
- <Form.Item
|
|
|
- name="sequence"
|
|
|
- label={formatLabel("explorer.sequence")}
|
|
|
- help={formatHelp("explorer.sequenceHelp")}
|
|
|
- rules={[{ required: true }]}
|
|
|
- >
|
|
|
-
|
|
|
- <Input
|
|
|
- onChange={onSequence}
|
|
|
- style={{ padding: "0 0 0 14px" }}
|
|
|
-
|
|
|
- allowClear
|
|
|
- suffix={
|
|
|
- <Button
|
|
|
- size="large"
|
|
|
- type="primary"
|
|
|
- style={{ width: 80 }}
|
|
|
- icon={
|
|
|
- <SearchOutlined style={{ fontSize: 16, color: 'black' }} />
|
|
|
- }
|
|
|
- htmlType="submit"
|
|
|
- disabled={
|
|
|
- // true if the value of any field is falsey, or
|
|
|
- (Object.values({ ...form.getFieldsValue(formFields) }).some(v => !v)) ||
|
|
|
- // true if the length of the errors array is true.
|
|
|
- !!form.getFieldsError().filter(({ errors }) => errors.length).length
|
|
|
- }
|
|
|
- />
|
|
|
- }
|
|
|
- />
|
|
|
-
|
|
|
- </Form.Item>
|
|
|
- )}
|
|
|
- </Form.Item>
|
|
|
+ {emitterAddress && emitterChain ? (
|
|
|
+ // show heading with the context of the address
|
|
|
+ <Title level={3} style={{ ...titleStyles }}>
|
|
|
+ Recent messages from {ChainID[emitterChain]}
|
|
|
+ {nativeExplorerUri(emitterChain, emitterAddress) ?
|
|
|
+ <OutboundLink
|
|
|
+ href={nativeExplorerUri(emitterChain, emitterAddress)}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ >
|
|
|
+ {contractNameFormatter(emitterAddress, emitterChain)}
|
|
|
+ </OutboundLink> : contractNameFormatter(emitterAddress, emitterChain)}
|
|
|
+ :
|
|
|
+ </Title>
|
|
|
+
|
|
|
+ ) : emitterChain ? (
|
|
|
+ // show heading with the context of the chain
|
|
|
+ <Title level={3} style={{ ...titleStyles }}>
|
|
|
+ Recent {ChainID[emitterChain]} activity
|
|
|
+ </Title>
|
|
|
+ ) : (
|
|
|
+ // show heading for root view, all chains
|
|
|
+ <Title level={3} style={{ ...titleStyles }}>
|
|
|
+ {intl.formatMessage({ id: 'explorer.stats.heading' })}
|
|
|
+ </Title>
|
|
|
|
|
|
- </Form>
|
|
|
- </div>
|
|
|
- {emitterChain && emitterAddress && sequence ? (
|
|
|
- <ExplorerQuery emitterChain={emitterChain} emitterAddress={emitterAddress} sequence={sequence} />
|
|
|
+ )}
|
|
|
+ {emitterAddress || emitterChain ?
|
|
|
+ <Link to={`/${intl.locale}/explorer`}>
|
|
|
+ <Button
|
|
|
+ shape="round"
|
|
|
+ icon={<CloseOutlined />}
|
|
|
+ size="large"
|
|
|
+ style={{ marginRight: !screens.md ? 0 : 40 }}
|
|
|
+
|
|
|
+ >clear</Button>
|
|
|
+ </Link> : null}
|
|
|
+ </div>
|
|
|
+ <ExplorerStats emitterChain={emitterChain} emitterAddress={emitterAddress} />
|
|
|
+ </>
|
|
|
) : null}
|
|
|
-
|
|
|
</div>
|
|
|
</div>
|
|
|
</Layout >
|
|
|
)
|
|
|
};
|
|
|
|
|
|
-export default injectIntl(Explorer)
|
|
|
+export default WithNetwork(Explorer)
|