chore: set up app, get basics working with EthereumProvider
This commit is contained in:
1 parent
033cf6a585
commit
43c67d0c81
55 files changed
+17042
No files matched your search
@@ -0,0 +1,3 @@
|
||||
REACT_APP_PROJECT_ID=39bc93c4affb2e20cb6f8d36ca107dd9
|
||||
REACT_APP_RELAY_URL=wss://relay.dev.walletconnect.com
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,33 @@
|
||||
# WalletConnect React App
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install the app's dependencies:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
Set up your local environment variables by copying the example into your own `.env.local` file:
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
+7
@@ -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";
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "walletconnect-react-app",
|
||||
"version": "2.0.0-beta.22",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"walletconnect",
|
||||
"ethereum",
|
||||
"web3",
|
||||
"crypto"
|
||||
],
|
||||
"author": "WalletConnect, Inc. <walletconnect.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"prettier": "prettier --check '**/*.{js,ts,jsx,tsx}'"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/walletconnect/walletconnect-monorepo.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/walletconnect/walletconnect-monorepo/issues"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-error-overlay": "6.0.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@walletconnect/client": "^2.0.0-beta.22",
|
||||
"@walletconnect/ethereum-provider": "2.0.0-beta.22",
|
||||
"@walletconnect/legacy-modal": "^2.0.0-beta.22",
|
||||
"@walletconnect/types": "^2.0.0-beta.22",
|
||||
"@walletconnect/utils": "^2.0.0-beta.22",
|
||||
"axios": "^0.21.1",
|
||||
"blockies-ts": "^1.0.0",
|
||||
"caip-api": "^2.0.0-beta.1",
|
||||
"cosmos-wallet": "^1.1.0",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"ethereumjs-util": "^7.0.6",
|
||||
"ethers": "^5.3.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qr-image": "^3.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^4.0.3",
|
||||
"styled-components": "^5.2.0",
|
||||
"typescript": "^4.3.2",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/eth-sig-util": "^2.1.1",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^17.0.14",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/prop-types": "^15.7.4",
|
||||
"@types/qr-image": "^3.2.5",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/styled-components": "^5.1.21",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.5.1 (25234) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="Group" sketch:type="MSLayerGroup">
|
||||
<path d="M16,32 C24.836556,32 32,24.836556 32,16 C32,7.163444 24.836556,0 16,0 C7.163444,0 0,7.163444 0,16 C0,24.836556 7.163444,32 16,32 Z" id="Oval" fill="#222222" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M12.699847,10.8212511 L10.66093,10.8212511 L10.66093,12.7593676 L13.1030485,12.7593676 L13.1030485,13.7673715 L9.36885235,13.7673715 L9.36885235,7.09621855 L13.0938849,7.09621855 L13.0938849,8.10880427 L10.66093,8.10880427 L10.66093,9.81324719 L12.699847,9.81324719 L12.699847,10.8212511 Z M15.2152748,11.0915794 L15.2152748,13.7673715 L13.9231971,13.7673715 L13.9231971,7.10080039 L15.9208776,7.10080039 C16.5837198,7.10080039 17.1052802,7.27338114 17.4855745,7.61854783 C17.8658688,7.96371452 18.056013,8.44480247 18.056013,9.06182611 C18.056013,9.40699281 17.9758317,9.70633642 17.8154667,9.95986593 C17.6551016,10.2133954 17.4237212,10.4149942 17.1213185,10.5646683 C17.4695398,10.680742 17.720011,10.8731772 17.8727396,11.1419796 C18.0254682,11.410782 18.1018314,11.7467799 18.1018314,12.1499835 L18.1018314,12.635658 C18.1018314,12.821987 18.1232131,13.0174767 18.1659771,13.222133 C18.2087411,13.4267894 18.2851043,13.5764612 18.3950689,13.6711529 L18.3950689,13.7673715 L17.0571728,13.7673715 C16.9502628,13.6696252 16.8822996,13.5100262 16.8532811,13.2885697 C16.8242627,13.0671131 16.8097537,12.8464236 16.8097537,12.6264944 L16.8097537,12.1591471 C16.8097537,11.8200896 16.7394996,11.5574003 16.5989893,11.3710714 C16.4584789,11.1847424 16.2568802,11.0915794 15.9941869,11.0915794 L15.2152748,11.0915794 Z M15.2152748,10.0835755 L15.9117139,10.0835755 C16.18968,10.0835755 16.401206,10.0003396 16.5462981,9.83386545 C16.6913903,9.66739125 16.7639353,9.43142906 16.7639353,9.12597182 C16.7639353,8.81440542 16.692154,8.56698876 16.5485891,8.38371441 C16.4050242,8.20044007 16.1957891,8.10880427 15.9208776,8.10880427 L15.2152748,8.10880427 L15.2152748,10.0835755 Z M23.471743,11.6963817 L23.4809066,11.7238727 C23.4931249,12.4386427 23.310617,12.9739485 22.9333773,13.3298062 C22.5561376,13.6856639 22.0177772,13.86359 21.3182801,13.86359 C20.6004556,13.86359 20.0307864,13.6520641 19.6092554,13.2290058 C19.1877244,12.8059475 18.976962,12.2049694 18.976962,11.4260534 L18.976962,9.44670032 C18.976962,8.67083891 19.1808517,8.06909716 19.5886371,7.64145701 C19.9964225,7.21381687 20.547001,7 21.2403889,7 C21.9704318,7 22.5309374,7.17716255 22.9219227,7.53149295 C23.312908,7.88582336 23.5022886,8.42189279 23.4900703,9.13971732 L23.4809066,9.16720834 L22.2254836,9.16720834 C22.2254836,8.73956819 22.1498841,8.44022458 21.9986828,8.26916852 C21.8474814,8.09811246 21.5947193,8.01258571 21.2403889,8.01258571 C20.9166042,8.01258571 20.6737694,8.12942136 20.511877,8.36309615 C20.3499847,8.59677095 20.2690397,8.9549142 20.2690397,9.43753665 L20.2690397,11.4260534 C20.2690397,11.9117304 20.3560937,12.2714009 20.5302044,12.5050757 C20.704315,12.7387505 20.9670043,12.8555862 21.3182801,12.8555862 C21.6390103,12.8555862 21.869627,12.7708231 22.0101373,12.6012943 C22.1506477,12.4317655 22.2209018,12.130131 22.2209018,11.6963817 L23.471743,11.6963817 Z M15.7527242,25.9660512 L9.21352811,25.9660512 L9.21352811,24.5878349 L12.1678958,20.8563879 C12.588205,20.2894592 12.8899923,19.8007349 13.0732666,19.3902004 C13.2565409,18.9796659 13.3481767,18.6057918 13.3481767,18.268567 C13.3481767,17.804272 13.2589846,17.436507 13.0805975,17.1652609 C12.9022105,16.8940149 12.6273031,16.7583939 12.2558671,16.7583939 C11.8697691,16.7583939 11.5728691,16.9306692 11.3651582,17.275225 C11.1574473,17.6197808 11.0535934,18.0608544 11.0535934,18.5984592 L9.02292374,18.5984592 L9.00093093,18.5544735 C8.98138167,17.6014469 9.26972898,16.7938301 9.86598153,16.1315988 C10.4622341,15.4693675 11.2588546,15.1382568 12.2558671,15.1382568 C13.2675415,15.1382568 14.0507221,15.4180514 14.6054325,15.9776491 C15.1601429,16.5372468 15.4374939,17.2984348 15.4374939,18.2612361 C15.4374939,18.9112491 15.2774367,19.5099363 14.9573175,20.0573157 C14.6371983,20.6046951 14.1008234,21.3353379 13.3481767,22.2492659 L11.8966512,24.353245 L15.7527242,24.353245 L15.7527242,25.9660512 Z M23.5455105,22.0953163 C23.5455105,23.3513565 23.2449451,24.3361359 22.6438052,25.049684 C22.0426654,25.7632321 21.2509321,26.1200008 20.2685816,26.1200008 C19.2715691,26.1200008 18.4712831,25.7632321 17.8676996,25.049684 C17.2641161,24.3361359 16.9623288,23.3513565 16.9623288,22.0953163 L16.9623288,19.1702723 C16.9623288,17.9240067 17.2628943,16.9404491 17.8640341,16.21957 C18.465174,15.4986909 19.2617945,15.1382568 20.2539197,15.1382568 C21.2411575,15.1382568 22.0365563,15.4986909 22.6401398,16.21957 C23.2437233,16.9404491 23.5455105,17.9240067 23.5455105,19.1702723 L23.5455105,22.0953163 Z M21.4708553,18.9869988 C21.4708553,18.2832253 21.3621141,17.7358542 21.1446286,17.3448689 C20.927143,16.9538836 20.630243,16.7583939 20.2539197,16.7583939 C19.8678217,16.7583939 19.5684781,16.9538836 19.3558799,17.3448689 C19.1432816,17.7358542 19.0369841,18.2832253 19.0369841,18.9869988 L19.0369841,22.2639278 C19.0369841,22.9823633 19.1445034,23.5358435 19.3595453,23.9243851 C19.5745872,24.3129267 19.8775963,24.5071946 20.2685816,24.5071946 C20.6449049,24.5071946 20.9393613,24.3129267 21.1519595,23.9243Line truncated
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
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;
|
||||
}
|
||||
`;
|
||||
@@ -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
Reference in new issue
Block a user