chore: set up app, get basics working with EthereumProvider

This commit is contained in:
Ben Kremer
2022-02-17 10:33:19 +01:00
parent 033cf6a585
commit 43c67d0c81
55 changed files with 17042 additions and 0 deletions
+253
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

@@ -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

@@ -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;
}
@@ -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;
}
@@ -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}`);
}
}
@@ -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;
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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>
);
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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%;
}
}
`;
@@ -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;
`;
@@ -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"],
};
@@ -0,0 +1,2 @@
export * from "./default";
export * from "./logo";
@@ -0,0 +1 @@
export const BLOCKCHAIN_LOGO_BASE_URL = "https://blockchain-api.xyz/logos/";
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
};
@@ -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,
};
@@ -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,
};
@@ -0,0 +1,6 @@
export * from "./api";
export * from "./eip712";
export * from "./eip1271";
export * from "./tx";
export * from "./types";
export * from "./utilities";
@@ -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;
}
@@ -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[];
}
@@ -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;
};
@@ -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"),
);
@@ -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;
@@ -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;
@@ -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;
@@ -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;
`;
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
@@ -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;
}
`;