chore: set up app, get basics working with EthereumProvider

This commit is contained in:
Ben Kremer 2022-02-14 11:14:39 +01:00
parent 033cf6a585
commit 43c67d0c81
55 changed files with 17042 additions and 0 deletions

View File

@ -0,0 +1,3 @@
REACT_APP_PROJECT_ID=39bc93c4affb2e20cb6f8d36ca107dd9
REACT_APP_RELAY_URL=wss://relay.dev.walletconnect.com

View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache

View File

@ -0,0 +1,7 @@
{
"tabWidth": 2,
"useTabs": false,
"trailingComma": "all",
"printWidth": 100,
"arrowParens": "avoid"
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 WalletConnect, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,33 @@
# WalletConnect React App
## Getting Started
Install the app's dependencies:
```bash
yarn
```
Set up your local environment variables by copying the example into your own `.env.local` file:
```bash
cp .env.local.example .env.local
```
## Develop
```bash
yarn start
```
## Test
```bash
yarn test
```
## Build
```bash
yarn build
```

View File

@ -0,0 +1,7 @@
declare module "*.svg";
declare module "*.png";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.gif";
declare module "*.bmp";
declare module "*.tiff";

View File

@ -0,0 +1,86 @@
{
"name": "walletconnect-react-app",
"version": "2.0.0-beta.22",
"private": true,
"keywords": [
"walletconnect",
"ethereum",
"web3",
"crypto"
],
"author": "WalletConnect, Inc. <walletconnect.com>",
"license": "MIT",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"prettier": "prettier --check '**/*.{js,ts,jsx,tsx}'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/walletconnect/walletconnect-monorepo.git"
},
"bugs": {
"url": "https://github.com/walletconnect/walletconnect-monorepo/issues"
},
"resolutions": {
"react-error-overlay": "6.0.9"
},
"dependencies": {
"@walletconnect/client": "^2.0.0-beta.22",
"@walletconnect/ethereum-provider": "2.0.0-beta.22",
"@walletconnect/legacy-modal": "^2.0.0-beta.22",
"@walletconnect/types": "^2.0.0-beta.22",
"@walletconnect/utils": "^2.0.0-beta.22",
"axios": "^0.21.1",
"blockies-ts": "^1.0.0",
"caip-api": "^2.0.0-beta.1",
"cosmos-wallet": "^1.1.0",
"eth-sig-util": "^2.5.3",
"ethereumjs-util": "^7.0.6",
"ethers": "^5.3.0",
"prop-types": "^15.7.2",
"qr-image": "^3.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.3",
"styled-components": "^5.2.0",
"typescript": "^4.3.2",
"web-vitals": "^0.2.4"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/bn.js": "^5.1.0",
"@types/eth-sig-util": "^2.1.1",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.14",
"@types/pino": "^7.0.5",
"@types/prop-types": "^15.7.4",
"@types/qr-image": "^3.2.5",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"@types/styled-components": "^5.1.21",
"prettier": "^2.5.1"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
<meta name="description" content="React App for WalletConnect" />
<style>
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,500,600,700,800");
</style>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "WalletConnect",
"name": "WalletConnect React App",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,253 @@
import React, { useEffect, useState } from "react";
import { version } from "@walletconnect/client/package.json";
import * as encoding from "@walletconnect/encoding";
import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
import Column from "./components/Column";
import Header from "./components/Header";
import Modal from "./components/Modal";
import { DEFAULT_MAIN_CHAINS, DEFAULT_TEST_CHAINS } from "./constants";
import {
getAllChainNamespaces,
AccountAction,
ChainNamespaces,
getLocalStorageTestnetFlag,
setLocaleStorageTestnetFlag,
} from "./helpers";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PairingModal from "./modals/PairingModal";
import PingModal from "./modals/PingModal";
import {
SAccounts,
SAccountsContainer,
SButtonContainer,
SConnectButton,
SContent,
SLanding,
SLayout,
SToggleContainer,
} from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import { utils } from "ethers";
interface IFormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
export default function App() {
const [isTestnet, setIsTestnet] = useState(getLocalStorageTestnetFlag());
const [chainData, setChainData] = useState<ChainNamespaces>({});
const [isRpcRequestPending, setIsRpcRequestPending] = useState(false);
const [rpcResult, setRpcResult] = useState<IFormattedRpcResponse | null>();
const [modal, setModal] = useState("");
const closeModal = () => setModal("");
const openPairingModal = () => setModal("pairing");
const openPingModal = () => setModal("ping");
const openRequestModal = () => setModal("request");
// Initialize the WalletConnect client.
const {
client,
session,
disconnect,
chains,
accounts,
balances,
isFetchingBalances,
isInitializing,
setChains,
onEnable,
web3Provider,
} = useWalletConnectClient();
// Close the pairing modal after a session is established.
useEffect(() => {
if (session && modal === "pairing") {
closeModal();
}
}, [session, modal]);
useEffect(() => {
loadChainData();
}, []);
const loadChainData = async () => {
const namespaces = getAllChainNamespaces();
const chainData: ChainNamespaces = {};
await Promise.all(
namespaces.map(async namespace => {
let chains: ChainsMap | undefined;
try {
chains = await apiGetChainNamespace(namespace);
} catch (e) {
// ignore error
}
if (typeof chains !== "undefined") {
chainData[namespace] = chains;
}
}),
);
setChainData(chainData);
};
// const onPing = async () => {
// openPingModal();
// await ping();
// };
const testSignMessage: () => Promise<IFormattedRpcResponse> = async () => {
if (!web3Provider) {
throw new Error("web3Provider not connected");
}
const msg = "hello world";
const hexMsg = encoding.utf8ToHex(msg, true);
const address = accounts[0];
const signature = await web3Provider.send("personal_sign", [hexMsg, address]);
const valid = utils.verifyMessage(msg, signature) === address;
return {
method: "personal_sign",
address,
valid,
result: signature,
};
};
const getEthereumActions = (): AccountAction[] => {
const onSignPersonalMessage = async () => {
openRequestModal();
try {
setIsRpcRequestPending(true);
const result = await testSignMessage();
setRpcResult(result);
} catch (error) {
console.error(error);
} finally {
setIsRpcRequestPending(false);
}
};
return [
// { method: "eth_sendTransaction", callback: onSendTransaction },
{ method: "personal_sign", callback: onSignPersonalMessage },
// { method: "eth_signTypedData", callback: onSignTypedData },
];
};
const getBlockchainActions = (chainId: string) => {
const [namespace] = chainId.split(":");
switch (namespace) {
case "eip155":
return getEthereumActions();
case "cosmos":
return [];
default:
break;
}
};
// Toggle between displaying testnet or mainnet chains as selection options.
const toggleTestnets = () => {
const nextIsTestnetState = !isTestnet;
setIsTestnet(nextIsTestnetState);
setLocaleStorageTestnetFlag(nextIsTestnetState);
};
const handleChainSelectionClick = (chainId: string) => {
if (chains.includes(chainId)) {
setChains(chains.filter(chain => chain !== chainId));
} else {
setChains([...chains, chainId]);
}
};
// Renders the appropriate model for the given request that is currently in-flight.
const renderModal = () => {
switch (modal) {
case "pairing":
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
return <PairingModal pairings={client.pairing.values} connect={onEnable} />;
case "request":
return <RequestModal pending={isRpcRequestPending} result={rpcResult} />;
case "ping":
return <PingModal pending={isRpcRequestPending} result={rpcResult} />;
default:
return null;
}
};
const renderContent = () => {
const chainOptions = isTestnet ? DEFAULT_TEST_CHAINS : DEFAULT_MAIN_CHAINS;
return !accounts.length && !Object.keys(balances).length ? (
<SLanding center>
<Banner />
<h6>
<span>{`Using v${version || "2.0.0-beta"}`}</span>
</h6>
<SButtonContainer>
<h6>Select chains:</h6>
<SToggleContainer>
<p>Testnets Only?</p>
<Toggle active={isTestnet} onClick={toggleTestnets} />
</SToggleContainer>
{chainOptions.map(chainId => (
<Blockchain
key={chainId}
chainId={chainId}
chainData={chainData}
onClick={handleChainSelectionClick}
active={chains.includes(chainId)}
/>
))}
<SConnectButton left onClick={onEnable} disabled={!chains.length}>
{"Connect"}
</SConnectButton>
</SButtonContainer>
</SLanding>
) : (
<SAccountsContainer>
<h3>Accounts</h3>
<SAccounts>
{accounts.map(account => {
// const [namespace, reference, address] = account.split(":");
// const chainId = `${namespace}:${reference}`;
const chainId = "eip155:42";
return (
<Blockchain
key={account}
active={true}
chainData={chainData}
fetching={isFetchingBalances}
address={account}
chainId={chainId}
balances={balances}
actions={getBlockchainActions(chainId)}
/>
);
})}
</SAccounts>
</SAccountsContainer>
);
};
return (
<SLayout>
<Column maxWidth={1000} spanHeight>
<Header ping={() => Promise.resolve()} disconnect={disconnect} session={session} />
<SContent>{isInitializing ? "Loading..." : renderContent()}</SContent>
</Column>
<Modal show={!!modal} closeModal={closeModal}>
{renderModal()}
</Modal>
</SLayout>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="26" viewBox="0 0 16 26">
<path
fill="#0C0C0D"
fillRule="nonzero"
d="M8 19.096l7.998-4.733L8 25.637 0 14.363l8 4.733zM8 0l8 12.6-8 4.733L0 12.6 8 0z"
/>
</svg>

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,38 @@
import { JsonRpcRequest } from "@walletconnect/jsonrpc-utils";
import { BLOCKCHAIN_LOGO_BASE_URL } from "../constants";
import { NamespaceMetadata, ChainMetadata, ChainRequestRender } from "../helpers";
export const CosmosMetadata: NamespaceMetadata = {
"cosmoshub-4": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "cosmos:cosmoshub-4.png",
rgb: "27, 31, 53",
},
};
export function getChainMetadata(chainId: string): ChainMetadata {
const reference = chainId.split(":")[1];
const metadata = CosmosMetadata[reference];
if (typeof metadata === "undefined") {
throw new Error(`No chain metadata found for chainId: ${chainId}`);
}
return metadata;
}
export function getChainRequestRender(request: JsonRpcRequest): ChainRequestRender[] {
let params = [{ label: "Method", value: request.method }];
switch (request.method) {
default:
params = [
...params,
{
label: "params",
value: JSON.stringify(request.params, null, "\t"),
},
];
break;
}
return params;
}

View File

@ -0,0 +1,148 @@
import { JsonRpcRequest } from "@walletconnect/jsonrpc-utils";
import {
NamespaceMetadata,
ChainMetadata,
ChainRequestRender,
convertHexToNumber,
convertHexToUtf8,
} from "../helpers";
import { BLOCKCHAIN_LOGO_BASE_URL } from "../constants";
export const EIP155Colors = {
ethereum: "99, 125, 234",
optimism: "233, 1, 1",
goerli: "189, 174, 155",
xdai: "73, 169, 166",
polygon: "130, 71, 229",
celo: "60, 203, 132",
arbitrum: "44, 55, 75",
};
export const EIP155Metadata: NamespaceMetadata = {
"1": {
name: "Ethereum",
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:1.png",
rgb: EIP155Colors.ethereum,
},
"5": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:5.png",
rgb: EIP155Colors.goerli,
},
"10": {
name: "Optimism",
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:10.png",
rgb: EIP155Colors.optimism,
},
"42": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:42.png",
rgb: EIP155Colors.ethereum,
},
"69": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:69.png",
rgb: EIP155Colors.optimism,
},
"100": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:100.png",
rgb: EIP155Colors.xdai,
},
"137": {
name: "Polygon",
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:137.png",
rgb: EIP155Colors.polygon,
},
"80001": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:80001.png",
rgb: EIP155Colors.polygon,
},
"42161": {
name: "Arbitrum",
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:42161.png",
rgb: EIP155Colors.arbitrum,
},
"42220": {
name: "Celo",
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:42220.png",
rgb: EIP155Colors.celo,
},
"44787": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:44787.png",
rgb: EIP155Colors.celo,
},
"421611": {
logo: BLOCKCHAIN_LOGO_BASE_URL + "eip155:421611.png",
rgb: EIP155Colors.arbitrum,
},
};
export function getChainMetadata(chainId: string): ChainMetadata {
const reference = chainId.split(":")[1];
const metadata = EIP155Metadata[reference];
if (typeof metadata === "undefined") {
throw new Error(`No chain metadata found for chainId: ${chainId}`);
}
return metadata;
}
export function getChainRequestRender(request: JsonRpcRequest): ChainRequestRender[] {
let params = [{ label: "Method", value: request.method }];
switch (request.method) {
case "eth_sendTransaction":
case "eth_signTransaction":
params = [
...params,
{ label: "From", value: request.params[0].from },
{ label: "To", value: request.params[0].to },
{
label: "Gas Limit",
value: request.params[0].gas
? convertHexToNumber(request.params[0].gas)
: request.params[0].gasLimit
? convertHexToNumber(request.params[0].gasLimit)
: "",
},
{
label: "Gas Price",
value: convertHexToNumber(request.params[0].gasPrice),
},
{
label: "Nonce",
value: convertHexToNumber(request.params[0].nonce),
},
{
label: "Value",
value: request.params[0].value ? convertHexToNumber(request.params[0].value) : "",
},
{ label: "Data", value: request.params[0].data },
];
break;
case "eth_sign":
params = [
...params,
{ label: "Address", value: request.params[0] },
{ label: "Message", value: request.params[1] },
];
break;
case "personal_sign":
params = [
...params,
{ label: "Address", value: request.params[1] },
{
label: "Message",
value: convertHexToUtf8(request.params[0]),
},
];
break;
default:
params = [
...params,
{
label: "params",
value: JSON.stringify(request.params, null, "\t"),
},
];
break;
}
return params;
}

