Explorer v2 (#789)

* initial commit

* cleanup, spacing fixes

* update copy

* responsive footer

* logo link

* router link active underline

* fix side-by-side padding

* app card links

* initial network table

* sort guardians

* network selector

* add envs to unbreak deployment

* 404

* wip explorer

* recent messages list

* fix activeNetwork context init

* add Oasis and Avalanche utils

* add title to RecentMessages

* add explorer ChainOverviewCard

* add explorer PastWeekCard

* save exact versions of npm packages

* add explorer search functionality

* mvp

* remove dupe page

* add basic social images

* add social sharing metadata

* update development siteUrl

* test with example prod url

* fix social card name

* update number of chains

* decode payload with WASM

* updated copy

* fix index portal link

* prod .env variables

* show more recent messages for chain or contract

* fix decodePayload summary

* delete explorer v1

* fix explorer dockerfile

* fix explorer serve settings for devent

* remove proto-gen-web for explorer

* rm proto-gen-web for explorer

Co-authored-by: Evan Gray <battledingo@gmail.com>
Co-authored-by: Evan Gray <56235822+evan-gray@users.noreply.github.com>
This commit is contained in:
Justin Schuldt 2022-02-01 09:40:53 -06:00 committed by GitHub
parent 613773d3fc
commit 497a1c6e83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
217 changed files with 21193 additions and 38648 deletions

View File

@ -45,4 +45,3 @@ COPY --from=go-build /app/node/pkg/proto pkg/proto
FROM scratch AS node-export
COPY --from=node-build /app/sdk/js/src/proto sdk/js/src/proto
COPY --from=node-build /app/spydk/js/src/proto spydk/js/src/proto
COPY --from=node-build /app/explorer/src/proto explorer/src/proto

View File

@ -386,7 +386,6 @@ if explorer:
k8s_resource(
"explorer",
resource_deps = ["proto-gen-web"],
port_forwards = [
port_forward(8001, name = "Explorer Web UI [:8001]", host = webHost),
],

View File

@ -1,14 +1,5 @@
version: v1beta1
plugins:
- name: tsproto
out: explorer/src/proto
path: tools/node_modules/.bin/protoc-gen-ts_proto
opt:
- paths=source_relative
- esModuleInterop=true
- env=browser
- forceLong=string
- outputClientImpl=grpc-web
- name: tsproto
out: sdk/js/src/proto
path: tools/node_modules/.bin/protoc-gen-ts_proto

View File

@ -1,29 +0,0 @@
{
"env": {
"production": {
"plugins": ["babel-plugin-jsx-remove-data-test-id"]
},
"test": {}
},
"plugins": [
[
"module-resolver",
{
"root": ["./src"],
"alias": {
"~": "./src"
}
}
]
],
"presets": [
[
"babel-preset-gatsby",
{
"targets": {
"browsers": [">0.25%", "not dead"]
}
}
]
]
}

View File

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

View File

@ -1,10 +0,0 @@
root = true
[*]
charset = utf-8
tab_width = 2
indent_size = 2
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

101
explorer/.env.development Normal file
View File

@ -0,0 +1,101 @@
GATSBY_SITE_URL=http://localhost:8000
GATSBY_GA_TAG=G-tag-goes-here
GATSBY_GUARDIAN_DEVNET_RPC_URL=http://localhost:7071
GATSBY_GUARDIAN_TESTNET_RPC_URL=https://wormhole-v2-testnet-api.certus.one
GATSBY_GUARDIAN_MAINNET_RPC_URL=https://wormhole-v2-mainnet-api.certus.one
GATSBY_BIGTABLE_FUNCTIONS_DEVNET_BASE_URL=http://localhost:8090
GATSBY_BIGTABLE_FUNCTIONS_TESTNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/testnet-
GATSBY_BIGTABLE_FUNCTIONS_MAINNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-
GATSBY_DEFAULT_NETWORK=mainnet
# contract addresses
## devnet addresses
GATSBY_DEVNET_SOLANA_CORE_BRIDGE=Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o
GATSBY_DEVNET_SOLANA_TOKEN_BRIDGE=B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE
GATSBY_DEVNET_SOLANA_NFT_BRIDGE=NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA
GATSBY_DEVNET_ETHEREUM_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_ETHEREUM_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_ETHEREUM_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_TERRA_CORE_BRIDGE=terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5
GATSBY_DEVNET_TERRA_TOKEN_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_DEVNET_TERRA_NFT_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_DEVNET_BSC_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_BSC_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_BSC_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_AVALANCHE_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_AVALANCHE_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_AVALANCHE_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_OASIS_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_OASIS_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_OASIS_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
## testnet addresses
GATSBY_TESTNET_SOLANA_CORE_BRIDGE=Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb
GATSBY_TESTNET_SOLANA_TOKEN_BRIDGE=A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg
GATSBY_TESTNET_SOLANA_NFT_BRIDGE=NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA
GATSBY_TESTNET_ETHEREUM_CORE_BRIDGE=0x44F3e7c20850B3B5f3031114726A9240911D912a
GATSBY_TESTNET_ETHEREUM_TOKEN_BRIDGE=0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6
GATSBY_TESTNET_ETHEREUM_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_TERRA_CORE_BRIDGE=terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5
GATSBY_TESTNET_TERRA_TOKEN_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_TESTNET_TERRA_NFT_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_TESTNET_BSC_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_TESTNET_BSC_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_TESTNET_BSC_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_TESTNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_TESTNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_AVALANCHE_CORE_BRIDGE=0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C
GATSBY_TESTNET_AVALANCHE_TOKEN_BRIDGE=0x61E44E506Ca5659E6c0bba9b678586fA2d729756
GATSBY_TESTNET_AVALANCHE_NFT_BRIDGE=0xD601BAf2EEE3C028344471684F6b27E789D9075D
GATSBY_TESTNET_OASIS_CORE_BRIDGE=0xc1C338397ffA53a2Eb12A7038b4eeb34791F8aCb
GATSBY_TESTNET_OASIS_TOKEN_BRIDGE=0x88d8004A9BdbfD9D28090A02010C19897a29605c
GATSBY_TESTNET_OASIS_NFT_BRIDGE=0xC5c25B41AB0b797571620F5204Afa116A44c0ebA
## mainnet addresses
GATSBY_MAINNET_SOLANA_CORE_BRIDGE=worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth
GATSBY_MAINNET_SOLANA_TOKEN_BRIDGE=wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb
GATSBY_MAINNET_SOLANA_NFT_BRIDGE=WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD
GATSBY_MAINNET_ETHEREUM_CORE_BRIDGE=0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B
GATSBY_MAINNET_ETHEREUM_TOKEN_BRIDGE=0x3ee18B2214AFF97000D974cf647E7C347E8fa585
GATSBY_MAINNET_ETHEREUM_NFT_BRIDGE=0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE
GATSBY_MAINNET_TERRA_CORE_BRIDGE=terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5
GATSBY_MAINNET_TERRA_TOKEN_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_MAINNET_TERRA_NFT_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_MAINNET_BSC_CORE_BRIDGE=0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B
GATSBY_MAINNET_BSC_TOKEN_BRIDGE=0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7
GATSBY_MAINNET_BSC_NFT_BRIDGE=0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE
GATSBY_MAINNET_POLYGON_CORE_BRIDGE=0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7
GATSBY_MAINNET_POLYGON_TOKEN_BRIDGE=0x5a58505a96d1dbf8df91cb21b54419fc36e93fde
GATSBY_MAINNET_POLYGON_NFT_BRIDGE=0x90bbd86a6fe93d3bc3ed6335935447e75fab7fcf
GATSBY_MAINNET_AVALANCHE_CORE_BRIDGE=0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c
GATSBY_MAINNET_AVALANCHE_TOKEN_BRIDGE=0x0e082F06FF657D94310cB8cE8B0D9a04541d8052
GATSBY_MAINNET_AVALANCHE_NFT_BRIDGE=0xf7B6737Ca9c4e08aE573F75A97B73D7a813f5De5
GATSBY_MAINNET_OASIS_CORE_BRIDGE=0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585
GATSBY_MAINNET_OASIS_TOKEN_BRIDGE=0x5848c791e09901b40a9ef749f2a6735b418d7564
GATSBY_MAINNET_OASIS_NFT_BRIDGE=0x04952d522ff217f40b5ef3cbf659eca7b952a6c1

101
explorer/.env.production Normal file
View File

@ -0,0 +1,101 @@
GATSBY_SITE_URL=https://wormholenetwork.com
GATSBY_GA_TAG=G-TF5EQYS7RL
GATSBY_GUARDIAN_DEVNET_RPC_URL=http://localhost:7071
GATSBY_GUARDIAN_TESTNET_RPC_URL=https://wormhole-v2-testnet-api.certus.one
GATSBY_GUARDIAN_MAINNET_RPC_URL=https://wormhole-v2-mainnet-api.certus.one
GATSBY_BIGTABLE_FUNCTIONS_DEVNET_BASE_URL=http://localhost:8090
GATSBY_BIGTABLE_FUNCTIONS_TESTNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/testnet-
GATSBY_BIGTABLE_FUNCTIONS_MAINNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-
GATSBY_DEFAULT_NETWORK=mainnet
# contract addresses
## devnet addresses
GATSBY_DEVNET_SOLANA_CORE_BRIDGE=Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o
GATSBY_DEVNET_SOLANA_TOKEN_BRIDGE=B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE
GATSBY_DEVNET_SOLANA_NFT_BRIDGE=NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA
GATSBY_DEVNET_ETHEREUM_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_ETHEREUM_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_ETHEREUM_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_TERRA_CORE_BRIDGE=terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5
GATSBY_DEVNET_TERRA_TOKEN_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_DEVNET_TERRA_NFT_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_DEVNET_BSC_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_BSC_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_BSC_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_AVALANCHE_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_AVALANCHE_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_AVALANCHE_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_OASIS_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_OASIS_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_OASIS_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
## testnet addresses
GATSBY_TESTNET_SOLANA_CORE_BRIDGE=Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb
GATSBY_TESTNET_SOLANA_TOKEN_BRIDGE=A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg
GATSBY_TESTNET_SOLANA_NFT_BRIDGE=NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA
GATSBY_TESTNET_ETHEREUM_CORE_BRIDGE=0x44F3e7c20850B3B5f3031114726A9240911D912a
GATSBY_TESTNET_ETHEREUM_TOKEN_BRIDGE=0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6
GATSBY_TESTNET_ETHEREUM_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_TERRA_CORE_BRIDGE=terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5
GATSBY_TESTNET_TERRA_TOKEN_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_TESTNET_TERRA_NFT_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_TESTNET_BSC_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_TESTNET_BSC_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_TESTNET_BSC_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_TESTNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_TESTNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_AVALANCHE_CORE_BRIDGE=0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C
GATSBY_TESTNET_AVALANCHE_TOKEN_BRIDGE=0x61E44E506Ca5659E6c0bba9b678586fA2d729756
GATSBY_TESTNET_AVALANCHE_NFT_BRIDGE=0xD601BAf2EEE3C028344471684F6b27E789D9075D
GATSBY_TESTNET_OASIS_CORE_BRIDGE=0xc1C338397ffA53a2Eb12A7038b4eeb34791F8aCb
GATSBY_TESTNET_OASIS_TOKEN_BRIDGE=0x88d8004A9BdbfD9D28090A02010C19897a29605c
GATSBY_TESTNET_OASIS_NFT_BRIDGE=0xC5c25B41AB0b797571620F5204Afa116A44c0ebA
## mainnet addresses
GATSBY_MAINNET_SOLANA_CORE_BRIDGE=worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth
GATSBY_MAINNET_SOLANA_TOKEN_BRIDGE=wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb
GATSBY_MAINNET_SOLANA_NFT_BRIDGE=WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD
GATSBY_MAINNET_ETHEREUM_CORE_BRIDGE=0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B
GATSBY_MAINNET_ETHEREUM_TOKEN_BRIDGE=0x3ee18B2214AFF97000D974cf647E7C347E8fa585
GATSBY_MAINNET_ETHEREUM_NFT_BRIDGE=0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE
GATSBY_MAINNET_TERRA_CORE_BRIDGE=terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5
GATSBY_MAINNET_TERRA_TOKEN_BRIDGE=terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf
GATSBY_MAINNET_TERRA_NFT_BRIDGE=terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4
GATSBY_MAINNET_BSC_CORE_BRIDGE=0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B
GATSBY_MAINNET_BSC_TOKEN_BRIDGE=0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7
GATSBY_MAINNET_BSC_NFT_BRIDGE=0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE
GATSBY_MAINNET_POLYGON_CORE_BRIDGE=0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7
GATSBY_MAINNET_POLYGON_TOKEN_BRIDGE=0x5a58505a96d1dbf8df91cb21b54419fc36e93fde
GATSBY_MAINNET_POLYGON_NFT_BRIDGE=0x90bbd86a6fe93d3bc3ed6335935447e75fab7fcf
GATSBY_MAINNET_AVALANCHE_CORE_BRIDGE=0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c
GATSBY_MAINNET_AVALANCHE_TOKEN_BRIDGE=0x0e082F06FF657D94310cB8cE8B0D9a04541d8052
GATSBY_MAINNET_AVALANCHE_NFT_BRIDGE=0xf7B6737Ca9c4e08aE573F75A97B73D7a813f5De5
GATSBY_MAINNET_OASIS_CORE_BRIDGE=0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585
GATSBY_MAINNET_OASIS_TOKEN_BRIDGE=0x5848c791e09901b40a9ef749f2a6735b418d7564
GATSBY_MAINNET_OASIS_NFT_BRIDGE=0x04952d522ff217f40b5ef3cbf659eca7b952a6c1

View File

@ -1,28 +1,15 @@
# General
GATSBY_TELEMETRY_DISABLED=1
GATSBY_SITE_URL=http://localhost:8001
GATSBY_SITE_URL=http://localhost:8000
GATSBY_GA_TAG=G-tag-goes-here
GATSBY_ENVIRONMENT=development
GATSBY_APP_RPC_URL=http://localhost:7071
GATSBY_GUARDIAN_DEVNET_RPC_URL=http://localhost:7071
GATSBY_GUARDIAN_TESTNET_RPC_URL=https://wormhole-v2-testnet-api.certus.one
GATSBY_GUARDIAN_MAINNET_RPC_URL=https://wormhole-v2-mainnet-api.certus.one
GATSBY_BIGTABLE_FUNCTIONS_DEVNET_BASE_URL=http://localhost:8090
GATSBY_BIGTABLE_FUNCTIONS_TESTNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/testnet
GATSBY_BIGTABLE_FUNCTIONS_MAINNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet
GATSBY_DEFAULT_NETWORK=devnet
# Profiling
ENABLE_BUNDLE_ANALYZER=0
# Feature flags
ENABLE_NETWORK_PAGE=true
ENABLE_EXPLORER_PAGE=true
GATSBY_BIGTABLE_FUNCTIONS_TESTNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/testnet-
GATSBY_BIGTABLE_FUNCTIONS_MAINNET_BASE_URL=https://europe-west3-wormhole-315720.cloudfunctions.net/mainnet-
GATSBY_DEFAULT_NETWORK=mainnet
# contract addresses
@ -47,6 +34,13 @@ GATSBY_DEVNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_AVALANCHE_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_AVALANCHE_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_AVALANCHE_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_DEVNET_OASIS_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_DEVNET_OASIS_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_DEVNET_OASIS_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
## testnet addresses
GATSBY_TESTNET_SOLANA_CORE_BRIDGE=Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb
@ -69,6 +63,13 @@ GATSBY_TESTNET_POLYGON_CORE_BRIDGE=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
GATSBY_TESTNET_POLYGON_TOKEN_BRIDGE=0x0290FB167208Af455bB137780163b7B7a9a10C16
GATSBY_TESTNET_POLYGON_NFT_BRIDGE=0x26b4afb60d6c903165150c6f0aa14f8016be4aec
GATSBY_TESTNET_AVALANCHE_CORE_BRIDGE=0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C
GATSBY_TESTNET_AVALANCHE_TOKEN_BRIDGE=0x61E44E506Ca5659E6c0bba9b678586fA2d729756
GATSBY_TESTNET_AVALANCHE_NFT_BRIDGE=0xD601BAf2EEE3C028344471684F6b27E789D9075D
GATSBY_TESTNET_OASIS_CORE_BRIDGE=0xc1C338397ffA53a2Eb12A7038b4eeb34791F8aCb
GATSBY_TESTNET_OASIS_TOKEN_BRIDGE=0x88d8004A9BdbfD9D28090A02010C19897a29605c
GATSBY_TESTNET_OASIS_NFT_BRIDGE=0xC5c25B41AB0b797571620F5204Afa116A44c0ebA
## mainnet addresses
GATSBY_MAINNET_SOLANA_CORE_BRIDGE=worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth
@ -90,3 +91,10 @@ GATSBY_MAINNET_BSC_NFT_BRIDGE=0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE
GATSBY_MAINNET_POLYGON_CORE_BRIDGE=0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7
GATSBY_MAINNET_POLYGON_TOKEN_BRIDGE=0x5a58505a96d1dbf8df91cb21b54419fc36e93fde
GATSBY_MAINNET_POLYGON_NFT_BRIDGE=0x90bbd86a6fe93d3bc3ed6335935447e75fab7fcf
GATSBY_MAINNET_AVALANCHE_CORE_BRIDGE=0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c
GATSBY_MAINNET_AVALANCHE_TOKEN_BRIDGE=0x0e082F06FF657D94310cB8cE8B0D9a04541d8052
GATSBY_MAINNET_AVALANCHE_NFT_BRIDGE=0xf7B6737Ca9c4e08aE573F75A97B73D7a813f5De5
GATSBY_MAINNET_OASIS_CORE_BRIDGE=0xfE8cD454b4A1CA468B57D79c0cc77Ef5B6f64585
GATSBY_MAINNET_OASIS_TOKEN_BRIDGE=0x5848c791e09901b40a9ef749f2a6735b418d7564
GATSBY_MAINNET_OASIS_NFT_BRIDGE=0x04952d522ff217f40b5ef3cbf659eca7b952a6c1

View File

@ -1,3 +0,0 @@
.cache
public
proto

View File

@ -1,191 +0,0 @@
const {
rules: baseImportsRules,
} = require('eslint-config-airbnb-base/rules/imports');
module.exports = {
globals: {
// Gatsby Config
__PATH_PREFIX__: true,
},
env: {
// Allow `window` global
browser: true,
},
// Global ESLint Settings
// =================================
settings: {
'import/resolver': {
node: {
paths: ['./', 'src'],
extensions: ['.js', '.jsx', '.ts', '.tsx', 'json'],
},
// Resolve Aliases
// =================================
alias: {
map: [
['~', './src'],
['@theme/styled', './src/styled'],
],
extensions: ['.js', '.jsx', '.ts', '.tsx', 'json', '.d.ts'],
},
},
},
// ===========================================
// Set up ESLint for .js / .jsx files
// ===========================================
// .js / .jsx uses babel-eslint
parser: 'babel-eslint',
// Plugins
// =================================
plugins: ['no-only-tests'],
// Extend Other Configs
// =================================
extends: [
'eslint:recommended',
'airbnb',
// Disable rules that conflict with Prettier
// !!! Prettier must be last to override other configs
'prettier/react',
'plugin:prettier/recommended',
],
rules: {
// This project uses TS. Disable prop-types check
'react/prop-types': 0,
// Allow snake_case due to inconsistent APIs
camelcase: 0,
// Prevents exclusion of tests from passing lint check
'no-only-tests/no-only-tests': 'error',
// dont enforce semicolon usage either way
semi: 0
},
// https://eslint.org/docs/user-guide/configuring#report-unused-eslint-disable-comments
reportUnusedDisableDirectives: true,
// =================================
// Overrides for Specific Files
// =================================
overrides: [
// =================================
// TypeScript Files
// =================================
{
files: ['**/*.{ts,tsx}'],
// allow ESLint to understand TypeScript syntax
// https://github.com/iamturns/eslint-config-airbnb-typescript/blob/master/lib/shared.js#L10
parserOptions: {
// Lint with Type Information
// https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
extends: [
// ESLint's inbuilt 'recommended' config
'eslint:recommended',
// Disables rules from the 'eslint:recommended' that are already covered by TypeScript's typechecker
'plugin:@typescript-eslint/eslint-recommended',
// Turns on rules from @typescript-eslint/eslint-plugin
'plugin:@typescript-eslint/recommended',
// Lint with Type Information
// https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'airbnb-typescript',
// Disable rules that conflict with Prettier
// !!! Prettier must be last to override other configs
'prettier/react',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
rules: {
// This project uses TS. Disable prop-types check
'react/prop-types': 'off',
// Allow snake_case due to inconsistent APIs
'@typescript-eslint/camelcase': 0,
// Makes no sense to allow type inferrence for expression parameters, but require typing the response
'@typescript-eslint/explicit-function-return-type': 0,
// Reduce props spreading rule to a warning, not an error
'react/jsx-props-no-spreading': 1,
'no-restricted-imports': [
'warn',
{
paths: [
],
},
],
},
},
// =================================
// index.ts Files (Re-exporting a directory's files)
// =================================
{
files: ['**/index.{js,ts,tsx}'],
rules: {
// Allow named exports in a directory's index files
'import/prefer-default-export': 0,
},
},
// =================================
// Gatsby Files
// =================================
{
files: ['**/**/gatsby-*.js'],
rules: {
'no-console': 0,
// Allow import devDependencies in Gatsby files.
'import/no-extraneous-dependencies': [
2,
{
devDependencies: true,
// Tells ESLint where the path to the folder containing package.json is for nested files like /plugin/**/gatsby-*.js
packageDir: './',
},
],
'react/no-danger': 0,
'react/jsx-props-no-spreading': 0,
// Allow 'jsx' in .js files
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'import/prefer-default-export': 0,
// Append 'ts' and 'tsx' when importing files from a folder/index.ts
'import/extensions': [
baseImportsRules['import/extensions'][0],
baseImportsRules['import/extensions'][1],
{
...baseImportsRules['import/extensions'][2],
ts: 'never',
tsx: 'never',
},
],
},
},
// =================================
// Test Files
// =================================
{
files: ['**/test-utils/*.{js,ts,tsx}', '**/**/*.test.{js,ts,tsx}'],
// Allow `jest` global
extends: ['plugin:jest/recommended'],
rules: {
// Allow import devDependencies in tests
'import/no-extraneous-dependencies': 0,
'react/jsx-props-no-spreading': 0,
'jsx-a11y/alt-text': 0,
},
},
// =================================
// Storybook Files
// =================================
{
files: ['**/*.stories.{js,ts,tsx}'],
rules: {
// Allow import devDependencies in stories
'import/no-extraneous-dependencies': 0,
'react/jsx-props-no-spreading': 0,
'jsx-a11y/alt-text': 0,
},
},
],
};

75
explorer/.gitignore vendored
View File

@ -1,76 +1,3 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env*
!.env.sample
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
# Storybook
.out
# protos
proto
public

View File

@ -1,3 +0,0 @@
.cache
public
proto

View File

@ -1,6 +0,0 @@
{
"$schema": "http://json.schemastore.org/prettierrc",
"singleQuote": true,
"trailingComma": "es5",
"semi": false
}

View File

@ -1,19 +0,0 @@
import React from 'react';
import { IntlContextProvider } from 'gatsby-plugin-intl/intl-context';
import { locales, messages } from '../preview';
const intlConfig = {
language: 'en',
languages: locales,
messages: messages,
originalPath: '/',
redirect: true,
routed: true,
};
const GatsbyIntlProvider = storyFn => (
<IntlContextProvider value={intlConfig}>{storyFn()}</IntlContextProvider>
);
export default GatsbyIntlProvider;

View File

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

View File

@ -1,16 +0,0 @@
module.exports = {
stories: ['../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-actions/register',
'@storybook/addon-viewport/register',
'storybook-addon-intl/register',
{
name: '@storybook/preset-typescript',
options: {
tsLoaderOptions: {
transpileOnly: true,
},
},
},
],
};

View File

@ -1,7 +0,0 @@
/**
* `manager.js` replaces `addons.js` and allows you to customize how Storybooks app UI renders.
* That is, everything outside of the Canvas (preview iframe).
* In common cases, you probably wont need this file except when youre theming Storybook.
*
* https://medium.com/storybookjs/declarative-storybook-configuration-49912f77b78
*/

View File

@ -1,5 +0,0 @@
<link
href="https://fonts.googleapis.com/css?family=Lora:400,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="https://use.typekit.net/amn5iwe.css" />

View File

@ -1,70 +0,0 @@
import { addDecorator, addParameters } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import { setIntlConfig, withIntl } from 'storybook-addon-intl';
import { GatsbyIntlProvider } from './decorators';
import supportedLanguages from '../src/utils/i18n/supportedLanguages'
// Gatsby Setup
// ============================================
// Gatsby's Link overrides:
// Gatsby defines a global called ___loader to prevent its method calls from creating console errors you override it here
global.___loader = {
enqueue: () => {},
hovering: () => {},
};
// Gatsby internal mocking to prevent unnecessary errors in storybook testing environment
global.__PATH_PREFIX__ = '';
// This is to utilized to override the window.___navigate method Gatsby defines and uses to report what path a Link would be taking us to if it wasn't inside a storybook
window.___navigate = pathname => {
action('NavigateTo:')(pathname);
};
// Storybook Addons
// ============================================
// TODO: Add our breakpoints to the list of viewport options
addParameters({
viewport: {
viewports: INITIAL_VIEWPORTS,
defaultViewport: 'responsive',
},
options: {
panelPosition: 'right',
},
});
// Storybook Decorators
// ============================================
// gatsby-plugin-intl Provider ================
// Set supported locales
export const locales = supportedLanguages.map(language => language.languageTag);
// TODO: import these with fs as esModules, rather than require('...json'), so that
// nested keys work (objects with key/values, rather than just "homepage.title" keys).
// Import translation messages
export const messages = locales.reduce((acc, locale) => {
return {
...acc,
[locale]: require(`../src/locales/${locale}.json`),
};
}, {});
const getMessages = locale => messages[locale];
// Set `storybook-addon-intl` configuration (handles `react-intl`)
setIntlConfig({
locales,
defaultLocale: 'en',
getMessages,
});
// Register decorators
// Adds gatsby-plugin-intl IntlContextProvider which wraps the Gatsby Link component
addDecorator(GatsbyIntlProvider);
// Adds react-intl
addDecorator(withIntl);

View File

@ -1,111 +0,0 @@
import path from 'path'
import dotenv from 'dotenv'
dotenv.config({
path: `.env.${process.env.NODE_ENV}`,
});
import antdThemeOverrides from '../src/AntdTheme'
import { getThemeVariables } from 'antd/dist/theme'
export default ({ config }) => {
// Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
// ========================================================
config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/];
// Add Babel rules
// ========================================================
// use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
config.module.rules[0].use[0].loader = require.resolve('babel-loader');
// use @babel/preset-react for JSX and env (instead of staged presets)
config.module.rules[0].use[0].options.presets = [
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-env'),
// Emotion preset must run BEFORE reacts preset to properly convert css-prop.
// Babel preset-ordering runs reversed (from last to first). Emotion has to be after React preset.
];
config.module.rules[0].use[0].options.plugins = [
// use @babel/plugin-proposal-class-properties for class arrow functions
require.resolve('@babel/plugin-proposal-class-properties'),
// use babel-plugin-remove-graphql-queries to remove static queries from components when rendering in storybook
require.resolve('babel-plugin-remove-graphql-queries'),
];
// Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
// ========================================================
config.resolve.mainFields = ['browser', 'module', 'main'];
// Add Webpack rules for TypeScript
// ========================================================
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('babel-loader'),
options: {
presets: [
['react-app', { flow: false, typescript: true }],
// Emotion preset must run BEFORE reacts preset to properly convert css-prop.
// Babel preset-ordering runs reversed (from last to first). Emotion has to be after React preset.
],
plugins: [
require.resolve('@babel/plugin-proposal-class-properties'),
// use babel-plugin-remove-graphql-queries to remove static queries from components when rendering in storybook
require.resolve('babel-plugin-remove-graphql-queries'),
['import', {libraryName: "antd", libraryDirectory: 'es', style: true}]
],
},
});
config.module.rules.push({
test: /\.less$/,
loaders: [
"style-loader",
"css-loader",
{
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true,
modifyVars: {
...getThemeVariables({
dark: true, // Enable dark mode
compact: true, // Enable compact mode,
}),
...antdThemeOverrides,
}
}
}
}
],
include: path.resolve(__dirname, "../")
})
config.resolve.extensions.push('.ts', '.tsx');
// Add SVGR Loader
// ========================================================
// Remove svg rules from existing webpack rule
const assetRule = config.module.rules.find(({ test }) => test.test('.svg'));
const assetLoader = {
loader: assetRule.loader,
options: assetRule.options || assetRule.query,
};
config.module.rules.unshift({
test: /\.svg$/,
use: ['@svgr/webpack', assetLoader],
});
// Mirror project aliases for some reason (should be picked up by .babelrc)
// ========================================================
config.resolve.alias['~/utils'] = path.resolve(__dirname, '../src/utils');
config.resolve.alias['~/components'] = path.resolve(
__dirname,
'../src/components'
);
config.resolve.alias['~/images'] = path.resolve(__dirname, '../src/images');
config.resolve.alias['~/icons'] = path.resolve(__dirname, '../src/icons');
return config;
};

View File

@ -1,35 +0,0 @@
# multipass: true
# full: true
plugins:
- cleanupIDs: true
minify: true
- cleanupListOfValues: true
- convertColors: true
- convertStyleToAttrs: true
- convertTransform: true
- cleanupNumericValues: true
floatPrecision: 3
- mergePaths: true
- minifyStyles: true
- moveElemesAttrsToGroup: true
- removeAttrs: true
attrs: 'fill-rule'
- removeComments: true
- removeDesc: true
removeAny: true
- removeDimensions: true
- removeViewBox: false
- removeDoctype: true
- removeEditorsNSData: true
- removeEmptyAttrs: true
- removeEmptyContainers: true
- removeEmptyText: true
- removeNonInheritableGroupAttrs: true
- removeTitle: false
- removeUnknownsAndDefaults: true
- removeUnusedNS: true
- removeUselessDefs: true
- removeUselessStrokeAndFill: true
- removeXMLProcInst: true
- sortAttrs: true

View File

@ -1,97 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// launches Chrome and allows debugging from vscode.
// you can set breakpoints in .tsx files, etc.
"type": "pwa-chrome",
"request": "launch",
"name": "Debug in Chrome",
"url": "http://localhost:8000",
"webRoot": "${workspaceFolder}"
},
{
// for debugging the gatsby dev server
"name": "Gatsby develop",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"preLaunchTask": {
"type": "npm",
"script": "clean"
},
"args": [
"develop"
],
"stopOnEntry": false,
"runtimeArgs": [
"--nolazy",
"--max-http-header-size=16385"
],
"sourceMaps": false,
"env": {
"NODE_OPTIONS": "-r esm"
}
},
{
// for debugging gatsby via it's 'debug' command
"name": "Gatsby debug",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"preLaunchTask": {
"type": "npm",
"script": "clean"
},
"args": [
"debug"
],
"stopOnEntry": false,
"runtimeArgs": [
"--nolazy",
"--inspect-brk"
],
"sourceMaps": false,
"env": {
"NODE_OPTIONS": "-r esm"
}
},
{
// for debugging the gatsby build process
"name": "Gatsby build",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": [
"build"
],
"stopOnEntry": false,
"sourceMaps": false,
"env": {
"NODE_OPTIONS": "-r esm",
"NODE_ENV": "production"
}
},
{
"type": "node",
"request": "launch",
"name": "translate.ts",
"runtimeArgs": [
"-r",
"ts-node/register"
],
"args": [
"${workspaceFolder}/translate.ts"
],
"env": {
"NODE_OPTIONS": "-r esm"
}
}
]
}

3
explorer/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"cSpell.words": ["bridgesplit", "buidl", "pyth", "roadmap", "Solana", "tiexo"]
}

View File

@ -1,12 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "clean",
"problemMatcher": [],
"label": "npm: clean",
"detail": "rm -rf public && rm -rf .cache"
}
]
}

View File

@ -13,9 +13,6 @@ RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
ADD . .
# create .env files from .env.sample, if they do not already exist.
RUN [[ ! -f .env.development ]] && cp .env.sample .env.development
RUN [[ ! -f .env.production ]] && cp .env.sample .env.production
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm run build

View File

