Merge branch 'feat/dapp-cosmos-provider' into main

This commit is contained in:
Ben Kremer 2022-02-24 10:46:34 +01:00
commit 2e6200b4d5
53 changed files with 16376 additions and 0 deletions

View File

@ -0,0 +1,4 @@
REACT_APP_PROJECT_ID=39bc93c...
REACT_APP_INFURA_ID=5dc0df...
REACT_APP_RELAY_URL=wss://relay.walletconnect.com

25
dapps/react-dapp-v2-cosmos/.gitignore vendored Normal file
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,51 @@
# React dApp (with v2 EthereumProvider + Ethers.js)
🔗 Live dapp demo - https://react-dapp-v2-with-ethers.vercel.app <br />
🔗 Live wallet demo - https://react-wallet-v2.vercel.app <br />
📚 WalletConnect v2 Docs - https://docs.walletconnect.com/2.0
## Overview
This is an example implementation of a React dApp (generated via `create-react-app`) using the v2 [`EthereumProvider`](https://docs.walletconnect.com/2.0/quick-start/dapps/ethereum-provider) together with [`Ethers.js`](https://docs.ethers.io/v5/) to:
- handle pairings
- manage sessions
- send JSON-RPC requests to a paired wallet
## Running locally
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
```
Your `.env.local` now contains the following environment variables:
- `REACT_APP_PROJECT_ID` (placeholder) - You can generate your own ProjectId at https://cloud.walletconnect.com
- `REACT_APP_INFURA_ID` (placeholder) - You can generate your own Infura ID via https://infura.io/
- `REACT_APP_RELAY_URL` (already set)
## 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": "react-dapp-v2-cosmos",
"version": "2.0.0-beta.23",
"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.23",
"@walletconnect/cosmos-provider": "2.0.0-beta.23",
"@walletconnect/qrcode-modal": "^1.7.1",
"@walletconnect/types": "2.0.0-beta.23",
"@walletconnect/utils": "2.0.0-beta.23",
"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,250 @@
import React, { useState } from "react";
import { version } from "@walletconnect/client/package.json";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
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 } from "./constants";
import { AccountAction } from "./helpers";
import RequestModal from "./modals/RequestModal";
import PingModal from "./modals/PingModal";
import {
SAccounts,
SAccountsContainer,
SButtonContainer,
SContent,
SLanding,
SLayout,
} from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
interface IFormattedRpcResponse {
method: string;
address: string;
valid: boolean;
result: string;
}
export default function App() {
const [isRpcRequestPending, setIsRpcRequestPending] = useState(false);
const [rpcResult, setRpcResult] = useState<IFormattedRpcResponse | null>();
const [modal, setModal] = useState("");
const closeModal = () => setModal("");
const openPingModal = () => setModal("ping");
const openRequestModal = () => setModal("request");
// Initialize the WalletConnect client.
const {
client,
session,
disconnect,
chain,
accounts,
balances,
chainData,
isInitializing,
onEnable,
cosmosProvider,
} = useWalletConnectClient();
const ping = async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect Client is not initialized");
}
try {
setIsRpcRequestPending(true);
const _session = await client.session.get(client.session.topics[0]);
await client.session.ping(_session.topic);
setRpcResult({
address: "",
method: "ping",
valid: true,
result: "success",
});
} catch (error) {
console.error("RPC request failed:", error);
} finally {
setIsRpcRequestPending(false);
}
};
const onPing = async () => {
openPingModal();
await ping();
};
const testSignDirect: () => Promise<IFormattedRpcResponse> = async () => {
if (!cosmosProvider) {
throw new Error("cosmosProvider not connected");
}
// 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(":");
// const reference = await web3Provider.getNetwork();
// format sign doc
const signDoc = formatDirectSignDoc(
inputs.fee,
inputs.pubkey,
inputs.gasLimit,
inputs.accountNumber,
inputs.sequence,
inputs.bodyBytes,
"cosmoshub-4",
);
const [address] = cosmosProvider.accounts;
// cosmos_signDirect params
const params = {
signerAddress: address,
signDoc: stringifySignDocValues(signDoc),
};
const signature = (await cosmosProvider.request({
method: "cosmos_signDirect",
params,
})) as string;
// const valid = utils.verifyMessage(msg, signature) === address;
return {
method: "cosmos_signDirect",
address,
valid: true,
result: signature,
};
};
const testSignAmino: () => Promise<IFormattedRpcResponse> = async () => {
if (!cosmosProvider) {
throw new Error("cosmosProvider not connected");
}
// test amino sign doc
const signDoc = {
msgs: [],
fee: { amount: [], gas: "23" },
chain_id: "foochain",
memo: "hello, world",
account_number: "7",
sequence: "54",
};
const [address] = cosmosProvider.accounts;
// cosmos_signAmino params
const params = { signerAddress: address, signDoc };
const signature = (await cosmosProvider.request({
method: "cosmos_signAmino",
params,
})) as string;
// const valid = utils.verifyMessage(msg, signature) === address;
return {
method: "cosmos_signAmino",
address,
valid: true,
result: signature,
};
};
const getCosmosActions = (): AccountAction[] => {
const wrapRpcRequest = (rpcRequest: () => Promise<IFormattedRpcResponse>) => async () => {
openRequestModal();
try {
setIsRpcRequestPending(true);
const result = await rpcRequest();
setRpcResult(result);
} catch (error) {
console.error("RPC request failed:", error);
} finally {
setIsRpcRequestPending(false);
}
};
return [
{ method: "cosmos_signDirect", callback: wrapRpcRequest(testSignDirect) },
{ method: "cosmos_signAmino", callback: wrapRpcRequest(testSignAmino) },
];
};
// Renders the appropriate model for the given request that is currently in-flight.
const renderModal = () => {
switch (modal) {
case "request":
return <RequestModal pending={isRpcRequestPending} result={rpcResult} />;
case "ping":
return <PingModal pending={isRpcRequestPending} result={rpcResult} />;
default:
return null;
}
};
const renderContent = () => {
const chainOptions = 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 Cosmos chain:</h6>
{chainOptions.map(chainId => (
<Blockchain key={chainId} chainId={chainId} chainData={chainData} onClick={onEnable} />
))}
</SButtonContainer>
</SLanding>
) : (
<SAccountsContainer>
<h3>Account</h3>
<SAccounts>
{accounts.map(account => {
return (
<Blockchain
key={account}
active={true}
chainData={chainData}
address={account}
chainId={chain}
balances={balances}
actions={getCosmosActions()}
/>
);
})}
</SAccounts>
</SAccountsContainer>
);
};
return (
<SLayout>
<Column maxWidth={1000} spanHeight>
<Header ping={onPing} 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,75 @@
import * as React from "react";
import styled from "styled-components";
import Icon from "./Icon";
import { AssetData } 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;
`;
// eslint-disable-next-line
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,180 @@
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,
ChainMetadata,
ChainNamespaces,
AccountBalances,
} from "../helpers";
import { fonts } from "../styles";
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?: AccountBalances;
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") {
return null;
}
const name = chain.meta.name || chain.data.name;
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>
) : (
<>
{!!address && !!balances && balances[address] ? (
<SFullWidthContainer>
<h6>Balances</h6>
<Column center>
<Asset key={balances[address].symbol} asset={balances[address]} />
</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 { SessionTypes } from "@walletconnect/types";
import * as React from "react";
import styled from "styled-components";
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,27 @@
export const DEFAULT_MAIN_CHAINS = [
// mainnets
"cosmos:cosmoshub-4",
];
export const DEFAULT_TEST_CHAINS = [
// testnets
];
export const DEFAULT_CHAINS = [...DEFAULT_MAIN_CHAINS, ...DEFAULT_TEST_CHAINS];
export const DEFAULT_PROJECT_ID = process.env.REACT_APP_PROJECT_ID;
export const DEFAULT_INFURA_ID = process.env.REACT_APP_INFURA_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_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,262 @@
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import CosmosProvider from "@walletconnect/cosmos-provider";
import QRCodeModal from "@walletconnect/qrcode-modal";
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL } from "../constants";
import { AccountBalances, ChainNamespaces, getAllChainNamespaces } from "../helpers";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
/**
* Types
*/
interface IContext {
client: Client | undefined;
session: SessionTypes.Created | undefined;
disconnect: () => Promise<void>;
isInitializing: boolean;
chain: string;
pairings: string[];
accounts: string[];
balances: AccountBalances;
chainData: ChainNamespaces;
onEnable: (chainId: string) => Promise<void>;
cosmosProvider?: CosmosProvider;
}
/**
* 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 [cosmosProvider, setCosmosProvider] = useState<CosmosProvider>();
const [isInitializing, setIsInitializing] = useState(false);
const [hasCheckedPersistedSession, setHasCheckedPersistedSession] = useState(false);
const [balances, setBalances] = useState<AccountBalances>({});
const [accounts, setAccounts] = useState<string[]>([]);
const [chainData, setChainData] = useState<ChainNamespaces>({});
const [chain, setChain] = useState<string>("");
const resetApp = () => {
setPairings([]);
setSession(undefined);
setBalances({});
setAccounts([]);
setChain("");
};
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 disconnect = useCallback(async () => {
if (typeof cosmosProvider === "undefined") {
throw new Error("cosmosProvider is not initialized");
}
await cosmosProvider.disconnect();
}, [cosmosProvider]);
const _subscribeToClientEvents = useCallback(async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
_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 () => {
setPairings(_client.pairing.topics);
});
_client.on(CLIENT_EVENTS.session.deleted, () => {
console.log("EVENT", "session_deleted");
resetApp();
});
}, []);
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 _subscribeToClientEvents(_client);
} catch (err) {
throw err;
} finally {
setIsInitializing(false);
}
}, [_subscribeToClientEvents]);
const onEnable = useCallback(
async (caipChainId: string) => {
if (!client) {
throw new ReferenceError("WalletConnect Client is not initialized.");
}
const chainId = caipChainId.split(":").pop();
if (!chainId) {
throw new Error("Could not derive chainId from CAIP chainId");
}
console.log("Enabling cosmosProvider for chainId: ", chainId);
// Create WalletConnect Provider
const cosmosProvider = new CosmosProvider({
chains: [chainId],
client,
});
console.log(cosmosProvider);
setCosmosProvider(cosmosProvider);
try {
await cosmosProvider.connect();
} catch (error) {
console.error(error);
return;
}
const _accounts = cosmosProvider.accounts;
const _session = await client.session.get(client.session.topics[0]);
setAccounts(_accounts);
setSession(_session);
setChain(caipChainId);
QRCodeModal.close();
},
[client],
);
const _checkForPersistedSession = 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]);
const [namespace, chainId] = _session.state.accounts[0].split(":");
const caipChainId = `${namespace}:${chainId}`;
onEnable(caipChainId);
}
},
[session, onEnable],
);
useEffect(() => {
loadChainData();
}, []);
useEffect(() => {
if (!client) {
createClient();
}
}, [client, createClient]);
useEffect(() => {
const getPersistedSession = async () => {
if (client && !hasCheckedPersistedSession) {
await _checkForPersistedSession(client);
setHasCheckedPersistedSession(true);
}
};
getPersistedSession();
}, [client, _checkForPersistedSession, hasCheckedPersistedSession]);
const value = useMemo(
() => ({
pairings,
isInitializing,
balances,
accounts,
chain,
client,
session,
disconnect,
chainData,
onEnable,
cosmosProvider,
}),
[
pairings,
isInitializing,
balances,
accounts,
chain,
client,
session,
disconnect,
chainData,
onEnable,
cosmosProvider,
],
);
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,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,158 @@
import { ChainsMap } from "caip-api";
export interface AssetData {
account: string;
symbol: string;
balance: string;
contractAddress?: 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,27 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components";
import { ClientContextProvider } from "./contexts/ClientContext";
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>
<App />
</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