View File

@ -0,0 +1,38 @@
import { JsonRpcRequest } from "@walletconnect/jsonrpc-utils";
import * as eip155 from "./eip155";
import * as cosmos from "./cosmos";
import * as polkadot from "./polkadot";
import { ChainMetadata, ChainRequestRender } from "../helpers";
export function getChainMetadata(chainId: string): ChainMetadata {
const namespace = chainId.split(":")[0];
switch (namespace) {
case "eip155":
return eip155.getChainMetadata(chainId);
case "cosmos":
return cosmos.getChainMetadata(chainId);
case "polkadot":
return polkadot.getChainMetadata(chainId);
default:
throw new Error(`No metadata handler for namespace ${namespace}`);
}
}
export function getChainRequestRender(
request: JsonRpcRequest,
chainId: string,
): ChainRequestRender[] {
const namespace = chainId.split(":")[0];
switch (namespace) {
case "eip155":
return eip155.getChainRequestRender(request);
case "cosmos":
return cosmos.getChainRequestRender(request);
case "polkadot":
return polkadot.getChainRequestRender(request);
default:
throw new Error(`No render handler for namespace ${namespace}`);
}
}

View File

@ -0,0 +1,38 @@
import { JsonRpcRequest } from "@walletconnect/jsonrpc-utils";
import { BLOCKCHAIN_LOGO_BASE_URL } from "../constants";
import { NamespaceMetadata, ChainMetadata, ChainRequestRender } from "../helpers";
export const PolkadotMetadata: NamespaceMetadata = {
// eslint-disable-next-line no-useless-computed-key
["91b171bb158e2d3848fa23a9f1c25182"]: {
logo: BLOCKCHAIN_LOGO_BASE_URL + "polkadot:91b171bb158e2d3848fa23a9f1c25182.png",
rgb: "230, 1, 122",
},
};
export function getChainMetadata(chainId: string): ChainMetadata {
const reference = chainId.split(":")[1];
const metadata = PolkadotMetadata[reference];
if (typeof metadata === "undefined") {
throw new Error(`No chain metadata found for chainId: ${chainId}`);
}
return metadata;
}
export function getChainRequestRender(request: JsonRpcRequest): ChainRequestRender[] {
let params = [{ label: "Method", value: request.method }];
switch (request.method) {
default:
params = [
...params,
{
label: "params",
value: JSON.stringify(request.params, null, "\t"),
},
];
break;
}
return params;
}

View File

@ -0,0 +1,74 @@
import * as React from "react";
import styled from "styled-components";
import Icon from "./Icon";
import { AssetData, fromWad } from "../helpers";
import eth from "../assets/eth.svg";
import erc20 from "../assets/erc20.svg";
import { getChainMetadata } from "../chains";
const xdai = getChainMetadata("eip155:100").logo;
const matic = getChainMetadata("eip155:137").logo;
const SAsset = styled.div`
width: 100%;
padding: 20px;
display: flex;
justify-content: space-between;
`;
const SAssetLeft = styled.div`
display: flex;
`;
const SAssetName = styled.div`
display: flex;
margin-left: 10px;
`;
const SAssetRight = styled.div`
display: flex;
`;
const SAssetBalance = styled.div`
display: flex;
`;
function getAssetIcon(asset: AssetData): JSX.Element {
if (!!asset.contractAddress) {
const src = `https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens/${asset.contractAddress.toLowerCase()}.png`;
return <Icon src={src} fallback={erc20} />;
}
switch (asset.symbol.toLowerCase()) {
case "eth":
return <Icon src={eth} />;
case "xdai":
return <Icon src={xdai} />;
case "matic":
return <Icon src={matic} />;
default:
return <Icon src={erc20} />;
}
}
interface AssetProps {
asset: { symbol: string; balance: string };
}
const Asset = (props: AssetProps) => {
const { asset } = props;
return (
<SAsset {...props}>
<SAssetLeft>
{/* {getAssetIcon(asset)} */}
<SAssetName>{asset.symbol}</SAssetName>
</SAssetLeft>
<SAssetRight>
<SAssetBalance>{`${asset.balance}`}</SAssetBalance>
</SAssetRight>
</SAsset>
);
};
export default Asset;

View File

@ -0,0 +1,25 @@
import * as React from "react";
import styled from "styled-components";
import logo from "../assets/walletconnect.png";
const SBannerWrapper = styled.div`
display: flex;
align-items: center;
position: relative;
`;
const SBanner = styled.div`
width: 275px;
height: 45px;
background: url(${logo}) no-repeat;
background-size: cover;
background-position: center;
`;
const Banner = () => (
<SBannerWrapper>
<SBanner />
</SBannerWrapper>
);
export default Banner;

View File