@ -1,101 +1,54 @@
# wormhole explorer
<p align="center">
<a href="https://www.gatsbyjs.com/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter">
<img alt="Gatsby" src="https://www.gatsbyjs.com/Gatsby-Monogram.svg" width="60" />
</a>
</p>
<h1 align="center">
Gatsby minimal starter
</h1>
A web app built with:
- [GatsbyJS](https://www.gatsbyjs.com/)
- [gatsby-plugin-intl](https://www.gatsbyjs.com/plugins/gatsby-plugin-intl/)
- [Typescript](https://www.typescriptlang.org/)
- [Ant Design](https://ant.design/)
## 🚀 Quick start
1. **Create a Gatsby site.**
## Notable files
Use the Gatsby CLI to create a new site, specifying the minimal starter.
- Supported Languages - add/remove supported languages here [./src/utils/i18n/supportedLanguages.js](../src/utils/i18n/supportedLanguages.js)
- Multilangual copy [./src/locales](./src/locales)
- Top level pages & client side routes. Adding a file here creates a @reach-router route [./src/pages](./src/pages)
- SEO config, inherited by all pages [./src/components/SEO/SEO.tsx](./src/components/SEO/SEO.tsx)
- Main layout HOC, contains top-menu nav and footer [./src/components/Layout/DefaultLayout.tsx](./src/components/Layout/DefaultLayout.tsx)
- Gatsby plugins [./gatsby-config.js](./gatsby-config.js)
- Ant Design theme variables, overrides Antd defaults [./src/AntdTheme.js](./src/AntdTheme.js)
```shell
# create a new Gatsby site using the minimal starter
npm init gatsby
```
2. **Start developing.**
## Repo setup
Navigate into your new sites directory and start it up.
Installing dependencies with npm:
```shell
cd my-gatsby-site/
npm run develop
```
npm install
3. **Open the code and start customizing!**
Create a `.env` file for your development environment, from the `.env.sample`:
Your site is now running at http://localhost:8000!
cp .env.sample .env.development
Edit `src/pages/index.js` to see your site update in real-time!
## Developing
4. **Learn more**
Start the development server with the npm script:
- [Documentation](https://www.gatsbyjs.com/docs/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
npm run dev
- [Tutorials](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
Then open the web app in your browser at [http://localhost:8000](http://localhost:8000)
- [Guides](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
## Debugging
### NodeJs debugging with VSCode
- [API Reference](https://www.gatsbyjs.com/docs/api-reference/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
You can debug the Gatsby dev server or build process using VSCode's debbuger. Checkout [.vscode/launch.json](./.vscode/launch.json) to see the NodeJS debugging options.
- [Plugin Library](https://www.gatsbyjs.com/plugins?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
These debugger configs will let you set breakpoints in the Gatsby node programs ([./gatsby-config.js](./gatsby-config.js), [./gatsby-node.js](./gatsby-node.js)) to debug webpack, Gatsby plugins, etc.
- [Cheat Sheet](https://www.gatsbyjs.com/docs/cheat-sheet/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
### Browser debugging with VSCode
## 🚀 Quick start (Gatsby Cloud)
With the [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) extension installed, you can inspect the web app and set broswer breakpoints from VSCode. With the dev server (`npm run dev`) running, select & run [Debug in Chrome](./.vscode/launch.json#L12) from the debugger pane.
Deploy this starter with one click on [Gatsby Cloud](https://www.gatsbyjs.com/cloud/):
## Storybook component rendering
[Storybook](https://storybook.js.org/) can render components with sytles and locales, for UI component development.
Run Storybook with:
npm run storybook
See [./src/components/Button/button.stories.tsx](./src/components/Button/button.stories.tsx)
## eslint linting & formatting
Check linting:
npm run lint
Fix linting errors:
npm run format
## Ant Design Theming
Ant Design [default less variables](https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less) can be overridden in [./src/AntdTheme.js](./src/AntdTheme.js), which is used in [./gatsby-config.js#L51](./gatsby-config.js#L51).
## Programmatic Translations
Translations can be made for the supported languages ([./src/utils/i18n/supportedLanguages.js](../src/utils/i18n/supportedLanguages.js)). The English language definition file ([./src/locales/en.json](./src/locales/en.json)) will be read and used as the source, using either DeepL or Google Translate to supply the translations.
### Translating with DeepL
Pass your DeepL Pro api key to the npm script:
npm run translate:deepl -- your-DeepL-Pro-api-key-here
### Translating with Google Translate
With your Service Account [credentials](https://github.com/leolabs/json-autotranslate#google-translate) saved to a file locally, pass the path to the .json file to the npm script:
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
[<img src="https://www.gatsbyjs.com/deploynow.svg" alt="Deploy to Gatsby Cloud">](https://www.gatsbyjs.com/dashboard/deploynow?url=https://github.com/gatsbyjs/gatsby-starter-minimal)

View File

@ -1,3 +0,0 @@
// Related to jest.config.js `moduleNameMapper` on how to handle imports.
// Use this stub to mock static file imports which Jest cant handle
module.exports = 'test-file-stub';

View File

@ -1,27 +0,0 @@
import React from 'react';
const gatsbyPluginIntl = jest.requireActual('gatsby-plugin-intl');
module.exports = {
...gatsbyPluginIntl,
Link: jest
.fn()
.mockImplementation(
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
language,
...rest
}) =>
React.createElement('a', {
...rest,
href: to,
})
),
};

View File

@ -1,29 +0,0 @@
import React from 'react';
const gatsby = jest.requireActual('gatsby');
// Mocks graphql() function, Link component, and StaticQuery component
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement('a', {
...rest,
href: to,
})
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
};

View File

@ -1 +0,0 @@
module.exports = { ReactComponent: 'icon-mock' };

View File

@ -1,6 +1 @@
import React from 'react';
import { App } from './src/components/App';
// Duplicated in gatsby-ssr.js for server side rendering during the build
export const wrapRootElement = props => <App {...props} />;
import "@fontsource/poppins"; // Defaults to weight 400 with all styles included.

View File

@ -1,101 +1,38 @@
import dotenv from 'dotenv';
import { getThemeVariables } from 'antd/dist/theme';
import supportedLanguages from './src/utils/i18n/supportedLanguages';
import antdThemeOverrides from './src/AntdTheme';
dotenv.config({
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
});
const languages = supportedLanguages.map(language => language.languageTag);
const plugins = [
'gatsby-plugin-react-helmet',
'gatsby-plugin-typescript',
'gatsby-plugin-remove-serviceworker',
'gatsby-plugin-svgr',
{
resolve: 'gatsby-plugin-intl',
options: {
path: `${__dirname}/src/locales`,
languages,
defaultLanguage: 'en',
redirect: true,
},
},
{
resolve: 'gatsby-plugin-antd',
options: {
style: true,
},
},
{
resolve: `gatsby-plugin-less`,
options: {
lessOptions: {
javascriptEnabled: true,
modifyVars: {
...getThemeVariables({
dark: true, // Enable dark mode
compact: true, // Enable compact mode,
}),
...antdThemeOverrides,
},
},
},
},
{
resolve: 'gatsby-plugin-robots-txt',
options: {
host: process.env.GATSBY_SITE_URL,
sitemap: `${process.env.GATSBY_SITE_URL}/sitemap.xml`,
env: {
development: {
policy: [{ userAgent: '*', disallow: ['/'] }]
},
production: {
policy: [{ userAgent: '*', allow: '/' }]
}
}
}
},
{
resolve: "gatsby-plugin-sitemap",
options: {
serialize: ({ site, allSitePage }) => {
// filter out pages that do not include a locale, along with locale specific 404 pages.
const edges = allSitePage.edges.filter(page => languages.some(lang => page.node.path.includes(lang)) && !page.node.path.includes('404'))
// return sitemap entries
return edges.map(page => {
return {
url: `${site.siteMetadata.siteUrl}${page.node.path}`,
// changefreq: `daily`,
// priority: 0.7,
// lastmod: modifiedGmt,
}
})
},
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)],
},
},
];
// Bundle analyzer, dev only
if (process.env.ENABLE_BUNDLE_ANALYZER === '1') {
plugins.push('gatsby-plugin-webpack-bundle-analyser-v2');
}
const siteMetadata = {
siteUrl: process.env.GATSBY_SITE_URL,
}
export { plugins, siteMetadata };
title: "Wormhole",
};
module.exports = {
siteMetadata,
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-top-layout`,
`gatsby-plugin-material-ui`,
{
resolve: "gatsby-plugin-robots-txt",
options: {
host: siteMetadata.siteUrl,
sitemap: `${siteMetadata.siteUrl}/sitemap/sitemap-index.xml`,
env: {
development: {
policy: [{ userAgent: "*", disallow: ["/"] }],
},
production: {
policy: [{ userAgent: "*", allow: "/" }],
},
},
},
},
`gatsby-plugin-sitemap`,
{
resolve: `gatsby-plugin-google-gtag`,
options: {
trackingIds: [String(process.env.GATSBY_GA_TAG)],
},
},
`gatsby-plugin-meta-redirect`, // make sure to put last in the array
],
};

View File

@ -1,79 +1,58 @@
import path from 'path';
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.com/docs/node-apis/
*/
import dotenv from 'dotenv';
const webpack = require("webpack");
dotenv.config({
path: `.env.${process.env.NODE_ENV}`,
});
export const onCreateWebpackConfig = function addPathMapping({
exports.onCreateWebpackConfig = function addPathMapping({
stage,
actions,
getConfig,
}) {
actions.setWebpackConfig({
experiments: {
asyncWebAssembly: true,
},
plugins: [
// Work around for Buffer is undefined:
// https://github.com/webpack/changelog-v5/issues/10
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
}),
],
resolve: {
alias: {
'~': path.resolve(__dirname, 'src'),
fallback: {
buffer: require.resolve("buffer"),
fs: false,
path: false,
stream: require.resolve("stream-browserify"),
},
},
});
// TODO: make sure this only runs in dev
actions.setWebpackConfig({
devtool: 'eval-source-map',
});
const wasmExtensionRegExp = /\.wasm$/;
actions.setWebpackConfig({
module: {
rules: [
{
test: wasmExtensionRegExp,
include: /node_modules\/(bridge|token-bridge|nft)/,
use: ['wasm-loader'],
type: "javascript/auto"
}
]
}
});
if (stage === 'build-html') {
// exclude wasm from SSR
actions.setWebpackConfig({
externals: getConfig().externals.concat(function (context, request, callback) {
const regex = wasmExtensionRegExp;
// exclude wasm from being bundled in SSR html, it will be loaded async at runtime.
if (regex.test(request)) {
return callback(null, 'commonjs ' + request); // use commonjs for wasm modules
}
const bridge = new RegExp('/wormhole-sdk/')
if (bridge.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
})
});
}
// Attempt to improve webpack vender code splitting
if (stage === 'build-javascript') {
const config = getConfig();
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
vendors: {
test: /[\\/]node_modules[\\/]/,
enforce: true,
chunks: 'all',
priority: 1,
},
};
// Ensure Gatsby does not do any css code splitting
config.optimization.splitChunks.cacheGroups.styles.priority = 10;
actions.replaceWebpackConfig(config);
}
};
exports.createPages = ({ actions }) => {
const { createRedirect } = actions;
createRedirect({
fromPath: "/en/",
toPath: "/",
isPermanent: true,
});
createRedirect({
fromPath: "/en/about/",
toPath: "/buidl/",
isPermanent: true,
});
createRedirect({
fromPath: "/en/network/",
toPath: "/network/",
isPermanent: true,
});
createRedirect({
fromPath: "/en/explorer/",
toPath: "/explorer/",
isPermanent: true,
});
};

View File

@ -1,37 +0,0 @@
import React from 'react';
import dotenv from 'dotenv';
import { App } from './src/components/App';
import supportedLanguages from './src/utils/i18n/supportedLanguages';
dotenv.config({
path: `.env.${process.env.NODE_ENV}`,
});
// Duplicated in gatsby-browser.js for client side rendering
export const wrapRootElement = props => <App {...props} />;
export const onRenderBody = ({ pathname, setHeadComponents }) => {
// Create a string to allow a regex replacement for SEO hreflang links: https://support.google.com/webmasters/answer/189077?hl=en
const supportedLocaleRegexGroups = supportedLanguages
.map(language => language.languageTag)
.join('|');
const hrefLangLinks = [
...supportedLanguages.map(language => {
// Must be a fully qualified site URL
const href = `${process.env.GATSBY_SITE_URL}/${language.languageTag +
pathname.replace(new RegExp(`^/(${supportedLocaleRegexGroups})`), '')}`;
return (
<link
hrefLang={language.languageTag}
href={href}
key={`href-lang-${language.languageTag}`}
/>
);
}),
];
setHeadComponents(hrefLangLinks);
};

View File

@ -1,42 +0,0 @@
const config = {
// all ts or tsx files need to be transformed using jest-preprocess.js
// Set up Babel config in jest-preprocess.js
transform: {
// Allow tests in TypeScript using the .ts or .tsx
'^.+\\.[jt]sx?$': '<rootDir>/test-utils/jest-preprocess.js',
},
testRegex: '(/__tests__/.*(test|spec))\\.([tj]sx?)$',
moduleDirectories: ['node_modules', __dirname],
// Works like webpack rules. Tells Jest how to handle imports
moduleNameMapper: {
// Mock static file imports and assets which Jest cant handle
// stylesheets use the package identity-obj-proxy
'.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
// Manual mock other files using file-mock.js
'.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/file-mock.js',
// Mock SVG
'\\.svg': '<rootDir>/__mocks__/svgr-mock.js',
'^~/(.*)$': '<rootDir>/src/$1',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testPathIgnorePatterns: ['node_modules', '.cache', 'public'],
// Gatsby includes un-transpiled ES6 code. Exclude the gatsby module.
transformIgnorePatterns: ['node_modules/(?!(gatsby)/)'],
globals: {
__PATH_PREFIX__: '',
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!<rootDir>/src/**/*.stories.{ts,tsx}',
'!<rootDir>/src/**/__tests__/**/*',
'!<rootDir>/src/components/**/index.ts',
'!<rootDir>/node_modules/',
'!<rootDir>/test-utils/',
],
testURL: 'http://localhost',
setupFiles: ['<rootDir>/test-utils/loadershim.js', 'jest-localstorage-mock'],
setupFilesAfterEnv: ['<rootDir>/test-utils/setup-test-env.ts'],
};
module.exports = config;

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["src/*"],
}
}
}

48866
explorer/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,146 +1,46 @@
{
"name": "wormhole-explorer",
"author": "https://certus.one",
"description": "A visualizer for Wormhole Bridge info",
"name": "wormhole",
"version": "1.0.0",
"homepage": "https://github.com/certusone/wormhole",
"bugs": "https://github.com/certusone/wormhole",
"keywords": [
"gatsby",
"typescript",
"storybook",
"react-intl",
"svgr",
"jest",
"grpc",
"antd",
"ethereum",
"solana",
"terra",
"bsc",
"web3",
"defi"
],
"license": "MIT",
"jest": {
"setupTestFrameworkScriptFile": "<rootDir>/setup-test-env.js"
},
"private": false,
"scripts": {
"build": "NODE_ENV=production NODE_OPTIONS='-r esm' gatsby build",
"clean": "rm -rf public && rm -rf .cache",
"dev": "npm run clean && NODE_OPTIONS='-r esm' node --max-http-header-size=16385 node_modules/.bin/gatsby develop --port=8000",
"debug": "npm run clean && NODE_OPTIONS='-r esm' node --nolazy --inspect-brk node_modules/.bin/gatsby develop",
"serve": "NODE_OPTIONS='-r esm' node --max-http-header-size=16385 node_modules/.bin/gatsby serve --port=8001 --host=0.0.0.0",
"build-and-serve": "npm run build && npm run serve",
"lint": "npm run lint:js && npm run lint:ts",
"lint:js": "./node_modules/.bin/eslint --color --ext .js,.jsx .",
"lint:ts": "./node_modules/.bin/eslint --color --ext .ts,.tsx .",
"eslint-prettier-check-all": "npm run eslint-prettier-check-ts && npm run eslint-prettier-check-js",
"eslint-prettier-check-ts": "eslint --print-config src/pages/index.tsx | eslint-config-prettier-check",
"eslint-prettier-check-js": "eslint --print-config gatsby-browser.js | eslint-config-prettier-check",
"test": "jest",
"test:coverage": "npm run test --coverage",
"test:watch": "jest --watch",
"format": "npm run lint:js --fix && npm run lint:ts --fix",
"storybook": "NODE_OPTIONS='-r esm' gatsby build && NODE_OPTIONS='-r esm' start-storybook",
"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"
"develop": "gatsby develop",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve --port=8001 --host=0.0.0.0",
"clean": "gatsby clean"
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@certusone/wormhole-sdk": "^0.0.6",
"@cosmjs/encoding": "^0.26.2",
"@fontsource/sora": "^4.5.0",
"@improbable-eng/grpc-web": "^0.14.0",
"@nivo/bar": "^0.73.1",
"@nivo/line": "^0.73.0",
"@reach/router": "^1.3.1",
"@solana/web3.js": "^1.29.2",
"@svgr/webpack": "^5.1.0",
"antd": "^4.15.4",
"babel-plugin-module-resolver": "^4.0.0",
"bridge": "file:./wasm/core",
"bs58": "4.0.1",
"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",
"gatsby-plugin-remove-serviceworker": "^1.0.0",
"gatsby-plugin-robots-txt": "^1.6.8",
"gatsby-plugin-sitemap": "^2.12.0",
"gatsby-plugin-svgr": "^2.0.2",
"gatsby-plugin-typescript": "^2.1.27",
"nft": "file:./wasm/nft",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-helmet": "^5.2.1",
"react-time-ago": "^7.1.3",
"styled-components": "5.3.3",
"token_bridge": "file:./wasm/token"
"@certusone/wormhole-sdk": "^0.1.6",
"@cosmjs/encoding": "^0.27.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@fontsource/poppins": "^4.5.0",
"@mui/icons-material": "^5.2.5",
"@mui/lab": "^5.0.0-alpha.64",
"@mui/material": "^5.2.7",
"@mui/system": "^5.2.6",
"@nivo/bar": "^0.79.1",
"buffer": "^6.0.3",
"dotenv": "^10.0.0",
"ethers": "^5.4.1",
"gatsby": "^4.4.0",
"gatsby-plugin-google-gtag": "^4.4.0",
"gatsby-plugin-material-ui": "^4.1.0",
"gatsby-plugin-meta-redirect": "1.1.1",
"gatsby-plugin-react-helmet": "^5.4.0",
"gatsby-plugin-robots-txt": "^1.6.14",
"gatsby-plugin-sitemap": "^5.4.0",
"javascript-time-ago": "^2.3.10",
"path-browserify": "^1.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
"react-time-ago": "^7.1.7",
"stream-browserify": "^3.0.0",
"util": "^0.12.4"
},
"devDependencies": {
"@babel/core": "^7.8.4",
"@storybook/addon-actions": "^5.3.13",
"@storybook/addon-info": "^5.3.13",
"@storybook/addon-viewport": "^5.3.13",
"@storybook/preset-typescript": "^1.2.0",
"@storybook/react": "^5.3.13",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.0",
"@types/bs58": "4.0.1",
"@types/google-protobuf": "^3.15.2",
"@types/javascript-time-ago": "^2.0.2",
"@types/jest": "^25.1.3",
"@types/mocha": "^8.2.2",
"@types/node": "^13.7.4",
"@types/react": "^16.9.50",
"@types/react-dom": "^16.9.5",
"@types/react-helmet": "^5.0.15",
"@types/react-intl": "2.3.18",
"@types/storybook__react": "^5.2.1",
"@types/styled-components": "5.1.15",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.20.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-jsx-remove-data-test-id": "^2.1.3",
"babel-plugin-remove-graphql-queries": "^2.7.23",
"babel-preset-gatsby": "^0.2.29",
"babel-preset-react-app": "^9.1.1",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-airbnb-typescript": "^7.0.0",
"eslint-config-prettier": "^6.10.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jest": "^23.7.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-no-only-tests": "^2.4.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-react-hooks": "^2.4.0",
"gatsby-plugin-webpack-bundle-analyser-v2": "^1.1.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^25.1.0",
"jest-localstorage-mock": "^2.4.0",
"json-autotranslate": "^1.8.0",
"prettier": "^1.19.1",
"react-docgen-typescript-loader": "^3.6.0",
"react-test-renderer": "^16.12.0",
"storybook-addon-intl": "^2.4.1",
"ts-essentials": "^6.0.1",
"ts-jest": "^25.2.1",
"ts-loader": "^6.2.1",
"typescript": "^4.3.5",
"@types/react-helmet": "6.1.5",
"wasm-loader": "^1.3.0"
}
}

View File

@ -0,0 +1,108 @@
import {
createTheme,
CssBaseline,
responsiveFontSizes,
ThemeProvider,
} from "@mui/material";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en";
import React from "react";
import { NetworkContextProvider } from "../../src/contexts/NetworkContext";
TimeAgo.addDefaultLocale(en);
let theme = createTheme({
palette: {
mode: "dark",
background: {
default: "#17153f",
},
},
typography: {
fontFamily: ["Poppins", "Arial"].join(","),
fontSize: 13,
h1: {
fontWeight: "bold",
},
},
components: {
MuiCssBaseline: {
styleOverrides: {
ul: {
paddingLeft: "0px",
},
"*": {
scrollbarWidth: "thin",
scrollbarColor: `#4e4e54 rgba(0,0,0,.25)`,
},
"*::-webkit-scrollbar": {
width: "8px",
height: "8px",
backgroundColor: "rgba(0, 0, 0, 0.25)",
},
"*::-webkit-scrollbar-thumb": {
backgroundColor: "#4e4e54",
borderRadius: "4px",
},
"*::-webkit-scrollbar-corner": {
// this hides an annoying white box which appears when both scrollbars are present
backgroundColor: "transparent",
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 22,
fontSize: 12,
fontWeight: 700,
letterSpacing: 1.5,
padding: "8px 22.5px 6px",
"&:hover .MuiButton-endIcon": {
marginLeft: 16,
},
},
contained: {
boxShadow: "none",
"&:hover": {
boxShadow: "none",
},
"&:active": {
boxShadow: "none",
},
},
endIcon: {
marginLeft: 12,
transition: "margin-left 300ms",
},
},
},
MuiOutlinedInput: {
styleOverrides: {
notchedOutline: {
borderRadius: 24,
},
},
},
MuiSelect: {
styleOverrides: {
select: {
paddingTop: 8,
paddingRight: "40px!important",
paddingBottom: 8,
paddingLeft: 20,
},
},
},
},
});
theme = responsiveFontSizes(theme);
const TopLayout = ({ children }) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<NetworkContextProvider>{children}</NetworkContextProvider>
</ThemeProvider>
);
export default TopLayout;

View File

@ -0,0 +1,6 @@
import React from "react";
import TopLayout from "./TopLayout";
export const wrapRootElement = ({ element }) => (
<TopLayout>{element}</TopLayout>
);

View File

@ -0,0 +1,6 @@
import React from "react";
import TopLayout from "./TopLayout";
export const wrapRootElement = ({ element }) => (
<TopLayout>{element}</TopLayout>
);

View File

@ -0,0 +1,3 @@
{
"name": "gatsby-plugin-top-layout"
}

BIN
explorer/src/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,21 +0,0 @@
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
declare module '*.jpg' {
const jpgContent: string;
export { jpgContent };
}
declare module '*.png' {
const pngContent: string;
export { pngContent };
}
declare module '*.json' {
const jsonContent: string;
export { jsonContent };
}

View File

@ -1 +0,0 @@
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View File

@ -1 +0,0 @@
declare module 'react-time-ago';

View File

@ -1,31 +0,0 @@
export default {
// antd variables. see https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
'font-family':
"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',
'highlight-color': '#0074FF',
// 'processing-color': '#',
'menu-item-font-size': '18px',
'border-divider-color': 'darken(#808088, 20%)',
'link-color': 'lighten(@primary-color, 20%);', // lighten for proper contrast
'menu-dark-color': '@text-color-dark',
// make the header the same color as the body
'layout-header-background': '@layout-body-background',
'menu-dark-bg': '@layout-body-background',
'menu-dark-inline-submenu-bg': '@layout-body-background',
'menu-inline-submenu-bg': '@layout-body-background',
'menu-popup-bg': '@layout-body-background',
// shadow when hovering a card
'card-shadow': '0 0px 0px rgba(0, 239, 216, 0.80), 0 5px 32px 0 rgba(0, 239, 216, 0.60), 0 4px 12px 4px rgba(0, 239, 216, 0.4)',
// 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

@ -1,80 +0,0 @@
// import theme to get antd less variables + overrides from AntdTheme.js
@import "~antd/lib/style/themes/dark";
html {
scroll-behavior: smooth;
}
/* the selector matches and external link (ie. starting with http),
not including links to the current domain */
[href^="http"]:not(.no-external-icon)::after {
content: "(external link)";
display: inline-block;
width: 0.8em;
height: 0.8em;
text-indent: 0.8em;
white-space: nowrap;
overflow: hidden;
background-image: url(../../icons/external.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
margin-left: 0.3em;
}
// classes used throughout
.full-width {
width: '100%'
}
.max-content-width {
max-width: @max-content-width;
}
.center-content {
display: flex;
justify-content: center;
}
.blue-background {
background-color: @blue-background;
}
.responsive-padding {
// small left padding for mobile, more left padding for larger screens.
@media only screen and (min-width: 767px) {
padding: 0px 16px 0px 160px;
}
@media only screen and (max-width: 767px) {
padding: 0px 16px 0px 16px;
}
}
.wider-responsive-padding {
// small left padding for mobile, more left padding for larger screens.
@media only screen and (min-width: 767px) {
padding: 0px 16px 0px 80px;
}
@media only screen and (max-width: 767px) {
padding: 0px 16px 0px 16px;
}
}
.background-mask-from-left {
background: linear-gradient(to right, @body-background, rgba(@body-background, 0));
}
.hover-z-index:hover {
z-index: 10;
}
// scroll bar styles for wide <pre></pre> elements (ExplorerSummary)
.styled-scrollbar{
pre::-webkit-scrollbar {
width: 1em;
}
pre::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px darken(@primary-color, 20%);
}
pre::-webkit-scrollbar-thumb {
background-color: darken(@primary-color, 12%);
outline: 1px solid darken(@primary-color, 10%);
}
}

View File

@ -1,20 +0,0 @@
import { ReactNode } from 'react';
import "@fontsource/sora"
import TimeAgo from 'javascript-time-ago'
import en from 'javascript-time-ago/locale/en'
TimeAgo.addDefaultLocale(en)
import './App.less'
/**
* This component exists to provide a reusable application wrapper for use in Gatsby API's, testing, etc.
*/
const App = ({ element }: { element: ReactNode }) => {
return element;
};
export default App;

View File

@ -1,219 +0,0 @@
import React, { ChangeEventHandler, useEffect, useState } from 'react';
import { PageProps } from "gatsby"
import { Grid, Form, Input, Button, Radio, RadioChangeEvent } from 'antd';
const { TextArea } = Input
const { useBreakpoint } = Grid
import { SearchOutlined } from '@ant-design/icons';
import { FormattedMessage, useIntl } from 'gatsby-plugin-intl';
import { ExplorerQuery } from '~/components/ExplorerQuery'
import { ChainID, chainIDs } from '~/utils/misc/constants';
// form props
interface ExplorerFormValues {
emitterChain: number,
emitterAddress: string,
sequence: string
}
const formFields = ['emitterChain', 'emitterAddress', 'sequence']
const emitterChains = [
{ label: ChainID[1], value: chainIDs['solana'] },
{ label: ChainID[2], value: chainIDs['ethereum'] },
{ label: ChainID[3], value: chainIDs['terra'] },
{ label: ChainID[4], value: chainIDs['bsc'] },
{ label: ChainID[5], value: chainIDs['polygon'] },
]
interface ExplorerSearchProps {
location: PageProps["location"],
navigate: PageProps["navigate"]
}
const ExplorerSearchForm: React.FC<ExplorerSearchProps> = ({ 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"]>()
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 seqQuery = searchParams.get('sequence')
// get the current values from the form fields
const { emitterChain, emitterAddress, sequence: seqForm } = form.getFieldsValue(true)
// if the search params are different form values, update the form.
if (Number(chain) !== emitterChain) {
form.setFieldsValue({ emitterChain: Number(chain) })
}
setEmitterChain(Number(chain))
if (address !== emitterAddress) {
form.setFieldsValue({ emitterAddress: address || undefined })
}
setEmitterAddress(address || undefined)
if (seqQuery !== seqForm) {
form.setFieldsValue({ sequence: seqQuery || undefined })
}
setSequence(seqQuery || undefined)
} else {
// clear state
setEmitterChain(undefined)
setEmitterAddress(undefined)
setSequence(undefined)
}
}, [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 onChain = (e: RadioChangeEvent) => {
if (e.target.value) {
setEmitterChain(e.target.value)
}
}
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 (
<>
<div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
<Form
layout="vertical"
form={form}
name="explorer-message-query"
onFinish={onFinish}
size="large"
style={{ width: '90%', maxWidth: 800, 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: '60%'
}}
>
<Radio.Group
optionType="button"
options={emitterChains}
onChange={onChain}
/>
</Form.Item>
<Form.Item shouldUpdate
style={
screens.md === false ? {
display: 'block', width: '100%'
} : {
display: 'inline-block', width: '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>
</Form>
</div>
{emitterChain && emitterAddress && sequence ? (
<ExplorerQuery emitterChain={emitterChain} emitterAddress={emitterAddress} sequence={sequence} />
) : null}
</ >
)
};
export default ExplorerSearchForm

View File

@ -1,152 +0,0 @@
import React, { ChangeEventHandler, useEffect, useState } from 'react';
import { PageProps } from "gatsby"
import { Grid, Form, Input, Button, } from 'antd';
const { useBreakpoint } = Grid
import { SearchOutlined } from '@ant-design/icons';
import { FormattedMessage, useIntl } from 'gatsby-plugin-intl';
import { ExplorerQuery } from '~/components/ExplorerQuery'
// form props
interface ExplorerTxValues {
txId: string,
}
const formFields = ['txId']
interface ExplorerSearchProps {
location: PageProps["location"],
navigate: PageProps["navigate"]
}
const ExplorerTxForm: React.FC<ExplorerSearchProps> = ({ location, navigate }) => {
const intl = useIntl()
const screens = useBreakpoint()
const [, forceUpdate] = useState({});
const [form] = Form.useForm<ExplorerTxValues>();
const [txId, setTxId] = useState<ExplorerTxValues["txId"]>()
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 txQuery = searchParams.get('txId')
// get the current values from the form fields
const { txId: txForm } = form.getFieldsValue(true)
// if the search params are different form values, update the form.
if (txQuery) {
if (txQuery !== txForm) {
form.setFieldsValue({ txId: txQuery })
}
setTxId(txQuery)
}
} else {
// clear state
setTxId(undefined)
}
}, [location.search])
const onFinish = ({ txId }: ExplorerTxValues) => {
// pushing to the history stack will cause the component to get new props, and useEffect will run.
navigate(`/${intl.locale}/explorer/?txId=${txId}`)
};
const onTxId: ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.currentTarget.value) {
// trim whitespace
form.setFieldsValue({ txId: e.currentTarget.value.replace(/\s/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 (
<>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Form
layout="vertical"
form={form}
name="explorer-tx-query"
onFinish={onFinish}
size="large"
style={{ width: '90%', maxWidth: 800, fontSize: 14 }}
colon={false}
requiredMark={false}
validateMessages={{ required: "'${label}' is required", }}
>
<Form.Item shouldUpdate
style={
screens.md === false ? {
display: 'block', width: '100%'
} : {
display: 'inline-block', width: '100%'
}}
>
{() => (
<Form.Item
name="txId"
label={formatLabel("explorer.txId")}
help={formatHelp("explorer.txIdHelp")}
rules={[{ required: true }]}
>
<Input
onChange={onTxId}
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>
{txId ? (
<ExplorerQuery txId={txId} />
) : null}
</ >
)
};
export default ExplorerTxForm

View File

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

View File

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

View File

@ -0,0 +1,16 @@
import { Box } from "@mui/material";
import React from "react";
const AvoidBreak = ({ spans }: { spans: string[] }) => (
<>
{spans.map((span, idx) => (
<React.Fragment key={`${idx}|${span}`}>
<Box component="span" sx={{ display: "inline-block" }}>
{span}
</Box>{" "}
</React.Fragment>
))}
</>
);
export default AvoidBreak;

View File

@ -1,13 +0,0 @@
# Example Stories and Tests
## Storybook: component rendering
Files that end in `*.stories.tsx` are rendered by storybook.
Storybook can be used to check component rendering, with styles and locales.
## Jest: component testing
Files that end in `*.test.tsx` are included in Jest testing (run with `yarn test`).
Jest can be used to ensure content (ie. language definiton) and accessiblity requirements are met.

View File

@ -1,16 +0,0 @@
import React from 'react';
import { render } from 'test-utils';
import { Button } from 'antd';
describe('<Button />', () => {
describe('Antd button rendering', () => {
test('should render Button component', () => {
const { getByText } = render(<Button>Click Me</Button>);
const button = getByText('Click Me');
expect(button).toBeTruthy()
});
});
});

View File

@ -1,45 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'gatsby-plugin-intl';
import { Button } from 'antd';
export default {
title: 'Button',
};
export const Link = () => (
<Button type="link">
<FormattedMessage id="homepage.title" />
</Button>
);
export const Primary = () => (
<Button type="primary" >
<FormattedMessage id="homepage.title" />
</Button>
);
export const Large = () => (
<Button type="primary" size="large">
<FormattedMessage id="homepage.title" />
</Button>
);
export const Loading = () => (
<Button type="primary" loading>
<FormattedMessage id="homepage.title" />
</Button>
);
export const Outline = () => (
<Button type="ghost">
<FormattedMessage id="homepage.title" />
</Button>
);
export const Danger = () => (
<Button danger>
<FormattedMessage id="homepage.title" />
</Button>
);

View File

@ -0,0 +1,51 @@
import * as React from "react";
import binanceChainIcon from "../images/bsc.svg";
import ethereumIcon from "../images/eth.svg";
import solanaIcon from "../images//solana.svg";
import terraIcon from "../images/terra.svg";
import polygonIcon from "../images/polygon.svg";
import avalancheIcon from "../images/avalanche.svg";
import oasisIcon from "../images/oasis.svg";
import {
ChainId,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk";
import { chainEnums } from "../utils/consts";
import { Box } from "@mui/material";
const chainIdToSrc = {
[CHAIN_ID_SOLANA]: solanaIcon,
[CHAIN_ID_ETH]: ethereumIcon,
[CHAIN_ID_TERRA]: terraIcon,
[CHAIN_ID_BSC]: binanceChainIcon,
[CHAIN_ID_POLYGON]: polygonIcon,
[CHAIN_ID_AVAX]: avalancheIcon,
[CHAIN_ID_OASIS]: oasisIcon,
};
const ChainIcon = ({ chainId }: { chainId: ChainId }) =>
chainIdToSrc[chainId] ? (
<Box
sx={{
display: "flex",
alignItems: "center",
px: chainId === CHAIN_ID_ETH ? 0 : 0.25,
"&:first-of-type": { pl: 0 },
"&:last-of-type": { pr: 0 },
}}
>
<img
src={chainIdToSrc[chainId]}
alt={chainEnums[chainId] || ""}
style={{ width: 16 }}
/>
</Box>
) : null;
export default ChainIcon;

View File

@ -1,15 +1,12 @@
import { BigNumber } from "ethers";
import React, { useEffect, useState } from "react";
import { chainEnums, ChainIDs, chainIDs, METADATA_REPLACE } from "~/utils/misc/constants";
import { Typography } from '@mui/material'
import { importCoreWasm } from '@certusone/wormhole-sdk'
import { Statistic, Typography } from 'antd'
import { FormattedMessage } from "gatsby-plugin-intl";
import { titleStyles } from "~/styles";
import { TransferDetails } from "../ExplorerQuery/ExplorerQuery";
const { Title } = Typography
import { chainEnums, ChainIDs, chainIDs, METADATA_REPLACE } from "../utils/consts";
import { usdFormatter } from "../utils/explorer";
import { TransferDetails, TokenTransferPayload as TransferPayload } from "./ExplorerSearch/ExplorerQuery";
const validChains = Object.values(chainIDs)
@ -167,7 +164,10 @@ function useBase64ToBuffer(base64VAA: string = "") {
useEffect(() => {
async function asyncWork(vaaString: string) {
const vaa = convertbase64ToBinary(vaaString)
const bridgeWasm = await import('bridge')
// TODO - need to export parse_vaa from @certusone/wormhole-sdk
// const bridgeWasm = await import('bridge')
const bridgeWasm = await importCoreWasm()
const parsedVaa = bridgeWasm.parse_vaa(vaa)
@ -184,6 +184,7 @@ interface DecodePayloadProps {
showType?: boolean
showSummary?: boolean
showPayload?: boolean
transferPayload?: TransferPayload
transferDetails?: TransferDetails
}
@ -253,7 +254,8 @@ const DecodePayload = (props: DecodePayloadProps) => {
}
}, [buf])
const titleCase = (str: string) => <span style={{ textTransform: 'capitalize' }}>{str}</span>
const unknown = "Unknown message"
return (
<>
@ -265,19 +267,22 @@ const DecodePayload = (props: DecodePayloadProps) => {
{"AssetMeta:"}&nbsp;{chainEnums[payloadBundle.payload.tokenChain]}&nbsp; {payloadBundle.payload.symbol} {payloadBundle.payload.name}
</>) :
payloadBundle.type === "tokenTransfer" ?
props.transferDetails && props.transferDetails.OriginSymbol ? (<>
{Math.round(Number(props.transferDetails.Amount) * 100) / 100}{' '}{props.transferDetails.OriginSymbol}{' -> '}{chainEnums[payloadBundle.payload.targetChain]}
props.transferDetails && payloadBundle.payload && props.transferDetails.OriginSymbol ? (<>
{"Transfer"}&nbsp;
{(Math.round(Number(props.transferDetails.Amount) * 100) / 100).toLocaleString()}&nbsp;{props.transferDetails.OriginSymbol}&nbsp;
{'from'}&nbsp;{titleCase(props.emitterChainName)}&nbsp;{'to'}&nbsp;{chainEnums[Number(payloadBundle.payload.targetChain)]}&nbsp;
{'('}{usdFormatter.format(Number(props.transferDetails.NotionalUSDStr))}{')'}
</>) : (<>
{"Native "}{chainEnums[payloadBundle.payload.originChain]}{' asset -> '}{chainEnums[payloadBundle.payload.targetChain]}
{"Token transfer: "}{chainEnums[payloadBundle.payload.originChain]}{' asset -> '}{chainEnums[payloadBundle.payload.targetChain]}
</>) :
payloadBundle.type === "nftTransfer" ? (<>
{payloadBundle.payload.symbol || "?"}&nbsp;{"-"}&nbsp;{chainEnums[payloadBundle.payload.originChain]}{' -> '}{chainEnums[payloadBundle.payload.targetChain]}
</>) : null
) : null}
{"NFT: "}&nbsp;{payloadBundle.payload.name || "?"}&nbsp;{" wormholed "}&nbsp;{chainEnums[payloadBundle.payload.originChain]}{' -> '}{chainEnums[payloadBundle.payload.targetChain]}
</>) : unknown
) : unknown}
</span> : props.showPayload && payloadBundle ? (
<>
<div style={{ margin: "20px 0" }} className="styled-scrollbar">
<Title level={3} style={titleStyles}><FormattedMessage id={`explorer.payloads.${payloadBundle.type}`} /> payload</Title>
<Typography variant="h4"> payload</Typography>
<pre style={{ fontSize: 14 }}>{JSON.stringify(payloadBundle.payload, undefined, 2)}</pre>
</div>
{/* TODO - prettier formatting of payload data. POC below. */}
@ -288,7 +293,7 @@ const DecodePayload = (props: DecodePayloadProps) => {
) : <span>Can't decode unknown payloads</span>} */}
</>
) : null}
) : unknown}
</>
)

View File

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

View File

@ -0,0 +1,40 @@
import React from "react";
import { TextField, Typography, MenuItem } from "@mui/material";
export type explorerFormType = "txID" | "messageID";
interface ExplorerFormSelect {
currentlyActive: explorerFormType;
toggleFormType: () => void;
}
const ExplorerFormSelect: React.FC<ExplorerFormSelect> = ({
currentlyActive,
toggleFormType,
}) => {
const onQueryType = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (value !== currentlyActive) {
// toggle the other form type
toggleFormType();
}
};
const formatOption = (message: string) => (
<Typography variant="body2">{message}</Typography>
);
return (
<TextField
select
value={currentlyActive}
onChange={onQueryType}
sx={{
mb: 2,
}}
>
<MenuItem value="txID">{formatOption("Search Transaction")}</MenuItem>
<MenuItem value="messageID">{formatOption("Search Message ID")}</MenuItem>
</TextField>
);
};
export default ExplorerFormSelect;

View File

@ -0,0 +1,195 @@
import React, { useEffect, useState } from "react";
import { PageProps, navigate } from "gatsby";
import ExplorerQuery from "./ExplorerQuery";
import { chainEnums, ChainID, chainIDs } from "../../utils/consts";
import { useNetworkContext } from "../../contexts/NetworkContext";
import { truncateAddress } from "../../utils/explorer";
import {
Autocomplete,
Button,
FormControl,
TextField,
Typography,
MenuItem,
Box,
} from "@mui/material";
import ExplorerFormSelect, { explorerFormType } from "./ExplorerFormSelect";
// form props
interface ExplorerMessageSearchValues {
emitterChain: number;
emitterAddress: string;
sequence: string;
}
const emitterChains = [
{ label: ChainID[1], value: chainIDs["solana"] },
{ label: ChainID[2], value: chainIDs["ethereum"] },
{ label: ChainID[3], value: chainIDs["terra"] },
{ label: ChainID[4], value: chainIDs["bsc"] },
{ label: ChainID[5], value: chainIDs["polygon"] },
{ label: ChainID[6], value: chainIDs["avalanche"] },
{ label: ChainID[7], value: chainIDs["oasis"] },
];
interface ExplorerMessageSearchProps {
location: PageProps["location"];
toggleFormType: () => void;
formName: explorerFormType;
}
const ExplorerMessageSearchForm: React.FC<ExplorerMessageSearchProps> = ({
location,
toggleFormType,
formName,
}) => {
const [emitterChain, setEmitterChain] =
useState<ExplorerMessageSearchValues["emitterChain"]>();
const [emitterAddress, setEmitterAddress] =
useState<ExplorerMessageSearchValues["emitterAddress"]>();
const [sequence, setSequence] =
useState<ExplorerMessageSearchValues["sequence"]>();
const { activeNetwork } = useNetworkContext();
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 seqQuery = searchParams.get("sequence");
setEmitterChain(Number(chain));
setEmitterAddress(address || undefined);
setSequence(seqQuery || undefined);
} else {
// clear state
setEmitterChain(undefined);
setEmitterAddress(undefined);
setSequence(undefined);
}
}, [location.search]);
const handleSubmit = (event: React.ChangeEvent<HTMLFormElement>) => {
event.preventDefault();
// pushing to the history stack will cause the component to get new props, and useEffect will run.
if (emitterChain && emitterAddress && sequence) {
navigate(
`?emitterChain=${emitterChain}&emitterAddress=${emitterAddress}&sequence=${sequence}`
);
}
};
const onChain = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setEmitterChain(Number(value));
};
const onAddress = (value: string) => {
// trim whitespace
setEmitterAddress(value.replace(/\s/g, ""));
};
const onSequence = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
// remove everything except numbers
setSequence(value.replace(/\D/g, ""));
};
return (
<>
<Box
component="form"
noValidate
autoComplete="off"
onSubmit={handleSubmit}
>
<ExplorerFormSelect
currentlyActive={formName}
toggleFormType={toggleFormType}
/>
<TextField
select
value={emitterChain || ""}
onChange={onChain}
placeholder="Chain"
label="Chain"
fullWidth
size="small"
sx={{ my: 1 }}
>
{emitterChains.map(({ label, value }) => (
<MenuItem key={label} value={value}>
{label}
</MenuItem>
))}
</TextField>
<Autocomplete
// TODO set value when loading the page with emitterAddress
// value={emitterAddress || ""}
freeSolo
fullWidth
size="small"
sx={{ my: 1 }}
onChange={(event, newVal: any) => onAddress(newVal.value)}
placeholder="Contract"
renderInput={(params: any) => (
<TextField {...params} label="Emitter Contract" />
)}
getOptionLabel={(option) => option.label}
// Get the chainID from the emitterChain form item, then use chainEnums to transform it to the
// lowercase chain name, in order to use it to lookup the emitterAdresses of the active network.
// Filter out keys that are not human readable names, by checking for a space in the key.
options={Object.entries(
activeNetwork.chains[
chainEnums[emitterChain || 1]?.toLowerCase()
] || {}
)
.filter(([key]) => key.includes(" "))
.map(([key, val]) => ({
label: `${key} (${truncateAddress(val)})`,
value: val,
}))}
/>
<TextField
type="number"
value={sequence ? Number(sequence) : ""}
onChange={onSequence}
label="Sequence"
fullWidth
size="small"
sx={{ my: 1 }}
/>
<Button
type="submit"
variant="contained"
sx={{
display: "block",
mt: 1,
ml: "auto",
}}
>
Search
</Button>
</Box>
{emitterChain && emitterAddress && sequence ? (
<ExplorerQuery
emitterChain={emitterChain}
emitterAddress={emitterAddress}
sequence={sequence}
/>
) : null}
</>
);
};
export default ExplorerMessageSearchForm;

