├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── NOTICE ├── README.md ├── firebase.site-xy.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── assets │ └── background.jpg ├── components │ ├── AccountLink.tsx │ ├── App.tsx │ ├── CodeLink.tsx │ ├── ContractLink.tsx │ ├── EndpointSelector.tsx │ ├── FlexibleRouter.tsx │ ├── FooterRow.tsx │ ├── Header.tsx │ ├── Login.tsx │ ├── NodeInfoModal.tsx │ ├── TransactionLink.tsx │ └── UserAddress.tsx ├── contexts │ └── ClientContext.tsx ├── hw-transport-webusb.d.ts ├── index.css ├── index.tsx ├── pages │ ├── account │ │ ├── AccountPage.tsx │ │ └── TransfersTable.tsx │ ├── code │ │ ├── CodeInfo.tsx │ │ ├── CodePage.css │ │ ├── CodePage.tsx │ │ ├── InstanceRow.tsx │ │ └── InstancesEmptyState.tsx │ ├── codes │ │ ├── Code.css │ │ ├── Code.tsx │ │ ├── Codes.css │ │ ├── Codes.tsx │ │ └── CodesPage.tsx │ ├── contract │ │ ├── ContractPage.css │ │ ├── ContractPage.tsx │ │ ├── ExecuteContract.tsx │ │ ├── ExecutionsTable.tsx │ │ ├── HistoryInfo.tsx │ │ ├── InitializationInfo.tsx │ │ └── QueryContract.tsx │ └── tx │ │ ├── ExecutionInfo.tsx │ │ ├── TxInfo.tsx │ │ ├── TxPage.css │ │ ├── TxPage.tsx │ │ └── msgs │ │ ├── MsgExecuteContract.tsx │ │ ├── MsgInstantiateContract.tsx │ │ ├── MsgSend.tsx │ │ ├── MsgStoreCode.css │ │ ├── MsgStoreCode.tsx │ │ ├── magic.spec.ts │ │ └── magic.ts ├── react-app-env.d.ts ├── react-json-editor-ajrm.d.ts ├── settings │ ├── backend.ts │ └── index.ts └── ui-utils │ ├── clients.ts │ ├── index.spec.ts │ ├── index.ts │ ├── jsonInput.ts │ ├── sdkhelpers.spec.ts │ ├── sdkhelpers.ts │ ├── states.ts │ └── txs.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | project: "./tsconfig.json", 6 | tsconfigRootDir: __dirname, 7 | }, 8 | plugins: ["@typescript-eslint", "simple-import-sort"], 9 | extends: [ 10 | "eslint:recommended", 11 | "react-app", 12 | "plugin:@typescript-eslint/recommended", 13 | "prettier", 14 | "plugin:prettier/recommended", 15 | ], 16 | rules: { 17 | curly: ["warn", "multi-line", "consistent"], 18 | "no-empty": "off", 19 | "no-console": [ 20 | "warn", 21 | { 22 | allow: ["error", "info", "warn"], 23 | }, 24 | ], 25 | "no-param-reassign": "warn", 26 | "prefer-const": "warn", 27 | "sort-imports": "off", // we use the simple-import-sort plugin instead 28 | "spaced-comment": ["warn", "always", { line: { markers: ["/ 2 | This repository was unmaintained for a long time and most likely contains security issues in the dependency tree. 3 | The code remains here for reference but it is highly recommended to not use it anymore.
4 |
5 | Many new tools have been developed by other teams in the meantime which go beyond what we developed here. Let's use and promote those tools to broaden the CosmWasm ecosystem. Giving up cosebases is part of innovation. Cheers! 6 |
7 | 🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦🪦 8 | 9 | # CosmWasm Code Explorer 10 | 11 | ## Use hosted 12 | 13 | The explorer is deployed at https://cosmwasm.github.io/code-explorer and is configured 14 | for Cliffnet. 15 | 16 | ## Use local 17 | 18 | You need to install the code-explorer dependencies before choosing which network you want to connect to: 19 | 20 | ```sh 21 | yarn install 22 | ``` 23 | 24 | ### Run against testnets 25 | 26 | You don't need to run a local network in order to connect to the testnets, you just need the right start script for each network. 27 | 28 | #### Musslenet 29 | 30 | Coming soon! 31 | 32 | ### Run against local networks 33 | 34 | Clone the CosmJS repo in order to run a local Launchpad or Stargate network: 35 | 36 | ```sh 37 | git clone --depth 1 --branch v0.24.0-alpha.11 https://github.com/cosmos/cosmjs.git 38 | ``` 39 | 40 | Also make sure to comply with the [prerequisites](https://github.com/cosmos/cosmjs/blob/v0.24.0-alpha.11/HACKING.md#prerequisite). 41 | 42 | #### Launchpad 43 | 44 | In order to run a local Launchpad network follow [these instructions](https://github.com/cosmos/cosmjs/tree/v0.24.0-alpha.11/scripts/launchpad). 45 | 46 | The start script that makes the code-explorer connect to a local Launchpad network is: 47 | 48 | ```sh 49 | yarn start-launchpad 50 | ``` 51 | 52 | #### Stargate 53 | 54 | In order to run a local Stargate network, follow [these instructions](https://github.com/cosmos/cosmjs/tree/v0.24.0-alpha.11/scripts/wasmd). 55 | 56 | The start script that makes the code-explorer connect to a local Stargate network is: 57 | 58 | ```sh 59 | yarn start-stargate 60 | ``` 61 | 62 | ## Build instructions 63 | 64 | You can build a code explorer and deploy it as static HTML/JS files. 65 | 66 | Requirements: yarn, Node.js 10+ 67 | 68 | ``` 69 | yarn install 70 | PUBLIC_URL=https://cosmwasm.github.io/code-explorer/ REACT_APP_BACKEND=cliffnet yarn build 71 | ``` 72 | 73 | ## Credits 74 | 75 | Background image from https://unsplash.com/photos/QqCLSA3EQUg 76 | -------------------------------------------------------------------------------- /firebase.site-xy.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "site": "site-xy", 4 | "public": "build", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ], 12 | "headers": [ 13 | { 14 | "source": "**", 15 | "headers": [{ "key": "Cache-Control", "value": "max-age=300" }] 16 | }, 17 | { 18 | "source": "static/**", 19 | "headers": [{ "key": "Cache-Control", "value": "max-age=31536000" }] 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-explorer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "contributors": [ 6 | "Simon Warta" 7 | ], 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "base-start": "react-scripts start", 11 | "start-devnet": "REACT_APP_BACKEND=devnet yarn base-start", 12 | "start-cliffnet": "REACT_APP_BACKEND=cliffnet yarn base-start", 13 | "start": "yarn start-devnet", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "lint": "eslint -c .eslintrc.js --max-warnings 0 'src/**/*.ts{,x}'", 17 | "lint-fix": "eslint -c .eslintrc.js 'src/**/*.ts{,x}' --fix", 18 | "deploy": "PUBLIC_URL=https://cosmwasm.github.io/code-explorer/ REACT_APP_BACKEND=cliffnet yarn build && gh-pages -d build" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | "last 5 chrome version", 26 | "last 5 firefox version", 27 | "last 1 safari version" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version" 32 | ] 33 | }, 34 | "dependencies": { 35 | "cosmwasm": "1.1.0", 36 | "@ledgerhq/hw-transport-webusb": "^5.36.0", 37 | "@types/jest": "^24.0.0", 38 | "@types/jquery": "^3.5", 39 | "@types/node": "^12.0.0", 40 | "@types/react": "^16.9.23", 41 | "@types/react-dom": "^16.9.0", 42 | "bootstrap": "^4.5", 43 | "cosmjs-types": "^0.4.1", 44 | "jquery": "^3.5", 45 | "react": "^16.13.0", 46 | "react-dom": "^16.13.0", 47 | "react-json-editor-ajrm": "^2.5.13", 48 | "react-json-view": "^1.21.3", 49 | "react-router": "^5.1.2", 50 | "react-router-dom": "^5.1.2", 51 | "react-scripts": "^4", 52 | "typescript": "~4.5" 53 | }, 54 | "devDependencies": { 55 | "@types/react-router-dom": "^5.1.3", 56 | "@typescript-eslint/eslint-plugin": "^5", 57 | "@typescript-eslint/parser": "^5", 58 | "eslint": "^7.1", 59 | "eslint-config-prettier": "^8", 60 | "eslint-config-react-app": "^5.2.0", 61 | "eslint-plugin-prettier": "^4", 62 | "eslint-plugin-simple-import-sort": "^7", 63 | "firebase-tools": "^9", 64 | "gh-pages": "^3.2.3", 65 | "prettier": "^2.5.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | CosmWasm Code Explorer 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/src/assets/background.jpg -------------------------------------------------------------------------------- /src/components/AccountLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { ellideMiddle } from "../ui-utils"; 5 | 6 | interface Props { 7 | readonly address: string; 8 | readonly maxLength?: number | null; 9 | } 10 | 11 | export function AccountLink({ address, maxLength = 20 }: Props): JSX.Element { 12 | return ( 13 | 14 | {ellideMiddle(address, maxLength || 99999)} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContract, MsgInstantiateContract, MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 2 | import { Registry } from "cosmwasm"; 3 | import React from "react"; 4 | import { Redirect, Route, Switch } from "react-router"; 5 | 6 | import { ClientContext, ClientContextValue } from "../contexts/ClientContext"; 7 | import { AccountPage } from "../pages/account/AccountPage"; 8 | import { CodePage } from "../pages/code/CodePage"; 9 | import { CodesPage } from "../pages/codes/CodesPage"; 10 | import { ContractPage } from "../pages/contract/ContractPage"; 11 | import { TxPage } from "../pages/tx/TxPage"; 12 | import { settings } from "../settings"; 13 | import { CosmWasmClient, SigningCosmWasmClient } from "../ui-utils/clients"; 14 | import { 15 | msgExecuteContractTypeUrl, 16 | msgInstantiateContractTypeUrl, 17 | msgStoreCodeTypeUrl, 18 | } from "../ui-utils/txs"; 19 | import { FlexibleRouter } from "./FlexibleRouter"; 20 | 21 | const { nodeUrls } = settings.backend; 22 | const typeRegistry = new Registry([ 23 | [msgStoreCodeTypeUrl, MsgStoreCode], 24 | [msgInstantiateContractTypeUrl, MsgInstantiateContract], 25 | [msgExecuteContractTypeUrl, MsgExecuteContract], 26 | ]); 27 | 28 | export function App(): JSX.Element { 29 | const [nodeUrl, setNodeUrl] = React.useState(nodeUrls[0]); 30 | const [userAddress, setUserAddress] = React.useState(); 31 | const [signingClient, setSigningClient] = React.useState(); 32 | const [contextValue, setContextValue] = React.useState({ 33 | nodeUrl: nodeUrl, 34 | client: null, 35 | typeRegistry: typeRegistry, 36 | resetClient: setNodeUrl, 37 | userAddress: userAddress, 38 | setUserAddress: setUserAddress, 39 | signingClient: signingClient, 40 | setSigningClient: setSigningClient, 41 | }); 42 | 43 | React.useEffect(() => { 44 | (async function updateContextValue() { 45 | const client = await CosmWasmClient.connect(nodeUrl); 46 | setContextValue((prevContextValue) => ({ ...prevContextValue, nodeUrl: nodeUrl, client: client })); 47 | })(); 48 | }, [nodeUrl]); 49 | 50 | React.useEffect(() => { 51 | setContextValue((prevContextValue) => ({ ...prevContextValue, signingClient: signingClient })); 52 | }, [signingClient]); 53 | 54 | React.useEffect(() => { 55 | setContextValue((prevContextValue) => ({ ...prevContextValue, userAddress: userAddress })); 56 | }, [userAddress]); 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | } /> 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/CodeLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface Props { 5 | readonly codeId: number; 6 | readonly text?: string; 7 | } 8 | 9 | export function CodeLink({ codeId, text }: Props): JSX.Element { 10 | return {text || `Code #${codeId}`}; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ContractLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { ellideMiddle } from "../ui-utils"; 5 | 6 | interface Props { 7 | readonly address: string; 8 | readonly maxLength?: number | null; 9 | } 10 | 11 | export function ContractLink({ address, maxLength = 20 }: Props): JSX.Element { 12 | return ( 13 | 14 | {ellideMiddle(address, maxLength || 99999)} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/EndpointSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | 3 | import { NonEmptyArray } from "../settings/backend"; 4 | 5 | interface Props { 6 | readonly currentUrl: string; 7 | readonly urls: NonEmptyArray; 8 | readonly urlChanged: (newUrl: string) => void; 9 | } 10 | 11 | export function EndpointSelector({ urls, currentUrl, urlChanged }: Props): JSX.Element { 12 | if (urls.length === 1) { 13 | return {currentUrl}; 14 | } else { 15 | return ( 16 | 17 | 27 |
28 | {urls.map((url) => ( 29 | 37 | ))} 38 |
39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/FlexibleRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, HashRouter } from "react-router-dom"; 3 | 4 | interface Props { 5 | readonly type: "browser-router" | "hash-router"; 6 | readonly children: React.ReactNode; 7 | } 8 | 9 | export function FlexibleRouter({ type, children }: Props): JSX.Element { 10 | switch (type) { 11 | case "browser-router": 12 | return {children}; 13 | case "hash-router": 14 | return {children}; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/FooterRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | 3 | import { ClientContext } from "../contexts/ClientContext"; 4 | import { settings } from "../settings"; 5 | import { ErrorState, errorState, LoadingState, loadingState } from "../ui-utils/states"; 6 | import { EndpointSelector } from "./EndpointSelector"; 7 | import { NodeInfoModal } from "./NodeInfoModal"; 8 | 9 | const hrStyle: React.CSSProperties = { 10 | borderColor: "rgba(255, 255, 255, 0.8)", 11 | }; 12 | const whiteText = { color: "#f0f0f0" }; 13 | 14 | function Separator(): JSX.Element { 15 | return | ; 16 | } 17 | 18 | /** Place me as a row in a container */ 19 | export function FooterRow(): JSX.Element { 20 | const { client, nodeUrl, resetClient } = React.useContext(ClientContext); 21 | 22 | const [chainId, setChainId] = React.useState(loadingState); 23 | const [height, setHeight] = React.useState(loadingState); 24 | 25 | const updateHeight = React.useCallback(() => { 26 | client 27 | ?.getHeight() 28 | .then(setHeight) 29 | .catch(() => setHeight(errorState)); 30 | }, [client]); 31 | 32 | React.useEffect(() => { 33 | client 34 | ?.getChainId() 35 | .then(setChainId) 36 | .catch(() => setChainId(errorState)); 37 | updateHeight(); 38 | }, [client, updateHeight]); 39 | 40 | return ( 41 |
42 |
43 |
44 | 45 |
46 | Endpoint{" "} 47 | resetClient(newUrl)} 51 | />{" "} 52 | 61 | 62 | 63 | Fork me on GitHub 64 | 65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Login } from "./Login"; 4 | import { UserAddress } from "./UserAddress"; 5 | 6 | export function Header(): JSX.Element { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ClientContext } from "../contexts/ClientContext"; 4 | import { settings } from "../settings"; 5 | import { 6 | getAddressAndStargateSigningClient, 7 | loadKeplrWallet, 8 | loadLedgerWallet, 9 | loadOrCreateWalletDirect, 10 | WalletLoaderDirect, 11 | webUsbMissing, 12 | } from "../ui-utils/clients"; 13 | 14 | export function Login(): JSX.Element { 15 | const { userAddress, setUserAddress, setSigningClient, client } = React.useContext(ClientContext); 16 | const [mnemonic, setMnemonic] = React.useState(); 17 | const [loading, setLoading] = React.useState(false); 18 | const [error, setError] = React.useState(); 19 | 20 | async function loginStargate(loadWallet: WalletLoaderDirect): Promise { 21 | setLoading(true); 22 | setError(undefined); 23 | 24 | try { 25 | const [userAddress, signingClient] = await getAddressAndStargateSigningClient(loadWallet, mnemonic); 26 | setUserAddress(userAddress); 27 | setSigningClient(signingClient); 28 | } catch (error: any) { 29 | setError(error.message); 30 | } 31 | 32 | setLoading(false); 33 | } 34 | 35 | function logout(): void { 36 | setError(undefined); 37 | setUserAddress(undefined); 38 | setMnemonic(undefined); 39 | setSigningClient(undefined); 40 | } 41 | 42 | function renderLoginButton(): JSX.Element { 43 | const { keplrChainInfo } = settings.backend; 44 | 45 | let keplrButton; 46 | if (keplrChainInfo !== undefined && client !== null) { 47 | keplrButton = ( 48 | 54 | ); 55 | } 56 | 57 | return loading ? ( 58 | 62 | ) : ( 63 | <> 64 |
65 | Mnemonic: 66 | setMnemonic(event.target.value)} 70 | /> 71 |
72 | 81 |
82 |
with
83 | 86 | {keplrButton} 87 | 94 |
95 | 96 | ); 97 | } 98 | 99 | function renderLogoutButton(): JSX.Element { 100 | return ( 101 | 104 | ); 105 | } 106 | 107 | const isUserLoggedIn = !!userAddress; 108 | 109 | return ( 110 |
111 | {error ?
{error}
: null} 112 | {isUserLoggedIn ? renderLogoutButton() : renderLoginButton()} 113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/components/NodeInfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ClientContext } from "../contexts/ClientContext"; 4 | import { ErrorState, isErrorState, isLoadingState, LoadingState } from "../ui-utils/states"; 5 | 6 | interface Props { 7 | readonly htmlId: string; 8 | readonly chainId: string | ErrorState | LoadingState; 9 | readonly height: number | ErrorState | LoadingState; 10 | } 11 | 12 | export function NodeInfoModal({ htmlId, chainId, height }: Props): JSX.Element { 13 | const clientContext = React.useContext(ClientContext); 14 | 15 | return ( 16 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/TransactionLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { ellideMiddle } from "../ui-utils"; 5 | 6 | interface Props { 7 | readonly transactionId: string; 8 | readonly maxLength?: number | null; 9 | } 10 | 11 | export function TransactionLink({ transactionId, maxLength = 20 }: Props): JSX.Element { 12 | return ( 13 | 14 | {ellideMiddle(transactionId, maxLength || 99999)} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/UserAddress.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { ClientContext } from "../contexts/ClientContext"; 4 | 5 | export function UserAddress(): JSX.Element { 6 | const { userAddress } = React.useContext(ClientContext); 7 | 8 | return userAddress ? ( 9 |
10 | My address: 11 | {userAddress} 12 |
13 | ) : ( 14 | <> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/contexts/ClientContext.tsx: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContract, MsgInstantiateContract, MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 2 | import { Registry } from "cosmwasm"; 3 | import React from "react"; 4 | 5 | import { CosmWasmClient, SigningCosmWasmClient } from "../ui-utils/clients"; 6 | import { 7 | msgExecuteContractTypeUrl, 8 | msgInstantiateContractTypeUrl, 9 | msgStoreCodeTypeUrl, 10 | } from "../ui-utils/txs"; 11 | 12 | export interface ClientContextValue { 13 | readonly nodeUrl: string; 14 | readonly client: CosmWasmClient | null; 15 | readonly typeRegistry: Registry; 16 | readonly resetClient: (nodeUrl: string) => void; 17 | readonly userAddress?: string; 18 | readonly setUserAddress: (newUserAddress?: string) => void; 19 | readonly signingClient?: SigningCosmWasmClient; 20 | readonly setSigningClient: (newSigningClient?: SigningCosmWasmClient) => void; 21 | } 22 | 23 | /** 24 | * "only used when a component does not have a matching Provider above it in the tree" 25 | * 26 | * @see https://reactjs.org/docs/context.html#reactcreatecontext 27 | */ 28 | const dummyContext: ClientContextValue = { 29 | nodeUrl: "", 30 | client: null, 31 | typeRegistry: new Registry([ 32 | [msgStoreCodeTypeUrl, MsgStoreCode], 33 | [msgInstantiateContractTypeUrl, MsgInstantiateContract], 34 | [msgExecuteContractTypeUrl, MsgExecuteContract], 35 | ]), 36 | resetClient: () => {}, 37 | setUserAddress: () => {}, 38 | setSigningClient: () => {}, 39 | }; 40 | 41 | export const ClientContext = React.createContext(dummyContext); 42 | -------------------------------------------------------------------------------- /src/hw-transport-webusb.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@ledgerhq/hw-transport-webusb"; 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | background-size: 100%; 10 | background-repeat: no-repeat; 11 | background-color: black !important; 12 | background-image: url(./assets/background.jpg); 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | 20 | /* 21 | * .page is the base node within the JavaScript application. It is the direct child of .root, 22 | * whichlives outside of JavaScript. The .page has a Header and a .container. 23 | */ 24 | /* 25 | .page { 26 | } 27 | */ 28 | 29 | .white-row { 30 | background-color: rgba(255, 255, 255, 0.8); 31 | color: #222; 32 | } 33 | 34 | .white-row-first { 35 | border-top-left-radius: 10px; 36 | border-top-right-radius: 10px; 37 | padding-top: 1rem; 38 | } 39 | 40 | .white-row-last { 41 | border-bottom-left-radius: 10px; 42 | border-bottom-right-radius: 10px; 43 | padding-bottom: 0; 44 | } 45 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "bootstrap/dist/js/bootstrap.bundle"; 3 | import "jquery/dist/jquery.slim"; 4 | import "./index.css"; 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | import { App } from "./components/App"; 10 | 11 | ReactDOM.render(, document.getElementById("root")); 12 | -------------------------------------------------------------------------------- /src/pages/account/AccountPage.tsx: -------------------------------------------------------------------------------- 1 | import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; 2 | import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; 3 | import { IndexedTx, Registry } from "cosmwasm"; 4 | import React from "react"; 5 | import { Link, useParams } from "react-router-dom"; 6 | 7 | import { FooterRow } from "../../components/FooterRow"; 8 | import { Header } from "../../components/Header"; 9 | import { ClientContext } from "../../contexts/ClientContext"; 10 | import { settings } from "../../settings"; 11 | import { ellideMiddle, printableBalance } from "../../ui-utils"; 12 | import { 13 | ErrorState, 14 | errorState, 15 | isErrorState, 16 | isLoadingState, 17 | LoadingState, 18 | loadingState, 19 | } from "../../ui-utils/states"; 20 | import { AnyMsgSend, isAnyMsgSend } from "../../ui-utils/txs"; 21 | import { Transfer, TransfersTable } from "./TransfersTable"; 22 | 23 | type ICoin = Coin; 24 | 25 | function getTransferFromStargateMsgSend(typeRegistry: Registry, tx: IndexedTx) { 26 | return (msg: AnyMsgSend, i: number) => { 27 | const decodedMsg = typeRegistry.decode({ typeUrl: msg.typeUrl, value: msg.value }); 28 | return { 29 | key: `${tx.hash}_${i}`, 30 | height: tx.height, 31 | transactionId: tx.hash, 32 | msg: decodedMsg, 33 | }; 34 | }; 35 | } 36 | 37 | export function AccountPage(): JSX.Element { 38 | const { client, typeRegistry } = React.useContext(ClientContext); 39 | const { address: addressParam } = useParams<{ readonly address: string }>(); 40 | const address = addressParam || ""; 41 | 42 | const [balance, setBalance] = React.useState(loadingState); 43 | const [transfers, setTransfers] = React.useState( 44 | loadingState, 45 | ); 46 | 47 | React.useEffect(() => { 48 | if (!client) return; 49 | 50 | Promise.all(settings.backend.denominations.map((denom) => client.getBalance(address, denom))) 51 | .then((balances) => { 52 | const filteredBalances = balances.filter((balance): balance is Coin => balance !== null); 53 | setBalance(filteredBalances); 54 | }) 55 | .catch(() => setBalance(errorState)); 56 | client 57 | .searchTx({ sentFromOrTo: address }) 58 | .then((txs) => { 59 | const out = txs.reduce((transfers: readonly Transfer[], tx: IndexedTx): readonly Transfer[] => { 60 | const decodedTx = Tx.decode(tx.tx); 61 | const txTransfers = (decodedTx?.body?.messages ?? []) 62 | .filter(isAnyMsgSend) 63 | .map(getTransferFromStargateMsgSend(typeRegistry, tx)); 64 | return [...transfers, ...txTransfers]; 65 | }, []); 66 | setTransfers(out); 67 | }) 68 | .catch(() => setTransfers(errorState)); 69 | }, [address, client, typeRegistry]); 70 | 71 | const pageTitle = Account {ellideMiddle(address, 15)}; 72 | 73 | return ( 74 |
75 |
76 |
77 |
78 |
79 | 89 |
90 |
91 | 92 |
93 |
94 |

{pageTitle}

95 |
    96 |
  • 97 | Balance:{" "} 98 | {isLoadingState(balance) 99 | ? "Loading …" 100 | : isErrorState(balance) 101 | ? "Error" 102 | : printableBalance(balance)} 103 |
  • 104 |
105 |
106 |
107 | 108 |
109 |
110 |

Token transfers

111 |

Incoming and outgoing bank token transfers

112 | {isLoadingState(transfers) ? ( 113 |

Loading …

114 | ) : isErrorState(transfers) ? ( 115 |

Error

116 | ) : transfers.length === 0 ? ( 117 |

No transfer found

118 | ) : ( 119 | 120 | )} 121 |
122 |
123 | 124 | 125 |
126 |
127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /src/pages/account/TransfersTable.tsx: -------------------------------------------------------------------------------- 1 | import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; 2 | import React from "react"; 3 | 4 | import { AccountLink } from "../../components/AccountLink"; 5 | import { TransactionLink } from "../../components/TransactionLink"; 6 | import { printableBalance } from "../../ui-utils"; 7 | 8 | export interface Transfer { 9 | readonly key: string; 10 | readonly height: number; 11 | readonly transactionId: string; 12 | readonly msg: MsgSend; 13 | } 14 | 15 | interface Props { 16 | readonly transfers: readonly Transfer[]; 17 | } 18 | 19 | export function TransfersTable({ transfers: executions }: Props): JSX.Element { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {executions.map((execution, index) => ( 34 | 35 | 36 | 37 | 40 | 43 | 46 | 47 | 48 | ))} 49 | 50 |
#HeightTransaction IDSenderRecipientAmount
{index + 1}{execution.height} 38 | 39 | 41 | 42 | 44 | 45 | {printableBalance(execution.msg.amount)}
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/code/CodeInfo.tsx: -------------------------------------------------------------------------------- 1 | import { CodeDetails } from "cosmwasm"; 2 | import React from "react"; 3 | 4 | import { AccountLink } from "../../components/AccountLink"; 5 | import { TransactionLink } from "../../components/TransactionLink"; 6 | import { ErrorState, isErrorState, isLoadingState, LoadingState } from "../../ui-utils/states"; 7 | 8 | interface Props { 9 | readonly code: CodeDetails; 10 | readonly uploadTxHash: string | undefined | ErrorState | LoadingState; 11 | } 12 | 13 | export function CodeInfo({ code, uploadTxHash }: Props): JSX.Element { 14 | return ( 15 |
16 |
    17 |
  • 18 | Upload transaction:{" "} 19 | {isLoadingState(uploadTxHash) ? ( 20 | "Loading …" 21 | ) : isErrorState(uploadTxHash) ? ( 22 | "Error" 23 | ) : uploadTxHash === undefined ? ( 24 | "–" 25 | ) : ( 26 | 27 | )} 28 |
  • 29 |
  • 30 | Creator: 31 |
  • 32 |
  • Checksum: {code.checksum}
  • 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/code/CodePage.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/src/pages/code/CodePage.css -------------------------------------------------------------------------------- /src/pages/code/CodePage.tsx: -------------------------------------------------------------------------------- 1 | import "./CodePage.css"; 2 | 3 | import { CodeDetails } from "cosmwasm"; 4 | import React from "react"; 5 | import { Link, useParams } from "react-router-dom"; 6 | 7 | import { FooterRow } from "../../components/FooterRow"; 8 | import { Header } from "../../components/Header"; 9 | import { ClientContext } from "../../contexts/ClientContext"; 10 | import { makeTags } from "../../ui-utils/sdkhelpers"; 11 | import { 12 | ErrorState, 13 | errorState, 14 | isErrorState, 15 | isLoadingState, 16 | LoadingState, 17 | loadingState, 18 | } from "../../ui-utils/states"; 19 | import { CodeInfo } from "./CodeInfo"; 20 | import InstanceRow from "./InstanceRow"; 21 | import { InstancesEmptyState } from "./InstancesEmptyState"; 22 | 23 | export function CodePage(): JSX.Element { 24 | const { client } = React.useContext(ClientContext); 25 | const { codeId: codeIdParam } = useParams<{ readonly codeId: string }>(); 26 | const codeId = parseInt(codeIdParam || "0", 10); 27 | 28 | const [details, setDetails] = React.useState(loadingState); 29 | const [contracts, setContracts] = React.useState( 30 | loadingState, 31 | ); 32 | const [uploadTxHash, setUploadTxHash] = React.useState( 33 | loadingState, 34 | ); 35 | 36 | React.useEffect(() => { 37 | client 38 | ?.getContracts(codeId) 39 | .then(setContracts) 40 | .catch(() => setContracts(errorState)); 41 | client 42 | ?.getCodeDetails(codeId) 43 | .then(setDetails) 44 | .catch(() => setDetails(errorState)); 45 | client 46 | ?.searchTx({ 47 | tags: makeTags(`message.module=wasm&store_code.code_id=${codeId}`), 48 | }) 49 | .then((results) => { 50 | const first = results.find(() => true); 51 | setUploadTxHash(first?.hash); 52 | }); 53 | }, [client, codeId]); 54 | 55 | const pageTitle = Code #{codeId}; 56 | 57 | return ( 58 |
59 |
60 |
61 |
62 |
63 | 73 |
74 |
75 |
76 |
77 |

{pageTitle}

78 |
    79 |
  • Type: Wasm
  • 80 |
  • 81 | Size:{" "} 82 | {isLoadingState(details) 83 | ? "Loading …" 84 | : isErrorState(details) 85 | ? "Error" 86 | : Math.round(details.data.length / 1024) + " KiB"} 87 |
  • 88 |
89 |
90 |
91 | {isLoadingState(details) ? ( 92 | Loading … 93 | ) : isErrorState(details) ? ( 94 | Error 95 | ) : ( 96 | 97 | )} 98 |
99 |
100 |
101 |
102 |

Instances

103 | {isLoadingState(contracts) ? ( 104 |

Loading …

105 | ) : isErrorState(contracts) ? ( 106 |

Error loading instances

107 | ) : contracts.length === 0 ? ( 108 | 109 | ) : ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {contracts.map((address, index) => ( 123 | 124 | ))} 125 | 126 |
#LabelContractCreatorAdminExecutions
127 | )} 128 |
129 |
130 | 131 |
132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/pages/code/InstanceRow.tsx: -------------------------------------------------------------------------------- 1 | import { Contract } from "cosmwasm"; 2 | import React from "react"; 3 | 4 | import { AccountLink } from "../../components/AccountLink"; 5 | import { ContractLink } from "../../components/ContractLink"; 6 | import { ClientContext } from "../../contexts/ClientContext"; 7 | import { 8 | ErrorState, 9 | errorState, 10 | isErrorState, 11 | isLoadingState, 12 | LoadingState, 13 | loadingState, 14 | } from "../../ui-utils/states"; 15 | 16 | interface Props { 17 | readonly position: number; 18 | readonly address: string; 19 | } 20 | 21 | function InstanceRow({ position, address }: Props): JSX.Element { 22 | const { client } = React.useContext(ClientContext); 23 | const [executionCount, setExecutionCount] = React.useState( 24 | loadingState, 25 | ); 26 | const [contract, setContractInfo] = React.useState(loadingState); 27 | 28 | React.useEffect(() => { 29 | (client?.getContract(address) as Promise) 30 | .then((execTxs) => setContractInfo(execTxs)) 31 | .catch(() => setContractInfo(errorState)); 32 | 33 | const tags = [ 34 | { 35 | key: "execute._contract_address", 36 | value: address, 37 | }, 38 | ]; 39 | (client?.searchTx({ tags: tags }) as Promise>) 40 | .then((execTxs) => setExecutionCount(execTxs.length)) 41 | .catch(() => setExecutionCount(errorState)); 42 | }, [client, address]); 43 | 44 | return isLoadingState(contract) ? ( 45 | 46 | Loading ... 47 | 48 | ) : isErrorState(contract) ? ( 49 | 50 | Error 51 | 52 | ) : ( 53 | 54 | {position} 55 | {contract.label} 56 | 57 | 58 | 59 | 60 | 61 | 62 | {contract.admin ? : "–"} 63 | 64 | {isLoadingState(executionCount) 65 | ? "Loading …" 66 | : isErrorState(executionCount) 67 | ? "Error" 68 | : executionCount} 69 | 70 | 71 | ); 72 | } 73 | 74 | export default InstanceRow; 75 | -------------------------------------------------------------------------------- /src/pages/code/InstancesEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function InstancesEmptyState(): JSX.Element { 4 | return

Code is not yet instantiated

; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/codes/Code.css: -------------------------------------------------------------------------------- 1 | /* See https://stackoverflow.com/a/51775832/2013738 */ 2 | .flex-element-two-two { 3 | flex-grow: 1; 4 | flex-shrink: 1; 5 | flex-basis: 50%; 6 | } 7 | 8 | a.code-content { 9 | padding: 10px; 10 | border-radius: 5px; 11 | background-color: rgba(255, 255, 255, 0.8); 12 | color: #222; 13 | display: block; 14 | height: 100%; 15 | } 16 | 17 | a.code-content:hover { 18 | background-color: rgba(255, 255, 255, 0.9); 19 | } 20 | 21 | .code-content .id { 22 | font-size: 30px; 23 | width: 70px; 24 | float: left; 25 | text-align: center; 26 | margin-right: 10px; 27 | } 28 | 29 | .code-content .details { 30 | display: inline-block; 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/codes/Code.tsx: -------------------------------------------------------------------------------- 1 | import "./Code.css"; 2 | 3 | import React from "react"; 4 | import { Link } from "react-router-dom"; 5 | 6 | import { ClientContext } from "../../contexts/ClientContext"; 7 | import { ellideMiddle } from "../../ui-utils"; 8 | import { 9 | ErrorState, 10 | errorState, 11 | isErrorState, 12 | isLoadingState, 13 | LoadingState, 14 | loadingState, 15 | } from "../../ui-utils/states"; 16 | 17 | export interface CodeData { 18 | readonly codeId: number; 19 | readonly checksum: string; 20 | readonly creator: string; 21 | } 22 | 23 | interface Props { 24 | readonly data: CodeData; 25 | readonly index: number; 26 | } 27 | 28 | interface InstantiationInfo { 29 | readonly instantiations: number; 30 | } 31 | 32 | export function Code({ data, index }: Props): JSX.Element { 33 | const { client } = React.useContext(ClientContext); 34 | const [instantiationInfo, setInstantiationInfo] = React.useState< 35 | InstantiationInfo | ErrorState | LoadingState 36 | >(loadingState); 37 | 38 | React.useEffect(() => { 39 | client 40 | ?.getContracts(data.codeId) 41 | .then((contracts) => { 42 | setInstantiationInfo({ 43 | instantiations: contracts.length, 44 | }); 45 | }) 46 | .catch(() => setInstantiationInfo(errorState)); 47 | // Don't make clientContext.client a dependency. Whenever it changes, this component is recreated entirely 48 | // eslint-disable-next-line react-hooks/exhaustive-deps 49 | }, [data.codeId]); 50 | 51 | return ( 52 |
53 | 54 |
#{data.codeId}
55 |
56 | Creator: {ellideMiddle(data.creator, 30)} 57 |
58 | Checksum: {data.checksum.slice(0, 10)} 59 |
60 | Instances:{" "} 61 | {isLoadingState(instantiationInfo) 62 | ? "Loading …" 63 | : isErrorState(instantiationInfo) 64 | ? "Error" 65 | : instantiationInfo.instantiations} 66 |
67 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/codes/Codes.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/src/pages/codes/Codes.css -------------------------------------------------------------------------------- /src/pages/codes/Codes.tsx: -------------------------------------------------------------------------------- 1 | import "./Codes.css"; 2 | 3 | import React from "react"; 4 | 5 | import { ClientContext } from "../../contexts/ClientContext"; 6 | import { 7 | ErrorState, 8 | errorState, 9 | isErrorState, 10 | isLoadingState, 11 | LoadingState, 12 | loadingState, 13 | } from "../../ui-utils/states"; 14 | import { Code, CodeData } from "./Code"; 15 | 16 | interface LoadedCode { 17 | readonly source: string; 18 | readonly data: CodeData; 19 | } 20 | 21 | function codeKey(code: LoadedCode): string { 22 | return `${code.source}__${code.data.codeId}`; 23 | } 24 | 25 | export function Codes(): JSX.Element { 26 | const { client, nodeUrl } = React.useContext(ClientContext); 27 | const [codes, setCodes] = React.useState(loadingState); 28 | 29 | React.useEffect(() => { 30 | if (!client) return; 31 | 32 | (async () => { 33 | try { 34 | const all = (await client.getCodes()).map( 35 | (entry): LoadedCode => ({ 36 | source: nodeUrl, 37 | data: { 38 | codeId: entry.id, 39 | checksum: entry.checksum, 40 | creator: entry.creator, 41 | }, 42 | }), 43 | ); 44 | all.reverse(); 45 | setCodes(all); 46 | } catch (_e: any) { 47 | setCodes(errorState); 48 | } 49 | })(); 50 | }, [client, nodeUrl]); 51 | 52 | // Display codes vertically on small devices and in a flex container on large and above 53 | return ( 54 |
55 | {isLoadingState(codes) ? ( 56 |

Loading …

57 | ) : isErrorState(codes) ? ( 58 |

Error loading codes

59 | ) : codes.length === 0 ? ( 60 |

No code uploaded yet

61 | ) : ( 62 | codes.map((code, index) => ) 63 | )} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/codes/CodesPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { FooterRow } from "../../components/FooterRow"; 4 | import { Header } from "../../components/Header"; 5 | import { Codes } from "./Codes"; 6 | 7 | export function CodesPage(): JSX.Element { 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/contract/ContractPage.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/src/pages/contract/ContractPage.css -------------------------------------------------------------------------------- /src/pages/contract/ContractPage.tsx: -------------------------------------------------------------------------------- 1 | import "./ContractPage.css"; 2 | 3 | import { Coin as ICoin } from "cosmjs-types/cosmos/base/v1beta1/coin"; 4 | import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; 5 | import { Any } from "cosmjs-types/google/protobuf/any"; 6 | import { Coin, Contract, ContractCodeHistoryEntry, IndexedTx, Registry } from "cosmwasm"; 7 | import React from "react"; 8 | import { Link, useParams } from "react-router-dom"; 9 | 10 | import { CodeLink } from "../../components/CodeLink"; 11 | import { FooterRow } from "../../components/FooterRow"; 12 | import { Header } from "../../components/Header"; 13 | import { ClientContext } from "../../contexts/ClientContext"; 14 | import { settings } from "../../settings"; 15 | import { ellideMiddle, printableBalance } from "../../ui-utils"; 16 | import { CosmWasmClient } from "../../ui-utils/clients"; 17 | import { makeTags } from "../../ui-utils/sdkhelpers"; 18 | import { 19 | ErrorState, 20 | errorState, 21 | isErrorState, 22 | isLoadingState, 23 | LoadingState, 24 | loadingState, 25 | } from "../../ui-utils/states"; 26 | import { ExecuteContract } from "./ExecuteContract"; 27 | import { Execution, ExecutionsTable } from "./ExecutionsTable"; 28 | import { HistoryInfo } from "./HistoryInfo"; 29 | import { InitializationInfo } from "./InitializationInfo"; 30 | import { QueryContract } from "./QueryContract"; 31 | 32 | type IAnyMsgExecuteContract = { 33 | readonly typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract"; 34 | readonly value: Uint8Array; 35 | }; 36 | 37 | export type Result = { readonly result?: T; readonly error?: string }; 38 | 39 | function isStargateMsgExecuteContract(msg: Any): msg is IAnyMsgExecuteContract { 40 | return msg.typeUrl === "/cosmwasm.wasm.v1.MsgExecuteContract" && !!msg.value; 41 | } 42 | 43 | const getAndSetDetails = ( 44 | client: CosmWasmClient, 45 | contractAddress: string, 46 | setDetails: (details: Contract | ErrorState | LoadingState) => void, 47 | ): void => { 48 | client 49 | .getContract(contractAddress) 50 | .then(setDetails) 51 | .catch(() => setDetails(errorState)); 52 | }; 53 | 54 | const getAndSetContractCodeHistory = ( 55 | client: CosmWasmClient, 56 | contractAddress: string, 57 | setContractCodeHistory: (contractCodeHistory: readonly ContractCodeHistoryEntry[]) => void, 58 | ): void => { 59 | client 60 | .getContractCodeHistory(contractAddress) 61 | .then(setContractCodeHistory) 62 | .catch((error) => { 63 | console.error(error); 64 | }); 65 | }; 66 | 67 | const getAndSetInstantiationTxHash = ( 68 | client: CosmWasmClient, 69 | contractAddress: string, 70 | setInstantiationTxHash: (instantiationTxHash: string | undefined | ErrorState | LoadingState) => void, 71 | ): void => { 72 | client 73 | .searchTx({ 74 | tags: makeTags(`message.module=wasm&instantiate._contract_address=${contractAddress}`), 75 | }) 76 | .then((results) => { 77 | const first = results.find(() => true); 78 | setInstantiationTxHash(first?.hash); 79 | }) 80 | .catch(() => setInstantiationTxHash(errorState)); 81 | }; 82 | 83 | function getExecutionFromStargateMsgExecuteContract(typeRegistry: Registry, tx: IndexedTx) { 84 | return (msg: IAnyMsgExecuteContract, i: number) => { 85 | const decodedMsg = typeRegistry.decode({ typeUrl: msg.typeUrl, value: msg.value }); 86 | return { 87 | key: `${tx.hash}_${i}`, 88 | height: tx.height, 89 | transactionId: tx.hash, 90 | msg: decodedMsg, 91 | }; 92 | }; 93 | } 94 | 95 | export function ContractPage(): JSX.Element { 96 | const { client, typeRegistry } = React.useContext(ClientContext); 97 | const { contractAddress: contractAddressParam } = useParams<{ readonly contractAddress: string }>(); 98 | const contractAddress = contractAddressParam || ""; 99 | 100 | const [details, setDetails] = React.useState(loadingState); 101 | const [balance, setBalance] = React.useState(loadingState); 102 | const [instantiationTxHash, setInstantiationTxHash] = React.useState< 103 | string | undefined | ErrorState | LoadingState 104 | >(loadingState); 105 | const [contractCodeHistory, setContractCodeHistory] = React.useState( 106 | [], 107 | ); 108 | const [executions, setExecutions] = React.useState( 109 | loadingState, 110 | ); 111 | 112 | React.useEffect(() => { 113 | if (!client) return; 114 | 115 | getAndSetContractCodeHistory(client, contractAddress, setContractCodeHistory); 116 | getAndSetDetails(client, contractAddress, setDetails); 117 | getAndSetInstantiationTxHash(client, contractAddress, setInstantiationTxHash); 118 | 119 | Promise.all(settings.backend.denominations.map((denom) => client.getBalance(contractAddress, denom))) 120 | .then((balances) => { 121 | const filteredBalances = balances.filter((balance): balance is Coin => balance !== null); 122 | setBalance(filteredBalances); 123 | }) 124 | .catch(() => setBalance(errorState)); 125 | 126 | client 127 | .searchTx({ 128 | tags: makeTags(`message.module=wasm&execute._contract_address=${contractAddress}`), 129 | }) 130 | .then((txs) => { 131 | const out = txs.reduce((executions: readonly Execution[], tx: IndexedTx): readonly Execution[] => { 132 | const decodedTx = Tx.decode(tx.tx); 133 | const txExecutions = (decodedTx?.body?.messages ?? []) 134 | .filter(isStargateMsgExecuteContract) 135 | .map(getExecutionFromStargateMsgExecuteContract(typeRegistry, tx)); 136 | return [...executions, ...txExecutions]; 137 | }, []); 138 | setExecutions(out); 139 | }) 140 | .catch(() => setExecutions(errorState)); 141 | }, [client, contractAddress, typeRegistry]); 142 | 143 | const pageTitle = Contract {ellideMiddle(contractAddress, 15)}; 144 | 145 | return ( 146 |
147 |
148 |
149 |
150 |
151 | 170 |
171 |
172 |
173 |
174 |

{pageTitle}

175 |
    176 |
  • 177 | Balance:{" "} 178 | {isLoadingState(balance) 179 | ? "Loading …" 180 | : isErrorState(balance) 181 | ? "Error" 182 | : printableBalance(balance)} 183 |
  • 184 |
185 |
186 |
187 | {isLoadingState(details) ? ( 188 |

Loading …

189 | ) : isErrorState(details) ? ( 190 |

An Error occurred when loading contract

191 | ) : ( 192 | <> 193 | 194 | 195 | 196 | 197 | 198 | )} 199 |
200 |
201 |
202 |
203 |

Executions

204 | {isLoadingState(executions) ? ( 205 |

Loading …

206 | ) : isErrorState(executions) ? ( 207 |

An Error occurred when loading transactions

208 | ) : executions.length !== 0 ? ( 209 | 210 | ) : ( 211 |

Contract was not yet executed

212 | )} 213 |
214 |
215 | 216 | 217 |
218 |
219 | ); 220 | } 221 | -------------------------------------------------------------------------------- /src/pages/contract/ExecuteContract.tsx: -------------------------------------------------------------------------------- 1 | import { calculateFee, Coin, ExecuteResult } from "cosmwasm"; 2 | import React from "react"; 3 | import JSONInput from "react-json-editor-ajrm"; 4 | 5 | import { ClientContext } from "../../contexts/ClientContext"; 6 | import { settings } from "../../settings"; 7 | import { jsonInputStyle } from "../../ui-utils/jsonInput"; 8 | import { Result } from "./ContractPage"; 9 | 10 | const executePlaceholder = { 11 | transfer: { recipient: "cosmos1zk4hr47hlch274x28j32dgnhuyewqjrwxn4mvm", amount: "1" }, 12 | }; 13 | 14 | const coinsPlaceholder = [{ denom: settings.backend.denominations[0], amount: "1" }]; 15 | 16 | interface Props { 17 | readonly contractAddress: string; 18 | } 19 | 20 | export function ExecuteContract({ contractAddress }: Props): JSX.Element { 21 | const { userAddress, signingClient } = React.useContext(ClientContext); 22 | 23 | const [executing, setExecuting] = React.useState(false); 24 | const [error, setError] = React.useState(); 25 | 26 | const [memo, setMemo] = React.useState(); 27 | 28 | const [msgObject, setMsgObject] = React.useState>>(); 29 | const [coinsObject, setCoinsObject] = React.useState>>(); 30 | 31 | const [executeResponse, setExecuteResponse] = React.useState>(); 32 | 33 | React.useEffect(() => { 34 | setMsgObject({ result: executePlaceholder }); 35 | setCoinsObject({ result: coinsPlaceholder }); 36 | }, []); 37 | 38 | React.useEffect(() => { 39 | if (msgObject?.error) { 40 | setError(msgObject.error); 41 | return; 42 | } 43 | 44 | if (executeResponse?.error) { 45 | setError(executeResponse.error); 46 | return; 47 | } 48 | 49 | if (coinsObject?.error) { 50 | setError(coinsObject.error); 51 | return; 52 | } 53 | 54 | setError(undefined); 55 | }, [coinsObject, executeResponse, msgObject]); 56 | 57 | async function executeContract(): Promise { 58 | if (!msgObject?.result || !userAddress || !signingClient) return; 59 | 60 | setExecuting(true); 61 | 62 | try { 63 | const executeResponseResult: ExecuteResult = await signingClient.execute( 64 | userAddress, 65 | contractAddress, 66 | msgObject.result, 67 | calculateFee(400000, settings.backend.gasPrice), 68 | memo, 69 | coinsObject?.result, 70 | ); 71 | setExecuteResponse({ result: executeResponseResult }); 72 | } catch (error: any) { 73 | setExecuteResponse({ error: `Execute error: ${error.message}` }); 74 | } 75 | 76 | setExecuting(false); 77 | } 78 | 79 | return ( 80 |
81 |
    82 |
  • 83 | Execute contract: 84 |
  • 85 |
  • 86 | setMsgObject({ result: jsObject })} 93 | /> 94 |
  • 95 |
  • 96 | Coins to transfer: 97 |
  • 98 |
  • 99 | setCoinsObject({ result: jsObject })} 106 | /> 107 |
  • 108 |
  • 109 | Memo: 110 | setMemo(event.target.value)} 114 | /> 115 |
  • 116 |
    117 | {executing ? ( 118 | 122 | ) : ( 123 | 130 | )} 131 |
    132 | {executeResponse?.result ? ( 133 |
  • 134 | Response: 135 |
    {JSON.stringify(executeResponse.result, undefined, "  ")}
    136 |
  • 137 | ) : null} 138 | {error ? ( 139 |
  • 140 | 141 | {error} 142 | 143 |
  • 144 | ) : null} 145 |
146 |
147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/pages/contract/ExecutionsTable.tsx: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 2 | import React from "react"; 3 | 4 | import { AccountLink } from "../../components/AccountLink"; 5 | import { TransactionLink } from "../../components/TransactionLink"; 6 | 7 | export interface Execution { 8 | readonly key: string; 9 | readonly height: number; 10 | readonly transactionId: string; 11 | readonly msg: MsgExecuteContract; 12 | } 13 | 14 | interface Props { 15 | readonly executions: readonly Execution[]; 16 | } 17 | 18 | export function ExecutionsTable({ executions }: Props): JSX.Element { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {executions.map((execution, index) => ( 31 | 32 | 33 | 34 | 37 | 40 | 41 | ))} 42 | 43 |
#HeightTransaction IDSender
{index + 1}{execution.height} 35 | 36 | 38 | 39 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/contract/HistoryInfo.tsx: -------------------------------------------------------------------------------- 1 | import { ContractCodeHistoryEntry } from "cosmwasm"; 2 | import React from "react"; 3 | import ReactJson from "react-json-view"; 4 | 5 | import { CodeLink } from "../../components/CodeLink"; 6 | 7 | interface Props { 8 | readonly contractCodeHistory: readonly ContractCodeHistoryEntry[]; 9 | } 10 | 11 | export function HistoryInfo({ contractCodeHistory }: Props): JSX.Element { 12 | return ( 13 |
14 |
    15 |
  • 16 | History 17 |
  • 18 | {contractCodeHistory.map((entry, index) => ( 19 |
  • 20 | 21 | {entry.operation} - 22 | 23 | 24 |
  • 25 | ))} 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/contract/InitializationInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Contract } from "cosmwasm"; 2 | import React from "react"; 3 | 4 | import { AccountLink } from "../../components/AccountLink"; 5 | import { TransactionLink } from "../../components/TransactionLink"; 6 | import { ErrorState, isErrorState, isLoadingState, LoadingState } from "../../ui-utils/states"; 7 | 8 | interface Props { 9 | readonly contract: Contract; 10 | readonly instantiationTxHash: string | undefined | ErrorState | LoadingState; 11 | } 12 | 13 | export function InitializationInfo({ contract, instantiationTxHash }: Props): JSX.Element { 14 | return ( 15 |
16 |
    17 |
  • 18 | Instantiation transaction:{" "} 19 | {isLoadingState(instantiationTxHash) ? ( 20 | "Loading …" 21 | ) : isErrorState(instantiationTxHash) ? ( 22 | "Error" 23 | ) : instantiationTxHash === undefined ? ( 24 | "–" 25 | ) : ( 26 | 27 | )} 28 |
  • 29 |
  • 30 | Creator: 31 |
  • 32 |
  • 33 | Admin: {contract.admin ? : "–"} 34 |
  • 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/contract/QueryContract.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import JSONInput from "react-json-editor-ajrm"; 3 | 4 | import { ClientContext } from "../../contexts/ClientContext"; 5 | import { jsonInputStyle } from "../../ui-utils/jsonInput"; 6 | import { Result } from "./ContractPage"; 7 | 8 | const queryPlaceholder = { get_balance: { address: "cosmos1zk4hr47hlch274x28j32dgnhuyewqjrwxn4mvm" } }; 9 | 10 | interface Props { 11 | readonly contractAddress: string; 12 | } 13 | 14 | export function QueryContract({ contractAddress }: Props): JSX.Element { 15 | const { client } = React.useContext(ClientContext); 16 | 17 | const [error, setError] = React.useState(); 18 | const [queryObject, setQueryObject] = React.useState>>(); 19 | const [queryResponse, setQueryResponse] = React.useState>(); 20 | 21 | React.useEffect(() => { 22 | setQueryObject({ result: queryPlaceholder }); 23 | }, []); 24 | 25 | React.useEffect(() => { 26 | if (queryObject?.error) { 27 | setError(queryObject.error); 28 | return; 29 | } 30 | 31 | if (queryResponse?.error) { 32 | setError(queryResponse.error); 33 | return; 34 | } 35 | 36 | setError(undefined); 37 | }, [queryObject, queryResponse]); 38 | 39 | async function runQuery(): Promise { 40 | if (!client || !queryObject?.result) return; 41 | 42 | try { 43 | const queryResponseResult: Record = await client.queryContractSmart( 44 | contractAddress, 45 | queryObject.result, 46 | ); 47 | 48 | const formattedResult = JSON.stringify(queryResponseResult, null, " "); 49 | setQueryResponse({ result: formattedResult }); 50 | } catch (error: any) { 51 | setQueryResponse({ error: `Query error: ${error.message}` }); 52 | } 53 | } 54 | 55 | return ( 56 |
57 |
    58 |
  • 59 | Query contract: 60 |
  • 61 |
  • 62 | setQueryObject({ result: jsObject })} 69 | /> 70 |
  • 71 |
  • 72 | 80 |
  • 81 | {queryResponse?.result ? ( 82 |
  • 83 | Response: 84 |
    {queryResponse.result}
    85 |
  • 86 | ) : null} 87 | {error ? ( 88 |
  • 89 | 90 | {error} 91 | 92 |
  • 93 | ) : null} 94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/tx/ExecutionInfo.tsx: -------------------------------------------------------------------------------- 1 | import { fromRfc3339, IndexedTx } from "cosmwasm"; 2 | import React from "react"; 3 | 4 | const checkMark = "✔"; // U+2714 HEAVY CHECK MARK 5 | const xMark = "🗙"; // U+1F5D9 CANCELLATION X 6 | 7 | interface Props { 8 | readonly tx: IndexedTx; 9 | readonly timestamp: string; 10 | } 11 | 12 | export function ExecutionInfo({ tx, timestamp }: Props): JSX.Element { 13 | const time = timestamp ? fromRfc3339(timestamp) : null; 14 | const success = tx.code === 0; 15 | 16 | return ( 17 |
    18 |
  • Height: {tx.height}
  • 19 |
  • 20 | Time: {time?.toLocaleString()} 21 |
  • 22 |
  • 23 | 24 | Success: {success ? checkMark : `${xMark} (error code ${tx.code})`} 25 | 26 |
  • 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/tx/TxInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; 2 | import React from "react"; 3 | 4 | import { printableBalance } from "../../ui-utils"; 5 | 6 | interface Props { 7 | readonly tx: Tx; 8 | } 9 | 10 | export function TxInfo({ tx }: Props): JSX.Element { 11 | return ( 12 |
13 |
    14 |
  • Memo: {tx.body?.memo || "–"}
  • 15 |
  • Fee: {printableBalance(tx.authInfo?.fee?.amount ?? [])}
  • 16 |
  • Gas: {tx.authInfo?.fee?.gasLimit?.toString() ?? "0"}
  • 17 |
  • Signatures: {tx.signatures?.length ?? 0}
  • 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/tx/TxPage.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmWasm/code-explorer/0f4b0f9102a6fe1d2aca4a01be351b0748421740/src/pages/tx/TxPage.css -------------------------------------------------------------------------------- /src/pages/tx/TxPage.tsx: -------------------------------------------------------------------------------- 1 | import "./TxPage.css"; 2 | 3 | import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx"; 4 | import { Block, IndexedTx } from "cosmwasm"; 5 | import React from "react"; 6 | import { useParams } from "react-router"; 7 | import { Link } from "react-router-dom"; 8 | 9 | import { FooterRow } from "../../components/FooterRow"; 10 | import { Header } from "../../components/Header"; 11 | import { ClientContext } from "../../contexts/ClientContext"; 12 | import { ellideMiddle } from "../../ui-utils"; 13 | import { 14 | ErrorState, 15 | errorState, 16 | isErrorState, 17 | isLoadingState, 18 | LoadingState, 19 | loadingState, 20 | } from "../../ui-utils/states"; 21 | import { 22 | isAnyMsgExecuteContract, 23 | isAnyMsgInstantiateContract, 24 | isAnyMsgSend, 25 | isAnyMsgStoreCode, 26 | } from "../../ui-utils/txs"; 27 | import { ExecutionInfo } from "./ExecutionInfo"; 28 | import { MsgExecuteContract } from "./msgs/MsgExecuteContract"; 29 | import { MsgInstantiateContract } from "./msgs/MsgInstantiateContract"; 30 | import { MsgSend } from "./msgs/MsgSend"; 31 | import { MsgStoreCode } from "./msgs/MsgStoreCode"; 32 | import { TxInfo } from "./TxInfo"; 33 | 34 | export function TxPage(): JSX.Element { 35 | const { client, typeRegistry } = React.useContext(ClientContext); 36 | const { txId: txIdParam } = useParams<{ readonly txId: string }>(); 37 | const txId = txIdParam || ""; 38 | 39 | const pageTitle = Tx {ellideMiddle(txId, 20)}; 40 | 41 | const [details, setDetails] = React.useState( 42 | loadingState, 43 | ); 44 | 45 | const [block, setBlockInfo] = React.useState(loadingState); 46 | 47 | React.useEffect(() => { 48 | if (!client) return; 49 | 50 | client 51 | .getTx(txId) 52 | .then((tx) => { 53 | setDetails(tx || undefined); 54 | if (!tx) return; 55 | client 56 | .getBlock(tx.height) 57 | .then((b) => { 58 | setBlockInfo(b); 59 | }) 60 | .catch(() => setBlockInfo(errorState)); 61 | }) 62 | .catch(() => setDetails(errorState)); 63 | }, [client, txId, typeRegistry]); 64 | 65 | return ( 66 |
67 |
68 |
69 |
70 |
71 | 81 |
82 |
83 | 84 |
85 |
86 |

{pageTitle}

87 | {isLoadingState(details) ? ( 88 |

Loading...

89 | ) : isErrorState(details) ? ( 90 |

Error

91 | ) : details === undefined ? ( 92 |

Transaction not found

93 | ) : ( 94 | 98 | )} 99 |
100 |
101 | {isLoadingState(details) ? ( 102 |

Loading …

103 | ) : isErrorState(details) ? ( 104 |

Error

105 | ) : details === undefined ? ( 106 |

Transaction not found

107 | ) : ( 108 | 109 | )} 110 |
111 |
112 | 113 |
114 |
115 |

Messages

116 |

117 | A Cosmos SDK transaction is composed of one or more messages, that represent actions to be 118 | executed. 119 |

120 | {isLoadingState(details) ? ( 121 |

Loading …

122 | ) : isErrorState(details) ? ( 123 |

Error

124 | ) : details === undefined ? ( 125 |

Transaction not found

126 | ) : ( 127 | Tx.decode(details.tx).body?.messages?.map((msg: any, index: number) => ( 128 |
129 |
130 | Message {index + 1} (Type: {msg.typeUrl || unset}) 131 |
132 |
    133 | {isAnyMsgSend(msg) ? ( 134 | 135 | ) : isAnyMsgStoreCode(msg) ? ( 136 | 137 | ) : isAnyMsgInstantiateContract(msg) ? ( 138 | 141 | ) : isAnyMsgExecuteContract(msg) ? ( 142 | 145 | ) : ( 146 |
  • 147 | This message type cannot be displayed 148 |
  • 149 | )} 150 |
151 |
152 | )) 153 | )} 154 |
155 |
156 | 157 | 158 |
159 |
160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/MsgExecuteContract.tsx: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContract as IMsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 2 | import React, { Fragment } from "react"; 3 | import ReactJson from "react-json-view"; 4 | 5 | import { AccountLink } from "../../../components/AccountLink"; 6 | import { ContractLink } from "../../../components/ContractLink"; 7 | import { parseMsgContract, printableBalance } from "../../../ui-utils"; 8 | 9 | interface Props { 10 | readonly msg: IMsgExecuteContract; 11 | } 12 | 13 | export function MsgExecuteContract({ msg }: Props): JSX.Element { 14 | return ( 15 | 16 |
  • 17 | Contract: 18 |
  • 19 |
  • 20 | Sender: 21 |
  • 22 |
  • Sent funds: {printableBalance(msg.funds)}
  • 23 |
  • 24 | Handle message: 25 | 26 |
  • 27 |
    28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/MsgInstantiateContract.tsx: -------------------------------------------------------------------------------- 1 | import { MsgInstantiateContract as IMsgInstantiateContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 2 | import React, { Fragment } from "react"; 3 | import ReactJson from "react-json-view"; 4 | 5 | import { AccountLink } from "../../../components/AccountLink"; 6 | import { CodeLink } from "../../../components/CodeLink"; 7 | import { parseMsgContract, printableBalance } from "../../../ui-utils"; 8 | 9 | interface Props { 10 | readonly msg: IMsgInstantiateContract; 11 | } 12 | 13 | export function MsgInstantiateContract({ msg }: Props): JSX.Element { 14 | return ( 15 | 16 |
  • 17 | Sender: 18 |
  • 19 |
  • 20 | Code ID: 21 |
  • 22 |
  • Label: {msg.label}
  • 23 |
  • Init funds: {printableBalance(msg.funds)}
  • 24 |
  • 25 | Init message: 26 | 27 |
  • 28 |
    29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/MsgSend.tsx: -------------------------------------------------------------------------------- 1 | import { MsgSend as IMsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; 2 | import React, { Fragment } from "react"; 3 | 4 | import { AccountLink } from "../../../components/AccountLink"; 5 | import { printableBalance } from "../../../ui-utils"; 6 | 7 | interface Props { 8 | readonly msg: IMsgSend; 9 | } 10 | 11 | export function MsgSend({ msg }: Props): JSX.Element { 12 | return ( 13 | 14 |
  • 15 | Sender: 16 |
  • 17 |
  • 18 | Recipient: 19 |
  • 20 |
  • Amount: {printableBalance(msg.amount ?? [])}
  • 21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/MsgStoreCode.css: -------------------------------------------------------------------------------- 1 | .long-inline-code { 2 | overflow-wrap: break-word; 3 | word-break: break-all; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/MsgStoreCode.tsx: -------------------------------------------------------------------------------- 1 | import "./MsgStoreCode.css"; 2 | 3 | import { MsgStoreCode as IMsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1beta1/tx"; 4 | import { toBase64 } from "cosmwasm"; 5 | import React, { Fragment } from "react"; 6 | 7 | import { AccountLink } from "../../../components/AccountLink"; 8 | import { ellideRight } from "../../../ui-utils"; 9 | import { getFileType } from "./magic"; 10 | 11 | interface Props { 12 | readonly msg: IMsgStoreCode; 13 | } 14 | 15 | export function MsgStoreCode({ msg }: Props): JSX.Element { 16 | const [showAllCode, setShowAllCode] = React.useState(false); 17 | 18 | const dataInfo = React.useMemo(() => { 19 | const data = msg.wasmByteCode ?? new Uint8Array(); 20 | return `${getFileType(data) || "unknown"}; ${data.length} bytes`; 21 | }, [msg.wasmByteCode]); 22 | 23 | return ( 24 | 25 |
  • 26 | Sender: 27 |
  • 28 |
  • Source: {msg.source || "–"}
  • 29 |
  • Builder: {msg.builder || "–"}
  • 30 |
  • 31 | Data: {dataInfo}{" "} 32 | {!showAllCode ? ( 33 | 34 | {ellideRight(toBase64(msg.wasmByteCode ?? new Uint8Array()), 300)}{" "} 35 | 38 | 39 | ) : ( 40 | {msg.wasmByteCode} 41 | )} 42 |
  • 43 |
    44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/magic.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromHex } from "cosmwasm"; 2 | 3 | import { getFileType } from "./magic"; 4 | 5 | describe("getFileType", () => { 6 | it("works", () => { 7 | expect(getFileType(fromHex(""))).toBeUndefined(); 8 | expect(getFileType(fromHex("1F"))).toBeUndefined(); 9 | expect(getFileType(fromHex("1F8B"))).toEqual("gzip"); 10 | expect(getFileType(fromHex("1F8Baa"))).toEqual("gzip"); 11 | expect(getFileType(fromHex("1F8Baabb"))).toEqual("gzip"); 12 | 13 | expect(getFileType(fromHex("00"))).toBeUndefined(); 14 | expect(getFileType(fromHex("0061"))).toBeUndefined(); 15 | expect(getFileType(fromHex("006173"))).toBeUndefined(); 16 | expect(getFileType(fromHex("0061736d"))).toEqual("wasm"); 17 | expect(getFileType(fromHex("0061736daa"))).toEqual("wasm"); 18 | expect(getFileType(fromHex("0061736daabb"))).toEqual("wasm"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/pages/tx/msgs/magic.ts: -------------------------------------------------------------------------------- 1 | import { fromHex } from "cosmwasm"; 2 | 3 | function arrayEqual(a: Uint8Array, b: Uint8Array): boolean { 4 | if (a.length !== b.length) return false; 5 | const difference = a.some((byte, index) => b[index] !== byte); 6 | return !difference; 7 | } 8 | 9 | function arrayStartsWith(a: Uint8Array, prefix: Uint8Array): boolean { 10 | return arrayEqual(a.slice(0, prefix.length), prefix); 11 | } 12 | 13 | const magic = { 14 | gzip: fromHex("1F8B"), 15 | wasm: fromHex("0061736d"), 16 | }; 17 | 18 | export type SupportedTypes = "gzip" | "wasm"; 19 | 20 | export function getFileType(data: Uint8Array): SupportedTypes | undefined { 21 | if (arrayStartsWith(data, magic.gzip)) return "gzip"; 22 | if (arrayStartsWith(data, magic.wasm)) return "wasm"; 23 | return undefined; 24 | } 25 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/react-json-editor-ajrm.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-json-editor-ajrm"; 2 | -------------------------------------------------------------------------------- /src/settings/backend.ts: -------------------------------------------------------------------------------- 1 | import { GasPrice } from "cosmwasm"; 2 | 3 | export type NonEmptyArray = { readonly 0: ElementType } & readonly ElementType[]; 4 | 5 | export interface BackendSettings { 6 | readonly nodeUrls: NonEmptyArray; 7 | readonly denominations: readonly string[]; 8 | readonly addressPrefix: string; 9 | readonly gasPrice: GasPrice; 10 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 11 | readonly keplrChainInfo?: any; 12 | } 13 | 14 | // Configuration matches local devnet as defined in 15 | // https://github.com/cosmos/cosmjs/tree/main/scripts/wasmd 16 | const devnetSettings: BackendSettings = { 17 | nodeUrls: ["http://localhost:26659"], 18 | denominations: ["ucosm", "ustake"], 19 | addressPrefix: "wasm", 20 | gasPrice: GasPrice.fromString("0.25ucosm"), 21 | }; 22 | 23 | // const oysternetSettings: BackendSettings = { 24 | // nodeUrls: ["http://rpc.oysternet.cosmwasm.com"], 25 | // denominations: ["usponge"], 26 | // addressPrefix: "wasm", 27 | // gasPrice: GasPrice.fromString("0.25ucosm"), 28 | // keplrChainInfo: { 29 | // rpc: "http://rpc.oysternet.cosmwasm.com", 30 | // rest: "http://lcd.oysternet.cosmwasm.com", 31 | // chainId: "oysternet-1", 32 | // chainName: "Wasm Oysternet", 33 | // stakeCurrency: { 34 | // coinDenom: "SPONGE", 35 | // coinMinimalDenom: "usponge", 36 | // coinDecimals: 6, 37 | // }, 38 | // bip44: { 39 | // coinType: 118, 40 | // }, 41 | // bech32Config: { 42 | // bech32PrefixAccAddr: "wasm", 43 | // bech32PrefixAccPub: "wasmpub", 44 | // bech32PrefixValAddr: "wasmvaloper", 45 | // bech32PrefixValPub: "wasmvaloperpub", 46 | // bech32PrefixConsAddr: "wasmvalcons", 47 | // bech32PrefixConsPub: "wasmvalconspub", 48 | // }, 49 | // currencies: [ 50 | // { 51 | // coinDenom: "SPONGE", 52 | // coinMinimalDenom: "usponge", 53 | // coinDecimals: 6, 54 | // }, 55 | // ], 56 | // feeCurrencies: [ 57 | // { 58 | // coinDenom: "SPONGE", 59 | // coinMinimalDenom: "usponge", 60 | // coinDecimals: 6, 61 | // }, 62 | // ], 63 | // features: ["stargate"], 64 | // }, 65 | // }; 66 | 67 | const knownBackends: Partial> = { 68 | devnet: devnetSettings, 69 | cliffnet: { 70 | nodeUrls: ["https://rpc.cliffnet.cosmwasm.com"], 71 | denominations: ["upebble", "urock"], 72 | addressPrefix: "wasm", 73 | gasPrice: GasPrice.fromString("0.25upebble"), 74 | }, 75 | }; 76 | 77 | export function getCurrentBackend(): BackendSettings { 78 | const id = process.env.REACT_APP_BACKEND || "devnet"; 79 | const backend = knownBackends[id]; 80 | if (!backend) { 81 | throw new Error(`No backend found for the given ID "${id}"`); 82 | } 83 | return backend; 84 | } 85 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | import { BackendSettings, getCurrentBackend } from "./backend"; 4 | 5 | export interface DeploymentSettings { 6 | readonly routerType: "browser-router" | "hash-router"; 7 | } 8 | 9 | export interface Settings { 10 | /** Where do we connect to */ 11 | readonly backend: BackendSettings; 12 | /** How are we hosted */ 13 | readonly deployment: DeploymentSettings; 14 | } 15 | 16 | const developmentServer: DeploymentSettings = { 17 | routerType: "browser-router", 18 | }; 19 | 20 | const ghPages: DeploymentSettings = { 21 | routerType: "hash-router", 22 | }; 23 | 24 | const firebaseHosting: DeploymentSettings = { 25 | routerType: "browser-router", 26 | }; 27 | 28 | export const settings: Settings = { 29 | backend: getCurrentBackend(), 30 | deployment: process.env.NODE_ENV === "production" ? ghPages : developmentServer, 31 | }; 32 | -------------------------------------------------------------------------------- /src/ui-utils/clients.ts: -------------------------------------------------------------------------------- 1 | import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; 2 | import { MsgExecuteContract, MsgInstantiateContract, MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx"; 3 | import { 4 | Bip39, 5 | CosmWasmClient, 6 | DirectSecp256k1HdWallet, 7 | LedgerSigner, 8 | makeCosmoshubPath, 9 | OfflineAminoSigner, 10 | OfflineDirectSigner, 11 | OfflineSigner, 12 | Random, 13 | Registry, 14 | SigningCosmWasmClient, 15 | } from "cosmwasm"; 16 | 17 | import { settings } from "../settings"; 18 | import { msgExecuteContractTypeUrl, msgInstantiateContractTypeUrl, msgStoreCodeTypeUrl } from "./txs"; 19 | 20 | export { CosmWasmClient, SigningCosmWasmClient }; 21 | 22 | export function generateMnemonic(): string { 23 | return Bip39.encode(Random.getBytes(16)).toString(); 24 | } 25 | 26 | export function loadOrCreateMnemonic(mnemonic?: string): string { 27 | const key = "burner-wallet"; 28 | const loaded = localStorage.getItem(key); 29 | if (loaded && !mnemonic) { 30 | return loaded; 31 | } 32 | const loadedMnemonic = mnemonic || generateMnemonic(); 33 | localStorage.setItem(key, loadedMnemonic); 34 | return loadedMnemonic; 35 | } 36 | 37 | export type WalletLoaderDirect = ( 38 | addressPrefix: string, 39 | mnemonic?: string, 40 | ) => Promise; 41 | 42 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 43 | export function loadKeplrWallet(client: CosmWasmClient, keplrChainInfo: any): WalletLoaderDirect { 44 | return async () => { 45 | const chaindId = await client.getChainId(); 46 | 47 | await registerKeplrChain(keplrChainInfo); 48 | const w = window as any; 49 | await w.keplr.enable(chaindId); 50 | 51 | return w.getOfflineSigner(chaindId); 52 | }; 53 | } 54 | 55 | async function registerKeplrChain(keplrChainInfo: any): Promise { 56 | const w = window as any; 57 | if (!w.getOfflineSigner || !w.keplr) { 58 | throw new Error("Please install keplr extension"); 59 | } 60 | 61 | if (!w.keplr.experimentalSuggestChain) { 62 | throw new Error("Please use the recent version of keplr extension"); 63 | } 64 | 65 | try { 66 | await w.keplr.experimentalSuggestChain(keplrChainInfo); 67 | } catch { 68 | throw new Error("Failed to suggest the chain"); 69 | } 70 | } 71 | 72 | export async function loadOrCreateWalletDirect( 73 | addressPrefix: string, 74 | mnemonic?: string, 75 | ): Promise { 76 | const loadedMnemonic = loadOrCreateMnemonic(mnemonic); 77 | const hdPath = makeCosmoshubPath(0); 78 | return DirectSecp256k1HdWallet.fromMnemonic(loadedMnemonic, { 79 | hdPaths: [hdPath], 80 | prefix: addressPrefix, 81 | }); 82 | } 83 | 84 | export async function loadLedgerWallet(addressPrefix: string): Promise { 85 | const interactiveTimeout = 120_000; 86 | const ledgerTransport = await TransportWebUSB.create(interactiveTimeout, interactiveTimeout); 87 | 88 | return new LedgerSigner(ledgerTransport, { hdPaths: [makeCosmoshubPath(0)], prefix: addressPrefix }); 89 | } 90 | 91 | async function createStargateSigningClient(signer: OfflineSigner): Promise { 92 | const { nodeUrls } = settings.backend; 93 | const endpoint = nodeUrls[0]; 94 | 95 | const typeRegistry = new Registry([ 96 | [msgStoreCodeTypeUrl, MsgStoreCode], 97 | [msgInstantiateContractTypeUrl, MsgInstantiateContract], 98 | [msgExecuteContractTypeUrl, MsgExecuteContract], 99 | ]); 100 | 101 | return SigningCosmWasmClient.connectWithSigner(endpoint, signer, { 102 | registry: typeRegistry, 103 | }); 104 | } 105 | 106 | export async function getAddressAndStargateSigningClient( 107 | loadWallet: WalletLoaderDirect, 108 | mnemonic?: string, 109 | ): Promise<[string, SigningCosmWasmClient]> { 110 | const signer = await loadWallet(settings.backend.addressPrefix, mnemonic); 111 | const userAddress = (await signer.getAccounts())[0].address; 112 | const signingClient = await createStargateSigningClient(signer); 113 | return [userAddress, signingClient]; 114 | } 115 | 116 | export function webUsbMissing(): boolean { 117 | const anyNavigator: any = navigator; 118 | return !anyNavigator?.usb; 119 | } 120 | -------------------------------------------------------------------------------- /src/ui-utils/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { ellideMiddle, ellideRight } from "."; 2 | 3 | describe("ellideMiddle", () => { 4 | it("works", () => { 5 | expect(ellideMiddle("abcde", 1)).toEqual("…"); 6 | expect(ellideMiddle("abcde", 2)).toEqual("a…"); 7 | expect(ellideMiddle("abcde", 3)).toEqual("a…e"); 8 | expect(ellideMiddle("abcde", 4)).toEqual("ab…e"); 9 | expect(ellideMiddle("abcde", 5)).toEqual("abcde"); 10 | expect(ellideMiddle("abcde", 6)).toEqual("abcde"); 11 | }); 12 | }); 13 | 14 | describe("ellideRight", () => { 15 | it("works", () => { 16 | expect(ellideRight("abcde", 1)).toEqual("…"); 17 | expect(ellideRight("abcde", 2)).toEqual("a…"); 18 | expect(ellideRight("abcde", 3)).toEqual("ab…"); 19 | expect(ellideRight("abcde", 4)).toEqual("abc…"); 20 | expect(ellideRight("abcde", 5)).toEqual("abcde"); 21 | expect(ellideRight("abcde", 6)).toEqual("abcde"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/ui-utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; 2 | import { Decimal, fromUtf8 } from "cosmwasm"; 3 | 4 | export function ellideMiddle(str: string, maxOutLen: number): string { 5 | if (str.length <= maxOutLen) { 6 | return str; 7 | } 8 | const ellide = "…"; 9 | const frontLen = Math.ceil((maxOutLen - ellide.length) / 2); 10 | const tailLen = Math.floor((maxOutLen - ellide.length) / 2); 11 | return str.slice(0, frontLen) + ellide + str.slice(str.length - tailLen, str.length); 12 | } 13 | 14 | export function ellideRight(str: string, maxOutLen: number): string { 15 | if (str.length <= maxOutLen) { 16 | return str; 17 | } 18 | const ellide = "…"; 19 | const frontLen = maxOutLen - ellide.length; 20 | return str.slice(0, frontLen) + ellide; 21 | } 22 | 23 | // NARROW NO-BREAK SPACE (U+202F) 24 | const thinSpace = "\u202F"; 25 | 26 | function printableCoin(coin: Coin): string { 27 | if (coin.denom?.startsWith("u")) { 28 | const ticker = coin.denom.slice(1).toUpperCase(); 29 | return Decimal.fromAtomics(coin.amount ?? "0", 6).toString() + thinSpace + ticker; 30 | } else { 31 | return coin.amount + thinSpace + coin.denom; 32 | } 33 | } 34 | 35 | export function printableBalance(balance: readonly Coin[]): string { 36 | if (balance.length === 0) return "–"; 37 | return balance.map(printableCoin).join(", "); 38 | } 39 | 40 | export function parseMsgContract(msg: Uint8Array): any { 41 | const json = fromUtf8(msg); 42 | 43 | return JSON.parse(json); 44 | } 45 | -------------------------------------------------------------------------------- /src/ui-utils/jsonInput.ts: -------------------------------------------------------------------------------- 1 | // Place error box below text box, so appearing error does not push text box down 2 | export const jsonInputStyle = { 3 | container: { display: "flex", flexDirection: "column" }, 4 | body: { order: "1" }, 5 | warningBox: { order: "2" }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/ui-utils/sdkhelpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { makeTags } from "./sdkhelpers"; 2 | 3 | describe("makeTags", () => { 4 | it("works for happy path", () => { 5 | expect(makeTags("a=b")).toEqual([{ key: "a", value: "b" }]); 6 | expect(makeTags("a=b&c=d")).toEqual([ 7 | { key: "a", value: "b" }, 8 | { key: "c", value: "d" }, 9 | ]); 10 | }); 11 | 12 | it("works for empty value", () => { 13 | expect(makeTags("a=")).toEqual([{ key: "a", value: "" }]); 14 | }); 15 | 16 | it("throws for missing assignment", () => { 17 | expect(() => makeTags("foo")).toThrowError(/equal sign missing/i); 18 | }); 19 | 20 | it("throws for multiple assignments", () => { 21 | expect(() => makeTags("a=b=c")).toThrowError(/multiple equal signs found/i); 22 | }); 23 | 24 | it("throws for empty key", () => { 25 | expect(() => makeTags("=a")).toThrowError(/key must not be empty/i); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/ui-utils/sdkhelpers.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | readonly key: string; 3 | readonly value: string; 4 | } 5 | 6 | export function makeTags(oneLiner: string): Tag[] { 7 | return oneLiner.split("&").map((pair) => { 8 | if (pair.indexOf("=") === -1) throw new Error("Parsing error: Equal sign missing"); 9 | const parts = pair.split("="); 10 | if (parts.length > 2) { 11 | throw new Error( 12 | "Parsing error: Multiple equal signs found. If you need escaping support, please create a PR.", 13 | ); 14 | } 15 | const [key, value] = parts; 16 | if (!key) throw new Error("Parsing error: Key must not be empty"); 17 | return { key, value }; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/ui-utils/states.ts: -------------------------------------------------------------------------------- 1 | import { isNonNullObject } from "cosmwasm"; 2 | 3 | const runtimeCodes = { 4 | error: "_\u2588_ErrorState_\u2588_" as const, 5 | loading: "_\u2588_LoadingState_\u2588_" as const, 6 | }; 7 | 8 | export interface ErrorState { 9 | readonly type: typeof runtimeCodes.error; 10 | } 11 | 12 | export interface LoadingState { 13 | readonly type: typeof runtimeCodes.loading; 14 | } 15 | 16 | export function isErrorState(state: unknown): state is ErrorState { 17 | if (!isNonNullObject(state)) return false; 18 | return (state as ErrorState).type === runtimeCodes.error; 19 | } 20 | 21 | export function isLoadingState(state: unknown): state is LoadingState { 22 | if (!isNonNullObject(state)) return false; 23 | return (state as LoadingState).type === runtimeCodes.loading; 24 | } 25 | 26 | export const errorState: ErrorState = { type: runtimeCodes.error }; 27 | export const loadingState: LoadingState = { type: runtimeCodes.loading }; 28 | -------------------------------------------------------------------------------- /src/ui-utils/txs.ts: -------------------------------------------------------------------------------- 1 | import { Any } from "cosmjs-types/google/protobuf/any"; 2 | 3 | type IAny = Any; 4 | 5 | export const msgSendTypeUrl = "/cosmos.bank.v1beta1.MsgSend"; 6 | export const msgStoreCodeTypeUrl = "/cosmwasm.wasm.v1.MsgStoreCode"; 7 | export const msgInstantiateContractTypeUrl = "/cosmwasm.wasm.v1.MsgInstantiateContract"; 8 | export const msgExecuteContractTypeUrl = "/cosmwasm.wasm.v1.MsgExecuteContract"; 9 | 10 | export interface AnyMsgSend { 11 | readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend"; 12 | readonly value: Uint8Array; 13 | } 14 | 15 | export interface AnyMsgStoreCode { 16 | readonly typeUrl: "/cosmwasm.wasm.v1.MsgStoreCode"; 17 | readonly value: Uint8Array; 18 | } 19 | 20 | export interface AnyMsgInstantiateContract { 21 | readonly typeUrl: "/cosmwasm.wasm.v1.MsgInstantiateContract"; 22 | readonly value: Uint8Array; 23 | } 24 | 25 | export interface AnyMsgExecuteContract { 26 | readonly typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract"; 27 | readonly value: Uint8Array; 28 | } 29 | 30 | export function isAnyMsgSend(msg: IAny): msg is AnyMsgSend { 31 | return msg.typeUrl === msgSendTypeUrl && !!msg.value; 32 | } 33 | 34 | export function isAnyMsgStoreCode(msg: IAny): msg is AnyMsgStoreCode { 35 | return msg.typeUrl === msgStoreCodeTypeUrl && !!msg.value; 36 | } 37 | 38 | export function isAnyMsgInstantiateContract(msg: IAny): msg is AnyMsgInstantiateContract { 39 | return msg.typeUrl === msgInstantiateContractTypeUrl && !!msg.value; 40 | } 41 | 42 | export function isAnyMsgExecuteContract(msg: IAny): msg is AnyMsgExecuteContract { 43 | return msg.typeUrl === msgExecuteContractTypeUrl && !!msg.value; 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------