@ -0,0 +1,188 @@
import React, { PropsWithChildren, FC } from "react";
import styled from "styled-components";
import { ChainData } from "caip-api";
import Asset from "./Asset";
import Button from "./Button";
import Column from "./Column";
import Loader from "./Loader";
import { getChainMetadata } from "../chains";
import {
AccountAction,
ellipseAddress,
AccountBalances,
ChainMetadata,
ChainNamespaces,
} from "../helpers";
import { fonts } from "../styles";
import { useWalletConnectClient } from "../contexts/ClientContext";
interface AccountStyleProps {
rgb: string;
}
const SAccount = styled.div<AccountStyleProps>`
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
border-radius: 8px;
padding: 8px;
margin: 5px 0;
border: ${({ rgb }) => `2px solid rgb(${rgb})`};
&.active {
box-shadow: ${({ rgb }) => `0 0 8px rgb(${rgb})`};
}
`;
const SChain = styled.div`
width: 100%;
display: flex;
align-items: center;
& p {
font-weight: 600;
}
& img {
border-radius: 50%;
width: 35px;
height: 35px;
margin-right: 10px;
}
`;
const SContainer = styled.div`
height: 100%;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
word-break: break-word;
`;
const SFullWidthContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
const SAction = styled(Button as any)`
border-radius: 8px;
font-size: ${fonts.size.medium};
height: 44px;
width: 100%;
margin: 12px 0;
background-color: ${({ rgb }) => `rgb(${rgb})`};
`;
const SBlockchainChildrenContainer = styled(SFullWidthContainer)`
flex-direction: column;
`;
interface BlockchainProps {
chainData: ChainNamespaces;
fetching?: boolean;
active?: boolean;
chainId: string;
address?: string;
onClick?: (chain: string) => void;
balances?: any[];
actions?: AccountAction[];
}
interface BlockchainDisplayData {
data: ChainData;
meta: ChainMetadata;
}
function getBlockchainDisplayData(
chainId: string,
chainData: ChainNamespaces,
): BlockchainDisplayData | undefined {
const [namespace, reference] = chainId.split(":");
let meta: ChainMetadata;
try {
meta = getChainMetadata(chainId);
} catch (e) {
return undefined;
}
const data: ChainData = chainData[namespace][reference];
if (typeof data === "undefined") return undefined;
return { data, meta };
}
const Blockchain: FC<PropsWithChildren<BlockchainProps>> = (
props: PropsWithChildren<BlockchainProps>,
) => {
const { chainData, fetching, chainId, address, onClick, balances, active, actions } = props;
if (!Object.keys(chainData).length) return null;
const chain = getBlockchainDisplayData(chainId, chainData);
if (typeof chain === "undefined") {
console.warn("CHAIN UNDEFINED");
return null;
}
const name = chain.meta.name || chain.data.name;
const account = typeof address !== "undefined" ? `${chainId}:${address}` : undefined;
// const assets =
// typeof account !== "undefined" && typeof balances !== "undefined" ? balances[account] : [];
return (
<React.Fragment>
<SAccount
rgb={chain.meta.rgb}
onClick={() => onClick && onClick(props.chainId)}
className={active ? "active" : ""}
>
<SChain>
<img src={chain.meta.logo} alt={name} />
<p>{name}</p>
</SChain>
{!!address && <p>{ellipseAddress(address)}</p>}
<SBlockchainChildrenContainer>
{fetching ? (
<Column center>
<SContainer>
<Loader rgb={`rgb(${chain.meta.rgb})`} />
</SContainer>
</Column>
) : (
<>
{!!balances && balances.length ? (
<SFullWidthContainer>
<h6>Balances</h6>
<Column center>
{balances.map(balance => (
<Asset key={balance.symbol} asset={balance} />
))}
</Column>
</SFullWidthContainer>
) : null}
{!!actions && actions.length ? (
<SFullWidthContainer>
<h6>Methods</h6>
{actions.map(action => (
<SAction
key={action.method}
left
rgb={chain.meta.rgb}
onClick={() => action.callback(chainId)}
>
{action.method}
</SAction>
))}
</SFullWidthContainer>
) : null}
</>
)}
</SBlockchainChildrenContainer>
</SAccount>
</React.Fragment>
);
};
export default Blockchain;

View File

@ -0,0 +1,128 @@
import * as React from "react";
import styled from "styled-components";
import Loader from "./Loader";
import { colors, fonts, shadows, transitions } from "../styles";
interface ButtonStyleProps {
fetching: boolean;
outline: boolean;
type: "button" | "submit" | "reset";
color: string;
disabled: boolean;
icon: any;
left: boolean;
}
interface ButtonProps extends ButtonStyleProps {
children: React.ReactNode;
onClick?: any;
}
const SIcon = styled.div`
position: absolute;
height: 15px;
width: 15px;
margin: 0 8px;
top: calc((100% - 15px) / 2);
`;
const SHoverLayer = styled.div`
transition: ${transitions.button};
position: absolute;
height: 100%;
width: 100%;
background-color: rgb(${colors.white}, 0.1);
top: 0;
bottom: 0;
right: 0;
left: 0;
pointer-events: none;
opacity: 0;
visibility: hidden;
`;
const SButton = styled.button<ButtonStyleProps>`
transition: ${transitions.button};
position: relative;
border: none;
border-style: none;
box-sizing: border-box;
background-color: ${({ outline, color }) => (outline ? "transparent" : `rgb(${colors[color]})`)};
border: ${({ outline, color }) => (outline ? `1px solid rgb(${colors[color]})` : "none")};
color: ${({ outline, color }) => (outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`)};
box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
border-radius: 8px;
font-size: ${fonts.size.medium};
font-weight: ${fonts.weight.semibold};
padding: ${({ icon, left }) =>
icon ? (left ? "7px 12px 8px 28px" : "7px 28px 8px 12px") : "8px 12px"};
cursor: ${({ disabled }) => (disabled ? "auto" : "pointer")};
will-change: transform;
&:disabled {
opacity: 0.6;
box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
}
@media (hover: hover) {
&:hover {
transform: ${({ disabled }) => (!disabled ? "translateY(-1px)" : "none")};
box-shadow: ${({ disabled, outline }) =>
!disabled ? (outline ? "none" : `${shadows.hover}`) : `${shadows.soft}`};
}
&:hover ${SHoverLayer} {
opacity: 1;
visibility: visible;
}
}
&:active {
transform: ${({ disabled }) => (!disabled ? "translateY(1px)" : "none")};
box-shadow: ${({ outline }) => (outline ? "none" : `${shadows.soft}`)};
color: ${({ outline, color }) =>
outline ? `rgb(${colors[color]})` : `rgba(${colors.white}, 0.24)`};
& ${SIcon} {
opacity: 0.8;
}
}
& ${SIcon} {
right: ${({ left }) => (left ? "auto" : "0")};
left: ${({ left }) => (left ? "0" : "auto")};
display: ${({ icon }) => (icon ? "block" : "none")};
mask: ${({ icon }) => (icon ? `url(${icon}) center no-repeat` : "none")};
background-color: ${({ outline, color }) =>
outline ? `rgb(${colors[color]})` : `rgb(${colors.white})`};
transition: 0.15s ease;
}
`;
const Button = (props: ButtonProps) => (
<SButton
{...props}
type={props.type}
outline={props.outline}
color={props.color}
disabled={props.disabled}
icon={props.icon}
left={props.left}
>
<SHoverLayer />
<SIcon />
{props.fetching ? <Loader size={20} color="white" /> : props.children}
</SButton>
);
Button.defaultProps = {
fetching: false,
outline: false,
type: "button",
color: "lightBlue",
disabled: false,
icon: null,
left: false,
};
export default Button;

View File

@ -0,0 +1,49 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled from "styled-components";
interface ColumnStyleProps {
spanHeight: boolean;
maxWidth: number;
center: boolean;
}
interface ColumnProps extends ColumnStyleProps {
children: React.ReactNode;
}
const SColumn = styled.div<ColumnStyleProps>`
position: relative;
width: 100%;
height: ${({ spanHeight }) => (spanHeight ? "100%" : "auto")};
max-width: ${({ maxWidth }) => `${maxWidth}px`};
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: ${({ center }) => (center ? "center" : "flex-start")};
`;
const Column = (props: ColumnProps) => {
const { children, spanHeight, maxWidth, center } = props;
return (
<SColumn {...props} spanHeight={spanHeight} maxWidth={maxWidth} center={center}>
{children}
</SColumn>
);
};
Column.propTypes = {
children: PropTypes.node.isRequired,
spanHeight: PropTypes.bool,
maxWidth: PropTypes.number,
center: PropTypes.bool,
};
Column.defaultProps = {
spanHeight: false,
maxWidth: 600,
center: false,
};
export default Column;

View File

@ -0,0 +1,80 @@
import * as React from "react";
import styled from "styled-components";
import { SessionTypes } from "@walletconnect/types";
import { fonts, responsive } from "../styles";
import Button from "./Button";
const SHeader = styled.div`
margin-top: -1px;
margin-bottom: 1px;
width: 100%;
height: 100px;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
@media screen and (${responsive.sm.max}) {
font-size: ${fonts.size.small};
}
`;
const SHeaderActions = styled.div`
display: flex;
& > button:first-child {
margin-right: 10px !important;
}
`;
const SActiveAccount = styled.div`
display: flex;
align-items: center;
position: relative;
font-weight: 500;
`;
const SActiveSession = styled(SActiveAccount as any)`
flex-direction: column;
text-align: left;
align-items: flex-start;
& p {
font-size: 0.8em;
margin: 0;
padding: 0;
}
& p:nth-child(n + 2) {
font-weight: bold;
}
`;
interface HeaderProps {
ping: () => Promise<void>;
disconnect: () => Promise<void>;
session: SessionTypes.Created | undefined;
}
const Header = (props: HeaderProps) => {
const { ping, disconnect, session } = props;
return (
<SHeader {...props}>
{session ? (
<>
<SActiveSession>
<p>{`Connected to`}</p>
<p>{session.peer.metadata.name}</p>
</SActiveSession>
<SHeaderActions>
<Button outline color="black" onClick={ping}>
{"Ping"}
</Button>
<Button outline color="red" onClick={disconnect}>
{"Disconnect"}
</Button>
</SHeaderActions>
</>
) : null}
</SHeader>
);
};
export default Header;

View File

@ -0,0 +1,42 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled from "styled-components";
interface IconStyleProps {
size: number;
}
const SIcon = styled.img<IconStyleProps>`
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
`;
const Icon = (props: any) => {
const { src, fallback, size } = props;
return (
<SIcon
{...props}
src={src}
size={size}
onError={(event: any) => {
if (fallback) {
event.target.src = fallback;
}
}}
/>
);
};
Icon.propTypes = {
src: PropTypes.string,
fallback: PropTypes.string,
size: PropTypes.number,
};
Icon.defaultProps = {
src: null,
fallback: "",
size: 20,
};
export default Icon;

View File

@ -0,0 +1,68 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled, { keyframes } from "styled-components";
import { colors } from "../styles";
const load = keyframes`
0% {
transform: scale(1.0);
}
5% {
transform: scale(1.0);
}
50% {
transform: scale(0.8);
}
95% {
transform: scale(1.0);
}
100% {
transform: scale(1.0);
}
`;
interface LoaderStyleProps {
size: number;
}
interface LoaderProps extends LoaderStyleProps {
color: string;
rgb?: string;
}
const SLoader = styled.svg<LoaderStyleProps>`
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
animation: ${load} 1s infinite cubic-bezier(0.25, 0, 0.75, 1);
transform: translateZ(0);
`;
const Loader = (props: LoaderProps) => {
const { size, color } = props;
const rgb = props.rgb || `rgb(${colors[color]})`;
return (
<SLoader viewBox="0 0 186 187" size={size}>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<path
d="M60,10.34375 C32.3857625,10.34375 10,32.7295125 10,60.34375 L10,126.34375 C10,153.957987 32.3857625,176.34375 60,176.34375 L126,176.34375 C153.614237,176.34375 176,153.957987 176,126.34375 L176,60.34375 C176,32.7295125 153.614237,10.34375 126,10.34375 L60,10.34375 Z M60,0.34375 L126,0.34375 C159.137085,0.34375 186,27.206665 186,60.34375 L186,126.34375 C186,159.480835 159.137085,186.34375 126,186.34375 L60,186.34375 C26.862915,186.34375 0,159.480835 0,126.34375 L0,60.34375 C0,27.206665 26.862915,0.34375 60,0.34375 Z"
id="Rectangle-Copy"
fill={rgb}
fillRule="nonzero"
/>
<rect id="Rectangle" fill={rgb} x="44" y="44.34375" width="98" height="98" rx="35" />
</g>
</SLoader>
);
};
Loader.propTypes = {
size: PropTypes.number,
color: PropTypes.string,
};
Loader.defaultProps = {
size: 40,
color: "lightBlue",
};
export default Loader;

View File

@ -0,0 +1,141 @@
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { colors, transitions } from "../styles";
const SLightbox = styled.div<{
show: boolean;
offset: number;
opacity?: number;
}>`
transition: opacity 0.1s ease-in-out;
text-align: center;
position: absolute;
width: 100vw;
height: 100%;
margin-left: -50vw;
top: ${({ offset }) => (offset ? `-${offset}px` : 0)};
left: 50%;
z-index: 2;
will-change: opacity;
background-color: ${({ opacity }) => {
let alpha = 0.4;
if (typeof opacity === "number") {
alpha = opacity;
}
return `rgba(0, 0, 0, ${alpha})`;
}};
opacity: ${({ show }) => (show ? 1 : 0)};
visibility: ${({ show }) => (show ? "visible" : "hidden")};
pointer-events: ${({ show }) => (show ? "auto" : "none")};
display: flex;
justify-content: center;
align-items: center;
`;
const SModalContainer = styled.div`
position: relative;
width: 100%;
height: 100%;
padding: 15px;
display: flex;
align-items: center;
justify-content: center;
`;
const SHitbox = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
`;
interface CloseButtonStyleProps {
size: number;
color: string;
onClick?: any;
}
const SCloseButton = styled.div<CloseButtonStyleProps>`
transition: ${transitions.short};
position: absolute;
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
right: ${({ size }) => `${size / 1.6667}px`};
top: ${({ size }) => `${size / 1.6667}px`};
opacity: 0.5;
cursor: pointer;
&:hover {
opacity: 1;
}
&:before,
&:after {
position: absolute;
content: " ";
height: ${({ size }) => `${size}px`};
width: 2px;
background: ${({ color }) => `rgb(${colors[color]})`};
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
`;
const SCard = styled.div`
position: relative;
width: 100%;
max-width: 500px;
padding: 25px;
background-color: rgb(${colors.white});
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const SModalContent = styled.div`
position: relative;
width: 100%;
position: relative;
word-wrap: break-word;
`;
interface IProps {
children: React.ReactNode;
show: boolean;
closeModal: () => void;
opacity?: number;
}
export default function Modal({ children, show, opacity, closeModal }: IProps) {
const [offset, setOffset] = useState(0);
const lightboxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (lightboxRef.current) {
const lightboxRect = lightboxRef.current.getBoundingClientRect();
const nextOffset = lightboxRect.top > 0 ? lightboxRect.top : 0;
if (nextOffset !== 0 && nextOffset !== offset) {
setOffset(nextOffset);
}
}
}, [offset]);
return (
<SLightbox show={show} offset={offset} opacity={opacity} ref={lightboxRef}>
<SModalContainer>
<SHitbox onClick={closeModal} />
<SCard>
<SCloseButton size={25} color={"dark"} onClick={closeModal} />
<SModalContent>{children}</SModalContent>
</SCard>
</SModalContainer>
</SLightbox>
);
}

View File

@ -0,0 +1,35 @@
import * as React from "react";
import styled from "styled-components";
import { PairingTypes } from "@walletconnect/types";
import Peer from "./Peer";
interface PairingProps {
pairing: PairingTypes.Settled;
onClick?: any;
}
const SPairingContainer = styled.div`
width: 100%;
cursor: pointer;
`;
const Pairing = (props: PairingProps) => {
const {
state: { metadata },
} = props.pairing;
return (
<SPairingContainer onClick={props.onClick}>
<div>
{typeof metadata !== "undefined" ? (
<Peer oneLiner metadata={metadata} />
) : (
<div>{`Unknown`}</div>
)}
</div>
</SPairingContainer>
);
};
export default Pairing;

View File

@ -0,0 +1,74 @@
import * as React from "react";
import styled from "styled-components";
import { AppMetadata } from "@walletconnect/types";
import { colors, fonts } from "../styles";
const SPeerOneLiner = styled.div`
display: flex;
align-items: center;
border-radius: 8px;
border: 2px solid rgb(${colors.darkGrey});
padding: 5px;
& img {
width: 40px;
height: 40px;
}
& > div {
margin-left: 10px;
}
`;
const SPeerCard = styled.div`
align-items: center;
justify-content: center;
display: flex;
width: 100%;
flex-direction: column;
border-radius: 8px;
border: 2px solid rgb(${colors.darkGrey});
padding: 5px;
& > div {
margin: 4px auto;
}
`;
const SIcon = styled.img`
width: 100px;
margin: 0 auto;
`;
const SCenter = styled.div`
text-align: center;
`;
const SUrl = styled(SCenter as any)`
font-size: ${fonts.size.small};
opacity: 0.8;
`;
const SName = styled(SCenter as any)`
font-weight: bold;
`;
interface PeerProps {
oneLiner?: boolean;
metadata: AppMetadata;
}
const Peer = (props: PeerProps) =>
props.oneLiner ? (
<SPeerOneLiner>
<img src={props.metadata.icons[0]} alt={props.metadata.name} />
<div>{props.metadata.name}</div>
</SPeerOneLiner>
) : (
<SPeerCard>
<SIcon src={props.metadata.icons[0]} alt={props.metadata.name} />
<SName>{props.metadata.name}</SName>
<SCenter>{props.metadata.description}</SCenter>
<SUrl>{props.metadata.url}</SUrl>
</SPeerCard>
);
export default Peer;

View File

@ -0,0 +1,74 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled from "styled-components";
import { colors, transitions } from "../styles";
interface IToggleStyleProps {
color: string;
active: boolean;
}
const SToggle = styled.div<IToggleStyleProps>`
position: relative;
margin: 0;
padding: 0;
box-sizing: border-box;
transition: ${transitions.base};
& div {
transition: ${transitions.base};
appearance: none;
transition: all 0.3s ease;
box-shadow: ${({ active, color }) =>
active
? `inset 0px 0px 0px 20px rgb(${colors[color]})`
: `inset 0px 0px 0px 1px rgb(${colors.grey})`};
border-radius: 1rem;
background-color: rgb(${colors.white});
padding: 1px;
display: inline-block;
width: 46px;
height: 26px;
position: relative;
cursor: pointer;
margin: 0px;
vertical-align: bottom;
outline: none;
border: none;
}
& div:after {
transition: ${transitions.base};
box-shadow: inset 0 1px 0 rgb(${colors.grey}), 0px 2px 2px 1px rgba(${colors.black}, 0.2);
border-radius: 1rem;
left: ${({ active }) => (active ? `20px` : `0`)};
content: "";
position: absolute;
width: 24px;
height: 24px;
cursor: pointer;
background-color: rgb(${colors.white});
}
`;
interface IToggleProps extends IToggleStyleProps {
onClick?: any;
}
const Toggle = (props: IToggleProps) => (
<SToggle color={props.color} active={props.active} onClick={props.onClick}>
<div />
</SToggle>
);
Toggle.propTypes = {
active: PropTypes.bool,
color: PropTypes.string,
onClick: PropTypes.func,
};
Toggle.defaultProps = {
active: false,
color: "green",
};
export default Toggle;

View File

@ -0,0 +1,50 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled, { keyframes } from "styled-components";
const fadeIn = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
interface WrapperStyleProps {
center: boolean;
}
const SWrapper = styled.div<WrapperStyleProps>`
will-change: transform, opacity;
animation: ${fadeIn} 0.7s ease 0s normal 1;
min-height: 200px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: ${({ center }) => (center ? `center` : `flex-start`)};
`;
interface WrapperProps extends WrapperStyleProps {
children: React.ReactNode;
}
const Wrapper = (props: WrapperProps) => {
const { children, center } = props;
return (
<SWrapper {...props} center={center}>
{children}
</SWrapper>
);
};
Wrapper.propTypes = {
children: PropTypes.node.isRequired,
center: PropTypes.bool,
};
Wrapper.defaultProps = {
center: false,
};
export default Wrapper;

View File

@ -0,0 +1,73 @@
import styled from "styled-components";
import { fonts } from "../../styles";
import Button from "../Button";
import Column from "../Column";
import Wrapper from "../Wrapper";
export const SLayout = styled.div`
position: relative;
width: 100%;
min-height: 100vh;
text-align: center;
`;
export const SContent = styled(Wrapper as any)`
width: 100%;
height: 100%;
padding: 0 16px;
`;
export const SLanding = styled(Column as any)`
/* height: 600px; */
`;
export const SButtonContainer = styled(Column as any)`
width: 250px;
margin: 50px 0;
`;
export const SConnectButton = styled(Button as any)`
border-radius: 8px;
font-size: ${fonts.size.medium};
height: 44px;
width: 100%;
margin: 12px 0;
`;
export const SAccountsContainer = styled(SLanding as any)`
height: 100%;
padding-bottom: 30px;
& h3 {
padding-top: 30px;
}
`;
export const SToggleContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 10px auto;
& > p {
margin-right: 10px;
}
`;
export const SFullWidthContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
export const SAccounts = styled(SFullWidthContainer)`
justify-content: space-between;
& > div {
margin: 12px 0;
flex: 1 0 100%;
@media (min-width: 648px) {
flex: 0 1 48%;
}
}
`;

View File

@ -0,0 +1,32 @@
import styled from "styled-components";
export const SContainer = styled.div`
height: 100%;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
word-break: break-word;
`;
export const STable = styled(SContainer as any)`
flex-direction: column;
text-align: left;
`;
export const SRow = styled.div`
width: 100%;
display: flex;
margin: 6px 0;
`;
export const SKey = styled.div`
width: 30%;
font-weight: 700;
`;
export const SValue = styled.div`
width: 70%;
font-family: monospace;
`;

View File

@ -0,0 +1,38 @@
export const DEFAULT_MAIN_CHAINS = [
// mainnets
"eip155:1",
"eip155:10",
"eip155:100",
"eip155:137",
"eip155:42161",
"eip155:42220",
"cosmos:cosmoshub-4",
];
export const DEFAULT_TEST_CHAINS = [
// testnets
"eip155:42",
"eip155:69",
"eip155:80001",
"eip155:421611",
"eip155:44787",
];
export const DEFAULT_CHAINS = [...DEFAULT_MAIN_CHAINS, ...DEFAULT_TEST_CHAINS];
export const DEFAULT_PROJECT_ID = process.env.REACT_APP_PROJECT_ID;
export const DEFAULT_RELAY_URL = process.env.REACT_APP_RELAY_URL;
export const DEFAULT_EIP155_METHODS = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"];
export const DEFAULT_COSMOS_METHODS = ["cosmos_signDirect", "cosmos_signAmino"];
export const DEFAULT_LOGGER = "debug";
export const DEFAULT_APP_METADATA = {
name: "React App",
description: "React App for WalletConnect",
url: "https://walletconnect.com/",
icons: ["https://avatars.githubusercontent.com/u/37784886"],
};

View File

@ -0,0 +1,2 @@
export * from "./default";
export * from "./logo";

View File

@ -0,0 +1 @@
export const BLOCKCHAIN_LOGO_BASE_URL = "https://blockchain-api.xyz/logos/";

View File

@ -0,0 +1,309 @@
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import QRCodeModal from "@walletconnect/legacy-modal";
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
DEFAULT_APP_METADATA,
DEFAULT_COSMOS_METHODS,
DEFAULT_EIP155_METHODS,
DEFAULT_LOGGER,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
} from "../constants";
import { ERROR } from "@walletconnect/utils";
import EthereumProvider from "@walletconnect/ethereum-provider";
import { providers, utils } from "ethers";
/**
* Types
*/
interface IContext {
client: Client | undefined;
session: SessionTypes.Created | undefined;
disconnect: () => Promise<void>;
isInitializing: boolean;
chains: string[];
pairings: string[];
accounts: string[];
balances: { symbol: string; balance: string }[];
isFetchingBalances: boolean;
setChains: any;
onEnable: () => Promise<void>;
web3Provider?: providers.Web3Provider;
}
/**
* Context
*/
export const ClientContext = createContext<IContext>({} as IContext);
/**
* Provider
*/
export function ClientContextProvider({ children }: { children: ReactNode | ReactNode[] }) {
const [client, setClient] = useState<Client>();
const [pairings, setPairings] = useState<string[]>([]);
const [session, setSession] = useState<SessionTypes.Created>();
const [ethereumProvider, setEthereumProvider] = useState<EthereumProvider>();
const [web3Provider, setWeb3Provider] = useState<providers.Web3Provider>();
const [isFetchingBalances, setIsFetchingBalances] = useState(false);
const [isInitializing, setIsInitializing] = useState(false);
const [balances, setBalances] = useState<{ symbol: string; balance: string }[]>([]);
const [accounts, setAccounts] = useState<string[]>([]);
const [chains, setChains] = useState<string[]>([]);
const resetApp = () => {
setPairings([]);
setSession(undefined);
setBalances([]);
setAccounts([]);
setChains([]);
};
const getSupportedNamespaces = useCallback(() => {
const supportedNamespaces: string[] = [];
chains.forEach(chainId => {
const [namespace] = chainId.split(":");
if (!supportedNamespaces.includes(namespace)) {
supportedNamespaces.push(namespace);
}
});
return supportedNamespaces;
}, [chains]);
const getSupportedMethods = (namespaces: string[]) => {
const supportedMethods: string[] = namespaces
.map(namespace => {
switch (namespace) {
case "eip155":
return DEFAULT_EIP155_METHODS;
case "cosmos":
return DEFAULT_COSMOS_METHODS;
default:
throw new Error(`No default methods for namespace: ${namespace}`);
}
})
.flat();
return supportedMethods;
};
const disconnect = useCallback(async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
await client.disconnect({
topic: session.topic,
reason: ERROR.USER_DISCONNECTED.format(),
});
}, [client, session]);
const _subscribeToEvents = useCallback(async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
let _session = {} as SessionTypes.Settled;
if (_client.session.topics.length) {
_session = await _client.session.get(_client.session.topics[0]);
}
_client.on(CLIENT_EVENTS.pairing.proposal, async (proposal: PairingTypes.Proposal) => {
const { uri } = proposal.signal.params;
console.log("EVENT", "QR Code Modal open");
QRCodeModal.open(uri, () => {
console.log("EVENT", "QR Code Modal closed");
});
});
_client.on(CLIENT_EVENTS.pairing.created, async () => {
debugger;
setPairings(_client.pairing.topics);
});
_client.on(CLIENT_EVENTS.session.deleted, (deletedSession: SessionTypes.Settled) => {
if (deletedSession.topic !== _session?.topic) return;
console.log("EVENT", "session_deleted");
resetApp();
});
}, []);
// const _checkPersistedState = useCallback(
// async (_client: Client) => {
// if (typeof _client === "undefined") {
// throw new Error("WalletConnect is not initialized");
// }
// // populates existing pairings to state
// setPairings(_client.pairing.topics);
// if (typeof session !== "undefined") return;
// // populates existing session to state (assume only the top one)
// if (_client.session.topics.length) {
// const _session = await _client.session.get(_client.session.topics[0]);
// onSessionConnected(_session);
// }
// },
// [session, onSessionConnected],
// );
const createClient = useCallback(async () => {
try {
setIsInitializing(true);
const _client = await Client.init({
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
projectId: DEFAULT_PROJECT_ID,
});
setClient(_client);
await _subscribeToEvents(_client);
// await _checkPersistedState(_client);
} catch (err) {
throw err;
} finally {
setIsInitializing(false);
}
}, [_subscribeToEvents]);
useEffect(() => {
if (!client) {
createClient();
}
}, [client, createClient]);
useEffect(() => {
const init = async () => {
if (!client) return;
// Create WalletConnect Provider
const ethereumProvider = new EthereumProvider({
chainId: 42,
rpc: {
infuraId: "5dc0df7abe4645dfb06a9a8c39ede422",
},
// FIXME: `signer-connection` sub-dep is already specifying beta.23 -> typings mismatch.
// @ts-ignore
client,
});
const web3Provider = new providers.Web3Provider(ethereumProvider);
setEthereumProvider(ethereumProvider);
setWeb3Provider(web3Provider);
// Enable session (triggers QR Code modal if we bound the listener on `client` for it).
// const accounts = await ethereumProvider.enable();
// console.log("accounts:", accounts);
// const provider = new providers.Web3Provider(ethereumProvider);
// console.log(provider);
// console.log(await provider.listAccounts());
// console.log(await provider.getNetwork());
// console.log(provider.getSigner());
// console.log(await provider.getBalance(accounts[0]));
// const TEST_ETH_TRANSFER = {
// from: accounts[0],
// to: accounts[0],
// value: utils.parseEther("1").toHexString(),
// data: "0x",
// };
// const signer = provider.getSigner();
// const transferTx = await signer.sendTransaction(TEST_ETH_TRANSFER);
};
init();
}, [client]);
const onEnable = useCallback(async () => {
if (!ethereumProvider) {
throw new ReferenceError("ethereumProvider is not defined.");
}
if (!web3Provider) {
throw new ReferenceError("web3Provider is not defined.");
}
const accounts = await ethereumProvider.enable();
setAccounts(accounts);
setChains(["eip155:42"]);
const balances = await Promise.all(
accounts.map(async account => {
const balance = await web3Provider.getBalance(account);
return { symbol: "ETH", balance: utils.formatEther(balance) };
}),
);
setBalances(balances);
QRCodeModal.close();
}, [ethereumProvider, web3Provider]);
console.log(balances);
const value = useMemo(
() => ({
pairings,
isInitializing,
balances,
isFetchingBalances,
accounts,
chains,
client,
session,
disconnect,
setChains,
onEnable,
web3Provider,
}),
[
pairings,
isInitializing,
balances,
isFetchingBalances,
accounts,
chains,
client,
session,
disconnect,
setChains,
onEnable,
web3Provider,
],
);
return (
<ClientContext.Provider
value={{
...value,
}}
>
{children}
</ClientContext.Provider>
);
}
export function useWalletConnectClient() {
const context = useContext(ClientContext);
if (context === undefined) {
throw new Error("useWalletConnectClient must be used within a ClientContextProvider");
}
return context;
}

View File

@ -0,0 +1,415 @@
import { BigNumber } from "ethers";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
import * as encoding from "@walletconnect/encoding";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import {
ChainNamespaces,
eip712,
formatTestTransaction,
getAllChainNamespaces,
hashPersonalMessage,
hashTypedDataMessage,
verifySignature,
} from "../helpers";
import { useWalletConnectClient } from "./ClientContext";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
/**
* Types
*/
interface IFormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
interface IRpcResult {
method: string;
valid: boolean;
}
interface IContext {
ping: () => Promise<void>;
ethereumRpc: {
testSendTransaction: (chainId: string) => Promise<void>;
testSignPersonalMessage: (chainId: string) => Promise<void>;
testSignTypedData: (chainId: string) => Promise<void>;
};
cosmosRpc: {
testSignDirect: (chainId: string) => Promise<void>;
testSignAmino: (chainId: string) => Promise<void>;
};
chainData: ChainNamespaces;
rpcResult?: IRpcResult | null;
isRpcRequestPending: boolean;
}
/**
* Context
*/
export const JsonRpcContext = createContext<IContext>({} as IContext);
/**
* Provider
*/
export function JsonRpcContextProvider({ children }: { children: ReactNode | ReactNode[] }) {
const [pending, setPending] = useState(false);
const [result, setResult] = useState<IRpcResult | null>();
const [chainData, setChainData] = useState<ChainNamespaces>({});
const { client, session, accounts, balances } = useWalletConnectClient();
useEffect(() => {
loadChainData();
}, []);
const loadChainData = async () => {
const namespaces = getAllChainNamespaces();
const chainData: ChainNamespaces = {};
await Promise.all(
namespaces.map(async namespace => {
let chains: ChainsMap | undefined;
try {
chains = await apiGetChainNamespace(namespace);
} catch (e) {
// ignore error
}
if (typeof chains !== "undefined") {
chainData[namespace] = chains;
}
}),
);
setChainData(chainData);
};
const getAddressByChainId = (chainId: string) => {
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error(`Account for chainId ${chainId} not found.`);
const address = account.split(":").pop();
if (address === undefined) throw new Error(`Address for account ${account} is invalid`);
return address;
};
const _createJsonRpcRequestHandler =
(rpcRequest: (...requestArgs: [any]) => Promise<IFormattedRpcResponse>) =>
async (chainId: string) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
const result = await rpcRequest(chainId);
setResult(result);
} catch (err) {
console.error(err);
setResult(null);
} finally {
setPending(false);
}
};
const ping = async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
try {
setPending(true);
let valid = false;
try {
await client.session.ping(session.topic);
valid = true;
} catch (e) {
valid = false;
}
// display result
setResult({
method: "ping",
valid,
});
} catch (e) {
console.error(e);
setResult(null);
} finally {
setPending(false);
}
};
// -------- ETHEREUM/EIP155 RPC METHODS --------
const ethereumRpc = {
testSendTransaction: _createJsonRpcRequestHandler(async (chainId: string) => {
// get ethereum address
const account = accounts.find(account => account.startsWith(chainId));
if (account === undefined) throw new Error("Account is not found");
const address = account.split(":").pop();
if (address === undefined) throw new Error("Address is invalid");
const tx = await formatTestTransaction(account);
const balance = BigNumber.from(balances[0].balance || "0");
if (balance.lt(BigNumber.from(tx.gasPrice).mul(tx.gasLimit))) {
return {
method: "eth_sendTransaction",
address,
valid: false,
result: "Insufficient funds for intrinsic transaction cost",
};
}
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_sendTransaction",
params: [tx],
},
});
// format displayed result
return {
method: "eth_sendTransaction",
address,
valid: true,
result,
};
}),
testSignPersonalMessage: _createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = `My email is john@doe.com - ${Date.now()}`;
// encode message (hex)
const hexMsg = encoding.utf8ToHex(message, true);
const address = getAddressByChainId(chainId);
// personal_sign params
const params = [hexMsg, address];
// send message
const result: string = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "personal_sign",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashPersonalMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "personal_sign",
address,
valid,
result,
};
}),
testSignTypedData: _createJsonRpcRequestHandler(async (chainId: string) => {
// test message
const message = JSON.stringify(eip712.example);
const address = getAddressByChainId(chainId);
// eth_signTypedData params
const params = [address, message];
// send message
const result = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "eth_signTypedData",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = targetChainData.rpc[0];
// verify signature
const hash = hashTypedDataMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
return {
method: "eth_signTypedData",
address,
valid,
result,
};
}),
};
// -------- COSMOS RPC METHODS --------
const cosmosRpc = {
testSignDirect: _createJsonRpcRequestHandler(async (chainId: string) => {
// test direct sign doc inputs
const inputs = {
fee: [{ amount: "2000", denom: "ucosm" }],
pubkey: "AgSEjOuOr991QlHCORRmdE5ahVKeyBrmtgoYepCpQGOW",
gasLimit: 200000,
accountNumber: 1,
sequence: 1,
bodyBytes:
"0a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d120731323334353637",
authInfoBytes:
"0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c",
};
// split chainId
const [namespace, reference] = chainId.split(":");
// format sign doc
const signDoc = formatDirectSignDoc(
inputs.fee,
inputs.pubkey,
inputs.gasLimit,
inputs.accountNumber,
inputs.sequence,
inputs.bodyBytes,
reference,
);
const address = getAddressByChainId(chainId);
// cosmos_signDirect params
const params = {
signerAddress: address,
signDoc: stringifySignDocValues(signDoc),
};
// send message
const result = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "cosmos_signDirect",
params,
},
});
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
return {
method: "cosmos_signDirect",
address,
valid,
result: result.signature.signature,
};
}),
testSignAmino: _createJsonRpcRequestHandler(async (chainId: string) => {
// split chainId
const [namespace, reference] = chainId.split(":");
// test amino sign doc
const signDoc = {
msgs: [],
fee: { amount: [], gas: "23" },
chain_id: "foochain",
memo: "hello, world",
account_number: "7",
sequence: "54",
};
const address = getAddressByChainId(chainId);
// cosmos_signAmino params
const params = { signerAddress: address, signDoc };
// send message
const result = await client!.request({
topic: session!.topic,
chainId,
request: {
method: "cosmos_signAmino",
params,
},
});
const targetChainData = chainData[namespace][reference];
if (typeof targetChainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
return {
method: "cosmos_signAmino",
address,
valid,
result: result.signature.signature,
};
}),
};
return (
<JsonRpcContext.Provider
value={{
chainData,
ping,
ethereumRpc,
cosmosRpc,
rpcResult: result,
isRpcRequestPending: pending,
}}
>
{children}
</JsonRpcContext.Provider>
);
}
export function useJsonRpc() {
const context = useContext(JsonRpcContext);
if (context === undefined) {
throw new Error("useJsonRpc must be used within a JsonRpcContextProvider");
}
return context;
}

View File

@ -0,0 +1,113 @@
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import QRCodeModal from "@walletconnect/legacy-modal";
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
DEFAULT_APP_METADATA,
DEFAULT_COSMOS_METHODS,
DEFAULT_EIP155_METHODS,
DEFAULT_LOGGER,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
} from "../constants";
import { AccountBalances, apiGetAccountAssets } from "../helpers";
import { ERROR, getAppMetadata } from "@walletconnect/utils";
import EthereumProvider from "@walletconnect/ethereum-provider";
import { providers } from "ethers";
import { useWalletConnectClient } from "./ClientContext";
/**
* Types
*/
type IContext = any;
/**
* Context
*/
export const Web3ProviderContext = createContext<IContext>({} as IContext);
/**
* Provider
*/
export function Web3ContextProvider({ children }: { children: ReactNode | ReactNode[] }) {
const [ethereumProvider, setEthereumProvider] = useState<EthereumProvider>();
const [web3Provider, setWeb3Provider] = useState<providers.Web3Provider>();
const { client } = useWalletConnectClient();
useEffect(() => {
const init = async () => {
if (!client) return;
// Create WalletConnect Provider
const ethereumProvider = new EthereumProvider({
chainId: 42,
rpc: {
infuraId: "5dc0df7abe4645dfb06a9a8c39ede422",
},
// FIXME: `signer-connection` sub-dep is already specifying beta.23 -> typings mismatch.
// @ts-ignore
client,
});
const web3Provider = new providers.Web3Provider(ethereumProvider);
setEthereumProvider(ethereumProvider);
setWeb3Provider(web3Provider);
// Enable session (triggers QR Code modal if we bound the listener on `client` for it).
// const accounts = await ethereumProvider.enable();
// console.log("accounts:", accounts);
// const provider = new providers.Web3Provider(ethereumProvider);
// console.log(provider);
// console.log(await provider.listAccounts());
// console.log(await provider.getNetwork());
// console.log(provider.getSigner());
// console.log(await provider.getBalance(accounts[0]));
// const TEST_ETH_TRANSFER = {
// from: accounts[0],
// to: accounts[0],
// value: utils.parseEther("1").toHexString(),
// data: "0x",
// };
// const signer = provider.getSigner();
// const transferTx = await signer.sendTransaction(TEST_ETH_TRANSFER);
// console.log(transferTx);
// const signer = provider.getSigner();
// const msg = "Hello world";
// const signature = await signer.signMessage(msg);
// console.log("signature:", signature);
};
init();
}, [client]);
const onEnable = async () => {
await ethereumProvider?.enable();
};
return (
<Web3ProviderContext.Provider value={{ onEnable }}>{children}</Web3ProviderContext.Provider>
);
}
export function useWeb3Provider() {
const context = useContext(Web3ProviderContext);
if (context === undefined) {
throw new Error("useWeb3Provider must be used within a Web3ContextProvider");
}
return context;
}

View File