View File

@ -1,15 +1,13 @@
import React, { useContext, useEffect, useState } from 'react';
import { Spin, Typography } from 'antd'
const { Title } = Typography
import React, { useEffect, useState } from 'react';
import { Typography } from '@mui/material'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { arrayify, isHexString, zeroPad, hexlify } from "ethers/lib/utils";
import { Bech32, toHex, fromHex } from "@cosmjs/encoding"
import { ExplorerSummary } from '~/components/ExplorerSummary';
import { titleStyles } from '~/styles';
import { NetworkContext } from '~/components/NetworkSelect';
import { getEmitterAddressSolana } from "@certusone/wormhole-sdk";
import { ChainIDs, chainIDs } from '~/utils/misc/constants';
import ExplorerSummary from './ExplorerSummary';
import { useNetworkContext } from '../../contexts/NetworkContext';
import { ChainId, getEmitterAddressSolana, isEVMChain } from "@certusone/wormhole-sdk";
import { ChainIDs, chainIDs } from '../../utils/consts';
import { PublicKey } from '@solana/web3.js';
export interface VAA {
@ -60,7 +58,7 @@ interface ExplorerQuery {
txId?: string,
}
const ExplorerQuery = (props: ExplorerQuery) => {
const { activeNetwork } = useContext(NetworkContext)
const { activeNetwork } = useNetworkContext()
const [error, setError] = useState<string>();
const [loading, setLoading] = useState<boolean>(true);
const [message, setMessage] = useState<BigTableMessage>();
@ -90,7 +88,7 @@ const ExplorerQuery = (props: ExplorerQuery) => {
} else {
paddedAddress = emitterAddress
}
} else if (emitterChain === chainIDs["ethereum"] || emitterChain === chainIDs["bsc"] || emitterChain === chainIDs["polygon"]) {
} else if (isEVMChain(emitterChain as ChainId)) {
if (isHexString(emitterAddress)) {
let paddedAddressArray = zeroPad(arrayify(emitterAddress, { hexPad: "left" }), 32);
@ -121,7 +119,7 @@ const ExplorerQuery = (props: ExplorerQuery) => {
} else {
paddedSequence = sequence
}
url = `${base}/readrow?emitterChain=${emitterChain}&emitterAddress=${paddedAddress}&sequence=${paddedSequence}`
url = `${base}readrow?emitterChain=${emitterChain}&emitterAddress=${paddedAddress}&sequence=${paddedSequence}`
} else if (txId) {
let transformedTxId = txId
if (isHexString(txId)) {
@ -142,7 +140,7 @@ const ExplorerQuery = (props: ExplorerQuery) => {
}
}
}
url = `${base}/transaction?id=${transformedTxId}`
url = `${base}transaction?id=${transformedTxId}`
}
fetch(url)
@ -219,9 +217,9 @@ const ExplorerQuery = (props: ExplorerQuery) => {
return (
<>
{loading ? <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin />
loading...
</div> :
error ? <Title level={2} style={titleStyles}><FormattedMessage id={error} /></Title> :
error ? <Typography variant="h4" >error</Typography> :
message ? (
<ExplorerSummary
{...props}

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react'
import { navigate, PageProps } from 'gatsby'
import { Box } from "@mui/material";
import ExplorerMessageSearchForm from "./ExplorerMessageSearchForm";
import ExplorerTxSearchForm from "./ExplorerTxSearchForm";
interface ExplorerSearchProps {
location: PageProps["location"],
}
const ExplorerSearch = ({ location }: ExplorerSearchProps) => {
const [showMessageIdForm, setShowMessageIdForm] = useState<boolean>(false);
useEffect(() => {
if (location.search) {
const searchParams = new URLSearchParams(location.search);
const chain = searchParams.get("emitterChain");
const address = searchParams.get("emitterAddress");
const seq = searchParams.get("sequence");
const tx = searchParams.get("txId");
if (!tx && chain && address && seq) {
setShowMessageIdForm(true);
}
} else {
setShowMessageIdForm(false)
}
}, [location.search])
const switchForm = () => {
if (location.search) {
navigate('/explorer')
}
setShowMessageIdForm(!showMessageIdForm)
}
return (
<Box
sx={{
backgroundColor: "rgba(255,255,255,.07)",
borderRadius: "28px",
mt: 4,
p: 4,
}}
>
{showMessageIdForm ? (
<ExplorerMessageSearchForm
location={location}
toggleFormType={switchForm}
formName="messageID"
/>
) : (
<ExplorerTxSearchForm
location={location}
toggleFormType={switchForm}
formName="txID"
/>
)}
</Box>
)
}
export default ExplorerSearch

View File

@ -0,0 +1,250 @@
import React from "react";
import { Box, Button, Link, Typography } from "@mui/material";
import { BigTableMessage } from "./ExplorerQuery";
import { DecodePayload } from "../DecodePayload";
import ReactTimeAgo from "react-time-ago";
import { Link as RouterLink } from "gatsby";
import {
contractNameFormatter,
getNativeAddress,
nativeExplorerContractUri,
nativeExplorerTxUri,
truncateAddress,
usdFormatter,
} from "../../utils/explorer";
import { OutboundLink } from "gatsby-plugin-google-gtag";
import { ChainID, chainIDs } from "../../utils/consts";
import { hexToNativeString } from "@certusone/wormhole-sdk";
import { explorer } from "../../utils/urls";
interface SummaryProps {
emitterChain?: number;
emitterAddress?: string;
sequence?: string;
txId?: string;
message: BigTableMessage;
polling?: boolean;
lastFetched?: number;
refetch: () => void;
}
const textStyles = { fontSize: 16, margin: "6px 0" };
const ExplorerSummary = (props: SummaryProps) => {
const { SignedVAA, ...message } = props.message;
const {
EmitterChain,
EmitterAddress,
InitiatingTxID,
TokenTransferPayload,
TransferDetails,
} = message;
// get chainId from chain name
let chainId = chainIDs[EmitterChain];
let transactionId: string | undefined;
if (InitiatingTxID) {
if (
chainId === chainIDs["ethereum"] ||
chainId === chainIDs["bsc"] ||
chainId === chainIDs["polygon"]
) {
transactionId = InitiatingTxID;
} else {
if (chainId === chainIDs["solana"]) {
const txId = InitiatingTxID.slice(2); // remove the leading "0x"
transactionId = hexToNativeString(txId, chainId);
} else if (chainId === chainIDs["terra"]) {
transactionId = InitiatingTxID.slice(2); // remove the leading "0x"
}
}
}
return (
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
gap: 8,
alignItems: "baseline",
marginTop: 40,
}}
>
<Typography variant="h4">Message Summary</Typography>
{props.polling ? (
<>
<div style={{ flexGrow: 1 }}></div>
<Typography variant="caption">listening</Typography>
</>
) : (
<div>
<Button onClick={props.refetch}>Refresh</Button>
<Button component={RouterLink} to={explorer} sx={{ ml: 1 }}>
Clear
</Button>
</div>
)}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
margin: "20px 0 24px 20px",
}}
>
<ul>
{TokenTransferPayload &&
TokenTransferPayload.TargetAddress &&
TransferDetails &&
nativeExplorerContractUri(
Number(TokenTransferPayload.TargetChain),
TokenTransferPayload.TargetAddress
) ? (
<>
<li>
<span style={textStyles}>
This is a token transfer of{" "}
{Math.round(Number(TransferDetails.Amount) * 100) / 100}
{` `}
{!["UST", "LUNA"].includes(TransferDetails.OriginSymbol) ? (
<Link
component={OutboundLink}
href={nativeExplorerContractUri(
Number(TokenTransferPayload.OriginChain),
TokenTransferPayload.OriginAddress
)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: "nowrap" }}
>
{TransferDetails.OriginSymbol}
</Link>
) : (
TransferDetails.OriginSymbol
)}
{` `}from {ChainID[chainId]}, to{" "}
{ChainID[Number(TokenTransferPayload.TargetChain)]}, destined
for address{" "}
</span>
<Link
component={OutboundLink}
href={nativeExplorerContractUri(
Number(TokenTransferPayload.TargetChain),
TokenTransferPayload.TargetAddress
)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: "nowrap" }}
>
{truncateAddress(
getNativeAddress(
Number(TokenTransferPayload.TargetChain),
TokenTransferPayload.TargetAddress
)
)}
</Link>
<span style={textStyles}>.</span>
</li>
{TransferDetails.NotionalUSDStr && (
<>
<li>
<span style={textStyles}>
When these tokens were sent to Wormhole, the{" "}
{Math.round(Number(TransferDetails.Amount) * 100) / 100}{" "}
{TransferDetails.OriginSymbol} was worth about{" "}
{usdFormatter.format(
Number(TransferDetails.NotionalUSDStr)
)}
.
</span>
</li>
<li>
<span style={textStyles}>
At the time of the transfer, 1{" "}
{TransferDetails.OriginName} was worth about{" "}
{usdFormatter.format(
Number(TransferDetails.TokenPriceUSDStr)
)}
.{" "}
</span>
</li>
</>
)}
</>
) : null}
{EmitterChain &&
EmitterAddress &&
nativeExplorerContractUri(chainId, EmitterAddress) ? (
<li>
<span style={textStyles}>
This message was emitted by the {ChainID[chainId]}{" "}
</span>
<Link
component={OutboundLink}
href={nativeExplorerContractUri(chainId, EmitterAddress)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: "nowrap" }}
>
{contractNameFormatter(EmitterAddress, chainId)}
</Link>
<span style={textStyles}> contract</span>
{transactionId && (
<>
<span style={textStyles}>
{" "}
after the Wormhole Guardians observed transaction{" "}
</span>
<Link
component={OutboundLink}
href={nativeExplorerTxUri(chainId, transactionId)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: "nowrap" }}
>
{truncateAddress(transactionId)}
</Link>
</>
)}{" "}
<span style={textStyles}>.</span>
</li>
) : null}
</ul>
</div>
<Typography variant="h4">Raw message data:</Typography>
<Box component="div" sx={{ overflow: "auto", mb: 2.5 }}>
<pre style={{ fontSize: 14 }}>
{JSON.stringify(message, undefined, 2)}
</pre>
</Box>
<DecodePayload
base64VAA={props.message.SignedVAABytes}
emitterChainName={props.message.EmitterChain}
emitterAddress={props.message.EmitterAddress}
showPayload={true}
transferDetails={props.message.TransferDetails}
/>
<Box component="div" sx={{ overflow: "auto", mb: 2.5 }}>
<Typography variant="h4">Signed VAA</Typography>
<pre style={{ fontSize: 12 }}>
{JSON.stringify(SignedVAA, undefined, 2)}
</pre>
</Box>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
{props.lastFetched ? (
<span>
last updated:&nbsp;
<ReactTimeAgo
date={new Date(props.lastFetched)}
timeStyle="round"
/>
</span>
) : null}
</div>
</>
);
};
export default ExplorerSummary;

View File

@ -0,0 +1,88 @@
import React, { useEffect, useState } from "react";
import { PageProps, navigate } from "gatsby";
import { Button, TextField, Box } from "@mui/material";
import ExplorerQuery from "./ExplorerQuery";
import ExplorerFormSelect, { explorerFormType } from "./ExplorerFormSelect";
interface ExplorerTxSearchProps {
location: PageProps["location"];
toggleFormType: () => void;
formName: explorerFormType;
}
const ExplorerTxSearchForm: React.FC<ExplorerTxSearchProps> = ({
location,
toggleFormType,
formName,
}) => {
const [txId, setTxId] = useState<string>();
useEffect(() => {
if (location.search) {
// take searchparams from the URL and set the values in the form
const searchParams = new URLSearchParams(location.search);
const txQuery = searchParams.get("txId");
// if the search params are different form values, update the form.
if (txQuery) {
setTxId(txQuery);
}
} else {
// clear state
setTxId(undefined);
}
}, [location.search]);
const handleSubmit = (event: any) => {
event.preventDefault();
// pushing to the history stack will cause the component to get new props, and useEffect will run.
if (txId) {
navigate(`/explorer?txId=${txId}`);
}
};
const onTxId = (event: React.ChangeEvent<HTMLInputElement>) => {
const tx = event.target.value;
setTxId(tx.replace(/\s/g, ""));
};
return (
<>
<Box
component="form"
noValidate
autoComplete="off"
onSubmit={handleSubmit}
>
<ExplorerFormSelect
currentlyActive={formName}
toggleFormType={toggleFormType}
/>
<TextField
value={txId || ""}
onChange={onTxId}
label="Transaction"
fullWidth
size="small"
sx={{ my: 1 }}
/>
<Button
type="submit"
variant="contained"
sx={{
display: "block",
mt: 1,
ml: "auto",
}}
>
Search
</Button>
</Box>
{txId ? <ExplorerQuery txId={txId} /> : null}
</>
);
};
export default ExplorerTxSearchForm;

View File

@ -1,15 +0,0 @@
// import theme to get antd less variables + overrides from AntdTheme.js
@import "~antd/lib/style/themes/dark";
.highlight-new-val {
animation: highlight 2000ms ease-out;
}
@keyframes highlight {
0% {
color: @primary-color;
}
100% {
color: @text-color;
}
}

View File

@ -1,106 +1,138 @@
import React, { useState, useEffect } from 'react'
import { Card, Statistic, Tooltip, Typography, } from 'antd'
const { Text } = Typography
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { navigate } from 'gatsby'
import { Totals } from './ExplorerStats'
import './ChainOverviewCard.less'
import { Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import { chainIDStrings } from "../../utils/consts";
import { amountFormatter } from "../../utils/explorer";
import {
NotionalTransferred,
NotionalTransferredToCumulative,
Totals,
} from "./ExplorerStats";
interface ChainOverviewCardProps {
Icon: React.FC<React.SVGProps<SVGSVGElement>>
title: string
dataKey: "*" | "1" | "2" | "3" | "4" | "5"
totals?: Totals
iconStyle?: { [key: string]: string | number }
totalDays: number
dataKey: keyof typeof chainIDStrings;
totals?: Totals;
notionalTransferred?: NotionalTransferred;
notionalTransferredToCumulative?: NotionalTransferredToCumulative;
}
const ChainOverviewCard: React.FC<ChainOverviewCardProps> = ({ Icon, iconStyle, title, dataKey, totals, totalDays }) => {
const intl = useIntl()
const [lastDayCount, setLastDayColunt] = useState<number>()
const [totalCount, setTotalColunt] = useState<number>()
const [loading, setLoading] = useState<boolean>(true)
const [animate, setAnimate] = useState<boolean>(false)
const ChainOverviewCard: React.FC<ChainOverviewCardProps> = ({
dataKey,
totals,
notionalTransferred,
notionalTransferredToCumulative,
}) => {
const [totalCount, setTotalColunt] = useState<number>();
const [animate, setAnimate] = useState<boolean>(false);
useEffect(() => {
if (!totals) {
setLoading(true)
}
// hold values from props in state, so that we can detect changes and add animation class
setLastDayColunt(totals?.LastDayCount[dataKey])
setTotalColunt(totals?.TotalCount[dataKey])
useEffect(() => {
// hold values from props in state, so that we can detect changes and add animation class
setTotalColunt(totals?.TotalCount[dataKey]);
if (totals?.TotalCount && dataKey in totals?.TotalCount) {
setLoading(!totals?.TotalCount[dataKey] && !totals?.LastDayCount[dataKey])
}
let timeout: NodeJS.Timeout;
if (
totals?.LastDayCount[dataKey] &&
totalCount !== totals?.LastDayCount[dataKey]
) {
setAnimate(true);
timeout = setTimeout(() => {
setAnimate(false);
}, 2000);
}
return function cleanup() {
if (timeout) {
clearTimeout(timeout);
}
};
}, [
totals?.TotalCount[dataKey],
totals?.LastDayCount[dataKey],
dataKey,
totalCount,
]);
let timeout: NodeJS.Timeout
if (totals?.LastDayCount[dataKey] && totalCount !== totals?.LastDayCount[dataKey]) {
setAnimate(true)
timeout = setTimeout(() => {
setAnimate(false)
}, 2000)
}
return function cleanup() {
if (timeout) {
clearTimeout(timeout)
}
}
}, [totals?.TotalCount[dataKey], totals?.LastDayCount[dataKey], dataKey, totalCount])
const centerStyles: any = {
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
flexDirection: "column",
};
return (
<>
<div style={{ ...centerStyles, gap: 8 }}>
{notionalTransferredToCumulative &&
notionalTransferredToCumulative.AllTime && (
<div style={centerStyles}>
<div>
<Typography
variant="h5"
className={animate ? "highlight-new-val" : ""}
>
$
{amountFormatter(
notionalTransferredToCumulative.AllTime[dataKey]["*"]
)}
</Typography>
</div>
<div style={{ marginTop: -10 }}>
<Typography variant="subtitle1">received</Typography>
</div>
</div>
)}
{notionalTransferred &&
notionalTransferred.WithinPeriod &&
dataKey in notionalTransferred.WithinPeriod &&
"*" in notionalTransferred.WithinPeriod[dataKey] &&
"*" in notionalTransferred.WithinPeriod[dataKey]["*"] &&
notionalTransferred.WithinPeriod[dataKey]["*"]["*"] > 0 ? (
<div style={centerStyles}>
<div>
<Typography
variant="h5"
className={animate ? "highlight-new-val" : ""}
>
{notionalTransferred.WithinPeriod[dataKey]["*"]["*"]
? "$" +
amountFormatter(
notionalTransferred.WithinPeriod[dataKey]["*"]["*"]
)
: "..."}
</Typography>
</div>
<div style={{ marginTop: -10 }}>
<Typography variant="subtitle1">sent</Typography>
</div>
</div>
) : (
<div style={centerStyles}>
<div style={{ marginTop: -10 }}>
<Typography variant="body1">
amount sent
<br />
coming soon
</Typography>
</div>
</div>
)}
{!!totalCount && (
<div style={centerStyles}>
<div>
<Typography
variant="h5"
className={animate ? "highlight-new-val" : ""}
>
{amountFormatter(totalCount)}
</Typography>
</div>
<div style={{ marginTop: -10 }}>
<Typography variant="subtitle1"> messages </Typography>
</div>
</div>
)}
</div>
useEffect(() => {
// for chains that do not have a key in the bigtable result, no messages have been seen yet.
if (totals && "TotalCount" in totals && !(dataKey in totals?.TotalCount)) {
// if we have TotalCount, but the dataKey is not in it, no transactions for this chain
setLoading(false)
} else if (!totals) {
setLoading(true)
}
}, [totals?.TotalCount, dataKey])
return (
<Tooltip title={!!totalCount ?
intl.formatMessage({ id: "explorer.clickToView" }) :
loading ? "loading" : intl.formatMessage({ id: "explorer.comingSoon" })}>
<Card
style={{
width: 190,
paddingTop: 10,
}}
className="hover-z-index"
cover={<Icon style={{ height: 140, ...iconStyle }} />}
hoverable={!!totalCount}
bordered={false}
onClick={() => !!totalCount && navigate(`/${intl.locale}/explorer/?emitterChain=${dataKey}`)}
loading={loading}
bodyStyle={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
{totalCount === 0 && <Typography variant="h6">coming soon</Typography>}
</>
);
};
}}
>
<Card.Meta title={title} style={{ margin: '12px 0' }} />
{!!totalCount ? (
<>
<div style={{ display: 'flex', justifyContent: "space-between", alignItems: 'center', gap: 12 }}>
<div><Text type="secondary" style={{ fontSize: 14 }}>last&nbsp;24&nbsp;hours</Text></div>
<div><Text className={animate ? "highlight-new-val" : ""} style={{ fontSize: 26 }}>{lastDayCount}</Text></div>
</div>
<div style={{ display: 'flex', justifyContent: "center", alignItems: 'center', gap: 12 }}>
<div><Text type="secondary" style={{ fontSize: 14 }}>last&nbsp;{totalDays}&nbsp;days</Text></div>
<div><Text className={animate ? "highlight-new-val" : ""} style={{ fontSize: 26 }}>{totalCount}</Text></div>
</div>
{/* <Statistic title={<span>last 24 hours</span>} value={totals?.LastDayCount[dataKey]} style={{ display: 'flex', justifyContent: "space-between", alignItems: 'center', gap: 12 }} valueStyle={{ fontSize: 26 }} /> */}
{/* <Statistic title={<span>last {totalDays} days</span>} value={totals?.TotalCount[dataKey]} style={{ display: 'flex', justifyContent: "center", alignItems: 'center', gap: 12, }} valueStyle={{ fontSize: 26 }} /> */}
</>
) : <Text type="secondary" style={{ height: 86, fontSize: 14 }}>{intl.formatMessage({ id: "explorer.comingSoon" })}</Text>}
</Card>
</Tooltip>
)
}
export default ChainOverviewCard
export default ChainOverviewCard;

View File

@ -0,0 +1,145 @@
import React, { useEffect, useState } from "react";
import { Totals } from "./ExplorerStats";
import { ResponsiveBar, BarDatum } from "@nivo/bar";
import {
chainColors,
chainIdColors,
makeDate,
makeGroupName,
} from "../../utils/explorer";
import { useNetworkContext } from "../../contexts/NetworkContext";
import { chainEnums } from "../../utils/consts";
import { Typography } from "@mui/material";
interface DailyCountProps {
dailyCount: Totals["DailyTotals"];
showBottomLedgend?: boolean;
}
const DailyCountBarChart = (props: DailyCountProps) => {
const { activeNetwork } = useNetworkContext();
const [data, setData] = useState<Array<BarDatum>>([]);
useEffect(() => {
const datum = Object.keys(props.dailyCount).reduce<Array<BarDatum>>(
(accum, date) => {
const chains = props.dailyCount[date];
return [
...accum,
Object.keys(chains).reduce<BarDatum>(
(subAccum, chain) => {
const group = makeGroupName(chain, activeNetwork);
return {
...subAccum,
[group]: chains[chain],
};
},
{ date: date }
),
];
},
[]
);
setData(datum);
}, [props.dailyCount, activeNetwork]);
const keys = chainEnums.slice(1);
const today = new Date().toISOString().slice(0, 10);
return (
<div style={{ height: 400, minWidth: 200, flex: "1", marginBottom: 40 }}>
<Typography variant="subtitle1">Messages/Day</Typography>
<ResponsiveBar
theme={{ textColor: "rgba(255, 255, 255, 0.85)" }}
data={data}
keys={keys}
colors={chainIdColors.slice(1)}
groupMode="stacked"
indexBy="date"
margin={{
top: 10,
right: 0,
bottom: props.showBottomLedgend ? 80 : 24,
left: 40,
}}
padding={0.3}
valueScale={{ type: "linear" }}
indexScale={{ type: "band", round: true }}
borderColor={{ from: "color", modifiers: [["darker", 1.6]] }}
axisTop={null}
axisRight={null}
axisBottom={{
format: (value) => {
if (value === today) {
return "today";
}
return makeDate(value);
},
}}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor={{ from: "color", modifiers: [["darker", 3]] }}
tooltip={(data) => {
let { id, value, indexValue } = data;
return (
<div
style={{
background: "#827db8",
borderRadius: "14px",
padding: "9px 12px",
border: "1px solid rgba(255, 255, 255, 0.85)",
color: "rgba(255, 255, 255, 0.85)",
fontSize: 14,
}}
>
<Typography
variant="subtitle1"
style={{ color: "rgba(255, 255, 255, 0.85)" }}
>
{id} - {makeDate(String(indexValue))}
</Typography>
<div
style={{
display: "flex",
padding: "3px 0",
justifyContent: "flex-end",
}}
>
<span>{value} messages</span>
</div>
</div>
);
}}
/>
{props.showBottomLedgend && (
<div
style={{
display: "flex",
justifyContent: "space-evenly",
width: "100%",
}}
>
{chainEnums.slice(1).map((chain, index) => (
<div key={chain} style={{ display: "flex", alignItems: "center" }}>
<div
style={{
background: chainColors[String(index + 1)],
height: 16,
width: 16,
display: "inline-block",
}}
/>
&nbsp;
<span>{chain}</span>
</div>
))}
</div>
)}
</div>
);
};
export default DailyCountBarChart;

View File

@ -1,120 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Totals } from './ExplorerStats';
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { ResponsiveBar, BarDatum } from '@nivo/bar'
import { makeDate, makeGroupName } from "./utils"
interface DailyCountProps {
dailyCount: Totals["DailyTotals"]
}
const DailyCountColumnChart = (props: DailyCountProps) => {
const intl = useIntl()
const [data, setData] = useState<Array<BarDatum>>([])
useEffect(() => {
const datum = Object.keys(props.dailyCount).reduce<Array<BarDatum>>((accum, key) => {
const val = props.dailyCount[key]
return [...accum, Object.keys(val).reduce<BarDatum>((subAccum, subKey) => {
const group = makeGroupName(subKey)
return {
...subAccum,
[group]: val[subKey],
}
// "SolanaColor": "hsl(259, 70%, 50%)", "EthereumColor": "hsl(43, 70%, 50%)", "BSCColor": "hsl(164, 70%, 50%)", "allColor": "hsl(345, 70%, 50%)"
}, { "date": makeDate(key) })]
}, [])
// console.log('bar datum: ', datum)
// TODO - create a dynamic list of keys
setData(datum)
}, [props.dailyCount])
return (
<div style={{ flexGrow: 1, width: '100%', height: 400, color: 'rgba(0, 0, 0, 0.85)' }}>
<h2>daily totals</h2>
<ResponsiveBar
theme={{ textColor: "rgba(255, 255, 255, 0.85)" }}
data={data}
keys={["All Messages", "Solana", "Ethereum", "BSC"]}
groupMode="grouped"
indexBy="date"
margin={{
top: 50,
right: 130,
bottom: 50,
left: 60
}}
padding={0.3}
valueScale={{ type: 'linear' }}
indexScale={{ type: 'band', round: true }}
colors={{ scheme: 'category10' }}
borderColor={{ from: 'color', modifiers: [['darker', 1.6]] }}
axisTop={null}
axisRight={null}
axisBottom={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'date',
legendPosition: 'middle',
legendOffset: 32
}}
axisLeft={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'messages',
legendPosition: 'middle',
legendOffset: -40
}}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor={{ from: 'color', modifiers: [['darker', 1.6]] }}
legends={[
{
dataFrom: 'keys',
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 120,
translateY: 0,
itemsSpacing: 2,
itemWidth: 100,
itemHeight: 20,
itemDirection: 'left-to-right',
itemOpacity: 0.85,
symbolSize: 20,
effects: [
{
on: 'hover',
style: {
itemOpacity: 1
}
}
]
}
]}
// tooltip={(props) => {
// console.log(props)
// // formattedValue: "21"
// // height: 114
// // hidden: false
// // id: "Ethereum"
// // index: 29
// // indexValue: "09/21"
// // label: "Ethereum - 09/21"
// return <div>tooltip</div>
// }}
/>
</div>
)
}
export default DailyCountColumnChart

View File

