├── .nvmrc
├── tsconfig.prod.json
├── .env
├── public
├── favicon.ico
├── manifest.json
├── index.html
└── blockies.min.js
├── src
├── assets
│ ├── xdai.png
│ ├── walletconnect-banner.png
│ ├── eth.svg
│ └── erc20.svg
├── index.tsx
├── components
│ ├── Banner.tsx
│ ├── ERC20Icon.tsx
│ ├── Icon.tsx
│ ├── Blockie.tsx
│ ├── Wrapper.tsx
│ ├── Column.tsx
│ ├── AccountAssets.tsx
│ ├── AssetRow.tsx
│ ├── Loader.tsx
│ ├── Header.tsx
│ ├── Button.tsx
│ └── Modal.tsx
├── helpers
│ ├── eip712.ts
│ ├── eip1271.ts
│ ├── api.ts
│ ├── types.ts
│ ├── bignumber.ts
│ ├── chains.ts
│ └── utilities.ts
├── styles.ts
└── App.tsx
├── .prettierrc
├── tsconfig.test.json
├── images.d.ts
├── README.md
├── .gitignore
├── tslint.json
├── tsconfig.json
├── LICENSE.md
├── .eslintrc
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.2.0
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 | SKIP_PREFLIGHT_CHECK=true
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WalletConnect/walletconnect-example-dapp/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/xdai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WalletConnect/walletconnect-example-dapp/HEAD/src/assets/xdai.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "trailingComma": "all",
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/assets/walletconnect-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WalletConnect/walletconnect-example-dapp/HEAD/src/assets/walletconnect-banner.png
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 | declare module '*.jpeg'
5 | declare module '*.gif'
6 | declare module '*.bmp'
7 | declare module '*.tiff'
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WalletConnect Example Dapp
2 |
3 | ## Develop
4 |
5 | ```bash
6 | npm run start
7 | ```
8 |
9 | ## Test
10 |
11 | ```bash
12 | npm run test
13 | ```
14 |
15 | ## Build
16 |
17 | ```bash
18 | npm run build
19 | ```
20 |
--------------------------------------------------------------------------------
/src/assets/eth.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "WalletConnect Example",
3 | "name": "WalletConnect Example",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import { createGlobalStyle } from "styled-components";
4 |
5 | import App from "./App";
6 | import { globalStyle } from "./styles";
7 | const GlobalStyle = createGlobalStyle`
8 | ${globalStyle}
9 | `;
10 |
11 | declare global {
12 | // tslint:disable-next-line
13 | interface Window {
14 | blockies: any;
15 | }
16 | }
17 |
18 | ReactDOM.render(
19 | <>
20 |
21 |
22 | >,
23 | document.getElementById("root"),
24 | );
25 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "rules": {
4 | "prefer-for-of": false,
5 | "no-shadowed-variable": false,
6 | "no-bitwise": false,
7 | "variable-name": false,
8 | "object-literal-sort-keys": false,
9 | "ordered-imports": false,
10 | "jsx-no-lambda": false,
11 | "jsx-boolean-value": false,
12 | "no-console": false
13 | },
14 | "linterOptions": {
15 | "exclude": [
16 | "config/**/*.js",
17 | "node_modules/**/*.ts",
18 | "coverage/lcov-report/*.js"
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Banner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 | import banner from "../assets/walletconnect-banner.png";
4 |
5 | const SBannerWrapper = styled.div`
6 | display: flex;
7 | align-items: center;
8 | position: relative;
9 | `;
10 |
11 | const SBanner = styled.div`
12 | width: 275px;
13 | height: 45px;
14 | background: url(${banner}) no-repeat;
15 | background-size: cover;
16 | background-position: center;
17 | `;
18 |
19 | const Banner = () => (
20 |
21 |
22 |
23 | );
24 |
25 | export default Banner;
26 |
--------------------------------------------------------------------------------
/src/components/ERC20Icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import Icon from "./Icon";
4 | import erc20 from "../assets/erc20.svg";
5 |
6 | const ERC20Icon = (props: any) => {
7 | const src = `https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens/${props.contractAddress.toLowerCase()}.png`;
8 | return ;
9 | };
10 |
11 | ERC20Icon.propTypes = {
12 | contractAddress: PropTypes.string,
13 | size: PropTypes.number,
14 | };
15 |
16 | ERC20Icon.defaultProps = {
17 | contractAddress: null,
18 | size: 20,
19 | };
20 |
21 | export default ERC20Icon;
22 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | WalletConnect Example
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | interface IIconStyleProps {
6 | size: number;
7 | }
8 |
9 | const SIcon = styled.img`
10 | width: ${({ size }) => `${size}px`};
11 | height: ${({ size }) => `${size}px`};
12 | `;
13 |
14 | const Icon = (props: any) => {
15 | const { src, fallback, size } = props;
16 | return (
17 | {
22 | if (fallback) {
23 | event.target.src = fallback;
24 | }
25 | }}
26 | />
27 | );
28 | };
29 |
30 | Icon.propTypes = {
31 | src: PropTypes.string,
32 | fallback: PropTypes.string,
33 | size: PropTypes.number,
34 | };
35 |
36 | Icon.defaultProps = {
37 | src: null,
38 | fallback: "",
39 | size: 20,
40 | };
41 |
42 | export default Icon;
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "build/dist",
5 | "module": "esnext",
6 | "target": "es5",
7 | "lib": ["es7", "dom"],
8 | "sourceMap": true,
9 | "allowJs": true,
10 | "jsx": "react",
11 | "moduleResolution": "node",
12 | "rootDir": "src",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "importHelpers": true,
18 | "skipLibCheck": true,
19 | "strictNullChecks": true,
20 | "allowSyntheticDefaultImports": true,
21 | "suppressImplicitAnyIndexErrors": true,
22 | "noUnusedLocals": true,
23 | "resolveJsonModule": true,
24 | "types": ["react", "jest"]
25 | },
26 | "exclude": [
27 | "node_modules",
28 | "build",
29 | "scripts",
30 | "acceptance-tests",
31 | "webpack",
32 | "jest",
33 | "src/setupTests.ts"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Blockie.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 |
4 | interface IBlockieStyleProps {
5 | size?: number;
6 | }
7 |
8 | interface IBlockieProps extends IBlockieStyleProps {
9 | address: string;
10 | }
11 |
12 | const SBlockieWrapper = styled.div`
13 | width: ${({ size }) => `${size}px`};
14 | height: ${({ size }) => `${size}px`};
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | border-radius: 6px;
19 | overflow: hidden;
20 | & img {
21 | width: 100%;
22 | }
23 | `;
24 |
25 | const Blockie = (props: IBlockieProps) => {
26 | const seed = props.address.toLowerCase() || "";
27 | const imgUrl = window.blockies
28 | .create({
29 | seed,
30 | })
31 | .toDataURL();
32 | return (
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | Blockie.defaultProps = {
40 | address: "0x0000000000000000000000000000000000000000",
41 | size: 30,
42 | };
43 |
44 | export default Blockie;
45 |
--------------------------------------------------------------------------------
/src/helpers/eip712.ts:
--------------------------------------------------------------------------------
1 | const example = {
2 | types: {
3 | EIP712Domain: [
4 | { name: "name", type: "string" },
5 | { name: "version", type: "string" },
6 | { name: "chainId", type: "uint256" },
7 | { name: "verifyingContract", type: "address" },
8 | ],
9 | Permit: [
10 | { name: "owner", type: "address" },
11 | { name: "spender", type: "address" },
12 | { name: "value", type: "uint256" },
13 | { name: "nonce", type: "uint256" },
14 | { name: "deadline", type: "uint256" },
15 | ],
16 | },
17 | domain: {
18 | name: "Liquid staked Ether 2.0",
19 | version: "2",
20 | chainId: 1,
21 | verifyingContract: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84",
22 | },
23 | primaryType: "Permit",
24 | message: {
25 | owner: "0x01Badd833a247cABf0c2da49a61d06eCb1b7E993",
26 | spender: "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1",
27 | value: "10000978944812",
28 | nonce: "0x0",
29 | deadline: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
30 | },
31 | };
32 |
33 | export const eip712 = {
34 | example,
35 | };
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 WalletConnect, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Wrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import styled, { keyframes } from "styled-components";
4 |
5 | const fadeIn = keyframes`
6 | 0% {
7 | opacity: 0;
8 | }
9 | 100% {
10 | opacity: 1;
11 | }
12 | `;
13 |
14 | interface IWrapperStyleProps {
15 | center: boolean;
16 | }
17 |
18 | const SWrapper = styled.div`
19 | will-change: transform, opacity;
20 | animation: ${fadeIn} 0.7s ease 0s normal 1;
21 | min-height: 200px;
22 | display: flex;
23 | flex-wrap: wrap;
24 | justify-content: center;
25 | align-items: ${({ center }) => (center ? `center` : `flex-start`)};
26 | `;
27 |
28 | interface IWrapperProps extends IWrapperStyleProps {
29 | children: React.ReactNode;
30 | }
31 |
32 | const Wrapper = (props: IWrapperProps) => {
33 | const { children, center } = props;
34 | return (
35 |
36 | {children}
37 |
38 | );
39 | };
40 |
41 | Wrapper.propTypes = {
42 | children: PropTypes.node.isRequired,
43 | center: PropTypes.bool,
44 | };
45 |
46 | Wrapper.defaultProps = {
47 | center: false,
48 | };
49 |
50 | export default Wrapper;
51 |
--------------------------------------------------------------------------------
/src/helpers/eip1271.ts:
--------------------------------------------------------------------------------
1 | import { Contract, providers, utils } from "ethers";
2 |
3 | const spec = {
4 | magicValue: "0x1626ba7e",
5 | abi: [
6 | {
7 | constant: true,
8 | inputs: [
9 | {
10 | name: "_hash",
11 | type: "bytes32",
12 | },
13 | {
14 | name: "_sig",
15 | type: "bytes",
16 | },
17 | ],
18 | name: "isValidSignature",
19 | outputs: [
20 | {
21 | name: "magicValue",
22 | type: "bytes4",
23 | },
24 | ],
25 | payable: false,
26 | stateMutability: "view",
27 | type: "function",
28 | },
29 | ],
30 | };
31 |
32 | async function isValidSignature(
33 | address: string,
34 | sig: string,
35 | data: string,
36 | provider: providers.Provider,
37 | abi = eip1271.spec.abi,
38 | magicValue = eip1271.spec.magicValue,
39 | ): Promise {
40 | let returnValue;
41 | try {
42 | returnValue = await new Contract(address, abi, provider).isValidSignature(
43 | utils.arrayify(data),
44 | sig,
45 | );
46 | } catch (e) {
47 | return false;
48 | }
49 | return returnValue.toLowerCase() === magicValue.toLowerCase();
50 | }
51 |
52 | export const eip1271 = {
53 | spec,
54 | isValidSignature,
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/Column.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | interface IColumnStyleProps {
6 | spanHeight: boolean;
7 | maxWidth: number;
8 | center: boolean;
9 | }
10 |
11 | interface IColumnProps extends IColumnStyleProps {
12 | children: React.ReactNode;
13 | }
14 |
15 | const SColumn = styled.div`
16 | position: relative;
17 | width: 100%;
18 | height: ${({ spanHeight }) => (spanHeight ? "100%" : "auto")};
19 | max-width: ${({ maxWidth }) => `${maxWidth}px`};
20 | margin: 0 auto;
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | justify-content: ${({ center }) => (center ? "center" : "flex-start")};
25 | `;
26 |
27 | const Column = (props: IColumnProps) => {
28 | const { children, spanHeight, maxWidth, center } = props;
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | };
35 |
36 | Column.propTypes = {
37 | children: PropTypes.node.isRequired,
38 | spanHeight: PropTypes.bool,
39 | maxWidth: PropTypes.number,
40 | center: PropTypes.bool,
41 | };
42 |
43 | Column.defaultProps = {
44 | spanHeight: false,
45 | maxWidth: 600,
46 | center: false,
47 | };
48 |
49 | export default Column;
50 |
--------------------------------------------------------------------------------
/src/helpers/api.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance } from "axios";
2 | import { IAssetData, IGasPrices, IParsedTx } from "./types";
3 |
4 | const api: AxiosInstance = axios.create({
5 | baseURL: "https://ethereum-api.xyz",
6 | timeout: 30000, // 30 secs
7 | headers: {
8 | Accept: "application/json",
9 | "Content-Type": "application/json",
10 | },
11 | });
12 |
13 | export async function apiGetAccountAssets(address: string, chainId: number): Promise {
14 | const response = await api.get(`/account-assets?address=${address}&chainId=${chainId}`);
15 | const { result } = response.data;
16 | return result;
17 | }
18 |
19 | export async function apiGetAccountTransactions(
20 | address: string,
21 | chainId: number,
22 | ): Promise {
23 | const response = await api.get(`/account-transactions?address=${address}&chainId=${chainId}`);
24 | const { result } = response.data;
25 | return result;
26 | }
27 |
28 | export const apiGetAccountNonce = async (address: string, chainId: number): Promise => {
29 | const response = await api.get(`/account-nonce?address=${address}&chainId=${chainId}`);
30 | const { result } = response.data;
31 | return result;
32 | };
33 |
34 | export const apiGetGasPrices = async (): Promise => {
35 | const response = await api.get(`/gas-prices`);
36 | const { result } = response.data;
37 | return result;
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/AccountAssets.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Column from "./Column";
3 | import AssetRow from "./AssetRow";
4 | import { IAssetData } from "../helpers/types";
5 |
6 | const AccountAssets = (props: any) => {
7 | const { assets, chainId } = props;
8 | const defaultNativeCurrency: IAssetData =
9 | chainId === 100
10 | ? {
11 | contractAddress: "",
12 | symbol: "xDAI",
13 | name: "xDAI",
14 | decimals: "18",
15 | balance: "0",
16 | }
17 | : {
18 | contractAddress: "",
19 | name: "Ethereum",
20 | symbol: "ETH",
21 | decimals: "18",
22 | balance: "0",
23 | };
24 |
25 | let nativeCurrency: IAssetData = defaultNativeCurrency;
26 | let tokens: IAssetData[] = [];
27 | if (assets && assets.length) {
28 | const filteredNativeCurrency = assets.filter((asset: IAssetData) =>
29 | asset && asset.symbol
30 | ? asset.symbol.toLowerCase() === nativeCurrency.symbol.toLowerCase()
31 | : false,
32 | );
33 | nativeCurrency =
34 | filteredNativeCurrency && filteredNativeCurrency.length
35 | ? filteredNativeCurrency[0]
36 | : defaultNativeCurrency;
37 | tokens = assets.filter((asset: IAssetData) =>
38 | asset && asset.symbol
39 | ? asset.symbol.toLowerCase() !== nativeCurrency.symbol.toLowerCase()
40 | : false,
41 | );
42 | }
43 | return (
44 |
45 |
46 | {tokens.map(token => (
47 |
48 | ))}
49 |
50 | );
51 | };
52 |
53 | export default AccountAssets;
54 |
--------------------------------------------------------------------------------
/src/components/AssetRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 | import Icon from "./Icon";
4 | import ERC20Icon from "./ERC20Icon";
5 | import eth from "../assets/eth.svg";
6 | import xdai from "../assets/xdai.png";
7 | import { handleSignificantDecimals, convertAmountFromRawNumber } from "../helpers/bignumber";
8 |
9 | const SAssetRow = styled.div`
10 | width: 100%;
11 | padding: 20px;
12 | display: flex;
13 | justify-content: space-between;
14 | `;
15 | const SAssetRowLeft = styled.div`
16 | display: flex;
17 | `;
18 | const SAssetName = styled.div`
19 | display: flex;
20 | margin-left: 10px;
21 | `;
22 | const SAssetRowRight = styled.div`
23 | display: flex;
24 | `;
25 | const SAssetBalance = styled.div`
26 | display: flex;
27 | `;
28 |
29 | const AssetRow = (props: any) => {
30 | const { asset } = props;
31 | const nativeCurrencyIcon =
32 | asset.symbol && asset.symbol.toLowerCase() === "eth"
33 | ? eth
34 | : asset.symbol && asset.symbol.toLowerCase() === "xdai"
35 | ? xdai
36 | : null;
37 | return (
38 |
39 |
40 | {nativeCurrencyIcon ? (
41 |
42 | ) : (
43 |
44 | )}
45 | {asset.name}
46 |
47 |
48 |
49 | {`${handleSignificantDecimals(convertAmountFromRawNumber(asset.balance), 8)} ${
50 | asset.symbol
51 | }`}
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default AssetRow;
59 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "@typescript-eslint/ban-ts-ignore": ["off"],
4 | "@typescript-eslint/camelcase": ["off"],
5 | "@typescript-eslint/explicit-function-return-type": ["off"],
6 | "@typescript-eslint/interface-name-prefix": ["off"],
7 | "@typescript-eslint/no-explicit-any": ["off"],
8 | "@typescript-eslint/no-unused-expressions": ["off"],
9 | "@typescript-eslint/no-var-requires": ["off"],
10 | "@typescript-eslint/no-use-before-define": ["off"],
11 | "@typescript-eslint/no-unused-vars": ["off"],
12 | "comma-dangle": ["error", "always-multiline"],
13 | "no-async-promise-executor": ["off"],
14 | "no-empty-pattern": ["off"],
15 | "no-undef": ["error"],
16 | "no-var": ["error"],
17 | "object-curly-spacing": ["error", "always"],
18 | "quotes": ["error", "double", { "allowTemplateLiterals": true }],
19 | "semi": ["error", "always"],
20 | "spaced-comment": ["off"],
21 | "no-prototype-builtins": ["off"],
22 | "sort-keys": ["off"],
23 | "space-before-function-paren": ["off"],
24 | "indent": ["off"],
25 | "no-console": ["off"],
26 | "no-useless-catch": ["off"]
27 | },
28 | "settings": {
29 | "react": {
30 | "pragma": "React",
31 | "version": "detect"
32 | }
33 | },
34 | "env": {
35 | "browser": true,
36 | "jasmine": true,
37 | "jest": true,
38 | "es6": true
39 | },
40 | "extends": [
41 | "standard",
42 | "eslint:recommended",
43 | "plugin:react/recommended",
44 | "plugin:@typescript-eslint/eslint-recommended",
45 | "plugin:@typescript-eslint/recommended",
46 | "prettier/@typescript-eslint",
47 | "plugin:prettier/recommended"
48 | ],
49 | "parser": "@typescript-eslint/parser",
50 | "plugins": ["react", "@typescript-eslint", "prettier"]
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import styled, { keyframes } from "styled-components";
4 | import { colors } from "../styles";
5 |
6 | const load = keyframes`
7 | 0% {
8 | transform: scale(1.0);
9 | }
10 | 5% {
11 | transform: scale(1.0);
12 | }
13 | 50% {
14 | transform: scale(0.8);
15 | }
16 | 95% {
17 | transform: scale(1.0);
18 | }
19 | 100% {
20 | transform: scale(1.0);
21 | }
22 | `;
23 |
24 | interface ILoaderStyleProps {
25 | size: number;
26 | }
27 |
28 | interface ILoaderProps extends ILoaderStyleProps {
29 | color: string;
30 | }
31 |
32 | const SLoader = styled.svg`
33 | width: ${({ size }) => `${size}px`};
34 | height: ${({ size }) => `${size}px`};
35 | animation: ${load} 1s infinite cubic-bezier(0.25, 0, 0.75, 1);
36 | transform: translateZ(0);
37 | `;
38 |
39 | const Loader = (props: ILoaderProps) => {
40 | const { size, color } = props;
41 | const rgb = `rgb(${colors[color]})`;
42 | return (
43 |
44 |
45 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | Loader.propTypes = {
58 | size: PropTypes.number,
59 | color: PropTypes.string,
60 | };
61 |
62 | Loader.defaultProps = {
63 | size: 40,
64 | color: "lightBlue",
65 | };
66 |
67 | export default Loader;
68 |
--------------------------------------------------------------------------------
/public/blockies.min.js:
--------------------------------------------------------------------------------
1 | !(function() {
2 | function e(e) {
3 | for (var o = 0; o < c.length; o++) c[o] = 0;
4 | for (var o = 0; o < e.length; o++)
5 | c[o % 4] = (c[o % 4] << 5) - c[o % 4] + e.charCodeAt(o);
6 | }
7 | function o() {
8 | var e = c[0] ^ (c[0] << 11);
9 | return (
10 | (c[0] = c[1]),
11 | (c[1] = c[2]),
12 | (c[2] = c[3]),
13 | (c[3] = c[3] ^ (c[3] >> 19) ^ e ^ (e >> 8)),
14 | (c[3] >>> 0) / ((1 << 31) >>> 0)
15 | );
16 | }
17 | function r() {
18 | var e = Math.floor(360 * o()),
19 | r = 60 * o() + 40 + "%",
20 | t = 25 * (o() + o() + o() + o()) + "%",
21 | l = "hsl(" + e + "," + r + "," + t + ")";
22 | return l;
23 | }
24 | function t(e) {
25 | for (
26 | var r = e, t = e, l = Math.ceil(r / 2), n = r - l, a = [], c = 0;
27 | t > c;
28 | c++
29 | ) {
30 | for (var i = [], f = 0; l > f; f++) i[f] = Math.floor(2.3 * o());
31 | var s = i.slice(0, n);
32 | s.reverse(), (i = i.concat(s));
33 | for (var h = 0; h < i.length; h++) a.push(i[h]);
34 | }
35 | return a;
36 | }
37 | function l(o) {
38 | var t = {};
39 | return (
40 | (t.seed =
41 | o.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16)),
42 | e(t.seed),
43 | (t.size = o.size || 8),
44 | (t.scale = o.scale || 4),
45 | (t.color = o.color || r()),
46 | (t.bgcolor = o.bgcolor || r()),
47 | (t.spotcolor = o.spotcolor || r()),
48 | t
49 | );
50 | }
51 | function n(e, o) {
52 | var r = t(e.size),
53 | l = Math.sqrt(r.length);
54 | o.width = o.height = e.size * e.scale;
55 | var n = o.getContext("2d");
56 | (n.fillStyle = e.bgcolor),
57 | n.fillRect(0, 0, o.width, o.height),
58 | (n.fillStyle = e.color);
59 | for (var a = 0; a < r.length; a++)
60 | if (r[a]) {
61 | var c = Math.floor(a / l),
62 | i = a % l;
63 | (n.fillStyle = 1 == r[a] ? e.color : e.spotcolor),
64 | n.fillRect(i * e.scale, c * e.scale, e.scale, e.scale);
65 | }
66 | return o;
67 | }
68 | function a(e) {
69 | var e = l(e || {}),
70 | o = document.createElement("canvas");
71 | return n(e, o), o;
72 | }
73 | var c = new Array(4),
74 | i = { create: a, render: n };
75 | "undefined" != typeof module && (module.exports = i),
76 | "undefined" != typeof window && (window.blockies = i);
77 | })();
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "walletconnect-example-dapp",
3 | "version": "1.8.0",
4 | "private": true,
5 | "keywords": [
6 | "walletconnect",
7 | "ethereum",
8 | "web3",
9 | "crypto"
10 | ],
11 | "author": "WalletConnect, Inc. ",
12 | "homepage": "https://walletconnect.org",
13 | "license": "MIT",
14 | "scripts": {
15 | "start": "react-scripts-ts start",
16 | "build": "react-scripts-ts build",
17 | "test": "react-scripts-ts test --env=jsdom",
18 | "eject": "react-scripts-ts eject",
19 | "lint": "eslint -c './.eslintrc' --fix './src/**/*{.ts,.tsx}'"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/walletconnect/walletconnect-example-dapp.git"
24 | },
25 | "bugs": {
26 | "url": "https://github.com/walletconnect/walletconnect-example-dapp/issues"
27 | },
28 | "dependencies": {
29 | "@types/eth-sig-util": "^2.1.1",
30 | "@walletconnect/client": "^1.8.0",
31 | "@walletconnect/qrcode-modal": "^1.8.0",
32 | "axios": "^0.25.0",
33 | "bignumber.js": "^9.0.1",
34 | "blockies-ts": "^1.0.0",
35 | "eth-sig-util": "^2.5.3",
36 | "ethereumjs-util": "^7.0.6",
37 | "ethers": "^5.0.17",
38 | "node-gyp": "^8.1.0",
39 | "prop-types": "^15.7.2",
40 | "qr-image": "^3.2.0",
41 | "react": "^17.0.2",
42 | "react-dom": "^17.0.2",
43 | "react-scripts-ts": "^4.0.8",
44 | "styled-components": "^5.2.0"
45 | },
46 | "devDependencies": {
47 | "@types/axios": "^0.14.0",
48 | "@types/better-sqlite3": "^5.4.1",
49 | "@types/bignumber.js": "^5.0.0",
50 | "@types/bn.js": "^4.11.5",
51 | "@types/eth-sig-util": "^2.1.1",
52 | "@types/jest": "^24.0.25",
53 | "@types/node": "^13.1.6",
54 | "@types/prop-types": "^15.7.3",
55 | "@types/qr-image": "^3.2.3",
56 | "@types/react": "^17.0.38",
57 | "@types/react-dom": "^17.0.11",
58 | "@types/styled-components": "^5.1.3",
59 | "@typescript-eslint/eslint-plugin": "^2.20.0",
60 | "@typescript-eslint/parser": "^2.20.0",
61 | "eslint-config-prettier": "^6.12.0",
62 | "eslint-config-react": "^1.1.7",
63 | "eslint-config-standard": "^14.1.1",
64 | "eslint-plugin-import": "^2.22.1",
65 | "eslint-plugin-node": "^11.1.0",
66 | "eslint-plugin-prettier": "^3.1.4",
67 | "eslint-plugin-promise": "^4.2.1",
68 | "eslint-plugin-standard": "^4.0.1",
69 | "prettier": "^1.19.1",
70 | "typescript": "^3.7.4"
71 | },
72 | "eslintConfig": {
73 | "extends": "react-app"
74 | },
75 | "browserslist": [
76 | ">0.2%",
77 | "not dead",
78 | "not ie <= 11",
79 | "not op_mini all"
80 | ]
81 | }
82 |
--------------------------------------------------------------------------------
/src/helpers/types.ts:
--------------------------------------------------------------------------------
1 | export interface IAssetData {
2 | symbol: string;
3 | name: string;
4 | decimals: string;
5 | contractAddress: string;
6 | balance?: string;
7 | }
8 |
9 | export interface IChainData {
10 | name: string;
11 | short_name: string;
12 | chain: string;
13 | network: string;
14 | chain_id: number;
15 | network_id: number;
16 | rpc_url: string;
17 | native_currency: IAssetData;
18 | }
19 | export interface ITxData {
20 | from: string;
21 | to: string;
22 | nonce: string;
23 | gasPrice: string;
24 | gasLimit: string;
25 | value: string;
26 | data: string;
27 | }
28 |
29 | export interface IBlockScoutTx {
30 | value: string;
31 | txreceipt_status: string;
32 | transactionIndex: string;
33 | to: string;
34 | timeStamp: string;
35 | nonce: string;
36 | isError: string;
37 | input: string;
38 | hash: string;
39 | gasUsed: string;
40 | gasPrice: string;
41 | gas: string;
42 | from: string;
43 | cumulativeGasUsed: string;
44 | contractAddress: string;
45 | confirmations: string;
46 | blockNumber: string;
47 | blockHash: string;
48 | }
49 |
50 | export interface IBlockScoutTokenTx {
51 | value: string;
52 | transactionIndex: string;
53 | tokenSymbol: string;
54 | tokenName: string;
55 | tokenDecimal: string;
56 | to: string;
57 | timeStamp: string;
58 | nonce: string;
59 | input: string;
60 | hash: string;
61 | gasUsed: string;
62 | gasPrice: string;
63 | gas: string;
64 | from: string;
65 | cumulativeGasUsed: string;
66 | contractAddress: string;
67 | confirmations: string;
68 | blockNumber: string;
69 | blockHash: string;
70 | }
71 |
72 | export interface IParsedTx {
73 | timestamp: string;
74 | hash: string;
75 | from: string;
76 | to: string;
77 | nonce: string;
78 | gasPrice: string;
79 | gasUsed: string;
80 | fee: string;
81 | value: string;
82 | input: string;
83 | error: boolean;
84 | asset: IAssetData;
85 | operations: ITxOperation[];
86 | }
87 |
88 | export interface ITxOperation {
89 | asset: IAssetData;
90 | value: string;
91 | from: string;
92 | to: string;
93 | functionName: string;
94 | }
95 |
96 | export interface IGasPricesResponse {
97 | fastWait: number;
98 | avgWait: number;
99 | blockNum: number;
100 | fast: number;
101 | fastest: number;
102 | fastestWait: number;
103 | safeLow: number;
104 | safeLowWait: number;
105 | speed: number;
106 | block_time: number;
107 | average: number;
108 | }
109 |
110 | export interface IGasPrice {
111 | time: number;
112 | price: number;
113 | }
114 |
115 | export interface IGasPrices {
116 | timestamp: number;
117 | slow: IGasPrice;
118 | average: IGasPrice;
119 | fast: IGasPrice;
120 | }
121 |
122 | export interface IMethodArgument {
123 | type: string;
124 | }
125 |
126 | export interface IMethod {
127 | signature: string;
128 | name: string;
129 | args: IMethodArgument[];
130 | }
131 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Blockie from "./Blockie";
4 | import { ellipseAddress, getChainData } from "../helpers/utilities";
5 | import { transitions } from "../styles";
6 |
7 | const SHeader = styled.div`
8 | margin-top: -1px;
9 | margin-bottom: 1px;
10 | width: 100%;
11 | height: 100px;
12 | display: flex;
13 | align-items: center;
14 | justify-content: space-between;
15 | padding: 0 16px;
16 | `;
17 |
18 | const SActiveAccount = styled.div`
19 | display: flex;
20 | align-items: center;
21 | position: relative;
22 | font-weight: 500;
23 | `;
24 |
25 | const SActiveChain = styled(SActiveAccount as any)`
26 | flex-direction: column;
27 | text-align: left;
28 | align-items: flex-start;
29 | & p {
30 | font-size: 0.8em;
31 | margin: 0;
32 | padding: 0;
33 | }
34 | & p:nth-child(2) {
35 | font-weight: bold;
36 | }
37 | `;
38 |
39 | const SBlockie = styled(Blockie as any)`
40 | margin-right: 10px;
41 | `;
42 |
43 | interface IHeaderStyle {
44 | connected: boolean;
45 | }
46 |
47 | const SAddress = styled.p`
48 | transition: ${transitions.base};
49 | font-weight: bold;
50 | margin: ${({ connected }) => (connected ? "-2px auto 0.7em" : "0")};
51 | `;
52 |
53 | const SUnsupportedChain = styled.div`
54 | transition: ${transitions.base};
55 | font-weight: bold;
56 | color: red;
57 | `;
58 |
59 | const SDisconnect = styled.div`
60 | transition: ${transitions.button};
61 | font-size: 12px;
62 | font-family: monospace;
63 | position: absolute;
64 | right: 0;
65 | top: 20px;
66 | opacity: 0.7;
67 | cursor: pointer;
68 |
69 | opacity: ${({ connected }) => (connected ? 1 : 0)};
70 | visibility: ${({ connected }) => (connected ? "visible" : "hidden")};
71 | pointer-events: ${({ connected }) => (connected ? "auto" : "none")};
72 |
73 | &:hover {
74 | transform: translateY(-1px);
75 | opacity: 0.5;
76 | }
77 | `;
78 |
79 | interface IHeaderProps {
80 | killSession: () => void;
81 | connected: boolean;
82 | address: string;
83 | chainId: number;
84 | }
85 |
86 | const Header = ({ connected, address, chainId, killSession }: IHeaderProps) => {
87 | let activeChain = null;
88 |
89 | try {
90 | activeChain = chainId ? getChainData(chainId).name : null;
91 | } catch (error) {
92 | console.error(error);
93 | }
94 |
95 | return (
96 |
97 | {connected && (
98 |
99 | {activeChain ? (
100 | <>
101 | Connected to
102 | {activeChain}
103 | >
104 | ) : (
105 |
106 | Chain not supported.
107 | Please switch to a supported chain in your wallet.
108 |
109 | )}
110 |
111 | )}
112 | {address && (
113 |
114 |
115 | {ellipseAddress(address)}
116 |
117 | {"Disconnect"}
118 |
119 |
120 | )}
121 |
122 | );
123 | };
124 |
125 | export default Header;
126 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 | import Loader from "./Loader";
4 | import { colors, fonts, shadows, transitions } from "../styles";
5 |
6 | interface IButtonStyleProps {
7 | fetching: boolean;
8 | outline: boolean;
9 | type: "button" | "submit" | "reset";
10 | color: string;
11 | disabled: boolean;
12 | icon: any;
13 | left: boolean;
14 | }
15 |
16 | interface IButtonProps extends IButtonStyleProps {
17 | children: React.ReactNode;
18 | onClick?: any;
19 | }
20 |
21 | const SIcon = styled.div`
22 | position: absolute;
23 | height: 15px;
24 | width: 15px;
25 | margin: 0 8px;
26 | top: calc((100% - 15px) / 2);
27 | `;
28 |
29 | const SHoverLayer = styled.div`
30 | transition: ${transitions.button};
31 | position: absolute;
32 | height: 100%;
33 | width: 100%;
34 | background-color: rgb(${colors.white}, 0.1);
35 | top: 0;
36 | bottom: 0;
37 | right: 0;
38 | left: 0;
39 | pointer-events: none;
40 | opacity: 0;
41 | visibility: hidden;
42 | `;
43 |
44 | const SButton = styled.button`
45 | transition: ${transitions.button};
46 | position: relative;
47 | border: none;
48 | border-style: none;
49 | box-sizing: border-box;
50 | background-color: ${({ outline, color }) => (outline ? "transparent" : `rgb(${colors[color]})`)};
51 | border: ${({ outline, color }) => (outline ? `1px solid rgb(${colors[color]})` : "none")};
52 | color: ${({ outline, color }) => (outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`)};
53 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
54 | border-radius: 8px;
55 | font-size: ${fonts.size.medium};
56 | font-weight: ${fonts.weight.semibold};
57 | padding: ${({ icon, left }) =>
58 | icon ? (left ? "7px 12px 8px 28px" : "7px 28px 8px 12px") : "8px 12px"};
59 | cursor: ${({ disabled }) => (disabled ? "auto" : "pointer")};
60 | will-change: transform;
61 |
62 | &:disabled {
63 | opacity: 0.6;
64 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
65 | }
66 |
67 | @media (hover: hover) {
68 | &:hover {
69 | transform: ${({ disabled }) => (!disabled ? "translateY(-1px)" : "none")};
70 | box-shadow: ${({ disabled, outline }) =>
71 | !disabled ? (outline ? "none" : `${shadows.hover}`) : `${shadows.soft}`};
72 | }
73 |
74 | &:hover ${SHoverLayer} {
75 | opacity: 1;
76 | visibility: visible;
77 | }
78 | }
79 |
80 | &:active {
81 | transform: ${({ disabled }) => (!disabled ? "translateY(1px)" : "none")};
82 | box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
83 | color: ${({ outline, color }) =>
84 | outline ? `rgb(${colors[color]})` : `rgba(${colors.white}, 0.24)`};
85 |
86 | & ${SIcon} {
87 | opacity: 0.8;
88 | }
89 | }
90 |
91 | & ${SIcon} {
92 | right: ${({ left }) => (left ? "auto" : "0")};
93 | left: ${({ left }) => (left ? "0" : "auto")};
94 | display: ${({ icon }) => (icon ? "block" : "none")};
95 | mask: ${({ icon }) => (icon ? `url(${icon}) center no-repeat` : "none")};
96 | background-color: ${({ outline, color }) =>
97 | outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`};
98 | transition: 0.15s ease;
99 | }
100 | `;
101 |
102 | const Button = (props: IButtonProps) => (
103 |
112 |
113 |
114 | {props.fetching ? : props.children}
115 |
116 | );
117 |
118 | Button.defaultProps = {
119 | fetching: false,
120 | outline: false,
121 | type: "button",
122 | color: "lightBlue",
123 | disabled: false,
124 | icon: null,
125 | left: false,
126 | };
127 |
128 | export default Button;
129 |
--------------------------------------------------------------------------------
/src/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import { colors, transitions } from "../styles";
5 |
6 | interface ILightboxStyleProps {
7 | show: boolean;
8 | offset: number;
9 | opacity?: number;
10 | }
11 |
12 | const SLightbox = styled.div`
13 | transition: opacity 0.1s ease-in-out;
14 | text-align: center;
15 | position: absolute;
16 | width: 100vw;
17 | height: 100vh;
18 | margin-left: -50vw;
19 | top: ${({ offset }) => (offset ? `-${offset}px` : 0)};
20 | left: 50%;
21 | z-index: 2;
22 | will-change: opacity;
23 | background-color: ${({ opacity }) => {
24 | let alpha = 0.4;
25 | if (typeof opacity === "number") {
26 | alpha = opacity;
27 | }
28 | return `rgba(0, 0, 0, ${alpha})`;
29 | }};
30 | opacity: ${({ show }) => (show ? 1 : 0)};
31 | visibility: ${({ show }) => (show ? "visible" : "hidden")};
32 | pointer-events: ${({ show }) => (show ? "auto" : "none")};
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | `;
37 |
38 | const SModalContainer = styled.div`
39 | position: relative;
40 | width: 100%;
41 | height: 100%;
42 | padding: 15px;
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 | `;
47 |
48 | const SHitbox = styled.div`
49 | position: absolute;
50 | top: 0;
51 | left: 0;
52 | right: 0;
53 | bottom: 0;
54 | `;
55 |
56 | interface ICloseButtonStyleProps {
57 | size: number;
58 | color: string;
59 | onClick?: any;
60 | }
61 |
62 | const SCloseButton = styled.div`
63 | transition: ${transitions.short};
64 | position: absolute;
65 | width: ${({ size }) => `${size}px`};
66 | height: ${({ size }) => `${size}px`};
67 | right: ${({ size }) => `${size / 1.6667}px`};
68 | top: ${({ size }) => `${size / 1.6667}px`};
69 | opacity: 0.5;
70 | cursor: pointer;
71 | &:hover {
72 | opacity: 1;
73 | }
74 | &:before,
75 | &:after {
76 | position: absolute;
77 | content: " ";
78 | height: ${({ size }) => `${size}px`};
79 | width: 2px;
80 | background: ${({ color }) => `rgb(${colors[color]})`};
81 | }
82 | &:before {
83 | transform: rotate(45deg);
84 | }
85 | &:after {
86 | transform: rotate(-45deg);
87 | }
88 | `;
89 |
90 | const SCard = styled.div`
91 | position: relative;
92 | width: 100%;
93 | max-width: 500px;
94 | padding: 25px;
95 | background-color: rgb(${colors.white});
96 | border-radius: 6px;
97 | display: flex;
98 | flex-direction: column;
99 | justify-content: center;
100 | align-items: center;
101 | `;
102 |
103 | const SModalContent = styled.div`
104 | position: relative;
105 | width: 100%;
106 | position: relative;
107 | word-wrap: break-word;
108 | `;
109 |
110 | interface IModalState {
111 | offset: number;
112 | }
113 |
114 | interface IModalProps {
115 | children: React.ReactNode;
116 | show: boolean;
117 | toggleModal: any;
118 | opacity?: number;
119 | }
120 |
121 | const INITIAL_STATE: IModalState = {
122 | offset: 0,
123 | };
124 |
125 | class Modal extends React.Component {
126 | public static propTypes = {
127 | children: PropTypes.node.isRequired,
128 | show: PropTypes.bool.isRequired,
129 | toggleModal: PropTypes.func.isRequired,
130 | opacity: PropTypes.number,
131 | };
132 |
133 | public lightbox?: HTMLDivElement | null;
134 |
135 | public state: IModalState = {
136 | ...INITIAL_STATE,
137 | };
138 |
139 | public componentDidUpdate() {
140 | if (this.lightbox) {
141 | const lightboxRect = this.lightbox.getBoundingClientRect();
142 | const offset = lightboxRect.top > 0 ? lightboxRect.top : 0;
143 |
144 | if (offset !== INITIAL_STATE.offset && offset !== this.state.offset) {
145 | this.setState({ offset });
146 | }
147 | }
148 | }
149 |
150 | public toggleModal = async () => {
151 | const d = typeof window !== "undefined" ? document : "";
152 | const body = d ? d.body || d.getElementsByTagName("body")[0] : "";
153 | if (body) {
154 | if (this.props.show) {
155 | body.style.position = "";
156 | } else {
157 | body.style.position = "fixed";
158 | }
159 | }
160 | this.props.toggleModal();
161 | };
162 |
163 | public render() {
164 | const { offset } = this.state;
165 | const { children, show, opacity } = this.props;
166 | return (
167 | (this.lightbox = c)}>
168 |
169 |
170 |
171 |
172 |
173 | {children}
174 |
175 |
176 |
177 | );
178 | }
179 | }
180 |
181 | export default Modal;
182 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | export const colors = {
2 | white: "255, 255, 255",
3 | black: "0, 0, 0",
4 | dark: "12, 12, 13",
5 | grey: "169, 169, 188",
6 | darkGrey: "113, 119, 138",
7 | lightGrey: "212, 212, 212",
8 | blue: "101, 127, 230",
9 | lightBlue: "64, 153, 255",
10 | yellow: "250, 188, 45",
11 | orange: "246, 133, 27",
12 | green: "84, 209, 146",
13 | pink: "255, 51, 102",
14 | red: "214, 75, 71",
15 | purple: "110, 107, 233",
16 | };
17 |
18 | export const fonts = {
19 | size: {
20 | tiny: "10px",
21 | small: "14px",
22 | medium: "16px",
23 | large: "18px",
24 | h1: "60px",
25 | h2: "50px",
26 | h3: "40px",
27 | h4: "32px",
28 | h5: "24px",
29 | h6: "20px",
30 | },
31 | weight: {
32 | normal: 400,
33 | medium: 500,
34 | semibold: 600,
35 | bold: 700,
36 | extrabold: 800,
37 | },
38 | family: {
39 | OpenSans: `"Open Sans", sans-serif`,
40 | },
41 | };
42 |
43 | export const transitions = {
44 | short: "all 0.1s ease-in-out",
45 | base: "all 0.2s ease-in-out",
46 | long: "all 0.3s ease-in-out",
47 | button: "all 0.15s ease-in-out",
48 | };
49 |
50 | export const shadows = {
51 | soft:
52 | "0 4px 6px 0 rgba(50, 50, 93, 0.11), 0 1px 3px 0 rgba(0, 0, 0, 0.08), inset 0 0 1px 0 rgba(0, 0, 0, 0.06)",
53 | medium:
54 | "0 3px 6px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(50, 50, 93, 0.02), 0 5px 10px 0 rgba(59, 59, 92, 0.08)",
55 | big: "0 15px 35px 0 rgba(50, 50, 93, 0.06), 0 5px 15px 0 rgba(50, 50, 93, 0.15)",
56 | hover:
57 | "0 7px 14px 0 rgba(50, 50, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08), inset 0 0 1px 0 rgba(0, 0, 0, 0.06)",
58 | };
59 |
60 | export const responsive = {
61 | xs: {
62 | min: "min-width: 467px",
63 | max: "max-width: 468px",
64 | },
65 | sm: {
66 | min: "min-width: 639px",
67 | max: "max-width: 640px",
68 | },
69 | md: {
70 | min: "min-width: 959px",
71 | max: "max-width: 960px",
72 | },
73 | lg: {
74 | min: "min-width: 1023px",
75 | max: "max-width: 1024px",
76 | },
77 | xl: {
78 | min: "min-width: 1399px",
79 | max: "max-width: 1400px",
80 | },
81 | };
82 |
83 | export const globalStyle = `
84 |
85 | html, body, #root {
86 | height: 100%;
87 | width: 100%;
88 | margin: 0;
89 | padding: 0;
90 | }
91 |
92 | body {
93 | font-family: ${fonts.family.OpenSans};
94 | font-style: normal;
95 | font-stretch: normal;
96 | font-weight: ${fonts.weight.normal};
97 | font-size: ${fonts.size.medium};
98 | background-color: rgb(${colors.white});
99 | color: rgb(${colors.dark});
100 | overflow-y:auto;
101 | text-rendering: optimizeLegibility;
102 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
103 | -webkit-font-smoothing: antialiased;
104 | -moz-osx-font-smoothing: grayscale;
105 | -webkit-text-size-adjust: 100%;
106 | -webkit-overflow-scrolling: touch;
107 | -ms-text-size-adjust: 100%;
108 | -webkit-text-size-adjust: 100%;
109 | }
110 |
111 | button {
112 | border-style: none;
113 | line-height: 1em;
114 | background-image: none;
115 | outline: 0;
116 | -webkit-box-shadow: none;
117 | box-shadow: none;
118 | }
119 |
120 | [tabindex] {
121 | outline: none;
122 | width: 100%;
123 | height: 100%;
124 | }
125 |
126 | a, p, h1, h2, h3, h4, h5, h6 {
127 | text-decoration: none;
128 | margin: 0;
129 | padding: 0;
130 | margin: 0.7em 0;
131 | }
132 |
133 | h1 {
134 | font-size: ${fonts.size.h1}
135 | }
136 | h2 {
137 | font-size: ${fonts.size.h2}
138 | }
139 | h3 {
140 | font-size: ${fonts.size.h3}
141 | }
142 | h4 {
143 | font-size: ${fonts.size.h4}
144 | }
145 | h5 {
146 | font-size: ${fonts.size.h5}
147 | }
148 | h6 {
149 | font-size: ${fonts.size.h6}
150 | }
151 |
152 | a {
153 | background-color: transparent;
154 | -webkit-text-decoration-skip: objects;
155 | text-decoration: none;
156 | color: inherit;
157 | outline: none;
158 | }
159 |
160 | b,
161 | strong {
162 | font-weight: inherit;
163 | font-weight: bolder;
164 | }
165 |
166 | ul, li {
167 | list-style: none;
168 | margin: 0;
169 | padding: 0;
170 | }
171 |
172 | * {
173 | box-sizing: border-box !important;
174 | }
175 |
176 |
177 | input {
178 | -webkit-appearance: none;
179 | }
180 |
181 | article,
182 | aside,
183 | details,
184 | figcaption,
185 | figure,
186 | footer,
187 | header,
188 | main,
189 | menu,
190 | nav,
191 | section,
192 | summary {
193 | display: block;
194 | }
195 | audio,
196 | canvas,
197 | progress,
198 | video {
199 | display: inline-block;
200 | }
201 |
202 | input[type="color"],
203 | input[type="date"],
204 | input[type="datetime"],
205 | input[type="datetime-local"],
206 | input[type="email"],
207 | input[type="month"],
208 | input[type="number"],
209 | input[type="password"],
210 | input[type="search"],
211 | input[type="tel"],
212 | input[type="text"],
213 | input[type="time"],
214 | input[type="url"],
215 | input[type="week"],
216 | select:focus,
217 | textarea {
218 | font-size: 16px;
219 | }
220 | `;
221 |
--------------------------------------------------------------------------------
/src/helpers/bignumber.ts:
--------------------------------------------------------------------------------
1 | import BigNumber from "bignumber.js";
2 |
3 | export function isNaN(value: string | number): boolean {
4 | return new BigNumber(`${value}`).isNaN();
5 | }
6 |
7 | export function isNumber(value: string | number): boolean {
8 | const isNaNResult = isNaN(value);
9 | return !isNaNResult;
10 | }
11 |
12 | export function isInteger(value: string | number): boolean {
13 | return new BigNumber(`${value}`).isInteger();
14 | }
15 |
16 | export function isPositive(value: string | number): boolean {
17 | return new BigNumber(`${value}`).isPositive();
18 | }
19 |
20 | export function isNegative(value: string | number): boolean {
21 | return new BigNumber(`${value}`).isNegative();
22 | }
23 |
24 | export function isZero(value: string | number): boolean {
25 | return new BigNumber(`${value}`).isZero();
26 | }
27 |
28 | export function countDecimalPlaces(value: string | number): number {
29 | return new BigNumber(`${value}`).dp() || 0;
30 | }
31 |
32 | export function convertNumberToString(value: string | number): string {
33 | return new BigNumber(`${value}`).toString();
34 | }
35 |
36 | export function convertStringToNumber(value: string | number): number {
37 | return new BigNumber(`${value}`).toNumber();
38 | }
39 |
40 | export function convertHexToString(hex: string): string {
41 | return new BigNumber(`${hex}`).toString();
42 | }
43 |
44 | export function convertStringToHex(value: string | number): string {
45 | return new BigNumber(`${value}`).toString(16);
46 | }
47 |
48 | export function greaterThan(numberOne: number | string, numberTwo: number | string): boolean {
49 | return new BigNumber(`${numberOne}`).comparedTo(new BigNumber(`${numberTwo}`)) === 1;
50 | }
51 |
52 | export function greaterThanOrEqual(numberOne: number, numberTwo: number): boolean {
53 | return new BigNumber(`${numberOne}`).comparedTo(new BigNumber(`${numberTwo}`)) >= 0;
54 | }
55 |
56 | export function smallerThan(numberOne: number | string, numberTwo: number | string): boolean {
57 | return new BigNumber(`${numberOne}`).comparedTo(new BigNumber(`${numberTwo}`)) === -1;
58 | }
59 |
60 | export function smallerThanOrEqual(numberOne: number, numberTwo: number): boolean {
61 | return new BigNumber(`${numberOne}`).comparedTo(new BigNumber(`${numberTwo}`)) <= 0;
62 | }
63 |
64 | export function multiply(numberOne: number | string, numberTwo: number | string): string {
65 | return new BigNumber(`${numberOne}`).times(new BigNumber(`${numberTwo}`)).toString();
66 | }
67 |
68 | export function divide(numberOne: number | string, numberTwo: number | string): string {
69 | return new BigNumber(`${numberOne}`).dividedBy(new BigNumber(`${numberTwo}`)).toString();
70 | }
71 |
72 | export function floorDivide(numberOne: number | string, numberTwo: number | string): string {
73 | return new BigNumber(`${numberOne}`).dividedToIntegerBy(new BigNumber(`${numberTwo}`)).toString();
74 | }
75 |
76 | export function mod(numberOne: number | string, numberTwo: number | string): string {
77 | return new BigNumber(`${numberOne}`).mod(new BigNumber(`${numberTwo}`)).toString();
78 | }
79 |
80 | export function add(numberOne: number | string, numberTwo: number | string): string {
81 | return new BigNumber(`${numberOne}`).plus(new BigNumber(`${numberTwo}`)).toString();
82 | }
83 |
84 | export function subtract(numberOne: number | string, numberTwo: number | string): string {
85 | return new BigNumber(`${numberOne}`).minus(new BigNumber(`${numberTwo}`)).toString();
86 | }
87 |
88 | export function convertAmountToRawNumber(value: string | number, decimals = 18): string {
89 | return new BigNumber(`${value}`).times(new BigNumber("10").pow(decimals)).toString();
90 | }
91 |
92 | export function convertAmountFromRawNumber(value: string | number, decimals = 18): string {
93 | return new BigNumber(`${value}`).dividedBy(new BigNumber("10").pow(decimals)).toString();
94 | }
95 |
96 | export function handleSignificantDecimals(
97 | value: string,
98 | decimals: number,
99 | buffer?: number,
100 | ): string | null {
101 | if (
102 | !new BigNumber(`${decimals}`).isInteger() ||
103 | (buffer && !new BigNumber(`${buffer}`).isInteger())
104 | ) {
105 | return null;
106 | }
107 | buffer = buffer ? convertStringToNumber(buffer) : 3;
108 | decimals = convertStringToNumber(decimals);
109 | const absolute = new BigNumber(`${value}`).abs().toNumber();
110 | if (smallerThan(absolute, 1)) {
111 | decimals = value.slice(2).search(/[^0]/g) + buffer;
112 | decimals = decimals < 8 ? decimals : 8;
113 | } else {
114 | decimals = decimals < buffer ? decimals : buffer;
115 | }
116 | let result = new BigNumber(`${value}`).toFixed(decimals);
117 | result = new BigNumber(`${result}`).toString();
118 | return (new BigNumber(`${result}`).dp() || 0) <= 2
119 | ? new BigNumber(`${result}`).toFormat(2)
120 | : new BigNumber(`${result}`).toFormat();
121 | }
122 |
123 | export function formatFixedDecimals(value: string, decimals: number): string {
124 | const _value = convertNumberToString(value);
125 | const _decimals = convertStringToNumber(decimals);
126 | const result = new BigNumber(new BigNumber(_value).toFixed(_decimals)).toString();
127 | return result;
128 | }
129 |
130 | export function formatInputDecimals(inputOne: string, inputTwo: string): string {
131 | const _nativeAmountDecimalPlaces = countDecimalPlaces(inputTwo);
132 | const decimals = _nativeAmountDecimalPlaces > 8 ? _nativeAmountDecimalPlaces : 8;
133 | const result = new BigNumber(formatFixedDecimals(inputOne, decimals))
134 | .toFormat()
135 | .replace(/,/g, "");
136 | return result;
137 | }
138 |
--------------------------------------------------------------------------------
/src/assets/erc20.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/helpers/chains.ts:
--------------------------------------------------------------------------------
1 | import { IChainData } from "../helpers/types";
2 |
3 | export const SUPPORTED_CHAINS: IChainData[] = [
4 | {
5 | name: "Ethereum Mainnet",
6 | short_name: "eth",
7 | chain: "ETH",
8 | network: "mainnet",
9 | chain_id: 1,
10 | network_id: 1,
11 | rpc_url: "https://mainnet.infura.io/v3/%API_KEY%",
12 | native_currency: {
13 | symbol: "ETH",
14 | name: "Ether",
15 | decimals: "18",
16 | contractAddress: "",
17 | balance: "",
18 | },
19 | },
20 | {
21 | name: "Ethereum Ropsten",
22 | short_name: "rop",
23 | chain: "ETH",
24 | network: "ropsten",
25 | chain_id: 3,
26 | network_id: 3,
27 | rpc_url: "https://ropsten.infura.io/v3/%API_KEY%",
28 | native_currency: {
29 | symbol: "ETH",
30 | name: "Ether",
31 | decimals: "18",
32 | contractAddress: "",
33 | balance: "",
34 | },
35 | },
36 | {
37 | name: "Ethereum Rinkeby",
38 | short_name: "rin",
39 | chain: "ETH",
40 | network: "rinkeby",
41 | chain_id: 4,
42 | network_id: 4,
43 | rpc_url: "https://rinkeby.infura.io/v3/%API_KEY%",
44 | native_currency: {
45 | symbol: "ETH",
46 | name: "Ether",
47 | decimals: "18",
48 | contractAddress: "",
49 | balance: "",
50 | },
51 | },
52 | {
53 | name: "Ethereum Görli",
54 | short_name: "gor",
55 | chain: "ETH",
56 | network: "goerli",
57 | chain_id: 5,
58 | network_id: 5,
59 | rpc_url: "https://goerli.infura.io/v3/%API_KEY%",
60 | native_currency: {
61 | symbol: "ETH",
62 | name: "Ether",
63 | decimals: "18",
64 | contractAddress: "",
65 | balance: "",
66 | },
67 | },
68 | {
69 | name: "RSK Mainnet",
70 | short_name: "rsk",
71 | chain: "RSK",
72 | network: "mainnet",
73 | chain_id: 30,
74 | network_id: 30,
75 | rpc_url: "https://public-node.rsk.co",
76 | native_currency: {
77 | symbol: "RSK",
78 | name: "RSK",
79 | decimals: "18",
80 | contractAddress: "",
81 | balance: "",
82 | },
83 | },
84 | {
85 | name: "Ethereum Kovan",
86 | short_name: "kov",
87 | chain: "ETH",
88 | network: "kovan",
89 | chain_id: 42,
90 | network_id: 42,
91 | rpc_url: "https://kovan.infura.io/v3/%API_KEY%",
92 | native_currency: {
93 | symbol: "ETH",
94 | name: "Ether",
95 | decimals: "18",
96 | contractAddress: "",
97 | balance: "",
98 | },
99 | },
100 | {
101 | name: "Ethereum Classic Mainnet",
102 | short_name: "etc",
103 | chain: "ETC",
104 | network: "mainnet",
105 | chain_id: 61,
106 | network_id: 1,
107 | rpc_url: "https://ethereumclassic.network",
108 | native_currency: {
109 | symbol: "ETH",
110 | name: "Ether Classic",
111 | decimals: "18",
112 | contractAddress: "",
113 | balance: "",
114 | },
115 | },
116 | {
117 | name: "POA Network Sokol",
118 | short_name: "poa",
119 | chain: "POA",
120 | network: "sokol",
121 | chain_id: 77,
122 | network_id: 77,
123 | rpc_url: "https://sokol.poa.network",
124 | native_currency: {
125 | symbol: "POA",
126 | name: "POA",
127 | decimals: "18",
128 | contractAddress: "",
129 | balance: "",
130 | },
131 | },
132 | {
133 | name: "POA Network Core",
134 | short_name: "skl",
135 | chain: "POA",
136 | network: "core",
137 | chain_id: 99,
138 | network_id: 99,
139 | rpc_url: "https://core.poa.network",
140 | native_currency: {
141 | symbol: "POA",
142 | name: "POA",
143 | decimals: "18",
144 | contractAddress: "",
145 | balance: "",
146 | },
147 | },
148 | {
149 | name: "xDAI Chain",
150 | short_name: "xdai",
151 | chain: "POA",
152 | network: "dai",
153 | chain_id: 100,
154 | network_id: 100,
155 | rpc_url: "https://dai.poa.network",
156 | native_currency: {
157 | symbol: "xDAI",
158 | name: "xDAI",
159 | decimals: "18",
160 | contractAddress: "",
161 | balance: "",
162 | },
163 | },
164 | {
165 | name: "Callisto Mainnet",
166 | short_name: "clo",
167 | chain: "callisto",
168 | network: "mainnet",
169 | chain_id: 820,
170 | network_id: 1,
171 | rpc_url: "https://clo-geth.0xinfra.com/",
172 | native_currency: {
173 | symbol: "CLO",
174 | name: "CLO",
175 | decimals: "18",
176 | contractAddress: "",
177 | balance: "",
178 | },
179 | },
180 | {
181 | name: "Polygon Testnet Mumbai",
182 | chain: "Polygon",
183 | network: "mumbai",
184 | rpc_url: "https://rpc-mumbai.maticvigil.com",
185 | native_currency: {
186 | name: "MATIC",
187 | symbol: "MATIC",
188 | decimals: "18",
189 | contractAddress: "",
190 | balance: "",
191 | },
192 | short_name: "maticmum",
193 | chain_id: 80001,
194 | network_id: 80001,
195 | },
196 | {
197 | name: "Polygon Mainnet",
198 | chain: "Polygon",
199 | network: "mainnet",
200 | rpc_url: "https://rpc-mainnet.maticvigil.com",
201 | native_currency: {
202 | name: "MATIC",
203 | symbol: "MATIC",
204 | decimals: "18",
205 | contractAddress: "",
206 | balance: "",
207 | },
208 | short_name: "MATIC",
209 | chain_id: 137,
210 | network_id: 137,
211 | },
212 | {
213 | name: "Avalanche Fuji Testnet",
214 | chain: "AVAX",
215 | network: "fuji",
216 | rpc_url: "https://api.avax-test.network/ext/bc/C/rpc",
217 | native_currency: {
218 | name: "Avalanche",
219 | symbol: "AVAX",
220 | decimals: "18",
221 | contractAddress: "",
222 | balance: "",
223 | },
224 | short_name: "Fuji",
225 | chain_id: 43113,
226 | network_id: 1,
227 | },
228 | {
229 | name: "Avalanche Mainnet",
230 | chain: "AVAX",
231 | network: "mainnet",
232 | rpc_url: "https://api.avax.network/ext/bc/C/rpc",
233 | native_currency: {
234 | name: "Avalanche",
235 | symbol: "AVAX",
236 | decimals: "18",
237 | contractAddress: "",
238 | balance: "",
239 | },
240 | short_name: "Avalanche",
241 | chain_id: 43114,
242 | network_id: 43114,
243 | },
244 | {
245 | name: "Arbitrum One",
246 | chain_id: 42161,
247 | network_id: 42161,
248 | network: "mainnet",
249 | short_name: "arb1",
250 | chain: "ETH",
251 | native_currency: {
252 | name: "Ether",
253 | symbol: "AETH",
254 | decimals: "18",
255 | contractAddress: "",
256 | balance: "",
257 | },
258 | rpc_url: "https://arbitrum-mainnet.infura.io/v3/%API_KEY%",
259 | },
260 | {
261 | name: "Optimism",
262 | chain: "ETH",
263 | rpc_url: "https://mainnet.optimism.io/",
264 | network: "mainnet",
265 | native_currency: {
266 | name: "Ether",
267 | symbol: "OETH",
268 | decimals: "18",
269 | contractAddress: "",
270 | balance: "",
271 | },
272 | short_name: "oeth",
273 | chain_id: 10,
274 | network_id: 10,
275 | },
276 | ];
277 |
--------------------------------------------------------------------------------
/src/helpers/utilities.ts:
--------------------------------------------------------------------------------
1 | import { providers } from "ethers";
2 | import { convertUtf8ToHex } from "@walletconnect/utils";
3 | import { TypedDataUtils } from "eth-sig-util";
4 | import * as ethUtil from "ethereumjs-util";
5 | import { IChainData } from "./types";
6 | import { SUPPORTED_CHAINS } from "./chains";
7 | import { eip1271 } from "./eip1271";
8 |
9 | export function capitalize(string: string): string {
10 | return string
11 | .split(" ")
12 | .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
13 | .join(" ");
14 | }
15 |
16 | export function ellipseText(text = "", maxLength = 9999): string {
17 | if (text.length <= maxLength) {
18 | return text;
19 | }
20 | const _maxLength = maxLength - 3;
21 | let ellipse = false;
22 | let currentLength = 0;
23 | const result =
24 | text
25 | .split(" ")
26 | .filter(word => {
27 | currentLength += word.length;
28 | if (ellipse || currentLength >= _maxLength) {
29 | ellipse = true;
30 | return false;
31 | } else {
32 | return true;
33 | }
34 | })
35 | .join(" ") + "...";
36 | return result;
37 | }
38 |
39 | export function ellipseAddress(address = "", width = 10): string {
40 | return `${address.slice(0, width)}...${address.slice(-width)}`;
41 | }
42 |
43 | export function padLeft(n: string, width: number, z?: string): string {
44 | z = z || "0";
45 | n = n + "";
46 | return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
47 | }
48 |
49 | export function sanitizeHex(hex: string): string {
50 | hex = hex.substring(0, 2) === "0x" ? hex.substring(2) : hex;
51 | if (hex === "") {
52 | return "";
53 | }
54 | hex = hex.length % 2 !== 0 ? "0" + hex : hex;
55 | return "0x" + hex;
56 | }
57 |
58 | export function removeHexPrefix(hex: string): string {
59 | return hex.toLowerCase().replace("0x", "");
60 | }
61 |
62 | export function getDataString(func: string, arrVals: any[]): string {
63 | let val = "";
64 | for (let i = 0; i < arrVals.length; i++) {
65 | val += padLeft(arrVals[i], 64);
66 | }
67 | const data = func + val;
68 | return data;
69 | }
70 |
71 | export function isMobile(): boolean {
72 | let mobile = false;
73 |
74 | function hasTouchEvent(): boolean {
75 | try {
76 | document.createEvent("TouchEvent");
77 | return true;
78 | } catch (e) {
79 | return false;
80 | }
81 | }
82 |
83 | function hasMobileUserAgent(): boolean {
84 | if (
85 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
86 | navigator.userAgent,
87 | ) ||
88 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
89 | navigator.userAgent.substr(0, 4),
90 | )
91 | ) {
92 | return true;
93 | } else if (hasTouchEvent()) {
94 | return true;
95 | }
96 | return false;
97 | }
98 |
99 | mobile = hasMobileUserAgent();
100 |
101 | return mobile;
102 | }
103 |
104 | export function getChainData(chainId: number): IChainData {
105 | const chainData = SUPPORTED_CHAINS.filter((chain: any) => chain.chain_id === chainId)[0];
106 |
107 | if (!chainData) {
108 | throw new Error("ChainId missing or not supported");
109 | }
110 |
111 | const API_KEY = process.env.REACT_APP_INFURA_PROJECT_ID;
112 |
113 | if (
114 | chainData.rpc_url.includes("infura.io") &&
115 | chainData.rpc_url.includes("%API_KEY%") &&
116 | API_KEY
117 | ) {
118 | const rpcUrl = chainData.rpc_url.replace("%API_KEY%", API_KEY);
119 |
120 | return {
121 | ...chainData,
122 | rpc_url: rpcUrl,
123 | };
124 | }
125 |
126 | return chainData;
127 | }
128 |
129 | export function encodePersonalMessage(msg: string): string {
130 | const data = ethUtil.toBuffer(convertUtf8ToHex(msg));
131 | const buf = Buffer.concat([
132 | Buffer.from("\u0019Ethereum Signed Message:\n" + data.length.toString(), "utf8"),
133 | data,
134 | ]);
135 | return ethUtil.bufferToHex(buf);
136 | }
137 |
138 | export function hashMessage(msg: string): string {
139 | const data = encodePersonalMessage(msg);
140 | const buf = ethUtil.toBuffer(data);
141 | const hash = ethUtil.keccak256(buf);
142 | return ethUtil.bufferToHex(hash);
143 | }
144 |
145 | export function encodeTypedDataMessage(msg: string): string {
146 | const data = TypedDataUtils.sanitizeData(JSON.parse(msg));
147 | const buf = Buffer.concat([
148 | Buffer.from("1901", "hex"),
149 | TypedDataUtils.hashStruct("EIP712Domain", data.domain, data.types),
150 | TypedDataUtils.hashStruct(data.primaryType as string, data.message, data.types),
151 | ]);
152 | return ethUtil.bufferToHex(buf);
153 | }
154 |
155 | export function hashTypedDataMessage(msg: string): string {
156 | const data = encodeTypedDataMessage(msg);
157 | const buf = ethUtil.toBuffer(data);
158 | const hash = ethUtil.keccak256(buf);
159 | return ethUtil.bufferToHex(hash);
160 | }
161 |
162 | export function recoverAddress(sig: string, hash: string): string {
163 | const params = ethUtil.fromRpcSig(sig);
164 | const result = ethUtil.ecrecover(ethUtil.toBuffer(hash), params.v, params.r, params.s);
165 | const signer = ethUtil.bufferToHex(ethUtil.publicToAddress(result));
166 | return signer;
167 | }
168 |
169 | export function recoverMessageSignature(sig: string, msg: string): string {
170 | const hash = hashMessage(msg);
171 | const signer = recoverAddress(sig, hash);
172 | return signer;
173 | }
174 |
175 | export function recoverTypedMessage(sig: string, msg: string): string {
176 | const hash = hashTypedDataMessage(msg);
177 | const signer = recoverAddress(sig, hash);
178 | return signer;
179 | }
180 |
181 | export async function verifySignature(
182 | address: string,
183 | sig: string,
184 | hash: string,
185 | chainId: number,
186 | ): Promise {
187 | const rpcUrl = getChainData(chainId).rpc_url;
188 | const provider = new providers.JsonRpcProvider(rpcUrl);
189 | const bytecode = await provider.getCode(address);
190 | if (!bytecode || bytecode === "0x" || bytecode === "0x0" || bytecode === "0x00") {
191 | const signer = recoverAddress(sig, hash);
192 | return signer.toLowerCase() === address.toLowerCase();
193 | } else {
194 | return eip1271.isValidSignature(address, sig, hash, provider);
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import styled from "styled-components";
3 | import WalletConnect from "@walletconnect/client";
4 | import QRCodeModal from "@walletconnect/qrcode-modal";
5 | import { convertUtf8ToHex } from "@walletconnect/utils";
6 | import { IInternalEvent } from "@walletconnect/types";
7 | import Button from "./components/Button";
8 | import Column from "./components/Column";
9 | import Wrapper from "./components/Wrapper";
10 | import Modal from "./components/Modal";
11 | import Header from "./components/Header";
12 | import Loader from "./components/Loader";
13 | import { fonts } from "./styles";
14 | import { apiGetAccountAssets, apiGetGasPrices, apiGetAccountNonce } from "./helpers/api";
15 | import {
16 | sanitizeHex,
17 | verifySignature,
18 | hashTypedDataMessage,
19 | hashMessage,
20 | } from "./helpers/utilities";
21 | import { convertAmountToRawNumber, convertStringToHex } from "./helpers/bignumber";
22 | import { IAssetData } from "./helpers/types";
23 | import Banner from "./components/Banner";
24 | import AccountAssets from "./components/AccountAssets";
25 | import { eip712 } from "./helpers/eip712";
26 |
27 | const SLayout = styled.div`
28 | position: relative;
29 | width: 100%;
30 | /* height: 100%; */
31 | min-height: 100vh;
32 | text-align: center;
33 | `;
34 |
35 | const SContent = styled(Wrapper as any)`
36 | width: 100%;
37 | height: 100%;
38 | padding: 0 16px;
39 | `;
40 |
41 | const SLanding = styled(Column as any)`
42 | height: 600px;
43 | `;
44 |
45 | const SButtonContainer = styled(Column as any)`
46 | width: 250px;
47 | margin: 50px 0;
48 | `;
49 |
50 | const SConnectButton = styled(Button as any)`
51 | border-radius: 8px;
52 | font-size: ${fonts.size.medium};
53 | height: 44px;
54 | width: 100%;
55 | margin: 12px 0;
56 | `;
57 |
58 | const SContainer = styled.div`
59 | height: 100%;
60 | min-height: 200px;
61 | display: flex;
62 | flex-direction: column;
63 | justify-content: center;
64 | align-items: center;
65 | word-break: break-word;
66 | `;
67 |
68 | const SModalContainer = styled.div`
69 | width: 100%;
70 | position: relative;
71 | word-wrap: break-word;
72 | `;
73 |
74 | const SModalTitle = styled.div`
75 | margin: 1em 0;
76 | font-size: 20px;
77 | font-weight: 700;
78 | `;
79 |
80 | const SModalParagraph = styled.p`
81 | margin-top: 30px;
82 | `;
83 |
84 | // @ts-ignore
85 | const SBalances = styled(SLanding as any)`
86 | height: 100%;
87 | & h3 {
88 | padding-top: 30px;
89 | }
90 | `;
91 |
92 | const STable = styled(SContainer as any)`
93 | flex-direction: column;
94 | text-align: left;
95 | `;
96 |
97 | const SRow = styled.div`
98 | width: 100%;
99 | display: flex;
100 | margin: 6px 0;
101 | `;
102 |
103 | const SKey = styled.div`
104 | width: 30%;
105 | font-weight: 700;
106 | `;
107 |
108 | const SValue = styled.div`
109 | width: 70%;
110 | font-family: monospace;
111 | `;
112 |
113 | const STestButtonContainer = styled.div`
114 | width: 100%;
115 | display: flex;
116 | justify-content: center;
117 | align-items: center;
118 | flex-wrap: wrap;
119 | `;
120 |
121 | const STestButton = styled(Button as any)`
122 | border-radius: 8px;
123 | font-size: ${fonts.size.medium};
124 | height: 44px;
125 | width: 100%;
126 | max-width: 200px;
127 | margin: 12px;
128 | `;
129 |
130 | interface IAppState {
131 | connector: WalletConnect | null;
132 | fetching: boolean;
133 | connected: boolean;
134 | chainId: number;
135 | showModal: boolean;
136 | pendingRequest: boolean;
137 | uri: string;
138 | accounts: string[];
139 | address: string;
140 | result: any | null;
141 | assets: IAssetData[];
142 | }
143 |
144 | const INITIAL_STATE: IAppState = {
145 | connector: null,
146 | fetching: false,
147 | connected: false,
148 | chainId: 1,
149 | showModal: false,
150 | pendingRequest: false,
151 | uri: "",
152 | accounts: [],
153 | address: "",
154 | result: null,
155 | assets: [],
156 | };
157 |
158 | class App extends React.Component {
159 | public state: IAppState = {
160 | ...INITIAL_STATE,
161 | };
162 |
163 | public connect = async () => {
164 | // bridge url
165 | const bridge = "https://bridge.walletconnect.org";
166 |
167 | // create new connector
168 | const connector = new WalletConnect({ bridge, qrcodeModal: QRCodeModal });
169 |
170 | await this.setState({ connector });
171 |
172 | // check if already connected
173 | if (!connector.connected) {
174 | // create new session
175 | await connector.createSession();
176 | }
177 |
178 | // subscribe to events
179 | await this.subscribeToEvents();
180 | };
181 | public subscribeToEvents = () => {
182 | const { connector } = this.state;
183 |
184 | if (!connector) {
185 | return;
186 | }
187 |
188 | connector.on("session_update", async (error, payload) => {
189 | console.log(`connector.on("session_update")`);
190 |
191 | if (error) {
192 | throw error;
193 | }
194 |
195 | const { chainId, accounts } = payload.params[0];
196 | this.onSessionUpdate(accounts, chainId);
197 | });
198 |
199 | connector.on("connect", (error, payload) => {
200 | console.log(`connector.on("connect")`);
201 |
202 | if (error) {
203 | throw error;
204 | }
205 |
206 | this.onConnect(payload);
207 | });
208 |
209 | connector.on("disconnect", (error, payload) => {
210 | console.log(`connector.on("disconnect")`);
211 |
212 | if (error) {
213 | throw error;
214 | }
215 |
216 | this.onDisconnect();
217 | });
218 |
219 | if (connector.connected) {
220 | const { chainId, accounts } = connector;
221 | const address = accounts[0];
222 | this.setState({
223 | connected: true,
224 | chainId,
225 | accounts,
226 | address,
227 | });
228 | this.onSessionUpdate(accounts, chainId);
229 | }
230 |
231 | this.setState({ connector });
232 | };
233 |
234 | public killSession = async () => {
235 | const { connector } = this.state;
236 | if (connector) {
237 | connector.killSession();
238 | }
239 | this.resetApp();
240 | };
241 |
242 | public resetApp = async () => {
243 | await this.setState({ ...INITIAL_STATE });
244 | };
245 |
246 | public onConnect = async (payload: IInternalEvent) => {
247 | const { chainId, accounts } = payload.params[0];
248 | const address = accounts[0];
249 | await this.setState({
250 | connected: true,
251 | chainId,
252 | accounts,
253 | address,
254 | });
255 | this.getAccountAssets();
256 | };
257 |
258 | public onDisconnect = async () => {
259 | this.resetApp();
260 | };
261 |
262 | public onSessionUpdate = async (accounts: string[], chainId: number) => {
263 | const address = accounts[0];
264 | await this.setState({ chainId, accounts, address });
265 | await this.getAccountAssets();
266 | };
267 |
268 | public getAccountAssets = async () => {
269 | const { address, chainId } = this.state;
270 | this.setState({ fetching: true });
271 | try {
272 | // get account balances
273 | const assets = await apiGetAccountAssets(address, chainId);
274 |
275 | await this.setState({ fetching: false, address, assets });
276 | } catch (error) {
277 | console.error(error);
278 | await this.setState({ fetching: false });
279 | }
280 | };
281 |
282 | public toggleModal = () => this.setState({ showModal: !this.state.showModal });
283 |
284 | public testSendTransaction = async () => {
285 | const { connector, address, chainId } = this.state;
286 |
287 | if (!connector) {
288 | return;
289 | }
290 |
291 | // from
292 | const from = address;
293 |
294 | // to
295 | const to = address;
296 |
297 | // nonce
298 | const _nonce = await apiGetAccountNonce(address, chainId);
299 | const nonce = sanitizeHex(convertStringToHex(_nonce));
300 |
301 | // gasPrice
302 | const gasPrices = await apiGetGasPrices();
303 | const _gasPrice = gasPrices.slow.price;
304 | const gasPrice = sanitizeHex(convertStringToHex(convertAmountToRawNumber(_gasPrice, 9)));
305 |
306 | // gasLimit
307 | const _gasLimit = 21000;
308 | const gasLimit = sanitizeHex(convertStringToHex(_gasLimit));
309 |
310 | // value
311 | const _value = 0;
312 | const value = sanitizeHex(convertStringToHex(_value));
313 |
314 | // data
315 | const data = "0x";
316 |
317 | // test transaction
318 | const tx = {
319 | from,
320 | to,
321 | nonce,
322 | gasPrice,
323 | gasLimit,
324 | value,
325 | data,
326 | };
327 |
328 | try {
329 | // open modal
330 | this.toggleModal();
331 |
332 | // toggle pending request indicator
333 | this.setState({ pendingRequest: true });
334 |
335 | // send transaction
336 | const result = await connector.sendTransaction(tx);
337 |
338 | // format displayed result
339 | const formattedResult = {
340 | method: "eth_sendTransaction",
341 | txHash: result,
342 | from: address,
343 | to: address,
344 | value: `${_value} ETH`,
345 | };
346 |
347 | // display result
348 | this.setState({
349 | connector,
350 | pendingRequest: false,
351 | result: formattedResult || null,
352 | });
353 | } catch (error) {
354 | console.error(error);
355 | this.setState({ connector, pendingRequest: false, result: null });
356 | }
357 | };
358 |
359 | public testSignTransaction = async () => {
360 | const { connector, address, chainId } = this.state;
361 |
362 | if (!connector) {
363 | return;
364 | }
365 |
366 | // from
367 | const from = address;
368 |
369 | // to
370 | const to = address;
371 |
372 | // nonce
373 | const _nonce = await apiGetAccountNonce(address, chainId);
374 | const nonce = sanitizeHex(convertStringToHex(_nonce));
375 |
376 | // gasPrice
377 | const gasPrices = await apiGetGasPrices();
378 | const _gasPrice = gasPrices.slow.price;
379 | const gasPrice = sanitizeHex(convertStringToHex(convertAmountToRawNumber(_gasPrice, 9)));
380 |
381 | // gasLimit
382 | const _gasLimit = 21000;
383 | const gasLimit = sanitizeHex(convertStringToHex(_gasLimit));
384 |
385 | // value
386 | const _value = 0;
387 | const value = sanitizeHex(convertStringToHex(_value));
388 |
389 | // data
390 | const data = "0x";
391 |
392 | // test transaction
393 | const tx = {
394 | from,
395 | to,
396 | nonce,
397 | gasPrice,
398 | gasLimit,
399 | value,
400 | data,
401 | };
402 |
403 | try {
404 | // open modal
405 | this.toggleModal();
406 |
407 | // toggle pending request indicator
408 | this.setState({ pendingRequest: true });
409 |
410 | // send transaction
411 | const result = await connector.signTransaction(tx);
412 |
413 | // format displayed result
414 | const formattedResult = {
415 | method: "eth_signTransaction",
416 | from: address,
417 | to: address,
418 | value: `${_value} ETH`,
419 | result,
420 | };
421 |
422 | // display result
423 | this.setState({
424 | connector,
425 | pendingRequest: false,
426 | result: formattedResult || null,
427 | });
428 | } catch (error) {
429 | console.error(error);
430 | this.setState({ connector, pendingRequest: false, result: null });
431 | }
432 | };
433 |
434 | public testLegacySignMessage = async () => {
435 | const { connector, address, chainId } = this.state;
436 |
437 | if (!connector) {
438 | return;
439 | }
440 |
441 | // test message
442 | const message = `My email is john@doe.com - ${new Date().toUTCString()}`;
443 |
444 | // hash message
445 | const hash = hashMessage(message);
446 |
447 | // eth_sign params
448 | const msgParams = [address, hash];
449 |
450 | try {
451 | // open modal
452 | this.toggleModal();
453 |
454 | // toggle pending request indicator
455 | this.setState({ pendingRequest: true });
456 |
457 | // send message
458 | const result = await connector.signMessage(msgParams);
459 |
460 | // verify signature
461 | const valid = await verifySignature(address, result, hash, chainId);
462 |
463 | // format displayed result
464 | const formattedResult = {
465 | method: "eth_sign (legacy)",
466 | address,
467 | valid,
468 | result,
469 | };
470 |
471 | // display result
472 | this.setState({
473 | connector,
474 | pendingRequest: false,
475 | result: formattedResult || null,
476 | });
477 | } catch (error) {
478 | console.error(error);
479 | this.setState({ connector, pendingRequest: false, result: null });
480 | }
481 | };
482 |
483 | public testStandardSignMessage = async () => {
484 | const { connector, address, chainId } = this.state;
485 |
486 | if (!connector) {
487 | return;
488 | }
489 |
490 | // test message
491 | const message = `My email is john@doe.com - ${new Date().toUTCString()}`;
492 |
493 | // encode message (hex)
494 | const hexMsg = convertUtf8ToHex(message);
495 |
496 | // eth_sign params
497 | const msgParams = [address, hexMsg];
498 |
499 | try {
500 | // open modal
501 | this.toggleModal();
502 |
503 | // toggle pending request indicator
504 | this.setState({ pendingRequest: true });
505 |
506 | // send message
507 | const result = await connector.signMessage(msgParams);
508 |
509 | // verify signature
510 | const hash = hashMessage(message);
511 | const valid = await verifySignature(address, result, hash, chainId);
512 |
513 | // format displayed result
514 | const formattedResult = {
515 | method: "eth_sign (standard)",
516 | address,
517 | valid,
518 | result,
519 | };
520 |
521 | // display result
522 | this.setState({
523 | connector,
524 | pendingRequest: false,
525 | result: formattedResult || null,
526 | });
527 | } catch (error) {
528 | console.error(error);
529 | this.setState({ connector, pendingRequest: false, result: null });
530 | }
531 | };
532 |
533 | public testPersonalSignMessage = async () => {
534 | const { connector, address, chainId } = this.state;
535 |
536 | if (!connector) {
537 | return;
538 | }
539 |
540 | // test message
541 | const message = `My email is john@doe.com - ${new Date().toUTCString()}`;
542 |
543 | // encode message (hex)
544 | const hexMsg = convertUtf8ToHex(message);
545 |
546 | // eth_sign params
547 | const msgParams = [hexMsg, address];
548 |
549 | try {
550 | // open modal
551 | this.toggleModal();
552 |
553 | // toggle pending request indicator
554 | this.setState({ pendingRequest: true });
555 |
556 | // send message
557 | const result = await connector.signPersonalMessage(msgParams);
558 |
559 | // verify signature
560 | const hash = hashMessage(message);
561 | const valid = await verifySignature(address, result, hash, chainId);
562 |
563 | // format displayed result
564 | const formattedResult = {
565 | method: "personal_sign",
566 | address,
567 | valid,
568 | result,
569 | };
570 |
571 | // display result
572 | this.setState({
573 | connector,
574 | pendingRequest: false,
575 | result: formattedResult || null,
576 | });
577 | } catch (error) {
578 | console.error(error);
579 | this.setState({ connector, pendingRequest: false, result: null });
580 | }
581 | };
582 |
583 | public testSignTypedData = async () => {
584 | const { connector, address, chainId } = this.state;
585 |
586 | if (!connector) {
587 | return;
588 | }
589 |
590 | const message = JSON.stringify(eip712.example);
591 |
592 | // eth_signTypedData params
593 | const msgParams = [address, message];
594 |
595 | try {
596 | // open modal
597 | this.toggleModal();
598 |
599 | // toggle pending request indicator
600 | this.setState({ pendingRequest: true });
601 |
602 | // sign typed data
603 | const result = await connector.signTypedData(msgParams);
604 |
605 | // verify signature
606 | const hash = hashTypedDataMessage(message);
607 | const valid = await verifySignature(address, result, hash, chainId);
608 |
609 | // format displayed result
610 | const formattedResult = {
611 | method: "eth_signTypedData_v4",
612 | address,
613 | valid,
614 | result,
615 | };
616 |
617 | // display result
618 | this.setState({
619 | connector,
620 | pendingRequest: false,
621 | result: formattedResult || null,
622 | });
623 | } catch (error) {
624 | console.error(error);
625 | this.setState({ connector, pendingRequest: false, result: null });
626 | }
627 | };
628 |
629 | public render = () => {
630 | const {
631 | assets,
632 | address,
633 | connected,
634 | chainId,
635 | fetching,
636 | showModal,
637 | pendingRequest,
638 | result,
639 | } = this.state;
640 | return (
641 |
642 |
643 |
649 |
650 | {!address && !assets.length ? (
651 |
652 |
653 | {`Try out WalletConnect`}
654 |
655 | {`v${process.env.REACT_APP_VERSION}`}
656 |
657 |
658 |
659 | {"Connect to WalletConnect"}
660 |
661 |
662 |
663 | ) : (
664 |
665 |
666 | Actions
667 |
668 |
669 |
670 | {"eth_sendTransaction"}
671 |
672 |
673 | {"eth_signTransaction"}
674 |
675 |
676 | {"eth_signTypedData_v4"}
677 |
678 |
679 | {"eth_sign (legacy)"}
680 |
681 |
682 | {"eth_sign (standard)"}
683 |
684 |
685 | {"personal_sign"}
686 |
687 |
688 |
689 | Balances
690 | {!fetching ? (
691 |
692 | ) : (
693 |
694 |
695 |
696 |
697 |
698 | )}
699 |
700 | )}
701 |
702 |
703 |
704 | {pendingRequest ? (
705 |
706 | {"Pending Call Request"}
707 |
708 |
709 | {"Approve or reject request using your wallet"}
710 |
711 |
712 | ) : result ? (
713 |
714 | {"Call Request Approved"}
715 |
716 | {Object.keys(result).map(key => (
717 |
718 | {key}
719 | {result[key].toString()}
720 |
721 | ))}
722 |
723 |
724 | ) : (
725 |
726 | {"Call Request Rejected"}
727 |
728 | )}
729 |
730 |
731 | );
732 | };
733 | }
734 |
735 | export default App;
736 |
--------------------------------------------------------------------------------