chore(dapps): sets up existing v2 react-app with yarn.lock

This commit is contained in:
Ben Kremer 2022-02-02 14:00:33 +01:00
parent d9b0f78982
commit a00fa094f7
51 changed files with 16570 additions and 0 deletions

View File

@ -0,0 +1,2 @@
REACT_APP_PROJECT_ID=b6bcd8c0647f1d9a7240fab22183d1cd

25
dapps/react/react-dapp-v2/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
# WalletConnect React App
## Develop
```bash
yarn start
```
## Test
```bash
yarn test
```
## Build
```bash
yarn build
```

7
dapps/react/react-dapp-v2/images.d.ts vendored Normal file
View File

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

View File

@ -0,0 +1,79 @@
{
"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"
},
"repository": {
"type": "git",
"url": "git+https://github.com/walletconnect/walletconnect-monorepo.git"
},
"bugs": {
"url": "https://github.com/walletconnect/walletconnect-monorepo/issues"
},
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/axios": "^0.14.0",
"@types/bn.js": "^4.11.5",
"@types/eth-sig-util": "^2.1.0",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/pino": "^6.3.4",
"@types/prop-types": "^15.7.3",
"@types/qr-image": "^3.2.3",
"@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8",
"@types/styled-components": "^5.1.3",
"@walletconnect/client": "^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"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

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

View File

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

View File