@ -1,198 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { Totals } from './ExplorerStats';
import { FormattedMessage, useIntl } from 'gatsby-plugin-intl'
import { Typography } from 'antd'
const { Title } = Typography
import ReactTimeAgo from 'react-time-ago'
import { ResponsiveLine, Serie } from '@nivo/line'
import { makeDate, makeGroupName, chainColors } from "./utils"
import { titleStyles } from '~/styles';
import { NetworkContext } from '../NetworkSelect';
interface DailyCountProps {
dailyCount: Totals["DailyTotals"]
lastFetched?: number
title: string,
emitterChain?: number,
emitterAddress?: string
}
const DailyCountLineChart = (props: DailyCountProps) => {
const intl = useIntl()
const { activeNetwork } = useContext(NetworkContext)
const [data, setData] = useState<Array<Serie>>([])
const colors = [
"hsl(9, 100%, 61%)",
"hsl(30, 100%, 61%)",
"hsl(54, 100%, 61%)",
"hsl(82, 100%, 61%)",
"hsl(114, 100%, 61%)",
"hsl(176, 100%, 61%)",
"hsl(224, 100%, 61%)",
"hsl(270, 100%, 61%)",
"hsl(320, 100%, 61%)",
"hsl(360, 100%, 61%)",
]
useEffect(() => {
const datum = Object.keys(props.dailyCount).reduce<{ [groupKey: string]: Serie }>((accum, key) => {
const vals = props.dailyCount[key]
const subKeyColors: { [key: string]: string } = {}
return Object.keys(vals).reduce<{ [groupKey: string]: Serie }>((subAccum, subKey) => {
if (props.emitterAddress && subKey === "*") {
// if this chart is for a single emitterAddress, no need for "all messages" line.
return subAccum
}
const group = makeGroupName(subKey, activeNetwork, props.emitterChain)
if (!(group in subAccum)) {
// first time this group has been seen
subAccum[group] = { id: group, data: [] }
if (subKey in chainColors) {
subAccum[group].color = chainColors[subKey]
} else {
if (!(subKey in subKeyColors)) {
let len = Object.keys(subKeyColors).length
subKeyColors[subKey] = colors[len]
}
subAccum[group].color = subKeyColors[subKey]
}
}
subAccum[group].data.push({
"y": vals[subKey],
"x": makeDate(key)
})
return subAccum
}, accum)
}, {})
setData(Object.values(datum))
}, [props.dailyCount, props.lastFetched, props.emitterChain, props.emitterAddress, activeNetwork])
const dateLabel = [{
id: "label",
label: "Dates are UTC"
}]
return (
<div style={{ flexGrow: 1, height: 500, width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Title level={3} style={{ ...titleStyles, marginLeft: 20 }}>{props.title}</Title>
{props.lastFetched ? (
<div style={{ marginRight: 40 }}>
<FormattedMessage id="explorer.lastUpdated" />:&nbsp;
<ReactTimeAgo date={new Date(props.lastFetched)} locale={intl.locale} timeStyle="twitter" />
</div>
) : null}
</div>
<ResponsiveLine
theme={{ textColor: "rgba(255, 255, 255, 0.85)", fontSize: 12, legends: { text: { fontSize: 16 } } }}
colors={({ color }) => color}
data={data}
curve={"monotoneX"}
margin={{ top: 20, right: 40, bottom: 160, left: 60 }}
xScale={{ type: 'point' }}
yScale={{
type: 'symlog',
constant: 400,
max: 'auto',
min: 0,
}}
enableGridX={false}
axisTop={null}
axisRight={null}
axisBottom={null}
axisLeft={{
tickPadding: 5,
tickRotation: 0,
}}
pointSize={4}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
enableSlices={"x"}
isInteractive={true}
legends={[
{
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: -20,
translateY: (30 + data.length * 20),
itemsSpacing: 10,
itemDirection: 'right-to-left',
itemWidth: 400,
itemHeight: 16,
itemOpacity: 0.85,
symbolSize: 12,
symbolShape: 'circle',
symbolBorderColor: 'rgba(0, 0, 0, .5)',
effects: [
{
on: 'hover',
style: {
itemBackground: 'rgba(0, 0, 0, .03)',
itemOpacity: 1
}
}
]
},
{
anchor: 'bottom-left',
direction: 'column',
justify: false,
translateX: 0,
translateY: 40,
itemsSpacing: 10,
itemDirection: 'left-to-right',
itemWidth: 60,
itemHeight: 16,
itemOpacity: 0.85,
data: dateLabel,
},
]}
sliceTooltip={({ slice }) => {
return (
<div
style={{
background: '#010114',
padding: '9px 12px',
border: '1px solid rgba(255, 255, 255, 0.85)',
color: "rgba(255, 255, 255, 0.85)",
fontSize: 14
}}
>
<Title level={4} style={{ color: 'rgba(255, 255, 255, 0.85)' }}>{slice.points[0].data.xFormatted}</Title>
{slice.points.map(point => (
<div
key={point.id}
style={{
display: 'flex',
padding: '3px 0',
}}
>
<div style={{ background: point.serieColor, height: 16, width: 16, }} />&nbsp;
<span>{point.serieId}</span>&nbsp;-&nbsp;{point.data.yFormatted}
</div>
))}
</div>
)
}}
/>
</div>
)
}
export default DailyCountLineChart

View File

@ -0,0 +1,143 @@
import React, { useEffect, useState } from 'react';
import { NotionalTransferredTo } from './ExplorerStats';
import { Typography } from '@mui/material';
import { ResponsiveBar, BarDatum } from '@nivo/bar'
import { makeDate, makeGroupName, chainColors, amountFormatter, usdFormatter, chainIdColors } from "../../utils/explorer"
import { useNetworkContext } from '../../contexts/NetworkContext';
import { chainEnums, } from '../../utils/consts';
function findMaxBoundOfIQR(array: number[]): number {
if (array.length < 4) {
array.sort((a, b) => a - b)
return array[0]
}
let values, q1, q3, iqr, maxValue: number
values = array.slice().sort((a, b) => a - b);//copy array fast and sort
if ((values.length / 4) % 1 === 0) {//find quartiles
q1 = 1 / 2 * (values[(values.length / 4)] + values[(values.length / 4) + 1]);
q3 = 1 / 2 * (values[(values.length * (3 / 4))] + values[(values.length * (3 / 4)) + 1]);
} else {
q1 = values[Math.floor(values.length / 4 + 1)];
q3 = values[Math.ceil(values.length * (3 / 4) + 1)];
}
iqr = q3 - q1;
maxValue = q3 + iqr * 1.5;
return maxValue
}
interface DailyCountProps {
daily: NotionalTransferredTo["Daily"]
}
const DailyNotionalBarChart = (props: DailyCountProps) => {
const { activeNetwork } = useNetworkContext()
const [barData, setBarData] = useState<Array<BarDatum>>([])
const [max, setMax] = useState<number>()
useEffect(() => {
// create a list of all data points in order to calculate min/max bounds of chart
const all: number[] = []
const data = Object.keys(props.daily).reduce<Array<BarDatum>>((accum, date) => {
const chains = props.daily[date]
return [...accum, Object.keys(chains).reduce<BarDatum>((subAccum, chain) => {
const group = makeGroupName(chain, activeNetwork)
// const group = chain
all.push(chains[chain]["*"])
return {
...subAccum,
[group]: chains[chain]["*"],
}
}, { "date": date })]
}, [])
// create a max value for the y axis, in order to exclude outliers so the chart looks nice.
let max = findMaxBoundOfIQR(all)
setMax(max)
setBarData(data)
}, [props.daily, activeNetwork])
const keys = chainEnums.slice(1)
const today = new Date().toISOString().slice(0, 10)
return (
<div style={{ height: 400, minWidth: 400, flex: '1', marginBottom: 40 }}>
<Typography variant="h4" style={{ marginLeft: 20 }}>value received (USD)</Typography>
<ResponsiveBar
theme={{ textColor: "rgba(255, 255, 255, 0.85)" }}
colors={chainIdColors.slice(1)}
data={barData}
keys={keys}
enableLabel={false}
groupMode="grouped"
indexBy="date"
margin={{
top: 10,
right: 0,
bottom: 24,
left: 40,
}}
padding={0.3}
valueScale={{ type: 'linear', max }}
indexScale={{ type: 'band', round: true }}
borderColor={{ from: 'color', modifiers: [['darker', 1.6]] }}
axisTop={null}
axisRight={null}
axisLeft={{
format: (value) => amountFormatter(Number(value))
}}
axisBottom={{
format: (value) => {
if (value === today) {
return "today"
}
return makeDate(value)
}
}}
tooltip={(data) => {
let { id, value, indexValue, } = data
return (
<div
style={{
background: '#010114',
padding: '9px 12px',
border: '1px solid rgba(255, 255, 255, 0.85)',
color: "rgba(255, 255, 255, 0.85)",
fontSize: 14
}}
>
<Typography variant="subtitle1" style={{ color: 'rgba(255, 255, 255, 0.85)' }}>{id} - {makeDate(String(indexValue))}</Typography>
<div
style={{
display: 'flex',
padding: '3px 0',
justifyContent: 'flex-end',
}}
>
{usdFormatter.format(Number(value))} received
</div>
</div>
)
}}
/>
</div>
)
}
export default DailyNotionalBarChart

View File

@ -1,205 +1,617 @@
import React, { useContext, useEffect, useState } from 'react';
import { Spin, } from 'antd'
import { useIntl, } from 'gatsby-plugin-intl'
import { BigTableMessage } from '~/components/ExplorerQuery/ExplorerQuery';
import { Box, Card, CircularProgress } from "@mui/material";
import React, { useEffect, useState } from "react";
import { useNetworkContext } from "../../contexts/NetworkContext";
import { ChainID } from "../../utils/consts";
import { contractNameFormatter } from "../../utils/explorer";
import { BigTableMessage } from "../ExplorerSearch/ExplorerQuery";
import RecentMessages from "./RecentMessages";
import ChainOverviewCard from "./ChainOverviewCard";
import PastWeekCard from "./PastWeekCard";
import RecentMessages from './RecentMessages';
import DailyCountLineChart from './DailyCountLineChart';
import { NetworkContext } from '~/components/NetworkSelect';
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 { ReactComponent as PolygonIcon } from '~/icons/polygon.svg';
import ChainOverviewCard from './ChainOverviewCard';
import { ChainID } from '~/utils/misc/constants';
import { contractNameFormatter } from './utils';
import binanceChainIcon from "../../images/bsc.svg";
import ethereumIcon from "../../images/eth.svg";
import solanaIcon from "../../images//solana.svg";
import terraIcon from "../../images/terra.svg";
import polygonIcon from "../../images/polygon.svg";
import avalancheIcon from "../../images/avalanche.svg";
import oasisIcon from "../../images/oasis.svg";
import GridWithCards from "../GridWithCards";
import { explorer } from "../../utils/urls";
export interface Totals {
LastDayCount: { [groupByKey: string]: number }
TotalCount: { [groupByKey: string]: number }
DailyTotals: {
// "2021-08-22": { "*": 0 },
[date: string]: { [groupByKey: string]: number }
}
LastDayCount: { [groupByKey: string]: number };
TotalCount: { [groupByKey: string]: number };
DailyTotals: {
// "2021-08-22": { "*": 0 },
[date: string]: { [groupByKey: string]: number };
};
}
// type GroupByKey = "*" | "emitterChain" | "emitterChain:emitterAddress"
export interface Recent {
[groupByKey: string]: Array<BigTableMessage>
[groupByKey: string]: Array<BigTableMessage>;
}
type GroupBy = undefined | "chain" | "address"
type ForChain = undefined | StatsProps["emitterChain"]
type ForAddress = undefined | StatsProps["emitterAddress"]
interface BidirectionalTransferData {
[leavingChainId: string]: {
[destinationChainId: string]: {
[tokenSymbol: string]: number;
};
};
}
export interface NotionalTransferred {
Last24Hours: BidirectionalTransferData;
WithinPeriod: BidirectionalTransferData;
PeriodDurationDays: Number;
Daily: {
[date: string]: BidirectionalTransferData;
};
}
interface DirectionalTransferData {
[chainId: string]: {
[tokenSymbol: string]: number;
};
}
export interface NotionalTransferredTo {
Last24Hours: DirectionalTransferData;
WithinPeriod: DirectionalTransferData;
PeriodDurationDays: Number;
Daily: {
[date: string]: DirectionalTransferData;
};
}
export interface NotionalTransferredToCumulative {
AllTime: DirectionalTransferData;
AllTimeDurationDays: Number;
Daily: {
[date: string]: DirectionalTransferData;
};
}
type GroupBy = undefined | "chain" | "address";
type ForChain = undefined | StatsProps["emitterChain"];
type ForAddress = undefined | StatsProps["emitterAddress"];
interface StatsProps {
emitterChain?: number,
emitterAddress?: string
emitterChain?: number;
emitterAddress?: string;
}
const Stats: React.FC<StatsProps> = ({ emitterChain, emitterAddress }) => {
const intl = useIntl()
const { activeNetwork } = useContext(NetworkContext)
const ExplorerStats: React.FC<StatsProps> = ({
emitterChain,
emitterAddress,
}) => {
const { activeNetwork } = useNetworkContext();
const [totals, setTotals] = useState<Totals>()
const [recent, setRecent] = useState<Recent>()
const [address, setAddress] = useState<StatsProps["emitterAddress"]>()
const [chain, setChain] = useState<StatsProps["emitterChain"]>()
const [lastFetched, setLastFetched] = useState<number>()
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout>()
const [controller, setController] = useState<AbortController>(new AbortController())
const [totals, setTotals] = useState<Totals>();
const [recent, setRecent] = useState<Recent>();
const [notionalTransferred, setNotionalTransferred] =
useState<NotionalTransferred>();
const [notionalTransferredTo, setNotionalTransferredTo] =
useState<NotionalTransferredTo>();
const [notionalTransferredToCumulative, setNotionalTransferredToCumulative] =
useState<NotionalTransferredToCumulative>();
const [address, setAddress] = useState<StatsProps["emitterAddress"]>();
const [chain, setChain] = useState<StatsProps["emitterChain"]>();
const [lastFetched, setLastFetched] = useState<number>();
const [pollInterval, setPollInterval] = useState<NodeJS.Timeout>();
const [controller, setController] = useState<AbortController>(
new AbortController()
);
const daysSinceDataStart = 30
const launchDate = new Date("2021-09-13T00:00:00.000+00:00");
// calculate the time difference between now and the launch day
const differenceInTime = new Date().getTime() - launchDate.getTime();
// calculate the number of days, rounding up
const daysSinceDataStart = Math.ceil(differenceInTime / (1000 * 3600 * 24));
const fetchTotals = (baseUrl: string, groupBy: GroupBy, forChain: ForChain, forAddress: ForAddress, signal: AbortSignal) => {
const totalsUrl = `${baseUrl}/totals`
let url = `${totalsUrl}?numDays=${daysSinceDataStart}`
if (groupBy) { url = `${url}&groupBy=${groupBy}` }
if (forChain) { url = `${url}&forChain=${forChain}` }
if (forAddress) { url = `${url}&forAddress=${forAddress}` }
return fetch(url, { signal })
.then<Totals>(res => {
if (res.ok) return res.json()
// throw an error with specific message, rather than letting the json decoding throw.
throw 'explorer.stats.failedFetchingTotals'
})
.then(result => {
setTotals(result)
setLastFetched(Date.now())
}, error => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error('failed fetching totals. error: ', error)
}
})
const fetchTotals = (
baseUrl: string,
groupBy: GroupBy,
forChain: ForChain,
forAddress: ForAddress,
signal: AbortSignal
) => {
const totalsUrl = `${baseUrl}totals`;
let url = `${totalsUrl}?${daysSinceDataStart}&daily=true`
if (groupBy) {
url = `${url}&groupBy=${groupBy}`;
}
const fetchRecent = (baseUrl: string, groupBy: GroupBy, forChain: ForChain, forAddress: ForAddress, signal: AbortSignal) => {
const recentUrl = `${baseUrl}/recent`
let url = `${recentUrl}?numRows=24`
if (groupBy) { url = `${url}&groupBy=${groupBy}` }
if (forChain) { url = `${url}&forChain=${forChain}` }
if (forAddress) { url = `${url}&forAddress=${forAddress}` }
return fetch(url, { signal })
.then<Recent>(res => {
if (res.ok) return res.json()
// throw an error with specific message, rather than letting the json decoding throw.
throw 'explorer.stats.failedFetchingRecent'
})
.then(result => {
setRecent(result)
setLastFetched(Date.now())
}, error => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error('failed fetching recent. error: ', error)
}
})
if (forChain) {
url = `${url}&forChain=${forChain}`;
}
if (forAddress) {
url = `${url}&forAddress=${forAddress}`;
}
const getData = (props: StatsProps, baseUrl: string, signal: AbortSignal) => {
let forChain: ForChain = undefined
let forAddress: ForAddress = undefined
let recentGroupBy: GroupBy = undefined
let totalsGroupBy: GroupBy = "chain"
if (props.emitterChain) {
forChain = props.emitterChain
totalsGroupBy = 'address'
recentGroupBy = 'address'
return fetch(url, { signal })
.then<Totals>((res) => {
if (res.ok) return res.json();
// throw an error with specific message, rather than letting the json decoding throw.
throw "explorer.stats.failedFetchingTotals";
})
.then(
(result) => {
setTotals(result);
setLastFetched(Date.now());
},
(error) => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error("failed fetching totals. error: ", error);
}
}
if (props.emitterChain && props.emitterAddress) {
forAddress = props.emitterAddress
);
};
const fetchRecent = (
baseUrl: string,
groupBy: GroupBy,
forChain: ForChain,
forAddress: ForAddress,
signal: AbortSignal
) => {
const recentUrl = `${baseUrl}recent`;
let numRows = 10
if (forChain) {
numRows = 30
}
if (forAddress) {
numRows = 80
}
let url = `${recentUrl}?numRows=${numRows}`;
if (groupBy) {
url = `${url}&groupBy=${groupBy}`;
}
if (forChain) {
url = `${url}&forChain=${forChain}`;
}
if (forAddress) {
url = `${url}&forAddress=${forAddress}`;
}
return fetch(url, { signal })
.then<Recent>((res) => {
if (res.ok) return res.json();
// throw an error with specific message, rather than letting the json decoding throw.
throw "explorer.stats.failedFetchingRecent";
})
.then(
(result) => {
setRecent(result);
setLastFetched(Date.now());
},
(error) => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error("failed fetching recent. error: ", error);
}
}
return Promise.all([
fetchTotals(baseUrl, totalsGroupBy, forChain, forAddress, signal),
fetchRecent(baseUrl, recentGroupBy, forChain, forAddress, signal)
])
);
};
const fetchTransferred = (
baseUrl: string,
groupBy: GroupBy,
forChain: ForChain,
forAddress: ForAddress,
signal: AbortSignal
) => {
const transferredUrl = `${baseUrl}notionaltransferred`;
let url = `${transferredUrl}?forPeriod=true&numDays=${daysSinceDataStart}`; // ${daysSinceDataStart}`
if (groupBy) {
url = `${url}&groupBy=${groupBy}`;
}
if (forChain) {
url = `${url}&forChain=${forChain}`;
}
if (forAddress) {
url = `${url}&forAddress=${forAddress}`;
}
if (groupBy === "address" || forChain || forAddress) {
return Promise.resolve();
}
const pollingController = (emitterChain: StatsProps["emitterChain"], emitterAddress: StatsProps["emitterAddress"], baseUrl: string) => {
// clear any ongoing intervals
if (pollInterval) {
clearInterval(pollInterval)
setPollInterval(undefined)
return fetch(url, { signal })
.then<NotionalTransferred>((res) => {
if (res.ok) return res.json();
// throw an error with specific message, rather than letting the json decoding throw.
throw "explorer.stats.failedFetchingTransferred";
})
.then(
(result) => {
setNotionalTransferred(result);
setLastFetched(Date.now());
},
(error) => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error("failed fetching transferred to. error: ", error);
}
}
// abort any in-flight requests
controller.abort()
// create a new controller for the new fetches, add it to state
const newController = new AbortController();
setController(newController)
// create a signal for requests
const { signal } = newController;
// start polling
let interval = setInterval(() => {
getData({ emitterChain, emitterAddress }, baseUrl, signal)
}, 5000)
setPollInterval(interval)
);
};
const fetchTransferredTo = (
baseUrl: string,
groupBy: GroupBy,
forChain: ForChain,
forAddress: ForAddress,
signal: AbortSignal
) => {
const transferredUrl = `${baseUrl}notionaltransferredto`;
let url = `${transferredUrl}?forPeriod=true&daily=true&numDays=${daysSinceDataStart}`; // ${daysSinceDataStart}`
if (groupBy) {
url = `${url}&groupBy=${groupBy}`;
}
if (forChain) {
url = `${url}&forChain=${forChain}`;
}
if (forAddress) {
url = `${url}&forAddress=${forAddress}`;
}
if (groupBy === "address" || forChain || forAddress) {
return Promise.resolve();
}
useEffect(() => {
// getData if first load (no totals or recents), or emitterAddress/emitterChain changed.
if (!totals && !recent || emitterAddress !== address || emitterChain !== chain) {
getData({ emitterChain, emitterAddress }, activeNetwork.endpoints.bigtableFunctionsBase, new AbortController().signal)
return fetch(url, { signal })
.then<NotionalTransferredTo>((res) => {
if (res.ok) return res.json();
// throw an error with specific message, rather than letting the json decoding throw.
throw "explorer.stats.failedFetchingTransferredTo";
})
.then(
(result) => {
setNotionalTransferredTo(result);
setLastFetched(Date.now());
},
(error) => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error("failed fetching transferred to. error: ", error);
}
}
controller.abort()
setTotals(undefined)
setRecent(undefined)
pollingController(emitterChain, emitterAddress, activeNetwork.endpoints.bigtableFunctionsBase)
// hold chain & address in state to detect changes
setChain(emitterChain)
setAddress(emitterAddress)
}, [emitterChain, emitterAddress, activeNetwork.endpoints.bigtableFunctionsBase])
useEffect(() => {
return function cleanup() {
if (pollInterval) {
clearInterval(pollInterval)
}
};
}, [pollInterval, activeNetwork.endpoints.bigtableFunctionsBase])
let title = "Recent messages"
let hideTableTitles = false
if (emitterChain) {
title = `Recent ${ChainID[Number(emitterChain)]} messages`
);
};
const fetchTransferredToCumulative = (
baseUrl: string,
groupBy: GroupBy,
forChain: ForChain,
forAddress: ForAddress,
signal: AbortSignal
) => {
const transferredToUrl = `${baseUrl}notionaltransferredtocumulative`;
let url = `${transferredToUrl}?allTime=true`; // &daily=true&numDays=${daysSinceDataStart}` // TEMP - rm daily=true //${daysSinceDataStart}`
if (groupBy) {
url = `${url}&groupBy=${groupBy}`;
}
if (emitterChain && emitterAddress) {
title = `Recent ${contractNameFormatter(emitterAddress, emitterChain, activeNetwork)} messages`
hideTableTitles = true
if (forChain) {
url = `${url}&forChain=${forChain}`;
}
if (forAddress) {
url = `${url}&forAddress=${forAddress}`;
}
if (groupBy === "address" || forChain || forAddress) {
return Promise.resolve();
}
return (
return fetch(url, { signal })
.then<NotionalTransferredToCumulative>((res) => {
if (res.ok) return res.json();
// throw an error with specific message, rather than letting the json decoding throw.
throw "explorer.stats.failedFetchingTransferredTo";
})
.then(
(result) => {
// let today = "2021-12-03"
// let { [today]: t, ...dailies } = result.Daily
// let r = { ...result, Daily: dailies }
// setNotionalTransferredTo(r)
setNotionalTransferredToCumulative(result);
setLastFetched(Date.now());
},
(error) => {
if (error.name !== "AbortError") {
// handle errors here instead of a catch(), so that we don't swallow exceptions from components
console.error("failed fetching transferred to. error: ", error);
}
}
);
};
const getData = (props: StatsProps, baseUrl: string, signal: AbortSignal) => {
let forChain: ForChain = undefined;
let forAddress: ForAddress = undefined;
let recentGroupBy: GroupBy = undefined;
let totalsGroupBy: GroupBy = "chain";
if (props.emitterChain) {
forChain = props.emitterChain;
totalsGroupBy = "address";
recentGroupBy = "address";
}
if (props.emitterChain && props.emitterAddress) {
forAddress = props.emitterAddress;
}
return Promise.all([
fetchTotals(baseUrl, totalsGroupBy, forChain, forAddress, signal),
fetchRecent(baseUrl, recentGroupBy, forChain, forAddress, signal),
fetchTransferred(baseUrl, recentGroupBy, forChain, forAddress, signal),
fetchTransferredTo(baseUrl, recentGroupBy, forChain, forAddress, signal),
fetchTransferredToCumulative(
baseUrl,
recentGroupBy,
forChain,
forAddress,
signal
),
]);
};
const pollingController = (
emitterChain: StatsProps["emitterChain"],
emitterAddress: StatsProps["emitterAddress"],
baseUrl: string
) => {
// clear any ongoing intervals
if (pollInterval) {
clearInterval(pollInterval);
setPollInterval(undefined);
}
// abort any in-flight requests
controller.abort();
// create a new controller for the new fetches, add it to state
const newController = new AbortController();
setController(newController);
// create a signal for requests
const { signal } = newController;
// start polling
let interval = setInterval(() => {
getData({ emitterChain, emitterAddress }, baseUrl, signal);
}, 12000);
setPollInterval(interval);
};
useEffect(() => {
// getData if first load (no totals or recents), or emitterAddress/emitterChain changed.
if (
(!totals && !recent) ||
emitterAddress !== address ||
emitterChain !== chain
) {
getData(
{ emitterChain, emitterAddress },
activeNetwork.endpoints.bigtableFunctionsBase,
new AbortController().signal
);
}
controller.abort();
setTotals(undefined);
setRecent(undefined);
setNotionalTransferred(undefined);
setNotionalTransferredTo(undefined);
setNotionalTransferredToCumulative(undefined);
pollingController(
emitterChain,
emitterAddress,
activeNetwork.endpoints.bigtableFunctionsBase
);
// hold chain & address in state to detect changes
setChain(emitterChain);
setAddress(emitterAddress);
}, [
emitterChain,
emitterAddress,
activeNetwork.endpoints.bigtableFunctionsBase,
]);
useEffect(() => {
return function cleanup() {
if (pollInterval) {
clearInterval(pollInterval);
}
};
}, [pollInterval, activeNetwork.endpoints.bigtableFunctionsBase]);
let title = "Recent messages";
let hideTableTitles = false;
if (emitterChain) {
title = `Recent ${ChainID[Number(emitterChain)]} messages`;
}
return (
<>
{!totals && !recent ? (
<Card
sx={{
backgroundColor: "rgba(255,255,255,.07)",
backgroundImage: "none",
borderRadius: "28px",
padding: "24px",
textAlign: "center",
mt: 5,
}}
>
<CircularProgress />
</Card>
) : (
<>
{!emitterChain && !emitterAddress &&
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end', flexWrap: 'wrap', marginBottom: 40, gap: 20 }}>
<ChainOverviewCard totalDays={daysSinceDataStart} totals={totals} dataKey="1" title={ChainID[1]} Icon={SolanaIcon} iconStyle={{ height: 120, margin: '10px 0' }} />
<ChainOverviewCard totalDays={daysSinceDataStart} totals={totals} dataKey="2" title={ChainID[2]} Icon={EthereumIcon} />
<ChainOverviewCard totalDays={daysSinceDataStart} totals={totals} dataKey="3" title={ChainID[3]} Icon={TerraIcon} />
<ChainOverviewCard totalDays={daysSinceDataStart} totals={totals} dataKey="4" title={ChainID[4]} Icon={BinanceChainIcon} />
<ChainOverviewCard totalDays={daysSinceDataStart} totals={totals} dataKey="5" title={ChainID[5]} Icon={PolygonIcon} />
</div>
}
<Spin spinning={!totals && !recent} style={{ width: '100%', height: 500 }} >
<div>
<DailyCountLineChart
dailyCount={totals?.DailyTotals || {}}
lastFetched={lastFetched}
title="messages/day"
emitterChain={emitterChain}
emitterAddress={emitterAddress}
/>
</div>
{recent && (
<div style={{ margin: "40px 0" }}>
<RecentMessages
recent={recent}
lastFetched={lastFetched}
title={title}
hideTableTitles={hideTableTitles}
/>
</div>
)}
{recent && <RecentMessages recent={recent} lastFetched={lastFetched} title={title} hideTableTitles={hideTableTitles} />}
</Spin>
{!emitterChain && !emitterAddress ? (
totals && notionalTransferredToCumulative && notionalTransferred ? (
<GridWithCards
spacing={3}
sm={6}
md={3}
cardPaddingTop={3}
imgAlignMd="center"
imgOffsetRightMd="0px"
imgOffsetTopXs="0px"
imgOffsetTopMd="-36px"
imgOffsetTopMdHover="-52px"
imgPaddingBottomXs={3}
headerTextAlign="center"
data={[
{
header: ChainID[1],
src: solanaIcon,
to: `${explorer}?emitterChain=1`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="1"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[2],
src: ethereumIcon,
to: `${explorer}?emitterChain=2`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="2"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[3],
src: terraIcon,
to: `${explorer}?emitterChain=3`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="3"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[4],
src: binanceChainIcon,
to: `${explorer}?emitterChain=4`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="4"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[5],
src: polygonIcon,
to: `${explorer}?emitterChain=5`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="5"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[6],
src: avalancheIcon,
to: `${explorer}?emitterChain=6`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="6"
/>
),
imgStyle: { height: 110 },
},
{
header: ChainID[7],
src: oasisIcon,
to: `${explorer}?emitterChain=7`,
description: (
<ChainOverviewCard
totals={totals}
notionalTransferredToCumulative={
notionalTransferredToCumulative
}
notionalTransferred={notionalTransferred}
dataKey="7"
/>
),
imgStyle: { height: 110 },
},
]}
/>
) : (
<Box
sx={{
padding: "24px",
textAlign: "center",
}}
>
<CircularProgress />
</Box>
)
) : null}
<div style={{ margin: "40px 0" }}>
{!emitterChain && !emitterAddress ? (
notionalTransferredTo && totals ? (
<PastWeekCard
title="Last 7 Days"
numDaysToShow={7}
messages={totals}
notionalTransferredTo={notionalTransferredTo}
notionalTransferred={notionalTransferred}
/>
) : (
<Card
sx={{
backgroundColor: "rgba(255,255,255,.07)",
backgroundImage: "none",
borderRadius: "28px",
padding: "24px",
textAlign: "center",
}}
>
<CircularProgress />
</Card>
)
) : null}
</div>
</>
)
}
)}
</>
);
};
export default Stats
export default ExplorerStats;

View File

@ -0,0 +1,111 @@
import React from "react";
import { Box, Card, Typography } from "@mui/material";
import {
NotionalTransferred,
NotionalTransferredTo,
Totals,
} from "./ExplorerStats";
import { chainColors } from "../../utils/explorer";
import DailyNotionalBarChart from "./DailyNotionalBarChart";
import DailyCountBarChart from "./DailyCountBarChart";
import { chainEnums } from "../../utils/consts";
interface PastWeekCardProps {
title: string;
messages: Totals;
numDaysToShow: number;
notionalTransferred?: NotionalTransferred;
notionalTransferredTo: NotionalTransferredTo;
}
const PastWeekCard: React.FC<PastWeekCardProps> = ({
title,
messages,
numDaysToShow,
notionalTransferredTo,
}) => {
const dates = [...Array(numDaysToShow)]
.map((_, i) => {
const d = new Date();
d.setDate(d.getDate() - i);
return d;
})
.map((d) => {
const isoStr = d.toISOString();
return isoStr.slice(0, 10);
})
.reverse();
let messagesForPeriod = dates
.filter((date) => messages && date in messages?.DailyTotals)
.reduce<{ [date: string]: { [groupByKey: string]: number } }>(
(accum, key) => ({ ...accum, [key]: messages.DailyTotals[key] }),
Object()
);
let notionalTransferredToInPeriod = dates
.filter((date) => date in notionalTransferredTo.Daily)
.reduce<NotionalTransferredTo["Daily"]>(
(accum, key) => ((accum[key] = notionalTransferredTo.Daily[key]), accum),
Object()
);
return (
<Card
sx={{
backgroundColor: "rgba(255,255,255,.07)",
backgroundImage: "none",
borderRadius: "28px",
padding: "24px",
}}
>
<Typography variant="h4" gutterBottom>
{title}
</Typography>
<div
style={{
display: "flex",
justifyContent: "space-evenly",
alignItems: "center",
flexWrap: "wrap",
gap: 16,
marginBottom: 10,
}}
>
<DailyCountBarChart dailyCount={messagesForPeriod} />
{/* <DailyNotionalBarChart daily={notionalTransferredToInPeriod} /> */}
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-evenly",
width: "100%",
}}
>
{chainEnums.slice(1).map((chain, index) => (
<Box
key={chain}
sx={{ display: "flex", alignItems: "center", mx: 1 }}
>
<div
style={{
background: chainColors[String(index + 1)],
height: 12,
width: 12,
display: "inline-block",
}}
/>
<div>&nbsp;{chain}</div>
</Box>
))}
</div>
</Card>
);
};
export default PastWeekCard;

View File

@ -1,17 +0,0 @@
// import theme to get antd less variables + overrides from AntdTheme.js
@import "~antd/lib/style/themes/dark";
.highlight-new-row {
animation: highlight-row 1500ms ease-out;
}
@keyframes highlight-row {
0% {
color: black;
background: darken(@primary-color, 5%);
}
100% {
color: @text-color;
background: @body-background;
}
}

View File

@ -1,136 +1,230 @@
import React from 'react';
import { Recent } from './ExplorerStats';
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { Grid, Table, Typography } from 'antd'
const { useBreakpoint } = Grid
const { Title } = Typography
import ReactTimeAgo from 'react-time-ago'
import { BigTableMessage } from '../ExplorerQuery/ExplorerQuery';
import { chainIDs, ChainID } from '~/utils/misc/constants';
import { Link } from 'gatsby';
import { ColumnsType } from 'antd/es/table';
import { titleStyles } from '~/styles';
import { DecodePayload } from '../Payload';
import { contractNameFormatter } from './utils';
import './RecentMessages.less'
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 { ReactComponent as PolygonIcon } from '~/icons/polygon.svg'
import { formatQuorumDate } from '~/utils/misc/utils';
import React from "react";
import { Link as RouterLink } from "gatsby";
import { Recent } from "./ExplorerStats";
import ReactTimeAgo from "react-time-ago";
import {
Box,
Card,
Link,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { contractNameFormatter } from "../../utils/explorer";
import { ChainID, chainIDs } from "../../utils/consts";
import { formatQuorumDate } from "../../utils/time";
import { explorer } from "../../utils/urls";
import ChainIcon from "../ChainIcon";
import { DecodePayload } from "../DecodePayload";
interface RecentMessagesProps {
recent: Recent
lastFetched?: number
title: string
hideTableTitles?: boolean
recent: Recent;
lastFetched?: number;
title: string;
hideTableTitles?: boolean;
}
const networkIcons = [
<></>,
<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' }} />,
<PolygonIcon key="5" style={{ height: 18, margin: '0 4px' }} />,
]
const RecentMessages = (props: RecentMessagesProps) => {
const intl = useIntl()
const screens = useBreakpoint()
const columns: ColumnsType<BigTableMessage> = [
{ title: '', key: 'icon', render: (item: BigTableMessage) => networkIcons[chainIDs[item.EmitterChain]], responsive: ['sm'] },
{
title: "contract",
key: "contract",
render: (item: BigTableMessage) => {
const name = contractNameFormatter(item.EmitterAddress, chainIDs[item.EmitterChain])
return <div>{name}</div>
},
responsive: ['sm']
},
{
title: "message",
key: "payload",
render: (item: BigTableMessage) => item.SignedVAABytes ? <DecodePayload
base64VAA={item.SignedVAABytes}
emitterChainName={item.EmitterChain}
emitterAddress={item.EmitterAddress}
showType={true}
showSummary={true}
transferDetails={item.TransferDetails}
/> : null
},
{
title: "sequence",
key: "sequence",
render: (item: BigTableMessage) => {
let sequence = item.Sequence.replace(/^0+/, "")
if (!sequence) sequence = "0"
// const columns: ColumnsType<BigTableMessage> = [
// {
// title: "",
// key: "icon",
// render: (item: BigTableMessage) =>
// networkIcons[chainIDs[item.EmitterChain]],
// responsive: ["sm"],
// },
// {
// title: "contract",
// key: "contract",
// render: (item: BigTableMessage) => {
// const name = contractNameFormatter(
// item.EmitterAddress,
// chainIDs[item.EmitterChain]
// );
// return <div>{name}</div>;
// },
// responsive: ["sm"],
// },
// {
// title: "message",
// key: "payload",
// render: (item: BigTableMessage) =>
// item.SignedVAABytes ? (
// <DecodePayload
// base64VAA={item.SignedVAABytes}
// emitterChainName={item.EmitterChain}
// emitterAddress={item.EmitterAddress}
// showType={true}
// showSummary={true}
// transferDetails={item.TransferDetails}
// />
// ) : null,
// },
// {
// title: "sequence",
// key: "sequence",
// render: (item: BigTableMessage) => {
// let sequence = item.Sequence.replace(/^0+/, "");
// if (!sequence) sequence = "0";
return sequence
},
responsive: ['md']
},
{
title: "attested",
dataIndex: "QuorumTime",
key: "time",
render: QuorumTime => <ReactTimeAgo date={QuorumTime ? Date.parse(formatQuorumDate(QuorumTime)) : new Date()} locale={intl.locale} timeStyle={!screens.md ? "twitter" : "round"} />
},
{
title: "",
key: "view",
render: (item: BigTableMessage) => <Link to={`/${intl.locale}/explorer/?emitterChain=${chainIDs[item.EmitterChain]}&emitterAddress=${item.EmitterAddress}&sequence=${item.Sequence}`}>View</Link>
},
]
// return sequence;
// },
// responsive: ["md"],
// },
// {
// title: "attested",
// dataIndex: "QuorumTime",
// key: "time",
// render: (QuorumTime) => (
// <ReactTimeAgo
// date={
// QuorumTime ? Date.parse(formatQuorumDate(QuorumTime)) : new Date()
// }
// locale={intl.locale}
// timeStyle={!screens.md ? "twitter" : "round"}
// />
// ),
// },
// {
// title: "",
// key: "view",
// render: (item: BigTableMessage) => (
// <Link
// to={`/${intl.locale}/explorer/?emitterChain=${
// chainIDs[item.EmitterChain]
// }&emitterAddress=${item.EmitterAddress}&sequence=${item.Sequence}`}
// >
// View
// </Link>
// ),
// },
// ];
const formatKey = (key: string) => {
if (props.hideTableTitles) {
return null
}
if (key.includes(":")) {
const parts = key.split(":")
const link = `/${intl.locale}/explorer/?emitterChain=${parts[0]}&emitterAddress=${parts[1]}`
return <Title level={4} style={titleStyles}>From {ChainID[Number(parts[0])]} contract: <Link to={link}>{contractNameFormatter(parts[1], Number(parts[0]))}</Link></Title>
} else if (key === "*") {
return <Title level={4} style={titleStyles}>From all chains and addresses</Title>
} else {
return <Title level={4} style={titleStyles}>From {ChainID[Number(key)]}</Title>
}
const formatKey = (key: string) => {
if (props.hideTableTitles) {
return null;
}
if (key.includes(":")) {
const parts = key.split(":");
const link = `${explorer}?emitterChain=${parts[0]}&emitterAddress=${parts[1]}`;
return (
<Typography variant="subtitle1" gutterBottom>
From {ChainID[Number(parts[0])]} contract:{" "}
<Link component={RouterLink} to={link} color="inherit">
{contractNameFormatter(parts[1], Number(parts[0]))}
</Link>
</Typography>
);
} else if (key === "*") {
return (
<Typography variant="subtitle1" gutterBottom>
From all chains and addresses
</Typography>
);
} else {
return (
<Typography variant="subtitle1" gutterBottom>
From {ChainID[Number(key)]}
</Typography>
);
}
};
return (
<>
<Title level={3} style={titleStyles} >{props.title}</Title>
{Object.keys(props.recent).map(key => (
<Table<BigTableMessage>
key={key}
rowKey={(item) => item.EmitterAddress + item.Sequence}
style={{ marginBottom: 40 }}
size={screens.lg ? "large" : "small"}
columns={columns}
dataSource={props.recent[key]}
title={() => formatKey(key)}
pagination={false}
rowClassName="highlight-new-row"
footer={() => {
return props.lastFetched ? (
<span>
<FormattedMessage id="explorer.lastUpdated" />:&nbsp;
<ReactTimeAgo date={new Date(props.lastFetched)} locale={intl.locale} timeStyle="twitter" />
</span>
return (
<Card
sx={{
backgroundColor: "rgba(255,255,255,.07)",
backgroundImage: "none",
borderRadius: "28px",
padding: "24px",
}}
>
<Typography variant="h4" gutterBottom>
{props.title}
</Typography>
{Object.keys(props.recent).map((key) => (
<TableContainer key={key}>
{formatKey(key)}
<Table size="small">
<TableBody>
{props.recent[key].map((item) => (
<TableRow key={item.EmitterAddress + item.Sequence}>
<TableCell>
<ChainIcon chainId={chainIDs[item.EmitterChain]} />
</TableCell>
<TableCell>
{contractNameFormatter(
item.EmitterAddress,
chainIDs[item.EmitterChain]
)}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>
{item.SignedVAABytes
? <DecodePayload
base64VAA={item.SignedVAABytes}
emitterChainName={item.EmitterChain}
emitterAddress={item.EmitterAddress}
showType={true}
showSummary={true}
transferDetails={item.TransferDetails}
/> : null}
</TableCell>
<TableCell sx={{ whiteSpace: "nowrap" }}>
{item.Sequence.replace(/^0+/, "") || "0"}
</TableCell>
<TableCell sx={{ "& > time": { whiteSpace: "nowrap" } }}>
{
<ReactTimeAgo
date={
item.QuorumTime
? Date.parse(formatQuorumDate(item.QuorumTime))
: new Date()
}
timeStyle={"round"}
/>
}
</TableCell>
<TableCell>
<Link
component={RouterLink}
to={`${explorer}?emitterChain=${
chainIDs[item.EmitterChain]
}&emitterAddress=${item.EmitterAddress}&sequence=${
item.Sequence
}`}
color="inherit"
>
View
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell
colSpan={6}
sx={{ textAlign: "right", borderBottom: "none" }}
>
{props.lastFetched ? (
<ReactTimeAgo
date={new Date(props.lastFetched)}
timeStyle="round"
/>
) : null}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
))}
</Card>
);
};
) : null
}}
/>
))}
</>
)
}
export default RecentMessages
export default RecentMessages;

View File

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

View File

@ -1,144 +0,0 @@
import { useContext } from 'react'
import { Bech32, fromHex } from "@cosmjs/encoding"
import { PublicKey } from '@solana/web3.js';
import { chainEnums, ChainID, chainIDs } from '~/utils/misc/constants';
import { ActiveNetwork, NetworkContext } from "~/components/NetworkSelect";
const makeDate = (date: string): string => {
const [_, month, day] = date.split("-")
if (!month || !day) {
throw Error("Invalid date supplied to makeDate. Expects YYYY-MM-DD.")
}
return `${month}/${day}`
}
const makeGroupName = (groupKey: string, activeNetwork: ActiveNetwork, emitterChain?: number): string => {
let ALL = "All Wormhole messages"
if (emitterChain) {
ALL = `All ${chainEnums[emitterChain]} messages`
}
let group = groupKey === "*" ? ALL : groupKey
if (group.includes(":")) {
// subKey is chainID:addresss
let parts = groupKey.split(":")
group = `${ChainID[Number(parts[0])]} ${contractNameFormatter(parts[1], Number(parts[0]), activeNetwork)}`
} else if (group != ALL) {
// subKey is a chainID
group = ChainID[Number(groupKey)]
}
return group
}
const getNativeAddress = (chainId: number, emitterAddress: string, activeNetwork?: ActiveNetwork): string => {
let nativeAddress = ""
if (chainId === chainIDs["ethereum"] || chainId === chainIDs["bsc"] || chainId === chainIDs["polygon"]) {
// remove zero-padding
let unpadded = emitterAddress.slice(-40)
nativeAddress = `0x${unpadded}`.toLowerCase()
} else if (chainId === chainIDs["terra"]) {
// remove zero-padding
let unpadded = emitterAddress.slice(-40)
nativeAddress = Bech32.encode("terra", fromHex(unpadded)).toLowerCase()
} else if (chainId === chainIDs["solana"]) {
if (!activeNetwork) {
activeNetwork = useContext(NetworkContext).activeNetwork
}
const chainName = chainEnums[chainId].toLowerCase()
// use the "chains" map of hex: nativeAdress first
if (emitterAddress in activeNetwork.chains[chainName]) {
let desc = activeNetwork.chains[chainName][emitterAddress]
if (desc in activeNetwork.chains[chainName]) {
// lookup the contract address
nativeAddress = activeNetwork.chains[chainName][desc]
}
} else {
let hex = fromHex(emitterAddress)
let pubKey = new PublicKey(hex)
nativeAddress = pubKey.toString()
}
}
return nativeAddress
}
const truncateAddress = (address: string): string => {
return `${address.slice(0, 4)}...${address.slice(-4)}`
}
const contractNameFormatter = (address: string, chainId: number, activeNetwork?: ActiveNetwork): string => {
if (!activeNetwork) {
activeNetwork = useContext(NetworkContext).activeNetwork
}
const chainName = chainEnums[chainId].toLowerCase()
let nativeAddress = getNativeAddress(chainId, address, activeNetwork)
let truncated = truncateAddress(nativeAddress || address)
let formatted = truncated
if (nativeAddress in activeNetwork.chains[chainName]) {
// add the description of the contract, if we know it
let desc = activeNetwork.chains[chainName][nativeAddress]
formatted = `${desc} (${truncated})`
}
return formatted
}
const nativeExplorerContractUri = (chainId: number, address: string, activeNetwork?: ActiveNetwork): string => {
if (!activeNetwork) {
activeNetwork = useContext(NetworkContext).activeNetwork
}
const nativeAddress = getNativeAddress(chainId, address, activeNetwork)
if (nativeAddress) {
if (chainId === chainIDs["solana"]) {
let base = "https://explorer.solana.com/address/"
return `${base}${nativeAddress}`
} else if (chainId === chainIDs["ethereum"]) {
let base = "https://etherscan.io/address/"
return `${base}${nativeAddress}`
} else if (chainId === chainIDs["terra"]) {
let base = "https://finder.terra.money/columbus-5/address/"
return `${base}${nativeAddress}`
} else if (chainId === chainIDs["bsc"]) {
let base = "https://bscscan.com/address/"
return `${base}${nativeAddress}`
} else if (chainId === chainIDs["polygon"]) {
let base = "https://polygonscan.com/address/"
return `${base}${nativeAddress}`
}
}
return ""
}
const nativeExplorerTxUri = (chainId: number, transactionId: string): string => {
if (chainId === chainIDs["solana"]) {
let base = "https://explorer.solana.com/address/"
return `${base}${transactionId}`
} else if (chainId === chainIDs["ethereum"]) {
let base = "https://etherscan.io/tx/"
return `${base}${transactionId}`
} else if (chainId === chainIDs["terra"]) {
let base = "https://finder.terra.money/columbus-5/tx/"
return `${base}${transactionId}`
} else if (chainId === chainIDs["bsc"]) {
let base = "https://bscscan.com/tx/"
return `${base}${transactionId}`
} else if (chainId === chainIDs["polygon"]) {
let base = "https://polygonscan.com/tx/"
return `${base}${transactionId}`
}
return ""
}
const chainColors: { [chain: string]: string } = {
"*": "hsl(183, 100%, 61%)",
"1": "hsl(297, 100%, 61%)",
"2": "hsl(235, 5%, 43%)",
"3": "hsl(235, 100%, 61%)",
"4": "hsl(54, 100%, 61%)",
"5": "hsl(271, 100%, 61%)",
}
export { makeDate, makeGroupName, chainColors, truncateAddress, contractNameFormatter, nativeExplorerContractUri, nativeExplorerTxUri, getNativeAddress }

View File

@ -1,154 +0,0 @@
import React 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 { DecodePayload } from '~/components/Payload'
import ReactTimeAgo from 'react-time-ago'
import { titleStyles } from '~/styles';
import { CloseOutlined, ReloadOutlined } from '@ant-design/icons';
import { Link } from 'gatsby';
import { contractNameFormatter, getNativeAddress, nativeExplorerContractUri, nativeExplorerTxUri, truncateAddress } from '../ExplorerStats/utils';
import { OutboundLink } from 'gatsby-plugin-google-gtag';
import { ChainID, chainIDs } from '~/utils/misc/constants';
import { hexToNativeString } from '@certusone/wormhole-sdk';
interface SummaryProps {
emitterChain?: number,
emitterAddress?: string,
sequence?: string
txId?: string
message: BigTableMessage
polling?: boolean
lastFetched?: number
refetch: () => void
}
const textStyles = { fontSize: 16, margin: '6px 0' }
const Summary = (props: SummaryProps) => {
const intl = useIntl()
const { SignedVAA, ...message } = props.message
const { EmitterChain, EmitterAddress, InitiatingTxID, TokenTransferPayload, TransferDetails } = message
// get chainId from chain name
let chainId = chainIDs[EmitterChain]
let transactionId: string | undefined
if (InitiatingTxID) {
if (chainId === chainIDs["ethereum"] || chainId === chainIDs["bsc"] || chainId === chainIDs["polygon"]) {
transactionId = InitiatingTxID
} else {
if (chainId === chainIDs["solana"]) {
const txId = InitiatingTxID.slice(2) // remove the leading "0x"
transactionId = hexToNativeString(txId, chainId)
} else if (chainId === chainIDs["terra"]) {
transactionId = InitiatingTxID.slice(2) // remove the leading "0x"
}
}
}
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>
</>
) : (
<div>
<Button onClick={props.refetch} icon={<ReloadOutlined />} size="large" shape="round" >refresh</Button>
<Link to={`/${intl.locale}/explorer`} style={{ marginLeft: 8 }}>
<Button icon={<CloseOutlined />} size='large' shape="round">clear</Button>
</Link>
</div>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: "20px 0 24px 20px" }}>
{EmitterChain && EmitterAddress && nativeExplorerContractUri(chainId, EmitterAddress) ?
<div>
<span style={textStyles}>This message was sent to the {ChainID[chainId]} </span>
<OutboundLink
href={nativeExplorerContractUri(chainId, EmitterAddress)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: 'nowrap' }}
>
{contractNameFormatter(EmitterAddress, chainId)}
</OutboundLink>
<span style={textStyles}> contract</span>
{transactionId &&
<>
<span style={textStyles}>, transaction </span>
<OutboundLink
href={nativeExplorerTxUri(chainId, transactionId)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: 'nowrap' }}
>
{truncateAddress(transactionId)}
</OutboundLink>
</>} <span style={textStyles}>.</span>
</div> : null}
{TokenTransferPayload &&
TokenTransferPayload.TargetAddress &&
TransferDetails &&
nativeExplorerContractUri(Number(TokenTransferPayload.TargetChain), TokenTransferPayload.TargetAddress) ?
<div>
<span style={textStyles}>This message is a token transfer, moving {Math.round(Number(TransferDetails.Amount) * 100) / 100}{` `}
{!["UST", "LUNA"].includes(TransferDetails.OriginSymbol) ? <OutboundLink
href={nativeExplorerContractUri(Number(TokenTransferPayload.OriginChain), TokenTransferPayload.OriginAddress)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: 'nowrap' }}
>
{TransferDetails.OriginSymbol}
</OutboundLink> : TransferDetails.OriginSymbol}
{` `}from {ChainID[chainId]}, to {ChainID[Number(TokenTransferPayload.TargetChain)]}, to address </span>
<OutboundLink
href={nativeExplorerContractUri(Number(TokenTransferPayload.TargetChain), TokenTransferPayload.TargetAddress)}
target="_blank"
rel="noopener noreferrer"
style={{ ...textStyles, whiteSpace: 'nowrap' }}
>
{truncateAddress(getNativeAddress(Number(TokenTransferPayload.TargetChain), TokenTransferPayload.TargetAddress))}
</OutboundLink>
</div> : null}
</div>
<Title level={3} style={titleStyles}>Raw message data:</Title>
<div className="styled-scrollbar">
<pre
style={{ fontSize: 14, marginBottom: 20 }}
>{JSON.stringify(message, undefined, 2)}</pre>
</div>
<DecodePayload
base64VAA={props.message.SignedVAABytes}
emitterChainName={props.message.EmitterChain}
emitterAddress={props.message.EmitterAddress}
showPayload={true}
transferDetails={props.message.TransferDetails}
/>
<div className="styled-scrollbar">
<Title level={3} style={titleStyles}>Signed VAA</Title>
<pre
style={{ fontSize: 12, marginBottom: 20 }}
>{JSON.stringify(SignedVAA, undefined, 2)}</pre>
</div>
<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>
</>
)
}
export default Summary

View File

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

View File

@ -0,0 +1,250 @@
import { Box, IconButton, Link, Typography } from "@mui/material";
import { Link as RouterLink } from "gatsby";
import React from "react";
import Discord from "../images/Discord.svg";
import shape from "../images/footer/shape.svg";
import Github from "../images/Github.svg";
import Medium from "../images/Medium.svg";
import Telegram from "../images/Telegram.svg";
import Twitter from "../images/Twitter.svg";
import {
apps,
blog,
buidl,
discord,
docs,
explorer,
github,
jobs,
network,
portal,
telegram,
twitter,
} from "../utils/urls";
import LogoLink from "./LogoLink";
const linkStyle = {
display: "block",
mr: { xs: 0, md: 7.5 },
mb: 1.5,
fontSize: 14,
textUnderlineOffset: 6,
};
const linkActiveStyle = { textDecoration: "underline" };
const socialIcon = {
"& img": {
height: 24,
width: 24,
},
};
const Footer = () => (
<Box sx={{ position: "relative" }}>
<Box
sx={{
position: "absolute",
zIndex: -1,
transform: { xs: "", md: "translate(-50%, 0%)" },
background: `url(${shape})`,
backgroundRepeat: "no-repeat",
backgroundPosition: { xs: "center center", md: "right top -426px" },
// backgroundSize: "cover",
width: "100%",
height: { xs: "100%", md: 540 },
}}
/>
<Box
sx={{
maxWidth: 1100,
mx: "auto",
pt: 21.5,
pb: { xs: 6.5, md: 12 },
}}
>
<Box
sx={{
display: "flex",
flexWrap: { xs: null, md: "wrap" },
flexDirection: { xs: "column", md: "row" },
alignItems: { xs: "center", md: "unset" },
mx: 3.5,
borderTop: "1px solid #585587",
pt: 7,
}}
>
<Box
sx={{
pl: { xs: 0, md: 2 },
pb: 2,
borderTop: { xs: "1px solid #585587", md: "none" },
pt: { xs: 7, md: 0 },
width: { xs: "100%", md: "auto" },
textAlign: { xs: "center", md: "left" },
}}
>
<LogoLink negMt />
</Box>
<Box sx={{ flexGrow: 1 }} />
<Box
sx={{
pl: { xs: 0, md: 2 },
order: { xs: -2, md: 0 },
textAlign: { xs: "center", md: "left" },
mb: { xs: 7, md: 0 },
}}
>
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
}}
>
<Box>
<Link
component={RouterLink}
to={apps}
color="inherit"
underline="hover"
sx={linkStyle}
activeStyle={linkActiveStyle}
>
Apps
</Link>
<Link
href={portal}
color="inherit"
underline="hover"
sx={linkStyle}
>
Portal
</Link>
<Link
component={RouterLink}
to={buidl}
color="inherit"
underline="hover"
sx={linkStyle}
activeStyle={linkActiveStyle}
>
Buidl
</Link>
<Link
href={blog}
color="inherit"
underline="hover"
sx={linkStyle}
>
Blog
</Link>
</Box>
<Box>
<Link
component={RouterLink}
to={network}
color="inherit"
underline="hover"
sx={linkStyle}
activeStyle={linkActiveStyle}
>
Network
</Link>
<Link
component={RouterLink}
to={explorer}
color="inherit"
underline="hover"
sx={linkStyle}
>
Explorer
</Link>
<Link
href={docs}
color="inherit"
underline="hover"
sx={linkStyle}
>
Docs
</Link>
<Link
href={jobs}
color="inherit"
underline="hover"
sx={linkStyle}
>
Jobs
</Link>
</Box>
</Box>
</Box>
<Box sx={{ flexGrow: 1 }} />
<Box
sx={{
px: 2,
order: { xs: -2, md: 0 },
textAlign: { xs: "center", md: "left" },
borderTop: { xs: "1px solid #585587", md: "none" },
pt: { xs: 7, md: 0 },
width: { xs: "100%", md: "auto" },
mb: { xs: 7, md: 0 },
}}
>
<Typography sx={{ mb: 3 }}>Let's be friends</Typography>
<Box>
<IconButton
href={discord}
target="_blank"
rel="noopener noreferrer"
sx={socialIcon}
>
<img src={Discord} alt="Discord" />
</IconButton>
<IconButton
href={github}
target="_blank"
rel="noopener noreferrer"
sx={socialIcon}
>
<img src={Github} alt="Github" />
</IconButton>
<IconButton
href={blog}
target="_blank"
rel="noopener noreferrer"
sx={socialIcon}
>
<img src={Medium} alt="Medium" />
</IconButton>
<IconButton
href={telegram}
target="_blank"
rel="noopener noreferrer"
sx={socialIcon}
>
<img src={Telegram} alt="Telegram" />
</IconButton>
<IconButton
href={twitter}
target="_blank"
rel="noopener noreferrer"
sx={socialIcon}
>
<img src={Twitter} alt="Twitter" />
</IconButton>
</Box>
</Box>
<Box
sx={{
flexBasis: "100%",
pt: { xs: 0, md: 8 },
textAlign: { xs: "center", md: "left" },
}}
>
<Typography variant="body2">
2022 &copy; Wormhole. All Rights Reserved.
</Typography>
</Box>
</Box>
</Box>
</Box>
);
export default Footer;

View File

@ -0,0 +1,121 @@
import {
Card,
CardActionArea,
Grid,
GridSize,
GridSpacing,
Typography,
} from "@mui/material";
import { Box, ResponsiveStyleValue } from "@mui/system";
import { Link as RouterLink } from "gatsby";
import { OutboundLink } from "gatsby-plugin-google-gtag";
import React from "react";
interface CardData {
key?: string;
src: string;
header: string;
description: JSX.Element | string;
href?: string;
to?: string;
imgStyle?: React.CSSProperties | undefined;
}
const GridWithCards = ({
data,
sm = 12,
md = 4,
spacing = 2,
cardPaddingTop = 0,
imgAlignMd = "right",
imgOffsetRightMd = "-16px",
imgOffsetTopXs = "-30px",
imgOffsetTopMd = "-16px",
imgOffsetTopMdHover,
imgPaddingBottomXs = 0,
imgPaddingBottomMd = 0,
headerTextAlign = "left",
}: {
data: CardData[];
sm?: boolean | GridSize | undefined;
md?: boolean | GridSize | undefined;
spacing?: ResponsiveStyleValue<GridSpacing>;
cardPaddingTop?: number;
imgAlignMd?: string;
imgOffsetRightMd?: string;
imgOffsetTopXs?: string;
imgOffsetTopMd?: string;
imgOffsetTopMdHover?: string;
imgPaddingBottomXs?: number;
imgPaddingBottomMd?: number;
headerTextAlign?: any;
}) => (
<Grid
container
spacing={spacing}
justifyContent="space-evenly"
sx={{ "& > .MuiGrid-item": { pt: { xs: 8.25, md: 5.25 } } }}
>
{data.map(({ key, src, header, description, href, to, imgStyle }) => (
<Grid key={key || header} item xs={12} sm={sm} md={md}>
<Card
sx={{
backgroundColor: "rgba(255,255,255,.07)",
backgroundImage: "none",
borderRadius: "28px",
display: "flex",
flexDirection: "column",
height: "100%",
overflow: "visible",
}}
>
<CardActionArea
component={to ? RouterLink : href ? OutboundLink : undefined}
to={to}
href={href}
target={href ? "_blank" : undefined}
rel={href ? "noreferrer" : undefined}
disabled={!(href || to)}
sx={{
px: 4.25,
pb: 3,
pt: cardPaddingTop,
borderRadius: "28px",
height: "100%",
"& > div": {
transition: { md: "300ms top" },
},
"&:hover > div": {
top: {
xs: imgOffsetTopXs,
md: imgOffsetTopMdHover || imgOffsetTopMd,
},
},
}}
>
<Box
sx={{
textAlign: { xs: "center", md: imgAlignMd },
position: "relative",
right: { xs: null, md: imgOffsetRightMd },
top: { xs: imgOffsetTopXs, md: imgOffsetTopMd },
pb: { xs: imgPaddingBottomXs, md: imgPaddingBottomMd },
zIndex: 1,
}}
>
<img src={src} alt="" style={imgStyle} />
</Box>
<Typography variant="h4" textAlign={headerTextAlign}>
{header}
</Typography>
<Typography component="div" sx={{ mt: 2, flexGrow: 1 }}>
{description}
</Typography>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
);
export default GridWithCards;

View File

@ -1,3 +0,0 @@
.ant-table.ant-table-small .ant-table-tbody .ant-table-wrapper:only-child .ant-table {
margin: -4px -4px -4px 1px;
}

View File

@ -1,85 +0,0 @@
import React from 'react';
import { Table } from 'antd';
import { ColumnsType } from 'antd/es/table'
import { IntlShape } from 'gatsby-plugin-intl';
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 { ReactComponent as PolygonIcon } from '~/icons/polygon.svg'
import './GuardiansTable.less'
import { ChainID } from '~/utils/misc/constants';
const networkIcons = [
<></>,
<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' }} />,
<PolygonIcon key="5" style={{ height: 18, margin: '0 4px' }} />,
]
const expandedRowRender = (intl: IntlShape) => (item: Heartbeat) => {
const columns: ColumnsType<Heartbeat_Network> = [
{ title: '', dataIndex: 'id', key: 'icon', render: (id: number) => networkIcons[id] },
{
title: intl.formatMessage({ id: 'network.network' }), dataIndex: 'id', key: 'id', responsive: ['md'],
render: (id: number) => ChainID[id]
},
{ title: intl.formatMessage({ id: 'network.contractAddress' }), dataIndex: 'contractAddress', key: 'contractAddress' },
{ title: intl.formatMessage({ id: 'network.blockHeight' }), dataIndex: 'height', key: 'height', responsive: ['md'], },
{ title: intl.formatMessage({ id: 'network.errorCount' }), dataIndex: 'errorCount', key: 'errorCount', responsive: ['lg'], },
];
return (
<Table<Heartbeat_Network>
rowKey="id"
columns={columns}
dataSource={item.networks}
pagination={false}
/>
)
};
const GuardiansTable = ({ heartbeats, intl }: { heartbeats: { [nodeName: string]: Heartbeat }, intl: IntlShape }) => {
const columns: ColumnsType<Heartbeat> = [
{
title: intl.formatMessage({ id: 'network.guardian' }), key: 'guardian',
render: (item: Heartbeat) => <>{item.nodeName}<br />{item.guardianAddr}</>
},
{ title: intl.formatMessage({ id: 'network.version' }), dataIndex: 'version', key: 'version', responsive: ['lg'] },
{
title: intl.formatMessage({ id: 'network.networks' }), dataIndex: 'networks', key: 'networks', responsive: ['md'],
render: (networks: Heartbeat_Network[]) => networks.map(network => networkIcons[network.id])
},
{ title: intl.formatMessage({ id: 'network.heartbeat' }), dataIndex: 'counter', key: 'counter', responsive: ['xl'] },
{
title: intl.formatMessage({ id: 'network.lastHeartbeat' }), dataIndex: 'timestamp', key: 'timestamp', responsive: ['sm'],
render: (timestamp: string) =>
<ReactTimeAgo date={new Date(Number(timestamp.slice(0, -6)))} locale={intl.locale} timeStyle="round" />
}
];
return (
<Table<Heartbeat>
columns={columns}
size="small"
expandable={{
expandedRowRender: expandedRowRender(intl),
expandRowByClick: true,
}}
dataSource={Object.values(heartbeats)}
loading={Object.keys(heartbeats).length === 0}
rowKey="nodeName"
pagination={false}
/>
)
}
export default GuardiansTable

View File

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

View File

@ -0,0 +1,28 @@
import { Box, Typography } from "@mui/material";
import React from "react";
import AvoidBreak from "./AvoidBreak";
const HeroText = ({
heroSpans,
subtitleText,
maxWidth = 1155 + 16 * 2,
}: {
heroSpans: string[];
subtitleText: string | string[];
maxWidth?: number;
}) => (
<Box sx={{ m: "auto", maxWidth, textAlign: "center", px: 2 }}>
<Typography variant="h1">
<AvoidBreak spans={heroSpans} />
</Typography>
<Typography sx={{ marginTop: 2 }}>
{Array.isArray(subtitleText) ? (
<AvoidBreak spans={subtitleText} />
) : (
subtitleText
)}
</Typography>
</Box>
);
export default HeroText;

View File

@ -0,0 +1,13 @@
import React from "react";
import Footer from "./Footer";
import NavBar from "./Navbar";
const Layout: React.FC = ({ children }) => (
<main>
<NavBar />
{children}
<Footer />
</main>
);
export default Layout;

View File

@ -1,94 +0,0 @@
// import theme to get antd less variables + overrides from AntdTheme.js
@import "~antd/lib/style/themes/dark";
header, header ul {
@media only screen and (min-width: 767px) {
border-bottom: 0.5px solid @border-divider-color !important;
}
@media only screen and (max-width: 767px) {
border: none !important;
}
}
svg:hover.external-icon {
path {
fill: @primary-color;
}
}
.external-links-left {
position: fixed;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
top: 0;
left: 0;
height: 650px;
width: 50px;
border-bottom: 0.5px solid @border-divider-color;
border-right: 0.5px solid @border-divider-color;
padding-block-end: 24px;
z-index: 10;
line-height: normal;
svg {
width: 26px;
margin: 8px;
}
@media only screen and (max-width: 767px) {
display:none;
}
}
.external-links-bottom {
left: 0px;
width: 100vw;
justify-content: space-evenly;
svg {
height: 26px;
}
display: none;
@media only screen and (max-width: 767px) {
display: flex;
}
}
// Hamburger menu + popover
.popover div.nav {
/* mobile nav */
flex-direction: column;
font-size: 28px;
font-weight: 400;
}
.affix {
/* fixed position prevents scrolling while the mobile nav popover is open. */
position: fixed !important;
width: 100%;
z-index: 1000;
}
.nav a {
color: rgba(255, 255, 255, 0.85);
font-size: 18px;
}
.nav a:hover{
color: @primary-color;
}
@media (max-width: 992px) {
.site-nav-right {
display: none;
}
}
@media (min-width: 992px) {
.popover div.nav {
display: none;
}
}

View File

@ -1,277 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useEffect, useState } from 'react';
import { Button, Layout, Grid } from 'antd';
const { Header, Content, Footer } = Layout;
const { useBreakpoint } = Grid
import { SendOutlined } from '@ant-design/icons';
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl';
import { OutboundLink } from "gatsby-plugin-google-gtag"
import { Link } from 'gatsby'
import './DefaultLayout.less'
import styled from "styled-components";
import { externalLinks, linkToService, socialLinks, socialAnchorArray } from '~/utils/misc/socials';
// brand assets
import { ReactComponent as AvatarAndName } from '~/icons/FullLogo_DarkBackground.svg';
import { ReactComponent as Avatar } from '~/icons/Avatar_DarkBackground.svg';
import { BRIDGE_URL, DOCS_URL, JOBS_URL } from '~/utils/misc/constants';
const Toggle = styled.div`
display: none;
height: 100%;
cursor: pointer;
padding: 0 4vw;
@media (max-width: 992px) {
display: flex;
}
`;
const Navbox = styled.div`
align-items: center;
@media (max-width: 992px) {
flex-direction: column;
position: fixed;
width: 100%;
justify-content: flex-start;
padding-top: 360px;
background-color: #010114;
transition: all 0.3s ease-in;
left: ${(props: { open: boolean }) => (props.open ? "-100%" : "0")};
}
`;
const Hamburger = styled.div`
background-color: #fff;
width: 30px;
height: 3px;
transition: all 0.3s linear;
align-self: center;
position: relative;
transform: ${(props: { open: boolean }) => (props.open ? "rotate(-45deg)" : "inherit")};
z-index: 1001;
::before,
::after {
width: 30px;
height: 3px;
background-color: #fff;
content: "";
position: absolute;
transition: all 0.3s linear;
}
::before {
transform: ${(props) =>
props.open
? "rotate(-90deg) translate(-10px, 0px)"
: "rotate(0deg)"};
top: -10px;
}
::after {
opacity: ${(props) => (props.open ? "0" : "1")};
transform: ${(props) =>
props.open ? "rotate(90deg) " : "rotate(0deg)"};
top: 10px;
}
`;
const externalLinkProps = { target: "_blank", rel: "noopener noreferrer", className: "no-external-icon" }
const DefaultLayout: React.FC<{}> = ({
children,
...props
}) => {
const intl = useIntl()
const screens = useBreakpoint();
const [navbarOpen, setNavbarOpen] = useState(false);
const menuItemProps: { style: { textAlign: CanvasTextAlign, padding: number } } = { style: { textAlign: 'center', padding: 0 } }
useEffect(() => {
if (screens.lg === true) {
setNavbarOpen(false)
}
}, [screens.lg])
const launchBridge = <div key="bridge" style={{ ...menuItemProps.style, zIndex: 1001 }}>
<OutboundLink
href={BRIDGE_URL}
target="_blank"
rel="noopener noreferrer"
className="no-external-icon"
>
<Button
style={{
height: 40,
fontSize: 16,
border: "1.5px solid",
paddingLeft: 20
}}
ghost
type="primary"
shape="round"
size="large"
>
{intl.formatMessage({ id: "nav.bridgeLink" })}
<SendOutlined style={{ fontSize: 16, marginRight: 0 }} />
</Button>
</OutboundLink>
</div>
const menuItems = [
<div key="about" {...menuItemProps}>
<Link to={`/${intl.locale}/about/`}>
<FormattedMessage id="nav.aboutLink" />
</Link>
</div>,
<div key="network" {...menuItemProps} >
<Link to={`/${intl.locale}/network/`}>
<FormattedMessage id="nav.networkLink" />
</Link>
</div>,
<div key="explorer" {...menuItemProps} >
<Link to={`/${intl.locale}/explorer/`}>
<FormattedMessage id="nav.explorerLink" />
</Link>
</div>,
<div key="docs" {...menuItemProps} >
<OutboundLink
href={DOCS_URL}
target="_blank"
rel="noopener noreferrer"
>
{intl.formatMessage({ id: "nav.docsLink" })}
</OutboundLink>
</div>,
<div key="jobs" {...menuItemProps} >
<OutboundLink
href={JOBS_URL}
target="_blank"
rel="noopener noreferrer"
>
{intl.formatMessage({ id: "nav.jobsLink" })}
</OutboundLink>
</div>,
screens.sm === false || screens.lg === true ? launchBridge : null,
screens.lg === false ? (<div key="socials" style={{ ...menuItemProps.style, height: '100%', padding: 0 }}>
<div
style={{ display: 'flex', justifyContent: 'space-evenly', borderStyle: 'none' }}
>
{Object.entries(externalLinks).map(([url, Icon]) => <div key={url} {...menuItemProps} style={{ margin: '12px 0' }} >
<OutboundLink
href={url}
{...externalLinkProps}
title={intl.formatMessage({ id: `nav.${linkToService[url]}AltText` })}
>
<Icon style={{ height: 26 }} className="external-icon" />
</OutboundLink>
</div>)}
</div>
</div>) : null
]
return (
<Layout style={{ minHeight: '100vh' }}>
<Header style={{
padding: 0,
height: 70
}} >
<div className="center-content">
<nav
className={`max-content-width ${navbarOpen ? " affix" : ""}`}
style={{
height: 70,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
padding: !screens.lg ? 0 : '0 16px 0 0'
}}
>
{/* wormhole logo, left side of nav */}
<div className="responsive-padding" style={{ zIndex: 1001 }}>
<Link to={`/${intl.locale}/`} style={{ height: 32 }} title={intl.formatMessage({ id: 'nav.homeLinkAltText' })}>
<AvatarAndName style={{ height: 45, margin: 'auto', verticalAlign: 'middle', display: 'inline-block' }} />
</Link>
</div>
{/* the list of menu items, right side of nav */}
<div className="nav site-nav-right">
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 16 }} >
{menuItems}
</div>
</div>
{/* show the "Launch Bridge" button next to the hamburger menu if the screen is large enough. */}
{screens.lg === false && screens.sm === true ? <>
<div style={{ flexGrow: 1 }} />
{launchBridge}
</> : null}
{/* hambuger button Toggle mobile popover menu*/}
<Toggle onClick={() => setNavbarOpen(!navbarOpen)}>
{navbarOpen ? <Hamburger open /> : <Hamburger open={false} />}
</Toggle>
{/* nav drawer with links */}
{navbarOpen ? (
<Navbox open={!navbarOpen}>
<div className="popover" style={{ marginTop: 100 }}>
{/* <Navigation data={navigation} /> */}
<div className="nav" style={{ display: 'flex' }}>
{menuItems}
</div>
</div>
</Navbox>
) : null}
</nav>
</div>
<div
className="external-links-left"
>
{socialAnchorArray(intl)}
</div>
</Header>
<Content>
<div
{...props}
>
{children}
</div>
</Content>
<Footer style={{ textAlign: 'center', paddingLeft: 0, paddingRight: 0 }}>
<div
className="external-links-bottom"
>
{socialAnchorArray(intl)}
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
gap: 16,
marginTop: 12
}}>
<Avatar style={{ maxHeight: 58 }} />
<div style={{ lineHeight: '1.5em' }}>
<OutboundLink href={socialLinks['github']} {...externalLinkProps} style={{ color: 'white' }}>
{intl.formatMessage({ id: "footer.openSource" })}
</OutboundLink>
<br />
{intl.formatMessage({ id: "footer.createdWith" })}&nbsp;
<span style={{ fontSize: '1.4em' }}></span>
<br />
©{new Date().getFullYear()}
</div>
</div>
</Footer>
</Layout>
);
};
export default DefaultLayout;

View File

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

View File

@ -0,0 +1,24 @@
import { Button } from "@mui/material";
import { Link as RouterLink } from "gatsby";
import React from "react";
import logo from "../images/logo-and-name.svg";
import { home } from "../utils/urls";
const LogoLink = ({ negMt = false }: { negMt?: boolean }) => (
<Button
size="small"
component={RouterLink}
to={home}
sx={{
display: "flex",
p: 1,
borderRadius: "8px",
ml: -1,
mt: negMt ? -1 : 0,
}}
>
<img src={logo} alt="Wormhole" />
</Button>
);
export default LogoLink;

View File

@ -0,0 +1,54 @@
import { AppBar, Box, Link, Toolbar } from "@mui/material";
import { Link as RouterLink } from "gatsby";
import React from "react";
import hamburger from "../images/hamburger.svg";
import { apps, blog, buidl, portal } from "../utils/urls";
import LogoLink from "./LogoLink";
const linkStyle = { ml: 3, textUnderlineOffset: 6 };
const linkActiveStyle = { textDecoration: "underline" };
const NavBar = () => (
<AppBar
position="static"
sx={{ backgroundColor: "transparent" }}
elevation={0}
>
<Toolbar disableGutters sx={{ mt: 2, mx: 4 }}>
<LogoLink />
<Box sx={{ flexGrow: 1 }} />
<Box sx={{ display: { xs: "none", md: "block" } }}>
<Link
component={RouterLink}
to={apps}
color="inherit"
underline="hover"
sx={linkStyle}
activeStyle={linkActiveStyle}
>
Apps
</Link>
<Link href={portal} color="inherit" underline="hover" sx={linkStyle}>
Portal
</Link>
<Link
component={RouterLink}
to={buidl}
color="inherit"
underline="hover"
sx={linkStyle}
activeStyle={linkActiveStyle}
>
Buidl
</Link>
<Link href={blog} color="inherit" underline="hover" sx={linkStyle}>
Blog
</Link>
</Box>
{/* <Box sx={{ display: "flex", ml: 8 }}>
<img src={hamburger} alt="menu" />
</Box> */}
</Toolbar>
</AppBar>
);
export default NavBar;

View File

@ -0,0 +1,31 @@
import * as React from "react";
import { Box, MenuItem, TextField, Typography } from "@mui/material";
import { useNetworkContext } from "../contexts/NetworkContext";
import { networks } from "../utils/consts";
const NetworkSelect = () => {
const { activeNetwork, setActiveNetwork } = useNetworkContext();
const handleNetworkChange = React.useCallback((e) => {
setActiveNetwork(e.target.value);
}, []);
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ pr: 2 }}>
Network
</Typography>
<TextField
select
margin="none"
value={activeNetwork.name}
onChange={handleNetworkChange}
>
{networks.map((n) => (
<MenuItem key={n} value={n}>{`${n[0].toUpperCase()}${n.substring(
1
)}`}</MenuItem>
))}
</TextField>
</Box>
);
};
export default NetworkSelect;

View File

@ -1,28 +0,0 @@
import React from 'react';
import { Select } from 'antd'
const { Option } = Select
import { FormattedMessage } from 'gatsby-plugin-intl'
import { NetworkContext } from "./network-context"
const NetworkSelect = ({ style }: { style?: { [key: string]: string | number } }) => {
return (
<NetworkContext.Consumer>
{({ activeNetwork, setActiveNetwork }) => (
<Select
defaultValue={activeNetwork.name}
onSelect={setActiveNetwork}
size="large"
style={style}
>
<Option value="devnet"><FormattedMessage id="networks.devnet" /></Option>
<Option value="testnet"><FormattedMessage id="networks.testnet" /></Option>
<Option value="mainnet"><FormattedMessage id="networks.mainnet" /></Option>
</Select>
)}
</NetworkContext.Consumer>
)
}
export default NetworkSelect

View File

@ -1,81 +0,0 @@
import React from 'react';
import { NetworkContext } from '~/components/NetworkSelect'
import { NetworkContextI } from './network-context'
import { endpoints, KnownContracts, knownContractsPromise, NetworkChains, networks } from '~/utils/misc/constants';
// Check if window is defined (so if in the browser or in node.js).
const isBrowser = typeof window !== "undefined"
const defaultNetwork = process.env.GATSBY_DEFAULT_NETWORK || "mainnet"
interface NetworkContextState extends NetworkContextI {
knownContracts: NetworkChains
}
const WithNetwork = (WrappedComponent: React.FC<any>) => {
return class extends React.Component<{}, NetworkContextState> {
constructor(props: any) {
super(props)
let network: string | undefined | null = ""
if (isBrowser) {
// isBrowser check for Gatsby develop's SSR
network = window.localStorage.getItem("networkName")
}
if (!network || !networks.includes(network)) {
network = defaultNetwork
}
this.state = {
// knownContracts are generated async and added to state
knownContracts: {
"devnet": {},
"testnet": {},
"mainnet": {}
},
activeNetwork: {
name: network,
endpoints: endpoints[network],
chains: {
// chains are generated async and added to state
}
},
setActiveNetwork: this.setActiveNetwork,
};
this.setActiveNetwork(network)
}
setActiveNetwork = async (network: string) => {
if (isBrowser) {
// isBrowser check for Gatsby develop's SSR
window.localStorage.setItem("networkName", network)
}
// generate knownContracts if needed
let contracts = this.state.knownContracts
if (!this.state.knownContracts.devent) {
contracts = await knownContractsPromise
this.setState(() => ({
knownContracts: contracts
}))
}
this.setState(() => ({
activeNetwork: {
name: network,
endpoints: endpoints[network],
chains: contracts[network],
}
}));
}
render() {
return (
<NetworkContext.Provider value={this.state}>
<WrappedComponent {...this.props} />
</NetworkContext.Provider>
)
}
}
}
export default WithNetwork

View File

@ -1,3 +0,0 @@
export { default as NetworkSelect } from './NetworkSelect';
export { NetworkContext, ActiveNetwork } from "./network-context"
export { default as WithNetwork } from "./WithNetwork"

View File

@ -1,30 +0,0 @@
import { createContext } from "react"
import { ChainContracts, endpoints, KnownContracts, NetworkConfig, } from '~/utils/misc/constants';
let defaultNetwork = process.env.GATSBY_DEFAULT_NETWORK || "mainnet"
// ensure the network value is valid
if (!(defaultNetwork in endpoints)) {
defaultNetwork = defaultNetwork
}
export interface ActiveNetwork {
name: string
endpoints: NetworkConfig
chains: ChainContracts
}
interface NetworkContextI {
activeNetwork: ActiveNetwork,
setActiveNetwork: (network: string) => void
}
const NetworkContext = createContext<NetworkContextI>({
activeNetwork: {
name: defaultNetwork,
endpoints: endpoints[defaultNetwork],
chains: {
// initalize empty object, will be replaced async by generated data
}
},
setActiveNetwork: (network: string) => { },
})
export { NetworkContext, NetworkContextI }

View File

@ -1 +0,0 @@
export { DecodePayload } from './DecodePayload';

View File

@ -0,0 +1,206 @@
import React from 'react'
import { Helmet, HelmetProps } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'
type Meta = ConcatArray<PropertyMetaObj | NameMetaObj>
type PropertyMetaObj = {
property: string
content: string
}
type NameMetaObj = {
name: string
content: string
}
type SEOProps = {
description?: string
meta?: Meta
title?: string
pathname?: string
} & HelmetProps
export function SEO({
children,
description = '',
meta = [],
title,
pathname,
...props
}: SEOProps) {
const { site } = useStaticQuery<QueryTypes>(SEOStaticQuery)
const siteUrl = site.siteMetadata.siteUrl
const defaultTitle = site.siteMetadata?.defaultTitle
const twitterUsername = `@wormholecrypto`
const socialImage = "/logo-and-name_social-card.png"
const socialImageWidth = '800'
const socialImageHeight = '400'
const image = `${siteUrl}${socialImage}`
const metaDescription = description || 'The best of blockchains'
const canonical = pathname ? `${siteUrl}${pathname}` : null
// for social sharing we want a little more context than just title,
// make a string like "Apps | Wormhole"
const socialTitle = title ? `${title} | ${defaultTitle}` : title
return (
<Helmet
{...props}
htmlAttributes={{
lang: 'en',
}}
title={title || defaultTitle}
link={[
{
rel: `apple-touch-icon`,
href: `${siteUrl}/apple-touch-icon.png`,
sizes: `180x180`,
},
{
rel: `icon`,
href: `${siteUrl}/favicon-32x32.png`,
sizes: `32x32`,
type: `image/png`,
},
{
rel: `icon`,
href: `${siteUrl}/favicon-16x16.png`,
sizes: `16x16`,
type: `image/png`,
},
{
rel: `manifest`,
href: `${siteUrl}/site.webmanifest`,
},
{
rel: `mask-icon`,
href: `${siteUrl}/safari-pinned-tab.svg`,
color: `#5bbad5`,
},
canonical
? {
rel: 'canonical',
href: canonical,
}
: {},
]}
meta={[
{
name: `description`,
content: metaDescription,
},
{
name: `msapplication-TileColor`,
content: `#603cba"`,
},
{
name: `theme-color`,
content: `#ffffff`,
},
// opengraph metadata
{
property: `og:title`,
content: socialTitle,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:site_name`,
content: defaultTitle, // "Wormhole" for all pages
},
{
property: `og:type`,
content: `website`,
},
canonical
? {
property: `og:url`,
content: canonical,
}
: {},
{
property: 'og:image',
content: image,
},
{
property: 'og:image:secure_url',
content: image,
},
{
property: `og:image:type`,
content: `image/png`,
},
{
property: `og:image:width`,
content: socialImageWidth,
},
{
property: `og:image:height`,
content: socialImageHeight,
},
{
property: `og:image:alt`,
content: `Wormhole logo`,
},
// twitter metadata
{
name: `twitter:title`,
content: socialTitle,
},
{
name: `twitter:description`,
content: metaDescription,
},
{
name: `twitter:image`,
content: image,
},
{
name: `twitter:image:alt`,
content: `Wormhole logo`,
},
{
name: 'twitter:card',
content: 'summary_large_image',
},
{
name: `twitter:site`,
content: twitterUsername,
},
{
name: `twitter:creator`,
content: twitterUsername,
},
]
// metadata from props
.concat(meta)}
>
{children}
</Helmet>
)
}
type QueryTypes = {
site: {
siteMetadata: {
siteUrl: string
defaultTitle: string
}
}
}
const SEOStaticQuery = graphql`
query SEO {
site {
siteMetadata {
siteUrl
defaultTitle: title
}
}
}
`

Some files were not shown because too many files have changed in this diff Show More