feat: cosmos example dapp (#102)

* feat: updates cosmos example dapp to latest "@walletconnect"

* refactor: uses default methods from `constants`

* chore: updates dependencies
This commit is contained in:
Gancho Radkov 2023-01-24 09:57:07 +02:00 committed by GitHub
parent a959f1e766
commit 0fd85d199a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 12091 additions and 10635 deletions

View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,16 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
distDir: "build",
webpack(config) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
};
return config;
},
};
module.exports = nextConfig;

View File

@ -11,10 +11,10 @@
"author": "WalletConnect, Inc. <walletconnect.com>",
"license": "MIT",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prettier": "prettier --check '**/*.{js,ts,jsx,tsx}'"
},
"repository": {
@ -28,11 +28,8 @@
"react-error-overlay": "6.0.9"
},
"dependencies": {
"@walletconnect/client": "2.0.0-beta.26",
"@walletconnect/cosmos-provider": "2.0.0-beta.26",
"@walletconnect/qrcode-modal": "^1.7.1",
"@walletconnect/types": "2.0.0-beta.26",
"@walletconnect/utils": "2.0.0-beta.26",
"@walletconnect/types": "2.3.0",
"@walletconnect/utils": "2.3.0",
"axios": "^0.21.1",
"blockies-ts": "^1.0.0",
"caip-api": "^2.0.0-beta.1",
@ -47,7 +44,12 @@
"react-scripts": "^4.0.3",
"styled-components": "^5.2.0",
"typescript": "^4.3.2",
"web-vitals": "^0.2.4"
"web-vitals": "^0.2.4",
"next": "12.2.4",
"@ethereumjs/tx": "^3.5.0",
"@walletconnect/encoding": "^1.0.1",
"@walletconnect/universal-provider": "2.3.0",
"@web3modal/standalone": "^2.0.0-beta.10"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.1",
@ -60,10 +62,11 @@
"@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/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/styled-components": "^5.1.21",
"prettier": "^2.5.1"
"prettier": "^2.5.1",
"eslint-config-next": "12.2.4"
},
"eslintConfig": {
"extends": [

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 234 B

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -5,8 +5,6 @@ import Icon from "./Icon";
import { AssetData } from "../helpers";
import eth from "../assets/eth.svg";
import erc20 from "../assets/erc20.svg";
import { getChainMetadata } from "../chains";
const xdai = getChainMetadata("eip155:100").logo;
@ -39,17 +37,17 @@ const SAssetBalance = styled.div`
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} />;
return <Icon src={src} fallback={"/assets/erc20.svg"} />;
}
switch (asset.symbol.toLowerCase()) {
case "eth":
return <Icon src={eth} />;
return <Icon src={"assets/eth.svg"} />;
case "xdai":
return <Icon src={xdai} />;
case "matic":
return <Icon src={matic} />;
default:
return <Icon src={erc20} />;
return <Icon src={"/assets/erc20.svg"} />;
}
}

View File

@ -1,6 +1,5 @@
import * as React from "react";
import styled from "styled-components";
import logo from "../assets/walletconnect.png";
const SBannerWrapper = styled.div`
display: flex;
@ -11,7 +10,7 @@ const SBannerWrapper = styled.div`
const SBanner = styled.div`
width: 275px;
height: 45px;
background: url(${logo}) no-repeat;
background: url(/assets/walletconnect.png) no-repeat;
background-size: cover;
background-position: center;
`;

View File

@ -50,7 +50,7 @@ const SActiveSession = styled(SActiveAccount as any)`
interface HeaderProps {
ping: () => Promise<void>;
disconnect: () => Promise<void>;
session: SessionTypes.Created | undefined;
session: SessionTypes.Struct | undefined;
}
const Header = (props: HeaderProps) => {

View File

@ -0,0 +1,17 @@
import Head from "next/head";
import * as React from "react";
import { DEFAULT_APP_METADATA } from "../constants";
const Metadata = () => (
<Head>
<title>{DEFAULT_APP_METADATA.name}</title>
<meta name="description" content={DEFAULT_APP_METADATA.description} />
<meta name="url" content={DEFAULT_APP_METADATA.url} />
{DEFAULT_APP_METADATA.icons.map((icon, index) => (
<link key={index} rel="icon" href={icon} />
))}
</Head>
);
export default Metadata;

View File

@ -6,7 +6,7 @@ import { PairingTypes } from "@walletconnect/types";
import Peer from "./Peer";
interface PairingProps {
pairing: PairingTypes.Settled;
pairing: PairingTypes.Struct;
onClick?: any;
}
@ -16,14 +16,12 @@ const SPairingContainer = styled.div`
`;
const Pairing = (props: PairingProps) => {
const {
state: { metadata },
} = props.pairing;
const { peerMetadata } = props.pairing;
return (
<SPairingContainer onClick={props.onClick}>
<div>
{typeof metadata !== "undefined" ? (
<Peer oneLiner metadata={metadata} />
{typeof peerMetadata !== "undefined" ? (
<Peer oneLiner metadata={peerMetadata} />
) : (
<div>{`Unknown`}</div>
)}

View File

@ -1,7 +1,7 @@
import * as React from "react";
import styled from "styled-components";
import { AppMetadata } from "@walletconnect/types";
import { colors, fonts } from "../styles";
import { Metadata } from "@walletconnect/universal-provider";
const SPeerOneLiner = styled.div`
display: flex;
@ -53,7 +53,7 @@ const SName = styled(SCenter as any)`
interface PeerProps {
oneLiner?: boolean;
metadata: AppMetadata;
metadata: Metadata;
}
const Peer = (props: PeerProps) =>

View File

@ -9,14 +9,16 @@ export const DEFAULT_TEST_CHAINS = [
export const DEFAULT_CHAINS = [...DEFAULT_MAIN_CHAINS, ...DEFAULT_TEST_CHAINS];
export const DEFAULT_PROJECT_ID = process.env.REACT_APP_PROJECT_ID;
export const DEFAULT_PROJECT_ID = process.env.NEXT_PUBLIC_PROJECT_ID;
export const DEFAULT_INFURA_ID = process.env.REACT_APP_INFURA_ID;
export const DEFAULT_INFURA_ID = process.env.NEXT_PUBLIC_INFURA_ID;
export const DEFAULT_RELAY_URL = process.env.REACT_APP_RELAY_URL;
export const DEFAULT_RELAY_URL = process.env.NEXT_PUBLIC_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 = {

View File

@ -1,7 +1,7 @@
import Client, { CLIENT_EVENTS } from "@walletconnect/client";
import { PairingTypes, SessionTypes } from "@walletconnect/types";
import CosmosProvider from "@walletconnect/cosmos-provider";
import QRCodeModal from "@walletconnect/qrcode-modal";
import SignClient from "@walletconnect/sign-client";
import { ISignClient, PairingTypes, SessionTypes } from "@walletconnect/types";
import UniversalProvider, { IUniversalProvider } from "@walletconnect/universal-provider";
import { Web3Modal } from "@web3modal/standalone";
import {
createContext,
ReactNode,
@ -11,7 +11,12 @@ import {
useMemo,
useState,
} from "react";
import { DEFAULT_LOGGER, DEFAULT_PROJECT_ID, DEFAULT_RELAY_URL } from "../constants";
import {
DEFAULT_COSMOS_METHODS,
DEFAULT_LOGGER,
DEFAULT_PROJECT_ID,
DEFAULT_RELAY_URL,
} from "../constants";
import { AccountBalances, ChainNamespaces, getAllChainNamespaces } from "../helpers";
import { apiGetChainNamespace, ChainsMap } from "caip-api";
@ -19,17 +24,17 @@ import { apiGetChainNamespace, ChainsMap } from "caip-api";
* Types
*/
interface IContext {
client: Client | undefined;
session: SessionTypes.Created | undefined;
client: ISignClient | undefined;
session: SessionTypes.Struct | undefined;
disconnect: () => Promise<void>;
isInitializing: boolean;
chain: string;
pairings: string[];
pairings: PairingTypes.Struct[];
accounts: string[];
balances: AccountBalances;
chainData: ChainNamespaces;
onEnable: (chainId: string) => Promise<void>;
cosmosProvider?: CosmosProvider;
cosmosProvider?: IUniversalProvider;
}
/**
@ -41,11 +46,12 @@ 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 [client, setClient] = useState<ISignClient>();
const [pairings, setPairings] = useState<PairingTypes.Struct[]>([]);
const [session, setSession] = useState<SessionTypes.Struct>();
const [web3Modal, setWeb3Modal] = useState<Web3Modal>();
const [cosmosProvider, setCosmosProvider] = useState<CosmosProvider>();
const [cosmosProvider, setCosmosProvider] = useState<UniversalProvider>();
const [isInitializing, setIsInitializing] = useState(false);
const [hasCheckedPersistedSession, setHasCheckedPersistedSession] = useState(false);
@ -86,68 +92,57 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
if (typeof cosmosProvider === "undefined") {
throw new Error("cosmosProvider is not initialized");
}
await cosmosProvider.disconnect();
cosmosProvider.disconnect();
resetApp();
}, [cosmosProvider]);
const onSessionConnected = useCallback(async (_session: SessionTypes.Settled) => {
const onSessionConnected = useCallback(async (_session: SessionTypes.Struct) => {
setSession(_session);
setChain(_session.permissions.blockchain.chains[0]);
setAccounts(_session.state.accounts);
}, []);
const _subscribeToClientEvents = useCallback(
async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
_client.on(CLIENT_EVENTS.pairing.proposal, async (proposal: PairingTypes.Proposal) => {
const { uri } = proposal.signal.params;
console.log("EVENT", "QR Code Modal open");
QRCodeModal.open(uri, () => {
console.log("EVENT", "QR Code Modal closed");
});
const _subscribeToProviderEvents = useCallback(
async (provider: UniversalProvider) => {
provider.on("display_uri", async (uri: string) => {
console.log("EVENT", "QR Code Modal open", uri);
web3Modal?.openModal({ uri });
});
_client.on(CLIENT_EVENTS.pairing.created, async () => {
setPairings(_client.pairing.topics);
});
_client.on(CLIENT_EVENTS.session.updated, (updatedSession: SessionTypes.Settled) => {
console.log("EVENT", "session_updated");
onSessionConnected(updatedSession);
});
_client.on(CLIENT_EVENTS.session.deleted, () => {
provider.on("session_delete", () => {
console.log("EVENT", "session_deleted");
resetApp();
});
},
[onSessionConnected],
[web3Modal],
);
const createClient = useCallback(async () => {
try {
setIsInitializing(true);
const _client = await Client.init({
const provider = await UniversalProvider.init({
projectId: DEFAULT_PROJECT_ID,
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
});
setCosmosProvider(provider);
setClient(provider.client);
const web3Modal = new Web3Modal({
projectId: DEFAULT_PROJECT_ID,
});
setClient(_client);
await _subscribeToClientEvents(_client);
setWeb3Modal(web3Modal);
} catch (err) {
throw err;
} finally {
setIsInitializing(false);
}
}, [_subscribeToClientEvents]);
}, []);
const onEnable = useCallback(
async (caipChainId: string) => {
if (!client) {
if (!cosmosProvider) {
throw new ReferenceError("WalletConnect Client is not initialized.");
}
@ -160,47 +155,50 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
console.log("Enabling cosmosProvider for chainId: ", chainId);
// Create WalletConnect Provider
const cosmosProvider = new CosmosProvider({
chains: [chainId],
client,
const session = await cosmosProvider.connect({
namespaces: {
cosmos: {
methods: DEFAULT_COSMOS_METHODS,
chains: [caipChainId],
events: ["chainChanged", "accountsChanged"],
},
},
});
console.log(cosmosProvider);
setCosmosProvider(cosmosProvider);
const _accounts = await cosmosProvider.enable();
setAccounts(_accounts);
setSession(session);
onSessionConnected(session!);
setChain(caipChainId);
try {
await cosmosProvider.connect();
} catch (error) {
console.error(error);
return;
}
const _session = await client.session.get(client.session.topics[0]);
onSessionConnected(_session);
QRCodeModal.close();
web3Modal?.closeModal();
},
[client, onSessionConnected],
[cosmosProvider, onSessionConnected, web3Modal],
);
const _checkForPersistedSession = useCallback(
async (_client: Client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
async (provider: IUniversalProvider) => {
if (!provider) {
throw new Error("Universal Provider is not initialized");
}
// populates existing pairings to state
setPairings(_client.pairing.topics);
setPairings(provider.client!.pairing.getAll({ active: true }));
if (typeof session !== "undefined") return;
// populates existing session to state (assume only the top one)
if (_client.session.topics.length) {
const _session = await _client.session.get(_client.session.topics[0]);
const [namespace, chainId] = _session.state.accounts[0].split(":");
if (provider.session) {
console.log("provider.session", provider.session);
const session = provider.session;
const accounts = session.namespaces[Object.keys(session.namespaces)[0]].accounts;
const [namespace, chainId] = accounts[0].split(":");
const caipChainId = `${namespace}:${chainId}`;
onEnable(caipChainId);
setAccounts(accounts);
setSession(session);
setChain(caipChainId);
onSessionConnected(session!);
}
},
[session, onEnable],
[session, onSessionConnected],
);
useEffect(() => {
@ -213,16 +211,20 @@ export function ClientContextProvider({ children }: { children: ReactNode | Reac
}
}, [client, createClient]);
useEffect(() => {
if (cosmosProvider && web3Modal) _subscribeToProviderEvents(cosmosProvider);
}, [_subscribeToProviderEvents, cosmosProvider, web3Modal]);
useEffect(() => {
const getPersistedSession = async () => {
if (client && !hasCheckedPersistedSession) {
await _checkForPersistedSession(client);
if (cosmosProvider && !hasCheckedPersistedSession) {
await _checkForPersistedSession(cosmosProvider);
setHasCheckedPersistedSession(true);
}
};
getPersistedSession();
}, [client, _checkForPersistedSession, hasCheckedPersistedSession]);
}, [cosmosProvider, _checkForPersistedSession, hasCheckedPersistedSession]);
const value = useMemo(
() => ({

View File

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

View File

@ -9,7 +9,7 @@ import { STable } from "../components/shared";
import { SModalContainer, SModalTitle } from "./shared";
interface PairingModalProps {
pairings: PairingTypes.Settled[];
pairings: PairingTypes.Struct[];
connect: (pairing?: { topic: string }) => Promise<void>;
}

View File

@ -0,0 +1,3 @@
export default function FourOhFour() {
return <h1>404 Page Not Found</h1>;
}

View File

@ -0,0 +1,24 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { createGlobalStyle } from "styled-components";
import { ClientContextProvider } from "../contexts/ClientContext";
import Metadata from "../components/Metadata";
import { globalStyle } from "./../styles";
const GlobalStyle = createGlobalStyle`
${globalStyle}
`;
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Metadata />
<GlobalStyle />
<ClientContextProvider>
<Component {...pageProps} />
</ClientContextProvider>
</>
);
}
export default MyApp;

View File

@ -0,0 +1,3 @@
export default function Error() {
return <div>An error as occured</div>;
}

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { version } from "@walletconnect/client/package.json";
import { version } from "@walletconnect/sign-client/package.json";
import {
formatDirectSignDoc,
stringifySignDocValues,
@ -7,15 +7,15 @@ import {
verifyDirectSignature,
} from "cosmos-wallet";
import Banner from "./components/Banner";
import Blockchain from "./components/Blockchain";
import Column from "./components/Column";
import Header from "./components/Header";
import Modal from "./components/Modal";
import { DEFAULT_MAIN_CHAINS } from "./constants";
import { AccountAction } from "./helpers";
import RequestModal from "./modals/RequestModal";
import PingModal from "./modals/PingModal";
import Banner from "./../components/Banner";
import Blockchain from "./../components/Blockchain";
import Column from "./../components/Column";
import Header from "./../components/Header";
import Modal from "./../components/Modal";
import { DEFAULT_MAIN_CHAINS } from "./../constants";
import { AccountAction } from "./../helpers";
import RequestModal from "./../modals/RequestModal";
import PingModal from "./../modals/PingModal";
import {
SAccounts,
SAccountsContainer,
@ -23,8 +23,8 @@ import {
SContent,
SLanding,
SLayout,
} from "./components/app";
import { useWalletConnectClient } from "./contexts/ClientContext";
} from "./../components/app";
import { useWalletConnectClient } from "./../contexts/ClientContext";
interface IFormattedRpcResponse {
method?: string;
@ -72,8 +72,9 @@ export default function App() {
try {
setIsRpcRequestPending(true);
const _session = await client.session.get(client.session.topics[0]);
await client.session.ping(_session.topic);
const session = cosmosProvider?.session;
if (!session) return;
await cosmosProvider.client?.ping({ topic: session.topic! });
setRpcResult({
address: "",
method: "ping",
@ -198,7 +199,7 @@ export default function App() {
setRpcResult(result);
} catch (error) {
console.error("RPC request failed:", error);
setRpcResult({ result: error as string });
setRpcResult({ result: (error as Error).message as string });
} finally {
setIsRpcRequestPending(false);
}
@ -228,17 +229,7 @@ export default function App() {
<SLanding center>
<Banner />
<h6>
<span>{`Using v${version || "2.0.0-beta"}`}</span>
<sup>
(
<a
style={{ textDecoration: "underline" }}
href="https://github.com/WalletConnect/web-examples/tree/main/dapps/react-dapp-v2-cosmos-provider"
>
outdated
</a>{" "}
)
</sup>
<span>{`Using v${version}`}</span>
</h6>
<SButtonContainer>
<h6>Select Cosmos chain:</h6>

View File

@ -0,0 +1,129 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}

View File

@ -0,0 +1,26 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}

View File

@ -1,26 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "preserve",
"incremental": true
},
"include": [
"src"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff