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:
parent
613773d3fc
commit
497a1c6e83
|
@ -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
|
||||
|
|
1
Tiltfile
1
Tiltfile
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
.env.development
|
||||
.env.production
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.cache
|
||||
public
|
||||
proto
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
.cache
|
||||
public
|
||||
proto
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/prettierrc",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"semi": false
|
||||
}
|
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
export { default as GatsbyIntlProvider } from './GatsbyIntlProvider';
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
/**
|
||||
* `manager.js` replaces `addons.js` and allows you to customize how Storybook’s app UI renders.
|
||||
* That is, everything outside of the Canvas (preview iframe).
|
||||
* In common cases, you probably won’t need this file except when you’re theming Storybook.
|
||||
*
|
||||
* https://medium.com/storybookjs/declarative-storybook-configuration-49912f77b78
|
||||
*/
|
|
@ -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" />
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"cSpell.words": ["bridgesplit", "buidl", "pyth", "roadmap", "Solana", "tiexo"]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "clean",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: clean",
|
||||
"detail": "rm -rf public && rm -rf .cache"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 site’s 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)
|
||||
|
|
|
@ -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 can’t handle
|
||||
module.exports = 'test-file-stub';
|
|
@ -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,
|
||||
})
|
||||
),
|
||||
};
|
|
@ -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(),
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
module.exports = { ReactComponent: 'icon-mock' };
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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 can’t 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;
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"],
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
|||
import React from "react";
|
||||
import TopLayout from "./TopLayout";
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<TopLayout>{element}</TopLayout>
|
||||
);
|
|
@ -0,0 +1,6 @@
|
|||
import React from "react";
|
||||
import TopLayout from "./TopLayout";
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<TopLayout>{element}</TopLayout>
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"name": "gatsby-plugin-top-layout"
|
||||
}
|
Binary file not shown.
|
@ -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 };
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
@ -1 +0,0 @@
|
|||
declare module 'react-time-ago';
|
|
@ -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',
|
||||
};
|
|
@ -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%);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
export { default as ExplorerSearchForm } from './ExplorerSearchForm'
|
||||
export { default as ExplorerTxForm } from './ExplorerTxForm'
|
|
@ -1 +0,0 @@
|
|||
export { default as App } from './App';
|
|
@ -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;
|
|
@ -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.
|
|
@ -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()
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
|
@ -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:"} {chainEnums[payloadBundle.payload.tokenChain]} {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"}
|
||||
{(Math.round(Number(props.transferDetails.Amount) * 100) / 100).toLocaleString()} {props.transferDetails.OriginSymbol}
|
||||
{'from'} {titleCase(props.emitterChainName)} {'to'} {chainEnums[Number(payloadBundle.payload.targetChain)]}
|
||||
{'('}{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 || "?"} {"-"} {chainEnums[payloadBundle.payload.originChain]}{' -> '}{chainEnums[payloadBundle.payload.targetChain]}
|
||||
</>) : null
|
||||
) : null}
|
||||
{"NFT: "} {payloadBundle.payload.name || "?"} {" wormholed "} {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}
|
||||
|
||||
</>
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
export { default as ExplorerQuery } from './ExplorerQuery';
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}
|
|
@ -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
|
|
@ -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:
|
||||
<ReactTimeAgo
|
||||
date={new Date(props.lastFetched)}
|
||||
timeStyle="round"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplorerSummary;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 24 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 {totalDays} 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;
|
||||
|
|
|
@ -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",
|
||||
}}
|
||||
/>
|
||||
|
||||
<span>{chain}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyCountBarChart;
|
|
@ -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
|
|
@ -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" />:
|
||||
<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, }} />
|
||||
<span>{point.serieId}</span> - {point.data.yFormatted}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DailyCountLineChart
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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> {chain}</div>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PastWeekCard;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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" />:
|
||||
<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;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default as ExplorerStats } from './ExplorerStats';
|
|
@ -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 }
|
|
@ -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" />:
|
||||
<ReactTimeAgo date={new Date(props.lastFetched)} locale={intl.locale} timeStyle="round" />
|
||||
</span>
|
||||
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Summary
|
|
@ -1,2 +0,0 @@
|
|||
export { default as ExplorerSummary } from './ExplorerSummary';
|
||||
|
|
@ -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 © Wormhole. All Rights Reserved.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
export default Footer;
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
.ant-table.ant-table-small .ant-table-tbody .ant-table-wrapper:only-child .ant-table {
|
||||
margin: -4px -4px -4px 1px;
|
||||
}
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
export { default as GuardiansTable } from './GuardiansTable';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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" })}
|
||||
<span style={{ fontSize: '1.4em' }}>♥</span>
|
||||
<br />
|
||||
©{new Date().getFullYear()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultLayout;
|
|
@ -1 +0,0 @@
|
|||
export { default as Layout } from './DefaultLayout';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
export { default as NetworkSelect } from './NetworkSelect';
|
||||
export { NetworkContext, ActiveNetwork } from "./network-context"
|
||||
export { default as WithNetwork } from "./WithNetwork"
|
|
@ -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 }
|
|
@ -1 +0,0 @@
|
|||
export { DecodePayload } from './DecodePayload';
|
|
@ -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
Loading…
Reference in New Issue