@ -0,0 +1,45 @@
import axios, { AxiosInstance } from "axios";
import { AssetData, GasPrices, ParsedTx } from "./types";
const ethereumApi: AxiosInstance = axios.create({
baseURL: "https://ethereum-api.xyz",
timeout: 30000, // 30 secs
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
export async function apiGetAccountAssets(address: string, chainId: string): Promise<AssetData[]> {
const ethChainId = chainId.split(":")[1];
const response = await ethereumApi.get(
`/account-assets?address=${address}&chainId=${ethChainId}`,
);
const { result } = response.data;
return result;
}
export async function apiGetAccountTransactions(
address: string,
chainId: string,
): Promise<ParsedTx[]> {
const ethChainId = chainId.split(":")[1];
const response = await ethereumApi.get(
`/account-transactions?address=${address}&chainId=${ethChainId}`,
);
const { result } = response.data;
return result;
}
export const apiGetAccountNonce = async (address: string, chainId: string): Promise<number> => {
const ethChainId = chainId.split(":")[1];
const response = await ethereumApi.get(`/account-nonce?address=${address}&chainId=${ethChainId}`);
const { result } = response.data;
return result;
};
export const apiGetGasPrices = async (): Promise<GasPrices> => {
const response = await ethereumApi.get(`/gas-prices`);
const { result } = response.data;
return result;
};

View File

@ -0,0 +1,55 @@
import { Contract, providers, utils } from "ethers";
const spec = {
magicValue: "0x1626ba7e",
abi: [
{
constant: true,
inputs: [
{
name: "_hash",
type: "bytes32",
},
{
name: "_sig",
type: "bytes",
},
],
name: "isValidSignature",
outputs: [
{
name: "magicValue",
type: "bytes4",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
],
};
async function isValidSignature(
address: string,
sig: string,
data: string,
provider: providers.Provider,
abi = eip1271.spec.abi,
magicValue = eip1271.spec.magicValue,
): Promise<boolean> {
let returnValue;
try {
returnValue = await new Contract(address, abi, provider).isValidSignature(
utils.arrayify(data),
sig,
);
} catch (e) {
return false;
}
return returnValue.toLowerCase() === magicValue.toLowerCase();
}
export const eip1271 = {
spec,
isValidSignature,
};

View File

@ -0,0 +1,50 @@
const example = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "verifyingContract", type: "address" },
],
RelayRequest: [
{ name: "target", type: "address" },
{ name: "encodedFunction", type: "bytes" },
{ name: "gasData", type: "GasData" },
{ name: "relayData", type: "RelayData" },
],
GasData: [
{ name: "gasLimit", type: "uint256" },
{ name: "gasPrice", type: "uint256" },
{ name: "pctRelayFee", type: "uint256" },
{ name: "baseRelayFee", type: "uint256" },
],
RelayData: [
{ name: "senderAddress", type: "address" },
{ name: "senderNonce", type: "uint256" },
{ name: "relayWorker", type: "address" },
{ name: "paymaster", type: "address" },
],
},
domain: {
name: "GSN Relayed Transaction",
version: "1",
chainId: 42,
verifyingContract: "0x6453D37248Ab2C16eBd1A8f782a2CBC65860E60B",
},
primaryType: "RelayRequest",
message: {
target: "0x9cf40ef3d1622efe270fe6fe720585b4be4eeeff",
encodedFunction:
"0xa9059cbb0000000000000000000000002e0d94754b348d208d64d52d78bcd443afa9fa520000000000000000000000000000000000000000000000000000000000000007",
gasData: { gasLimit: "39507", gasPrice: "1700000000", pctRelayFee: "70", baseRelayFee: "0" },
relayData: {
senderAddress: "0x22d491bde2303f2f43325b2108d26f1eaba1e32b",
senderNonce: "3",
relayWorker: "0x3baee457ad824c94bd3953183d725847d023a2cf",
paymaster: "0x957F270d45e9Ceca5c5af2b49f1b5dC1Abb0421c",
},
},
};
export const eip712 = {
example,
};

View File

@ -0,0 +1,6 @@
export * from "./api";
export * from "./eip712";
export * from "./eip1271";
export * from "./tx";
export * from "./types";
export * from "./utilities";

View File

@ -0,0 +1,35 @@
import * as encoding from "@walletconnect/encoding";
import { apiGetAccountNonce, apiGetGasPrices } from "./api";
import { toWad } from "./utilities";
export async function getGasPrice(chainId: string): Promise<string> {
if (chainId === "eip155:1") return toWad("20", 9).toHexString();
const gasPrices = await apiGetGasPrices();
return toWad(`${gasPrices.slow.price}`, 9).toHexString();
}
export async function formatTestTransaction(account: string) {
const [namespace, reference, address] = account.split(":");
const chainId = `${namespace}:${reference}`;
// nonce
const _nonce = await apiGetAccountNonce(address, chainId);
const nonce = encoding.sanitizeHex(encoding.numberToHex(_nonce));
// gasPrice
const _gasPrice = await getGasPrice(chainId);
const gasPrice = encoding.sanitizeHex(_gasPrice);
// gasLimit
const _gasLimit = 21000;
const gasLimit = encoding.sanitizeHex(encoding.numberToHex(_gasLimit));
// value
const _value = 0;
const value = encoding.sanitizeHex(encoding.numberToHex(_value));
const tx = { from: address, to: address, data: "0x", nonce, gasPrice, gasLimit, value };
return tx;
}

View File

@ -0,0 +1,159 @@
import { ChainsMap } from "caip-api";
export interface AssetData {
symbol: string;
name: string;
decimals: string;
contractAddress: string;
balance?: string;
}
export interface ChainData {
name: string;
short_name: string;
chain: string;
network: string;
chain_id: number;
network_id: number;
rpc_url: string;
native_currency: AssetData;
}
export interface TxData {
from: string;
to: string;
nonce: string;
gasPrice: string;
gasLimit: string;
value: string;
data: string;
}
export interface BlockScoutTx {
value: string;
txreceipt_status: string;
transactionIndex: string;
to: string;
timeStamp: string;
nonce: string;
isError: string;
input: string;
hash: string;
gasUsed: string;
gasPrice: string;
gas: string;
from: string;
cumulativeGasUsed: string;
contractAddress: string;
confirmations: string;
blockNumber: string;
blockHash: string;
}
export interface BlockScoutTokenTx {
value: string;
transactionIndex: string;
tokenSymbol: string;
tokenName: string;
tokenDecimal: string;
to: string;
timeStamp: string;
nonce: string;
input: string;
hash: string;
gasUsed: string;
gasPrice: string;
gas: string;
from: string;
cumulativeGasUsed: string;
contractAddress: string;
confirmations: string;
blockNumber: string;
blockHash: string;
}
export interface ParsedTx {
timestamp: string;
hash: string;
from: string;
to: string;
nonce: string;
gasPrice: string;
gasUsed: string;
fee: string;
value: string;
input: string;
error: boolean;
asset: AssetData;
operations: TxOperation[];
}
export interface TxOperation {
asset: AssetData;
value: string;
from: string;
to: string;
functionName: string;
}
export interface GasPricesResponse {
fastWait: number;
avgWait: number;
blockNum: number;
fast: number;
fastest: number;
fastestWait: number;
safeLow: number;
safeLowWait: number;
speed: number;
block_time: number;
average: number;
}
export interface GasPrice {
time: number;
price: number;
}
export interface GasPrices {
timestamp: number;
slow: GasPrice;
average: GasPrice;
fast: GasPrice;
}
export interface MethodArgument {
type: string;
}
export interface Method {
signature: string;
name: string;
args: MethodArgument[];
}
export interface ChainRequestRender {
label: string;
value: string;
}
export interface ChainMetadata {
name?: string;
logo: string;
rgb: string;
}
export interface NamespaceMetadata {
[reference: string]: ChainMetadata;
}
export interface ChainNamespaces {
[namespace: string]: ChainsMap;
}
export interface AccountAction {
method: string;
callback: (chainId: string) => Promise<void>;
}
export interface AccountBalances {
[account: string]: AssetData[];
}

View File

@ -0,0 +1,212 @@
import { BigNumber, BigNumberish, providers, utils } from "ethers";
import * as encoding from "@walletconnect/encoding";
import { TypedDataUtils } from "eth-sig-util";
import * as ethUtil from "ethereumjs-util";
import { eip1271 } from "./eip1271";
import { DEFAULT_CHAINS } from "../constants";
export function capitalize(string: string): string {
return string
.split(" ")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}
export function ellipseText(text = "", maxLength = 9999): string {
if (text.length <= maxLength) {
return text;
}
const _maxLength = maxLength - 3;
let ellipse = false;
let currentLength = 0;
const result =
text
.split(" ")
.filter(word => {
currentLength += word.length;
if (ellipse || currentLength >= _maxLength) {
ellipse = true;
return false;
} else {
return true;
}
})
.join(" ") + "...";
return result;
}
export function ellipseAddress(address = "", width = 10): string {
return `${address.slice(0, width)}...${address.slice(-width)}`;
}
export function getDataString(func: string, arrVals: any[]): string {
let val = "";
for (let i = 0; i < arrVals.length; i++) {
val += encoding.padLeft(arrVals[i], 64);
}
const data = func + val;
return data;
}
export function isMobile(): boolean {
let mobile = false;
function hasTouchEvent(): boolean {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
}
function hasMobileUserAgent(): boolean {
if (
/(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(
navigator.userAgent,
) ||
/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(
navigator.userAgent.substr(0, 4),
)
) {
return true;
} else if (hasTouchEvent()) {
return true;
}
return false;
}
mobile = hasMobileUserAgent();
return mobile;
}
export function encodePersonalMessage(msg: string): string {
const data = encoding.utf8ToBuffer(msg);
const buf = Buffer.concat([
Buffer.from("\u0019Ethereum Signed Message:\n" + data.length.toString(), "utf8"),
data,
]);
return ethUtil.bufferToHex(buf);
}
export function hashPersonalMessage(msg: string): string {
const data = encodePersonalMessage(msg);
const buf = ethUtil.toBuffer(data);
const hash = ethUtil.keccak256(buf);
return ethUtil.bufferToHex(hash);
}
export function encodeTypedDataMessage(msg: string): string {
const data = TypedDataUtils.sanitizeData(JSON.parse(msg));
const buf = Buffer.concat([
Buffer.from("1901", "hex"),
TypedDataUtils.hashStruct("EIP712Domain", data.domain, data.types),
TypedDataUtils.hashStruct(data.primaryType as string, data.message, data.types),
]);
return ethUtil.bufferToHex(buf);
}
export function hashTypedDataMessage(msg: string): string {
const data = encodeTypedDataMessage(msg);
const buf = ethUtil.toBuffer(data);
const hash = ethUtil.keccak256(buf);
return ethUtil.bufferToHex(hash);
}
export function recoverAddress(sig: string, hash: string): string {
const params = ethUtil.fromRpcSig(sig);
const result = ethUtil.ecrecover(ethUtil.toBuffer(hash), params.v, params.r, params.s);
const signer = ethUtil.bufferToHex(ethUtil.publicToAddress(result));
return signer;
}
export function recoverPersonalSignature(sig: string, msg: string): string {
const hash = hashPersonalMessage(msg);
const signer = recoverAddress(sig, hash);
return signer;
}
export function recoverTypedMessage(sig: string, msg: string): string {
const hash = hashTypedDataMessage(msg);
const signer = recoverAddress(sig, hash);
return signer;
}
export async function verifySignature(
address: string,
sig: string,
hash: string,
rpcUrl: string,
): Promise<boolean> {
const provider = new providers.JsonRpcProvider(rpcUrl);
const bytecode = await provider.getCode(address);
if (!bytecode || bytecode === "0x" || bytecode === "0x0" || bytecode === "0x00") {
const signer = recoverAddress(sig, hash);
return signer.toLowerCase() === address.toLowerCase();
} else {
return eip1271.isValidSignature(address, sig, hash, provider);
}
}
export function convertHexToNumber(hex: string) {
try {
return encoding.hexToNumber(hex);
} catch (e) {
return hex;
}
}
export function convertHexToUtf8(hex: string) {
try {
return encoding.hexToUtf8(hex);
} catch (e) {
return hex;
}
}
export const sanitizeDecimals = (value: string, decimals = 18): string => {
const [integer, fractional] = value.split(".");
const _fractional = fractional
? fractional.substring(0, decimals).replace(/0+$/gi, "")
: undefined;
return _fractional ? [integer, _fractional].join(".") : integer;
};
export const toWad = (amount: string, decimals = 18): BigNumber => {
return utils.parseUnits(sanitizeDecimals(amount, decimals), decimals);
};
export const fromWad = (wad: BigNumberish, decimals = 18): string => {
return sanitizeDecimals(utils.formatUnits(wad, decimals), decimals);
};
export const LOCALSTORAGE_KEY_TESTNET = "TESTNET";
export const INITIAL_STATE_TESTNET_DEFAULT = true;
export function setLocaleStorageTestnetFlag(value: boolean): void {
window.localStorage.setItem(LOCALSTORAGE_KEY_TESTNET, `${value}`);
}
export function getLocalStorageTestnetFlag(): boolean {
let value = INITIAL_STATE_TESTNET_DEFAULT;
const persisted = window.localStorage.getItem(LOCALSTORAGE_KEY_TESTNET);
if (!persisted) {
setLocaleStorageTestnetFlag(value);
} else {
value = persisted === "true" ? true : false;
}
return value;
}
export const getAllChainNamespaces = () => {
const namespaces: string[] = [];
DEFAULT_CHAINS.forEach(chainId => {
const [namespace] = chainId.split(":");
if (!namespaces.includes(namespace)) {
namespaces.push(namespace);
}
});
return namespaces;
};

View File

@ -0,0 +1,30 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components";
import { ClientContextProvider } from "./contexts/ClientContext";
import { JsonRpcContextProvider } from "./contexts/JsonRpcContext";
import App from "./App";
import { globalStyle } from "./styles";
const GlobalStyle = createGlobalStyle`
${globalStyle}
`;
declare global {
// tslint:disable-next-line
interface Window {
blockies: any;
}
}
ReactDOM.render(
<>
<GlobalStyle />
<ClientContextProvider>
<JsonRpcContextProvider>
<App />
</JsonRpcContextProvider>
</ClientContextProvider>
</>,
document.getElementById("root"),
);

View File

@ -0,0 +1,35 @@
import * as React from "react";
import { PairingTypes } from "@walletconnect/types";
import Button from "../components/Button";
import Pairing from "../components/Pairing";
import { STable } from "../components/shared";
import { SModalContainer, SModalTitle } from "./shared";
interface PairingModalProps {
pairings: PairingTypes.Settled[];
connect: (pairing?: { topic: string }) => Promise<void>;
}
const PairingModal = (props: PairingModalProps) => {
const { pairings, connect } = props;
return (
<SModalContainer>
<SModalTitle>{"Select available pairing or create new one"}</SModalTitle>
<STable>
{pairings.map(pairing => (
<Pairing
key={pairing.topic}
pairing={pairing}
onClick={() => connect({ topic: pairing.topic })}
/>
))}
</STable>
<Button onClick={() => connect()}>{`New Pairing`}</Button>
</SModalContainer>
);
};
export default PairingModal;

View File

@ -0,0 +1,39 @@
import * as React from "react";
import Loader from "../components/Loader";
import { SContainer } from "../components/shared";
import { SModalContainer, SModalTitle } from "./shared";
interface PingModalProps {
pending: boolean;
result: any;
}
const PingModal = (props: PingModalProps) => {
const { pending, result } = props;
return (
<>
{pending ? (
<SModalContainer>
<SModalTitle>{"Pending Session Ping"}</SModalTitle>
<SContainer>
<Loader />
</SContainer>
</SModalContainer>
) : result ? (
<SModalContainer>
<SModalTitle>
{result.valid ? "Successful Session Ping" : "Failed Session Ping"}
</SModalTitle>
</SModalContainer>
) : (
<SModalContainer>
<SModalTitle>{"Unknown Error with Session Ping"}</SModalTitle>
</SModalContainer>
)}
</>
);
};
export default PingModal;

View File

@ -0,0 +1,48 @@
import * as React from "react";
import Loader from "../components/Loader";
import { SContainer, STable, SRow, SKey, SValue } from "../components/shared";
import { SModalContainer, SModalTitle, SModalParagraph } from "./shared";
interface RequestModalProps {
pending: boolean;
result: any;
}
const RequestModal = (props: RequestModalProps) => {
const { pending, result } = props;
return (
<>
{pending ? (
<SModalContainer>
<SModalTitle>{"Pending JSON-RPC Request"}</SModalTitle>
<SContainer>
<Loader />
<SModalParagraph>{"Approve or reject request using your wallet"}</SModalParagraph>
</SContainer>
</SModalContainer>
) : result ? (
<SModalContainer>
<SModalTitle>
{result.valid ? "JSON-RPC Request Approved" : "JSON-RPC Request Failed"}
</SModalTitle>
<STable>
{Object.keys(result).map(key => (
<SRow key={key}>
<SKey>{key}</SKey>
<SValue>{result[key].toString()}</SValue>
</SRow>
))}
</STable>
</SModalContainer>
) : (
<SModalContainer>
<SModalTitle>{"JSON-RPC Request Rejected"}</SModalTitle>
</SModalContainer>
)}
</>
);
};
export default RequestModal;

View File

@ -0,0 +1,17 @@
import styled from "styled-components";
export const SModalContainer = styled.div`
width: 100%;
position: relative;
word-wrap: break-word;
`;
export const SModalTitle = styled.div`
margin: 1em 0;
font-size: 20px;
font-weight: 700;
`;
export const SModalParagraph = styled.p`
margin-top: 30px;
`;

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,219 @@
export const colors: Record<string, string> = {
white: "255, 255, 255",
black: "0, 0, 0",
dark: "12, 12, 13",
grey: "169, 169, 188",
darkGrey: "113, 119, 138",
lightGrey: "212, 212, 212",
blue: "101, 127, 230",
lightBlue: "64, 153, 255",
yellow: "250, 188, 45",
orange: "246, 133, 27",
green: "84, 209, 146",
pink: "255, 51, 102",
red: "214, 75, 71",
purple: "110, 107, 233",
};
export const fonts = {
size: {
tiny: "10px",
small: "14px",
medium: "16px",
large: "18px",
h1: "60px",
h2: "50px",
h3: "40px",
h4: "32px",
h5: "24px",
h6: "20px",
},
weight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
extrabold: 800,
},
family: {
OpenSans: `"Open Sans", sans-serif`,
},
};
export const transitions = {
short: "all 0.1s ease-in-out",
base: "all 0.2s ease-in-out",
long: "all 0.3s ease-in-out",
button: "all 0.15s ease-in-out",
};
export const shadows = {
soft: "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)",
medium:
"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)",
big: "0 15px 35px 0 rgba(50, 50, 93, 0.06), 0 5px 15px 0 rgba(50, 50, 93, 0.15)",
hover:
"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)",
};
export const responsive = {
xs: {
min: "min-width: 467px",
max: "max-width: 468px",
},
sm: {
min: "min-width: 639px",
max: "max-width: 640px",
},
md: {
min: "min-width: 959px",
max: "max-width: 960px",
},
lg: {
min: "min-width: 1023px",
max: "max-width: 1024px",
},
xl: {
min: "min-width: 1399px",
max: "max-width: 1400px",
},
};
export const globalStyle = `
html, body, #root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
body {
font-family: ${fonts.family.OpenSans};
font-style: normal;
font-stretch: normal;
font-weight: ${fonts.weight.normal};
font-size: ${fonts.size.medium};
background-color: rgb(${colors.white});
color: rgb(${colors.dark});
overflow-y:auto;
text-rendering: optimizeLegibility;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
-webkit-overflow-scrolling: touch;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
button {
border-style: none;
line-height: 1em;
background-image: none;
outline: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
[tabindex] {
outline: none;
width: 100%;
height: 100%;
}
a, p, h1, h2, h3, h4, h5, h6 {
text-decoration: none;
margin: 0;
padding: 0;
margin: 0.7em 0;
}
h1 {
font-size: ${fonts.size.h1}
}
h2 {
font-size: ${fonts.size.h2}
}
h3 {
font-size: ${fonts.size.h3}
}
h4 {
font-size: ${fonts.size.h4}
}
h5 {
font-size: ${fonts.size.h5}
}
h6 {
font-size: ${fonts.size.h6}
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
text-decoration: none;
color: inherit;
outline: none;
}
b,
strong {
font-weight: inherit;
font-weight: bolder;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
* {
box-sizing: border-box !important;
}
input {
-webkit-appearance: none;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
display: block;
}
audio,
canvas,
progress,
video {
display: inline-block;
}
input[type="color"],
input[type="date"],
input[type="datetime"],
input[type="datetime-local"],
input[type="email"],
input[type="month"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="time"],
input[type="url"],
input[type="week"],
select:focus,
textarea {
font-size: 16px;
}
`;

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

File diff suppressed because it is too large Load Diff