@ -0,0 +1,896 @@
import * as React from "react";
import styled from "styled-components";
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import QRCodeModal from "@walletconnect/legacy-modal";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import { ERROR, getAppMetadata } from "@walletconnect/utils";
import * as encoding from "@walletconnect/encoding";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
import { formatDirectSignDoc, stringifySignDocValues } from "cosmos-wallet";
import { BigNumber } from "ethers";
import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
import Button from "./components/Button";
import Column from "./components/Column";
import Header from "./components/Header";
import Modal from "./components/Modal";
import Wrapper from "./components/Wrapper";
import {
DEFAULT_APP_METADATA,
DEFAULT_MAIN_CHAINS,
DEFAULT_LOGGER,
DEFAULT_EIP155_METHODS,
DEFAULT_COSMOS_METHODS,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
DEFAULT_TEST_CHAINS,
DEFAULT_CHAINS,
} from "./constants";
import {
apiGetAccountAssets,
AccountAction,
eip712,
hashPersonalMessage,
hashTypedDataMessage,
verifySignature,
AccountBalances,
formatTestTransaction,
ChainNamespaces,
setInitialStateTestnet,
getInitialStateTestnet,
} from "./helpers";
import { fonts } from "./styles";
import Toggle from "./components/Toggle";
import RequestModal from "./modals/RequestModal";
import PairingModal from "./modals/PairingModal";
import PingModal from "./modals/PingModal";
const SLayout = styled.div`
position: relative;
width: 100%;
min-height: 100vh;
text-align: center;
`;
const SContent = styled(Wrapper as any)`
width: 100%;
height: 100%;
padding: 0 16px;
`;
const SLanding = styled(Column as any)`
/* height: 600px; */
`;
const SButtonContainer = styled(Column as any)`
width: 250px;
margin: 50px 0;
`;
const SConnectButton = styled(Button as any)`
border-radius: 8px;
font-size: ${fonts.size.medium};
height: 44px;
width: 100%;
margin: 12px 0;
`;
const SAccountsContainer = styled(SLanding as any)`
height: 100%;
padding-bottom: 30px;
& h3 {
padding-top: 30px;
}
`;
const SToggleContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 10px auto;
& > p {
margin-right: 10px;
}
`;
const SFullWidthContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
const SAccounts = styled(SFullWidthContainer)`
justify-content: space-between;
& > div {
margin: 12px 0;
flex: 1 0 100%;
@media (min-width: 648px) {
flex: 0 1 48%;
}
}
`;
interface AppState {
client: Client | undefined;
session: SessionTypes.Created | undefined;
testnet: boolean;
loading: boolean;
fetching: boolean;
chains: string[];
pairings: string[];
modal: string;
pending: boolean;
uri: string;
accounts: string[];
result: any | undefined;
balances: AccountBalances;
chainData: ChainNamespaces;
}
const INITIAL_STATE: AppState = {
client: undefined,
session: undefined,
testnet: true,
loading: false,
fetching: false,
chains: [],
pairings: [],
modal: "",
pending: false,
uri: "",
accounts: [],
result: undefined,
balances: {},
chainData: {},
};
class App extends React.Component<any, any> {
public state: AppState = {
...INITIAL_STATE,
testnet: getInitialStateTestnet(),
};
public componentDidMount() {
this.init();
}
public init = async () => {
this.setState({ loading: true });
try {
await this.loadChainData();
console.log(DEFAULT_RELAY_URL,DEFAULT_PROJECT_ID )
const client = await Client.init({
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
projectId: DEFAULT_PROJECT_ID,
});
this.setState({ loading: false, client });
this.subscribeToEvents();
await this.checkPersistedState();
} catch (e) {
this.setState({ loading: false });
throw e;
}
};
public getAllNamespaces() {
const namespaces: string[] = [];
DEFAULT_CHAINS.forEach(chainId => {
const [namespace] = chainId.split(":");
if (!namespaces.includes(namespace)) {
namespaces.push(namespace);
}
});
return namespaces;
}
public async loadChainData(): Promise<void> {
const namespaces = this.getAllNamespaces();
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;
}
}),
);
this.setState({ chainData });
}
public subscribeToEvents = () => {
if (typeof this.state.client === "undefined") {
return;
}
this.state.client.on(
CLIENT_EVENTS.pairing.proposal,
async (proposal: PairingTypes.Proposal) => {
const { uri } = proposal.signal.params;
this.setState({ uri });
console.log("EVENT", "QR Code Modal open");
QRCodeModal.open(uri, () => {
console.log("EVENT", "QR Code Modal closed");
});
},
);
this.state.client.on(CLIENT_EVENTS.pairing.created, async (proposal: PairingTypes.Settled) => {
if (typeof this.state.client === "undefined") return;
this.setState({ pairings: this.state.client.pairing.topics });
});
this.state.client.on(CLIENT_EVENTS.session.deleted, (session: SessionTypes.Settled) => {
if (session.topic !== this.state.session?.topic) return;
console.log("EVENT", "session_deleted");
this.resetApp();
});
};
public checkPersistedState = async () => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
// populates existing pairings to state
this.setState({ pairings: this.state.client.pairing.topics });
if (typeof this.state.session !== "undefined") return;
// populates existing session to state (assume only the top one)
if (this.state.client.session.topics.length) {
const session = await this.state.client.session.get(this.state.client.session.topics[0]);
const chains = session.state.accounts.map(account =>
account
.split(":")
.slice(0, -1)
.join(":"),
);
this.setState({ accounts: session.state.accounts, chains });
this.onSessionConnected(session);
}
};
public connect = async (pairing?: { topic: string }) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
console.log("connect", pairing);
if (this.state.modal === "pairing") {
this.closeModal();
}
try {
const chains = this.state.chains;
const supportedNamespaces: string[] = [];
chains.forEach(chainId => {
const [namespace] = chainId.split(":");
if (!supportedNamespaces.includes(namespace)) {
supportedNamespaces.push(namespace);
}
});
const methods: string[] = supportedNamespaces
.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();
const session = await this.state.client.connect({
metadata: getAppMetadata() || DEFAULT_APP_METADATA,
pairing,
permissions: {
blockchain: {
chains,
},
jsonrpc: {
methods,
},
},
});
this.onSessionConnected(session);
} catch (e) {
// ignore rejection
}
// close modal in case it was open
QRCodeModal.close();
};
public disconnect = async () => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
await this.state.client.disconnect({
topic: this.state.session.topic,
reason: ERROR.USER_DISCONNECTED.format(),
});
};
public resetApp = async () => {
const { client, chainData } = this.state;
this.setState({ ...INITIAL_STATE, client, chainData });
};
public toggleTestnets = () => {
const testnet = !this.state.testnet;
this.setState({ testnet });
setInitialStateTestnet(testnet);
};
public onSessionConnected = async (session: SessionTypes.Settled) => {
this.setState({ session });
this.onSessionUpdate(session.state.accounts, session.permissions.blockchain.chains);
};
public onSessionUpdate = async (accounts: string[], chains: string[]) => {
this.setState({ chains, accounts });
await this.getAccountBalances();
};
public getAccountBalances = async () => {
this.setState({ fetching: true });
try {
const arr = await Promise.all(
this.state.accounts.map(async account => {
const [namespace, reference, address] = account.split(":");
const chainId = `${namespace}:${reference}`;
const assets = await apiGetAccountAssets(address, chainId);
return { account, assets };
}),
);
const balances: AccountBalances = {};
arr.forEach(({ account, assets }) => {
balances[account] = assets;
});
this.setState({ fetching: false, balances });
} catch (e) {
console.error(e);
this.setState({ fetching: false });
}
};
public openPairingModal = () => this.setState({ modal: "pairing" });
public openRequestModal = () => this.setState({ pending: true, modal: "request" });
public openPingModal = () => this.setState({ pending: true, modal: "ping" });
public openModal = (modal: string) => this.setState({ modal });
public closeModal = () => this.setState({ modal: "" });
public onConnect = () => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (this.state.client.pairing.topics.length) {
return this.openPairingModal();
}
this.connect();
};
public testSendTransaction = async (chainId: string) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// get ethereum address
const account = this.state.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");
// open modal
this.openRequestModal();
const tx = await formatTestTransaction(account);
const balance = BigNumber.from(this.state.balances[account][0].balance || "0");
if (balance.lt(BigNumber.from(tx.gasPrice).mul(tx.gasLimit))) {
const formattedResult = {
method: "eth_sendTransaction",
address,
valid: false,
result: "Insufficient funds for intrinsic transaction cost",
};
this.setState({ pending: false, result: formattedResult || null });
return;
}
const result = await this.state.client.request({
topic: this.state.session.topic,
chainId,
request: {
method: "eth_sendTransaction",
params: [tx],
},
});
// format displayed result
const formattedResult = {
method: "eth_sendTransaction",
address,
valid: true,
result,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public testSignPersonalMessage = async (chainId: string) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// test message
const message = `My email is john@doe.com - ${Date.now()}`;
// encode message (hex)
const hexMsg = encoding.utf8ToHex(message, true);
// get ethereum address
const account = this.state.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");
// personal_sign params
const params = [hexMsg, address];
// open modal
this.openRequestModal();
// send message
const result = await this.state.client.request({
topic: this.state.session.topic,
chainId,
request: {
method: "personal_sign",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const chainData = this.state.chainData[namespace][reference];
if (typeof chainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = chainData.rpc[0];
// verify signature
const hash = hashPersonalMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
const formattedResult = {
method: "personal_sign",
address,
valid,
result,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public testSignTypedData = async (chainId: string) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// test message
const message = JSON.stringify(eip712.example);
// get ethereum address
const account = this.state.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");
// eth_signTypedData params
const params = [address, message];
// open modal
this.openRequestModal();
// send message
const result = await this.state.client.request({
topic: this.state.session.topic,
chainId,
request: {
method: "eth_signTypedData",
params,
},
});
// split chainId
const [namespace, reference] = chainId.split(":");
const chainData = this.state.chainData[namespace][reference];
if (typeof chainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
const rpcUrl = chainData.rpc[0];
// verify signature
const hash = hashTypedDataMessage(message);
const valid = await verifySignature(address, result, hash, rpcUrl);
// format displayed result
const formattedResult = {
method: "eth_signTypedData",
address,
valid,
result,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public testSignDirect = async (chainId: string) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// 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,
);
// get cosmos address
const account = this.state.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");
// cosmos_signDirect params
const params = {
signerAddress: address,
signDoc: stringifySignDocValues(signDoc),
};
// open modal
this.openRequestModal();
// send message
const result = await this.state.client.request({
topic: this.state.session.topic,
chainId,
request: {
method: "cosmos_signDirect",
params,
},
});
const chainData = this.state.chainData[namespace][reference];
if (typeof chainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
const formattedResult = {
method: "cosmos_signDirect",
address,
valid,
result: result.signature.signature,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public testSignAmino = async (chainId: string) => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// 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",
};
// get cosmos address
const account = this.state.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");
// cosmos_signAmino params
const params = { signerAddress: address, signDoc };
// open modal
this.openRequestModal();
// send message
const result = await this.state.client.request({
topic: this.state.session.topic,
chainId,
request: {
method: "cosmos_signAmino",
params,
},
});
const chainData = this.state.chainData[namespace][reference];
if (typeof chainData === "undefined") {
throw new Error(`Missing chain data for chainId: ${chainId}`);
}
// TODO: check if valid
const valid = true;
// format displayed result
const formattedResult = {
method: "cosmos_signAmino",
address,
valid,
result: result.signature.signature,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public ping = async () => {
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof this.state.session === "undefined") {
throw new Error("Session is not connected");
}
try {
// open modal
this.openPingModal();
let valid = false;
try {
await this.state.client.session.ping(this.state.session.topic);
valid = true;
} catch (e) {
valid = false;
}
// format displayed result
const formattedResult = {
method: "ping",
valid,
};
// display result
this.setState({ pending: false, result: formattedResult || null });
} catch (e) {
console.error(e);
this.setState({ pending: false, result: null });
}
};
public handleChainSelectionClick = (chainId: string) => {
const { chains } = this.state;
if (chains.includes(chainId)) {
this.setState({ chains: chains.filter(x => x !== chainId) });
} else {
this.setState({ chains: [...chains, chainId] });
}
};
public getBlockchainActions = (chainId: string) => {
const [namespace] = chainId.split(":");
switch (namespace) {
case "eip155":
return this.getEthereumActions();
case "cosmos":
return this.getCosmosActions();
default:
break;
}
};
public getEthereumActions = (): AccountAction[] => {
return [
{ method: "eth_sendTransaction", callback: this.testSendTransaction },
{ method: "personal_sign", callback: this.testSignPersonalMessage },
{ method: "eth_signTypedData", callback: this.testSignTypedData },
];
};
public getCosmosActions = (): AccountAction[] => {
return [
{ method: "cosmos_signDirect", callback: this.testSignDirect },
{ method: "cosmos_signAmino", callback: this.testSignAmino },
];
};
public renderModal = () => {
switch (this.state.modal) {
case "pairing":
if (typeof this.state.client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
return <PairingModal pairings={this.state.client.pairing.values} connect={this.connect} />;
case "request":
return <RequestModal pending={this.state.pending} result={this.state.result} />;
case "ping":
return <PingModal pending={this.state.pending} result={this.state.result} />;
default:
return null;
}
};
public renderContent = () => {
const { balances, accounts, chains, chainData, testnet, fetching } = this.state;
const chainOptions = testnet ? DEFAULT_TEST_CHAINS : DEFAULT_MAIN_CHAINS;
return !accounts.length && !Object.keys(balances).length ? (
<SLanding center>
<Banner />
<h6>
<span>{`Using v${process.env.REACT_APP_VERSION || "2.0.0-beta"}`}</span>
</h6>
<SButtonContainer>
<h6>Select chains:</h6>
<SToggleContainer>
<p>Testnets Only?</p>
<Toggle active={testnet} onClick={this.toggleTestnets} />
</SToggleContainer>
{chainOptions.map(chainId => (
<Blockchain
key={chainId}
chainId={chainId}
chainData={chainData}
onClick={this.handleChainSelectionClick}
active={chains.includes(chainId)}
/>
))}
<SConnectButton
left
onClick={this.onConnect}
fetching={fetching}
disabled={!chains.length}
>
{"Connect"}
</SConnectButton>
</SButtonContainer>
</SLanding>
) : (
<SAccountsContainer>
<h3>Accounts</h3>
<SAccounts>
{this.state.accounts.map(account => {
const [namespace, reference, address] = account.split(":");
const chainId = `${namespace}:${reference}`;
return (
<Blockchain
key={account}
active={true}
chainData={chainData}
fetching={fetching}
address={address}
chainId={chainId}
balances={balances}
actions={this.getBlockchainActions(chainId)}
/>
);
})}
</SAccounts>
</SAccountsContainer>
);
};
public render = () => {
const { loading, session, modal } = this.state;
return (
<SLayout>
<Column maxWidth={1000} spanHeight>
<Header ping={this.ping} disconnect={this.disconnect} session={session} />
<SContent>{loading ? "Loading..." : this.renderContent()}</SContent>
</Column>
<Modal show={!!modal} closeModal={this.closeModal}>
{this.renderModal()}
</Modal>
</SLayout>
);
};
}
export default App;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,181 @@
import * as React from "react";
import * as PropTypes from "prop-types";
import styled from "styled-components";
import { colors, transitions } from "../styles";
interface LightboxStyleProps {
show: boolean;
offset: number;
opacity?: number;
}
const SLightbox = styled.div<LightboxStyleProps>`
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 ModalState {
offset: number;
}
interface ModalProps {
children: React.ReactNode;
show: boolean;
closeModal: any;
opacity?: number;
}
const INITIAL_STATE: ModalState = {
offset: 0,
};
class Modal extends React.Component<ModalProps, ModalState> {
public static propTypes = {
children: PropTypes.node.isRequired,
show: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
opacity: PropTypes.number,
};
public lightbox?: HTMLDivElement | null;
public state: ModalState = {
...INITIAL_STATE,
};
public componentDidUpdate() {
if (this.lightbox) {
const lightboxRect = this.lightbox.getBoundingClientRect();
const offset = lightboxRect.top > 0 ? lightboxRect.top : 0;
if (offset !== INITIAL_STATE.offset && offset !== this.state.offset) {
this.setState({ offset });
}
}
}
public closeModal = async () => {
const d = typeof window !== "undefined" ? document : "";
const body = d ? d.body || d.getElementsByTagName("body")[0] : "";
if (body) {
if (this.props.show) {
body.style.position = "";
} else {
body.style.position = "fixed";
}
}
this.props.closeModal();
};
public render() {
const { offset } = this.state;
const { children, show, opacity } = this.props;
return (
<SLightbox show={show} offset={offset} opacity={opacity} ref={(c) => (this.lightbox = c)}>
<SModalContainer>
<SHitbox onClick={this.closeModal} />
<SCard>
<SCloseButton size={25} color={"dark"} onClick={this.closeModal} />
<SModalContent>{children}</SModalContent>
</SCard>
</SModalContainer>
</SLightbox>
);
}
}
export default Modal;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,200 @@
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";
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 setInitialStateTestnet(value: boolean): void {
window.localStorage.setItem(LOCALSTORAGE_KEY_TESTNET, `${value}`);
}
export function getInitialStateTestnet(): boolean {
let value = INITIAL_STATE_TESTNET_DEFAULT;
const persisted = window.localStorage.getItem(LOCALSTORAGE_KEY_TESTNET);
if (!persisted) {
setInitialStateTestnet(value);
} else {
value = persisted === "true" ? true : false;
}
return value;
}

View File

@ -0,0 +1,24 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components";
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 />
<App />
</>,
document.getElementById("root"),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff