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:
justinschuldt 2021-08-17 17:06:38 -05:00 committed by Justin Schuldt
parent 986b4b58f1
commit cf36c9bfe0
28 changed files with 2218 additions and 5710 deletions

View File

@ -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

2
explorer/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.env.development
.env.production

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)],
},
},
];

View File

@ -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();

23
explorer/generate-wasm.sh Executable file
View File

@ -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

View File

@ -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"
}
}

View File

@ -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',

View File

@ -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

View File

@ -0,0 +1,2 @@
export { default as ExplorerQuery } from './ExplorerQuery';

View File

@ -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" />:&nbsp;
<ReactTimeAgo date={new Date(props.lastFetched)} locale={intl.locale} timeStyle="round" />
</span>
) : null}
</div>
{/* <WasmTest base64VAA={props.message.SignedVAA} /> */}
</>
)
}
export default Summary

View File

@ -0,0 +1,2 @@
export { default as ExplorerSummary } from './ExplorerSummary';

View File

@ -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) => {

View File

@ -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" })}&nbsp;
<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>

View File

@ -0,0 +1 @@
export { default as WasmTest } from './wasm';

View File

@ -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

View File

@ -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"
}
}

View File

@ -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' }}>

View File

@ -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)

View File

@ -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>

View File

@ -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}&nbsp;
{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}&nbsp;
{intl.formatMessage({ id: 'network.guardiansFound' })}
</>}
</Paragraph>
</div>
<GuardiansTable heartbeats={heartbeats} intl={intl} />
</div>
<GuardiansTable heartbeats={heartbeats} intl={intl} />
</div>
</Layout>
)

View File

@ -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" }

View File

@ -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 }

View File

@ -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 }

View File

@ -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"
]
}