explorer: lookup BigTable data
- Explorer page fetch data from hosted Cloud Functions. - Network page use GetLastHeartbeats rather than gRPC stream. Change-Id: I57dce2ee0b84c4b31fcf7308855668a139ffe20e
This commit is contained in:
parent
986b4b58f1
commit
cf36c9bfe0
38
Tiltfile
38
Tiltfile
|
@ -224,28 +224,26 @@ k8s_resource(
|
|||
|
||||
# explorer web app
|
||||
|
||||
# TOOD: the explorer web app does not currently build
|
||||
if not ci:
|
||||
docker_build(
|
||||
ref = "explorer",
|
||||
context = "./explorer",
|
||||
dockerfile = "./explorer/Dockerfile",
|
||||
ignore = ["./explorer/node_modules"],
|
||||
live_update = [
|
||||
sync("./explorer/src", "/home/node/app/src"),
|
||||
sync("./explorer/public", "/home/node/app/public"),
|
||||
],
|
||||
)
|
||||
docker_build(
|
||||
ref = "explorer",
|
||||
context = "./explorer",
|
||||
dockerfile = "./explorer/Dockerfile",
|
||||
ignore = ["./explorer/node_modules"],
|
||||
live_update = [
|
||||
sync("./explorer/src", "/home/node/app/src"),
|
||||
sync("./explorer/public", "/home/node/app/public"),
|
||||
],
|
||||
)
|
||||
|
||||
k8s_yaml_with_ns("devnet/explorer.yaml")
|
||||
k8s_yaml_with_ns("devnet/explorer.yaml")
|
||||
|
||||
k8s_resource(
|
||||
"explorer",
|
||||
resource_deps = ["envoy-proxy", "proto-gen-web"],
|
||||
port_forwards = [
|
||||
port_forward(8001, name = "Explorer Web UI [:8001]"),
|
||||
],
|
||||
)
|
||||
k8s_resource(
|
||||
"explorer",
|
||||
resource_deps = ["proto-gen-web"],
|
||||
port_forwards = [
|
||||
port_forward(8001, name = "Explorer Web UI [:8001]"),
|
||||
],
|
||||
)
|
||||
|
||||
# terra devnet
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
.env.development
|
||||
.env.production
|
|
@ -1,12 +1,29 @@
|
|||
# General
|
||||
GATSBY_TELEMETRY_DISABLED=1
|
||||
GATSBY_SITE_URL=http://localhost:8000
|
||||
GATSBY_GA_TAG=G-tag-goes-here
|
||||
GATSBY_ENVIRONMENT=development
|
||||
|
||||
GATSBY_APP_RPC_URL=http://localhost:8080
|
||||
|
||||
GATSBY_BIGTABLE_URL=https://us-central1-wormhole-315720.cloudfunctions.net/BT-reader-test
|
||||
|
||||
# Profiling
|
||||
ENABLE_BUNDLE_ANALYZER=0
|
||||
|
||||
# Feature flags
|
||||
ENABLE_NETWORK_PAGE=true
|
||||
ENABLE_EXPLORER_PAGE=true
|
||||
|
||||
|
||||
ETH_CORE_BRIDGE=0x254dffcd3277c0b1660f6d42efbb754edababc2b
|
||||
ETH_TOKEN_BRIDGE=0xe982e462b094850f12af94d21d470e21be9d0e9c
|
||||
|
||||
BSC_CORE_BRIDGE=0x254dffcd3277c0b1660f6d42efbb754edababc2b
|
||||
BSC_TOKEN_BRIDGE=0xe982e462b094850f12af94d21d470e21be9d0e9c
|
||||
|
||||
SOL_CORE_BRIDGE=Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o
|
||||
SOL_TOKEN_BRIDGE=B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE
|
||||
|
||||
LUN_CORE_BRIDGE=terra18eezxhys9jwku67cm4w84xhnzt4xjj77w2qt62
|
||||
LUN_TOKEN_BRIDGE=terra1hqrdl6wstt8qzshwc6mrumpjk9338k0l93hqyd
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker.io/docker/dockerfile:experimental@sha256:de85b2f3a3e8a2f7fe48e8e84a65f6fdd5cd5183afa6412fff9caa6871649c44
|
||||
|
||||
# Derivative of ethereum/Dockerfile, look there for an explanation on how it works.
|
||||
FROM node:lts-alpine@sha256:2ae9624a39ce437e7f58931a5747fdc60224c6e40f8980db90728de58e22af7c
|
||||
FROM node:16-alpine@sha256:004dbac84fed48e20f9888a23e32fa7cf83c2995e174a78d41d9a9dd1e051a20
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
|
|
@ -88,3 +88,14 @@ With your Service Account [credentials](https://github.com/leolabs/json-autotran
|
|||
|
||||
npm run translate:google -- ./your-GCP-service-account.json
|
||||
|
||||
### Protobuf generation
|
||||
|
||||
You'll need to generate proto files by running:
|
||||
|
||||
npm run generate-protos
|
||||
|
||||
### WASM generation
|
||||
|
||||
To generate WASM files run:
|
||||
|
||||
npm run generate-wasm
|
||||
|
|
|
@ -49,7 +49,14 @@ const plugins = [
|
|||
options: {
|
||||
host: process.env.GATSBY_SITE_URL,
|
||||
sitemap: `${process.env.GATSBY_SITE_URL}/sitemap.xml`,
|
||||
policy: [{ userAgent: '*', allow: '/' }]
|
||||
env: {
|
||||
development: {
|
||||
policy: [{ userAgent: '*', disallow: ['/'] }]
|
||||
},
|
||||
production: {
|
||||
policy: [{ userAgent: '*', allow: '/' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -68,7 +75,16 @@ const plugins = [
|
|||
}
|
||||
})
|
||||
},
|
||||
exclude: process.env.ENABLE_NETWORK_PAGE !== 'true' ? ['/*/network/'] : []
|
||||
exclude: [
|
||||
process.env.ENABLE_NETWORK_PAGE !== 'true' ? '/*/network/' : '/',
|
||||
process.env.ENABLE_EXPLORER_PAGE !== 'true' ? '/*/explorer/' : '/',
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-google-gtag`,
|
||||
options: {
|
||||
trackingIds: [String(process.env.GATSBY_GA_TAG)],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -24,6 +24,23 @@ export const onCreateWebpackConfig = function addPathMapping({
|
|||
devtool: 'eval-source-map',
|
||||
});
|
||||
|
||||
actions.setWebpackConfig({
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
use: [
|
||||
'wasm-loader'
|
||||
],
|
||||
type: "javascript/auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const config = getConfig();
|
||||
config.resolve.extensions.push(".wasm");
|
||||
actions.replaceWebpackConfig(config);
|
||||
|
||||
// Attempt to improve webpack vender code splitting
|
||||
if (stage === 'build-javascript') {
|
||||
const config = getConfig();
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# Regenerate explorer
|
||||
set -euo pipefail
|
||||
|
||||
(
|
||||
cd ../solana
|
||||
mkdir -p ../explorer/wasm/core
|
||||
mkdir -p ../explorer/wasm/token
|
||||
|
||||
docker build -t localhost/certusone/wormhole-wasmpack:latest -f Dockerfile.wasm .
|
||||
|
||||
docker run --rm -it --workdir /usr/src/bridge/bridge/program \
|
||||
-v $(pwd)/../explorer/wasm/core:/usr/src/bridge/bridge/program/pkg \
|
||||
-e EMITTER_ADDRESS=11111111111111111111111111111115 \
|
||||
localhost/certusone/wormhole-wasmpack:latest \
|
||||
/usr/local/cargo/bin/wasm-pack build --target bundler -- --features wasm
|
||||
|
||||
docker run --rm -it --workdir /usr/src/bridge/modules/token_bridge/program \
|
||||
-v $(pwd)/../explorer/wasm/token:/usr/src/bridge/modules/token_bridge/program/pkg \
|
||||
-e EMITTER_ADDRESS=11111111111111111111111111111115 \
|
||||
localhost/certusone/wormhole-wasmpack:latest \
|
||||
/usr/local/cargo/bin/wasm-pack build --target bundler -- --features wasm
|
||||
)
|
File diff suppressed because it is too large
Load Diff
|
@ -46,7 +46,8 @@
|
|||
"storybook:build": "NODE_OPTIONS='-r esm' gatsby build && build-storybook -c .storybook -o .out",
|
||||
"translate:deepl": "node_modules/.bin/json-autotranslate -i src/locales -d -m none --directory-structure ngx-translate --service=deepl -c",
|
||||
"translate:google": "node_modules/.bin/json-autotranslate -i src/locales -d -m none --directory-structure ngx-translate --service=google-translate -c",
|
||||
"generate-protos": "../generate-web-protos.sh"
|
||||
"generate-protos": "../generate-web-protos.sh",
|
||||
"generate-wasm": "./generate-wasm.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
|
@ -56,12 +57,15 @@
|
|||
"@svgr/webpack": "^5.1.0",
|
||||
"antd": "^4.15.4",
|
||||
"babel-plugin-module-resolver": "^4.0.0",
|
||||
"bridge": "file:./wasm/core",
|
||||
"core-js": "2.6.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"esm": "^3.2.25",
|
||||
"ethers": "^5.4.4",
|
||||
"gatsby": "^2.19.19",
|
||||
"gatsby-image": "^2.2.41",
|
||||
"gatsby-plugin-antd": "^2.2.0",
|
||||
"gatsby-plugin-google-gtag": "^2.8.0",
|
||||
"gatsby-plugin-intl": "0.3.3",
|
||||
"gatsby-plugin-less": "4.6.0",
|
||||
"gatsby-plugin-react-helmet": "^3.1.22",
|
||||
|
@ -73,7 +77,8 @@
|
|||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-time-ago": "^6.2.2"
|
||||
"react-time-ago": "^6.2.2",
|
||||
"token_bridge": "file:./wasm/token"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.4",
|
||||
|
@ -127,6 +132,7 @@
|
|||
"ts-essentials": "^6.0.1",
|
||||
"ts-jest": "^25.2.1",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.8.2"
|
||||
"typescript": "^3.8.2",
|
||||
"wasm-loader": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
// antd variables. see https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
|
||||
'font-family':
|
||||
"-apple-system, BlinkMacSystemFont, 'Sora', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
|
||||
"Sora, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
|
||||
'body-background': '#010114',
|
||||
'component-background': '@body-background',
|
||||
'primary-color': '#00EFD8',
|
||||
|
@ -18,6 +18,10 @@ export default {
|
|||
'menu-inline-submenu-bg': '@layout-body-background',
|
||||
'menu-popup-bg': '@layout-body-background',
|
||||
|
||||
// table styles
|
||||
'table-header-bg': '#212130',
|
||||
'table-row-hover-bg': '#212130',
|
||||
|
||||
// global wormhole variables (not antd overrides)
|
||||
'max-content-width': '1400px',
|
||||
'blue-background': '#141449',
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Spin, Typography } from 'antd'
|
||||
const { Title } = Typography
|
||||
|
||||
import { FormattedMessage } from 'gatsby-plugin-intl'
|
||||
import { arrayify, isHexString, zeroPad } from "ethers/lib/utils";
|
||||
import { ExplorerSummary } from '~/components/ExplorerSummary';
|
||||
import { titleStyles } from '~/styles';
|
||||
|
||||
|
||||
export interface VAA {
|
||||
Version: number | string,
|
||||
GuardianSetIndex: number,
|
||||
Signatures: { Index: number, Signature: string }[],
|
||||
Timestamp: string, // "0001-01-01T00:00:00Z",
|
||||
Nonce: number,
|
||||
Sequence: number,
|
||||
ConsistencyLevel: number,
|
||||
EmitterChain: number,
|
||||
EmitterAddress: string,
|
||||
Payload: string // base64 encoded byte array
|
||||
}
|
||||
export interface BigTableMessage {
|
||||
InitiatingTxID: string
|
||||
GuardianAddresses: string[],
|
||||
SignedVAABytes: string // base64 encoded byte array
|
||||
SignedVAA: VAA
|
||||
QuorumTime: string // "2021-08-11 00:16:11.757 +0000 UTC"
|
||||
}
|
||||
|
||||
interface ExplorerQuery {
|
||||
emitterChain: number,
|
||||
emitterAddress: string,
|
||||
sequence: string
|
||||
}
|
||||
const ExplorerQuery = (props: ExplorerQuery) => {
|
||||
const [error, setError] = useState<string>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [message, setMessage] = useState<BigTableMessage>();
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [lastFetched, setLastFetched] = useState<number>()
|
||||
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout>()
|
||||
|
||||
const fetchMessage = (
|
||||
emitterChain: ExplorerQuery["emitterChain"],
|
||||
emitterAddress: ExplorerQuery["emitterAddress"],
|
||||
sequence: ExplorerQuery["sequence"]) => {
|
||||
let paddedAddress: string
|
||||
|
||||
if (emitterChain === 1) {
|
||||
// TODO - zero pad Solana address, if needed.
|
||||
paddedAddress = emitterAddress
|
||||
} else if (emitterChain === 2 || emitterChain === 4) {
|
||||
if (isHexString(emitterAddress)) {
|
||||
|
||||
let paddedAddressArray = zeroPad(arrayify(emitterAddress, { hexPad: "left" }), 32);
|
||||
|
||||
// TODO - properly encode the this to a hex string, Buffer is deprecated.
|
||||
let maybeString = new Buffer(paddedAddressArray).toString('hex');
|
||||
|
||||
paddedAddress = maybeString
|
||||
} else {
|
||||
// must already be padded
|
||||
paddedAddress = emitterAddress
|
||||
}
|
||||
} else {
|
||||
// TODO - zero pad Terra address, if needed
|
||||
paddedAddress = emitterAddress
|
||||
}
|
||||
|
||||
const base = process.env.GATSBY_BIGTABLE_URL
|
||||
const url = `${base}/?emitterChain=${emitterChain}&emitterAddress=${paddedAddress}&sequence=${sequence}`
|
||||
|
||||
fetch(url)
|
||||
.then<BigTableMessage>(res => {
|
||||
if (res.ok) return res.json()
|
||||
if (res.status === 404) {
|
||||
// show a specific message to the user if the query returned 404.
|
||||
throw 'explorer.notFound'
|
||||
}
|
||||
// if res is not ok, and not 404, throw an error with specific message,
|
||||
// rather than letting the json decoding throw.
|
||||
throw 'explorer.failedFetching'
|
||||
})
|
||||
.then(result => {
|
||||
|
||||
setMessage(result)
|
||||
setLoading(false)
|
||||
setLastFetched(Date.now())
|
||||
|
||||
// turn polling on/off
|
||||
if (!result.QuorumTime && !polling) {
|
||||
setPolling(true)
|
||||
} else if (result.QuorumTime && polling) {
|
||||
setPolling(false)
|
||||
}
|
||||
}, error => {
|
||||
// Note: it's important to handle errors here
|
||||
// instead of a catch() block so that we don't swallow
|
||||
// exceptions from actual bugs in components.
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
setLastFetched(Date.now())
|
||||
if (polling) {
|
||||
setPolling(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshCallback = () => {
|
||||
fetchMessage(props.emitterChain, props.emitterAddress, props.sequence)
|
||||
}
|
||||
|
||||
if (polling && !pollInterval) {
|
||||
let interval = setInterval(() => {
|
||||
fetchMessage(props.emitterChain, props.emitterAddress, props.sequence)
|
||||
}, 3000)
|
||||
setPollInterval(interval)
|
||||
} else if (!polling && pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
setPollInterval(undefined)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.emitterChain && props.emitterAddress && props.sequence) {
|
||||
setPolling(false)
|
||||
setLoading(true)
|
||||
setError(undefined)
|
||||
setMessage(undefined)
|
||||
fetchMessage(props.emitterChain, props.emitterAddress, props.sequence)
|
||||
}
|
||||
|
||||
}, [props.emitterChain, props.emitterAddress, props.sequence])
|
||||
|
||||
useEffect(() => {
|
||||
return function cleanup() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
}
|
||||
};
|
||||
}, [polling])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Spin />
|
||||
</div> :
|
||||
error ? <Title level={2} style={titleStyles}><FormattedMessage id={error} /></Title> :
|
||||
message ? (
|
||||
<ExplorerSummary
|
||||
{...props}
|
||||
message={message}
|
||||
polling={polling}
|
||||
lastFetched={lastFetched}
|
||||
refetch={refreshCallback}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExplorerQuery
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ExplorerQuery } from './ExplorerQuery';
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button, Spin, Typography } from 'antd'
|
||||
const { Title } = Typography
|
||||
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
|
||||
import { BigTableMessage } from '~/components/ExplorerQuery/ExplorerQuery';
|
||||
// import { WasmTest } from '~/components/wasm'
|
||||
import ReactTimeAgo from 'react-time-ago'
|
||||
import { buttonStylesLg, titleStyles } from '~/styles';
|
||||
|
||||
interface SummaryProps {
|
||||
emitterChain: number,
|
||||
emitterAddress: string,
|
||||
sequence: string
|
||||
message: BigTableMessage
|
||||
polling?: boolean
|
||||
lastFetched?: number
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
const Summary = (props: SummaryProps) => {
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: decode the payload. if applicable lookup other relevant messages.
|
||||
}, [props])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'baseline' }}>
|
||||
<Title level={2} style={titleStyles}><FormattedMessage id="explorer.messageSummary" /></Title>
|
||||
{props.polling ? (
|
||||
<>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<Spin />
|
||||
<Title level={2} style={titleStyles}><FormattedMessage id="explorer.listening" /></Title>
|
||||
</>
|
||||
) : (
|
||||
<Button style={buttonStylesLg} onClick={props.refetch} size="large"><FormattedMessage id="explorer.refresh" /></Button>
|
||||
)}
|
||||
</div>
|
||||
<pre>{JSON.stringify(props.message, undefined, 2)}</pre>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
|
||||
{props.lastFetched ? (
|
||||
<span>
|
||||
<FormattedMessage id="explorer.lastUpdated" />:
|
||||
<ReactTimeAgo date={new Date(props.lastFetched)} locale={intl.locale} timeStyle="round" />
|
||||
</span>
|
||||
|
||||
) : null}
|
||||
</div>
|
||||
{/* <WasmTest base64VAA={props.message.SignedVAA} /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Summary
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ExplorerSummary } from './ExplorerSummary';
|
||||
|
|
@ -7,18 +7,20 @@ import ReactTimeAgo from 'react-time-ago'
|
|||
|
||||
import { Heartbeat, Heartbeat_Network } from '~/proto/gossip/v1/gossip'
|
||||
|
||||
import { ReactComponent as BinanceChainIcon } from '~/icons/binancechain.svg';
|
||||
import { ReactComponent as EthereumIcon } from '~/icons/ethereum.svg';
|
||||
import { ReactComponent as SolanaIcon } from '~/icons/solana.svg';
|
||||
import { ReactComponent as TerraIcon } from '~/icons/terra.svg';
|
||||
|
||||
import './GuardiansTable.less'
|
||||
|
||||
const networkEnums = ['', 'Solana', 'Ethereum', 'Terra']
|
||||
const networkEnums = ['', 'Solana', 'Ethereum', 'Terra', 'BSC']
|
||||
const networkIcons = [
|
||||
<></>,
|
||||
<SolanaIcon key="2" style={{ height: 18, margin: '0 4px' }} />,
|
||||
<EthereumIcon key="3" style={{ height: 24, margin: '0 4px' }} />,
|
||||
<TerraIcon key="1" style={{ height: 18, margin: '0 4px' }} />,
|
||||
<SolanaIcon key="1" style={{ height: 18, maxWidth: 18, margin: '0 4px' }} />,
|
||||
<EthereumIcon key="2" style={{ height: 24, margin: '0 4px' }} />,
|
||||
<TerraIcon key="3" style={{ height: 18, margin: '0 4px' }} />,
|
||||
<BinanceChainIcon key="4" style={{ height: 18, margin: '0 4px' }} />,
|
||||
]
|
||||
|
||||
const expandedRowRender = (intl: IntlShape) => (item: Heartbeat) => {
|
||||
|
|
|
@ -5,11 +5,12 @@ const { Header, Content, Footer } = Layout;
|
|||
const { useBreakpoint } = Grid
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl';
|
||||
import { OutboundLink } from "gatsby-plugin-google-gtag"
|
||||
import { useLocation } from '@reach/router';
|
||||
import { Link } from 'gatsby'
|
||||
import './DefaultLayout.less'
|
||||
|
||||
import { socialLinks, socialAnchorArray } from '~/utils/misc/socials';
|
||||
import { externalLinks, linkToService, socialLinks, socialAnchorArray } from '~/utils/misc/socials';
|
||||
|
||||
// brand assets
|
||||
import { ReactComponent as AvatarAndName } from '~/icons/FullLogo_DarkBackground.svg';
|
||||
|
@ -24,7 +25,7 @@ const DefaultLayout: React.FC<{}> = ({
|
|||
const intl = useIntl()
|
||||
const location = useLocation()
|
||||
const screens = useBreakpoint();
|
||||
const menuItemProps: { style: { textAlign: CanvasTextAlign } } = { style: { textAlign: 'center' } }
|
||||
const menuItemProps: { style: { textAlign: CanvasTextAlign, padding: number } } = { style: { textAlign: 'center', padding: 0 } }
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
|
@ -60,24 +61,55 @@ const DefaultLayout: React.FC<{}> = ({
|
|||
</Menu.Item>
|
||||
{String(process.env.ENABLE_NETWORK_PAGE) === 'true' ? (
|
||||
<Menu.Item key="network" {...menuItemProps}>
|
||||
<Link to={`/${intl.locale}/network/`}>{intl.formatMessage({ id: "nav.networkLink" })}</Link>
|
||||
<Link to={`/${intl.locale}/network/`}>
|
||||
<FormattedMessage id="nav.networkLink" />
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{String(process.env.ENABLE_EXPLORER_PAGE) === 'true' ? (
|
||||
<Menu.Item key="explorer" {...menuItemProps}>
|
||||
<Link to={`/${intl.locale}/explorer`}>
|
||||
<FormattedMessage id="nav.explorerLink" />
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
<Menu.Item key="code" {...menuItemProps}>
|
||||
<a
|
||||
<OutboundLink
|
||||
href={socialLinks['github']}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{intl.formatMessage({ id: "nav.codeLink" })}
|
||||
</a>
|
||||
</OutboundLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="jobs" {...menuItemProps}>
|
||||
<OutboundLink
|
||||
href={"https://boards.greenhouse.io/wormhole"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{intl.formatMessage({ id: "nav.jobsLink" })}
|
||||
</OutboundLink>
|
||||
</Menu.Item>
|
||||
|
||||
{screens.md === false ? (
|
||||
<Menu.Item key="external" style={{ margin: '12px 0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-evenly', width: '100vw' }}>
|
||||
{socialAnchorArray(intl, { zIndex: 2 }, { height: 26 })}
|
||||
</div>
|
||||
<Menu.Item style={{ height: '100%', padding: 0 }}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
style={{ display: 'flex', justifyContent: 'space-between', width: '98vw', borderStyle: 'none' }}
|
||||
selectedKeys={[]} >
|
||||
{Object.entries(externalLinks).map(([url, Icon]) => <Menu.Item key={url} {...menuItemProps} style={{ margin: '12px 0' }} >
|
||||
<div style={{ display: 'flex', justifyContent: 'space-evenly', width: '100%' }}>
|
||||
<OutboundLink
|
||||
href={url}
|
||||
{...externalLinkProps}
|
||||
title={intl.formatMessage({ id: `nav.${linkToService[url]}AltText` })}
|
||||
>
|
||||
<Icon style={{ height: 26 }} className="external-icon" />
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</Menu.Item>)}
|
||||
</Menu>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu>
|
||||
|
@ -113,14 +145,12 @@ const DefaultLayout: React.FC<{}> = ({
|
|||
}}>
|
||||
<Avatar style={{ maxHeight: 58 }} />
|
||||
<div style={{ lineHeight: '1.5em' }}>
|
||||
<a href={socialLinks['github']} {...externalLinkProps} style={{ color: 'white' }}>
|
||||
<OutboundLink href={socialLinks['github']} {...externalLinkProps} style={{ color: 'white' }}>
|
||||
{intl.formatMessage({ id: "footer.openSource" })}
|
||||
</a>
|
||||
</OutboundLink>
|
||||
<br />
|
||||
{intl.formatMessage({ id: "footer.createdWith" })}
|
||||
<a href="https://certus.one/" {...externalLinkProps} style={{ color: 'white' }}>
|
||||
<span style={{ fontSize: '1.4em' }}>♥</span>
|
||||
</a>
|
||||
<span style={{ fontSize: '1.4em' }}>♥</span>
|
||||
<br />
|
||||
©{new Date().getFullYear()}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default as WasmTest } from './wasm';
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Typography } from 'antd'
|
||||
const { Title } = Typography
|
||||
|
||||
export type IWASMModule = typeof import("bridge")
|
||||
|
||||
function convertbase64ToBinary(base64: string) {
|
||||
var raw = window.atob(base64);
|
||||
var rawLength = raw.length;
|
||||
var array = new Uint8Array(new ArrayBuffer(rawLength));
|
||||
|
||||
for (let i = 0; i < rawLength; i++) {
|
||||
array[i] = raw.charCodeAt(i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
interface WasmProps {
|
||||
base64VAA: string
|
||||
}
|
||||
|
||||
const WasmTest = (props: WasmProps) => {
|
||||
|
||||
|
||||
const loadWasm = async (base64VAA: string) => {
|
||||
const vaa = convertbase64ToBinary(base64VAA)
|
||||
try {
|
||||
/*eslint no-useless-concat: "off"*/
|
||||
const wasm = await import('bridge')
|
||||
// debugger
|
||||
const parsed = wasm.parse_vaa(vaa)
|
||||
console.log('parsed vaa: ', parsed)
|
||||
// let addr = 'Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o'
|
||||
// let res = wasm.state_address(addr)
|
||||
// console.log('res', res)
|
||||
// alert('it worked.')
|
||||
// debugger
|
||||
} catch (err) {
|
||||
debugger
|
||||
console.error(`Unexpected error in loadWasm. [Message: ${err.message}]`)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (props.base64VAA) {
|
||||
loadWasm(props.base64VAA)
|
||||
}
|
||||
}, [props])
|
||||
|
||||
return <Title level={3}>wasm test</Title>
|
||||
}
|
||||
|
||||
export default WasmTest
|
|
@ -32,7 +32,11 @@
|
|||
"homeLinkAltText": "homepage",
|
||||
"aboutLink": "about",
|
||||
"networkLink": "network",
|
||||
"explorerLink": "explorer",
|
||||
"documentationLink": "documentation",
|
||||
"partnersLink": "partners",
|
||||
"codeLink": "code",
|
||||
"jobsLink": "jobs",
|
||||
"discordAltText": "Go to Wormhole's Discord",
|
||||
"githubAltText": "Go to Wormhole's Github",
|
||||
"mediumAltText": "Go to Wormhole's Medium",
|
||||
|
@ -75,7 +79,7 @@
|
|||
},
|
||||
"network": {
|
||||
"title": "Wormhole Network",
|
||||
"description": "The Wormhole Bridge Guardians",
|
||||
"description": "Live Wormhole Guardian statuses",
|
||||
"listening": "Listening for Guardian heartbeats...",
|
||||
"guardiansFound": "Guardians currently broadcasting",
|
||||
"heartbeat": "Heartbeat #",
|
||||
|
@ -86,5 +90,30 @@
|
|||
"version": "Version",
|
||||
"lastHeartbeat": "Last Heartbeat",
|
||||
"guardian": "Guardian"
|
||||
},
|
||||
"explorer": {
|
||||
"title": "Wormhole Explorer",
|
||||
"description": "Real time Wormhole message explorer",
|
||||
"lookupPrompt": "Lookup a message",
|
||||
"emitterChain": "Emitter Chain",
|
||||
"emitterChainHelp": "The chain where transaction took place.",
|
||||
"emitterAddress": "Emitter Address",
|
||||
"emitterAddressHelp": "The contract you interacted with",
|
||||
"sequence": "Sequence number",
|
||||
"sequenceHelp": "The sequence number from your contract interaction",
|
||||
"messageSummary": "Message Summary",
|
||||
"failedFetching": "Something went wrong. Please check your data and try again.",
|
||||
"notFound": "Nothing found for that query. Please check your data and try again.",
|
||||
"listening": "Listening for Updates",
|
||||
"lastUpdated": "Last updated",
|
||||
"refresh": "refresh"
|
||||
},
|
||||
"partners": {
|
||||
"title": "Wormhole Partners",
|
||||
"description": "The Wormhole network consists of guardians that are run independently by community leaders."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Wormhole Documentation",
|
||||
"description": "Wormhole technical reference"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ import { Typography, Grid, Button, Steps } from 'antd'
|
|||
const { Title, Paragraph } = Typography
|
||||
const { Step } = Steps;
|
||||
import { useIntl, FormattedMessage, IntlShape } from 'gatsby-plugin-intl';
|
||||
import { bodyStyles, headingStyles, titleStyles } from '~/styles'
|
||||
import { OutboundLink } from "gatsby-plugin-google-gtag"
|
||||
import { bodyStyles, buttonStylesLg, headingStyles, titleStyles } from '~/styles'
|
||||
|
||||
import { Layout } from '~/components/Layout';
|
||||
import { SEO } from '~/components/SEO';
|
||||
|
@ -87,14 +88,14 @@ const HowSection = ({ intl, smScreen }: { intl: IntlShape, smScreen: boolean })
|
|||
<Paragraph style={{ ...bodyStyles, maxWidth: smScreen ? '100%' : '80%', marginBottom: 50 }} >
|
||||
<FormattedMessage id="about.how.body" />
|
||||
</Paragraph>
|
||||
<a
|
||||
<OutboundLink
|
||||
href="https://github.com/certusone/wormhole/blob/dev.v2/design/navbar.md"
|
||||
target="_blank" rel="noopener noreferrer" className="no-external-icon"
|
||||
>
|
||||
<Button style={{ width: 255, height: 38, border: "1.5px solid", marginBottom: 50 }} size="large">
|
||||
<Button style={{ ...buttonStylesLg, marginBottom: 50 }} size="large">
|
||||
<FormattedMessage id="about.how.callToAction" />
|
||||
</Button>
|
||||
</a>
|
||||
</OutboundLink>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -163,15 +164,15 @@ const ReadMoreSection = ({ smScreen }: { intl: IntlShape, smScreen: boolean }) =
|
|||
</Title>
|
||||
{/* Placeholder link to Documentation page */}
|
||||
{/* <Link to={`/${intl.locale}/documentation`}>
|
||||
<Button ghost style={{ width: 255, height: 38, border: "1.5px solid", marginTop: 30 }} size="large">
|
||||
<Button ghost style={{ ...buttonStylesLg, width: 255, marginTop: 30 }} size="large">
|
||||
<FormattedMessage id="about.readMore.callToAction" />
|
||||
</Button>
|
||||
</Link> */}
|
||||
{/* <a href="mailto:contact@wormholenetwork.com" target="_blank" rel="noopener noreferrer" >
|
||||
<Button ghost style={{ width: 255, height: 38, border: "1.5px solid", marginTop: 30 }} size="large">
|
||||
{/* <OutboundLink href="mailto:contact@wormholenetwork.com" target="_blank" rel="noopener noreferrer" >
|
||||
<Button ghost style={{ ...buttonStylesLg, width: 255, marginTop: 30 }} size="large">
|
||||
<FormattedMessage id="about.emailUs" />
|
||||
</Button>
|
||||
</a> */}
|
||||
</OutboundLink> */}
|
||||
</div>
|
||||
{smScreen ? null : (
|
||||
<div style={{ position: 'absolute', right: 0, height: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
import React, { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { PageProps } from "gatsby"
|
||||
import { Typography, Grid, Form, Input, Button, Radio } 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 { Layout } from '~/components/Layout';
|
||||
import { SEO } from '~/components/SEO';
|
||||
import { ExplorerQuery } from '~/components/ExplorerQuery'
|
||||
import { titleStyles } from '~/styles';
|
||||
|
||||
|
||||
// form props
|
||||
interface ExplorerFormValues {
|
||||
emitterChain: number,
|
||||
emitterAddress: string,
|
||||
sequence: 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) => {
|
||||
|
||||
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"]>()
|
||||
|
||||
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')
|
||||
|
||||
|
||||
// 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 (address) {
|
||||
if (address !== emitterAddress) {
|
||||
form.setFieldsValue({ emitterAddress: address })
|
||||
}
|
||||
setEmitterAddress(address)
|
||||
}
|
||||
if (sequence) {
|
||||
if (sequence !== seq) {
|
||||
form.setFieldsValue({ sequence: sequence })
|
||||
}
|
||||
setSequence(sequence)
|
||||
}
|
||||
}
|
||||
}, [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
|
||||
title={intl.formatMessage({ id: 'explorer.title' })}
|
||||
description={intl.formatMessage({ id: 'explorer.description' })}
|
||||
/>
|
||||
<div
|
||||
className="center-content"
|
||||
style={{ paddingTop: screens.md === false ? 24 : 100 }}
|
||||
>
|
||||
<div
|
||||
className="responsive-padding max-content-width"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
|
||||
<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%'
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
|
||||
<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>
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
{emitterChain && emitterAddress && sequence ? (
|
||||
<ExplorerQuery emitterChain={emitterChain} emitterAddress={emitterAddress} sequence={sequence} />
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout >
|
||||
)
|
||||
};
|
||||
|
||||
export default injectIntl(Explorer)
|
|
@ -6,7 +6,7 @@ import { Link } from 'gatsby'
|
|||
|
||||
import { Layout } from '~/components/Layout';
|
||||
import { SEO } from '~/components/SEO';
|
||||
import { bodyStyles, headingStyles, titleStyles } from '~/styles'
|
||||
import { bodyStyles, buttonStylesLg, headingStyles, titleStyles } from '~/styles'
|
||||
|
||||
const { useBreakpoint } = Grid
|
||||
|
||||
|
@ -45,7 +45,7 @@ const OpenForBizSection = ({ intl, smScreen, howAnchor }: { intl: IntlShape, smS
|
|||
|
||||
{/* Placeholder: call to action from designs- to explorer or elsewhere */}
|
||||
{/* <Link to={`/${intl.locale}/explorer`}>
|
||||
<Button ghost style={{ width: 160, height: 36, border: "1.5px solid" }} size="large">
|
||||
<Button ghost style={buttonStylesLg} size="large">
|
||||
<FormattedMessage id="homepage.openForBiz.callToAction" />
|
||||
</Button>
|
||||
</Link> */}
|
||||
|
@ -103,7 +103,7 @@ const AboutUsSection = ({ intl, smScreen, howAnchor }: { intl: IntlShape, smScre
|
|||
<FormattedMessage id="homepage.aboutUs.body" />
|
||||
</Paragraph>
|
||||
<Link to={`/${intl.locale}/about`}>
|
||||
<Button style={{ width: 160, height: 36, border: "1.5px solid" }} size="large">
|
||||
<Button style={buttonStylesLg} size="large">
|
||||
<FormattedMessage id="homepage.aboutUs.callToAction" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
|
@ -3,21 +3,22 @@ import { Typography, Grid } from 'antd';
|
|||
const { Title, Paragraph } = Typography;
|
||||
const { useBreakpoint } = Grid
|
||||
import { injectIntl, WrappedComponentProps } from 'gatsby-plugin-intl';
|
||||
import { grpc } from '@improbable-eng/grpc-web';
|
||||
|
||||
import { Layout } from '~/components/Layout';
|
||||
import { SEO } from '~/components/SEO';
|
||||
import { GuardiansTable } from '~/components/GuardiansTable'
|
||||
|
||||
import { Heartbeat } from '~/proto/gossip/v1/gossip'
|
||||
import { PublicrpcGetRawHeartbeatsDesc, GetRawHeartbeatsRequest } from '~/proto/publicrpc/v1/publicrpc'
|
||||
import { GrpcWebImpl, PublicrpcClientImpl } from '~/proto/publicrpc/v1/publicrpc'
|
||||
|
||||
const rpc = new GrpcWebImpl(String(process.env.GATSBY_APP_RPC_URL), {});
|
||||
const publicRpc = new PublicrpcClientImpl(rpc)
|
||||
|
||||
const Network = ({ intl }: WrappedComponentProps) => {
|
||||
const [heartbeats, setHeartbeats] = useState<{ [nodeName: string]: Heartbeat }>({})
|
||||
const screens = useBreakpoint()
|
||||
|
||||
const addHeartbeat = (hb: grpc.ProtobufMessage) => {
|
||||
const hbObj = hb.toObject() as Heartbeat
|
||||
const addHeartbeat = (hbObj: Heartbeat) => {
|
||||
hbObj.networks.sort((a, b) => b.id - a.id)
|
||||
const { nodeName } = hbObj
|
||||
heartbeats[nodeName] = hbObj
|
||||
|
@ -25,15 +26,15 @@ const Network = ({ intl }: WrappedComponentProps) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
const client = grpc.client(PublicrpcGetRawHeartbeatsDesc, {
|
||||
host: String(process.env.GATSBY_APP_RPC_URL)
|
||||
})
|
||||
client.onMessage(addHeartbeat)
|
||||
client.start()
|
||||
client.send({ serializeBinary: () => GetRawHeartbeatsRequest.encode({}).finish(), toObject: () => { return {} } })
|
||||
|
||||
const interval = setInterval(() => {
|
||||
publicRpc.GetLastHeartbeats({}).then(res => {
|
||||
res.entries.map(entry => entry.rawHeartbeat ? addHeartbeat(entry.rawHeartbeat) : null)
|
||||
}, err => console.error('GetLastHearbeats err: ', err))
|
||||
}, 2000)
|
||||
|
||||
return function cleanup() {
|
||||
client.close()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -44,22 +45,27 @@ const Network = ({ intl }: WrappedComponentProps) => {
|
|||
description={intl.formatMessage({ id: 'network.description' })}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
padding: screens.md === false ? 'inherit' : '48px 0 0 100px'
|
||||
}} >
|
||||
<div style={{ padding: screens.md === false ? '100px 0 0 16px' : '' }} >
|
||||
<Title level={1} style={{ fontWeight: 'normal' }}>{intl.formatMessage({ id: 'network.title' })}</Title>
|
||||
<Paragraph style={{ fontSize: 24, fontWeight: 400, lineHeight: '36px' }} type="secondary">
|
||||
{Object.keys(heartbeats).length === 0 ? (
|
||||
intl.formatMessage({ id: 'network.listening' })
|
||||
) :
|
||||
<>
|
||||
{Object.keys(heartbeats).length}
|
||||
{intl.formatMessage({ id: 'network.guardiansFound' })}
|
||||
</>}
|
||||
</Paragraph>
|
||||
className="center-content"
|
||||
style={{ paddingTop: screens.md === false ? 24 : 100 }}
|
||||
>
|
||||
<div
|
||||
className="responsive-padding max-content-width"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<div style={{ padding: screens.md === false ? '100px 0 0 16px' : '' }} >
|
||||
<Title level={1} style={{ fontWeight: 'normal' }}>{intl.formatMessage({ id: 'network.title' })}</Title>
|
||||
<Paragraph style={{ fontSize: 24, fontWeight: 400, lineHeight: '36px' }} type="secondary">
|
||||
{Object.keys(heartbeats).length === 0 ? (
|
||||
intl.formatMessage({ id: 'network.listening' })
|
||||
) :
|
||||
<>
|
||||
{Object.keys(heartbeats).length}
|
||||
{intl.formatMessage({ id: 'network.guardiansFound' })}
|
||||
</>}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<GuardiansTable heartbeats={heartbeats} intl={intl} />
|
||||
</div>
|
||||
<GuardiansTable heartbeats={heartbeats} intl={intl} />
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -6,3 +6,5 @@ export const headingStyles = {
|
|||
}
|
||||
export const titleStyles = { fontWeight: 400 }
|
||||
export const bodyStyles = { fontSize: 26, fontWeight: 400, lineHeight: '36px' }
|
||||
|
||||
export const buttonStylesLg = { width: 160, height: 40, fontSize: 16, border: "1.5px solid" }
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
const addresses = {
|
||||
solana: {
|
||||
token: ['Solana Token Bridge', process.env.SOL_TOKEN_BRIDGE],
|
||||
bridge: ['Solana Core Bridge', process.env.SOL_CORE_BRIDGE],
|
||||
},
|
||||
ethereum: {
|
||||
token: ['Ethereum Token Bridge', process.env.ETH_TOKEN_BRIDGE],
|
||||
core: ['Ethereum Core Bridge', process.env.ETH_CORE_BRIDGE],
|
||||
},
|
||||
terra: {
|
||||
token: ['Terra Token Bridge', process.env.LUN_TOKEN_BRIDGE],
|
||||
core: ['Terra Core Bridge', process.env.LUN_CORE_BRIDGE],
|
||||
},
|
||||
bsc: {
|
||||
token: ['BSC Token Bridge', process.env.BSC_TOKEN_BRIDGE],
|
||||
core: ['BSC Core Bridge', process.env.BSC_CORE_BRIDGE],
|
||||
},
|
||||
}
|
||||
enum ChainID {
|
||||
Solana,
|
||||
Ethereum,
|
||||
Terra,
|
||||
'Binance Smart Chain'
|
||||
}
|
||||
|
||||
|
||||
export { addresses, ChainID }
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
|
||||
import { IntlShape } from 'gatsby-plugin-intl';
|
||||
import { OutboundLink } from "gatsby-plugin-google-gtag"
|
||||
import { ReactComponent as DiscordIcon } from '~/icons/Discord.svg';
|
||||
import { ReactComponent as GithubIcon } from '~/icons/Github.svg';
|
||||
import { ReactComponent as MediumIcon } from '~/icons/Medium.svg';
|
||||
|
@ -38,7 +39,7 @@ const externalLinkProps = { target: "_blank", rel: "noopener noreferrer", classN
|
|||
|
||||
|
||||
const socialAnchorArray = (intl: IntlShape, linkStyles: any = {}, iconStyle: any = {}) =>
|
||||
Object.entries(externalLinks).map(([url, Icon]) => <a
|
||||
Object.entries(externalLinks).map(([url, Icon]) => <OutboundLink
|
||||
href={url}
|
||||
key={url}
|
||||
{...externalLinkProps}
|
||||
|
@ -46,6 +47,6 @@ const socialAnchorArray = (intl: IntlShape, linkStyles: any = {}, iconStyle: any
|
|||
title={intl.formatMessage({ id: `nav.${linkToService[url]}AltText` })}
|
||||
>
|
||||
<Icon style={iconStyle} className="external-icon" />
|
||||
</a>)
|
||||
</OutboundLink>)
|
||||
|
||||
export { socialLinks, socialIcons, externalLinks, linkToService, socialAnchorArray }
|
||||
|
|
|
@ -3,11 +3,26 @@
|
|||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "es6",
|
||||
"types": ["node", "jest", "mocha"],
|
||||
"types": [
|
||||
"node",
|
||||
"jest",
|
||||
"mocha"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["./src/@types", "./node_modules/@types"],
|
||||
"lib": ["dom", "es2015", "es2017", "es2018", "esnext.intl", "es2017.intl", "es2018.intl"],
|
||||
"typeRoots": [
|
||||
"./src/@types",
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015",
|
||||
"es2017",
|
||||
"es2018",
|
||||
"esnext.intl",
|
||||
"es2017.intl",
|
||||
"es2018.intl"
|
||||
],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
|
@ -20,9 +35,18 @@
|
|||
"downlevelIteration": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~/*": ["src/*"],
|
||||
"~/*": [
|
||||
"src/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*", "./test-utils/**/*", "./__mocks__/**/*"],
|
||||
"exclude": ["node_modules", "plugins"]
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./test-utils/**/*",
|
||||
"./__mocks__/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"plugins"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue