Skip to content
This repository was archived by the owner on Apr 16, 2024. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INFURA_API_KEY=<api-key>
4 changes: 4 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
"@graphql-codegen/typescript-operations": "^2.5.3",
"@kleros/kleros-v2-contracts": "workspace:^",
"@kleros/ui-components-library": "^1.7.0",
"@web3-react/core": "8.0.36-beta.0",
"@web3-react/metamask": "8.0.31-beta.0",
"@web3-react/network": "8.0.28-beta.0",
"@web3-react/types": "8.0.21-beta.0",
"chart.js": "^3.9.1",
"chartjs-adapter-moment": "^1.0.0",
"core-js": "^3.21.1",
Expand Down
94 changes: 92 additions & 2 deletions web/src/components/ConnectButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,96 @@
import React from "react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { useWeb3React, Web3ReactHooks } from "@web3-react/core";

import { Button } from "@kleros/ui-components-library";
import { injectedConnection, networkConnection } from "../connections";
import { getConnection, isNetworkType } from "../connections/utils";
import { getChainNameById } from "consts/supportedChains";

const ConnectButton: React.FC = () => {
const { isActive, connector } = useWeb3React();

const ConnectButton: React.FC = () => <Button small text={"Connect"} />;
return isActive && !isNetworkType(connector) ? (
<NetworkDisplay />
) : (
<Button
small
text={"Connect"}
onClick={() => injectedConnection.connector.activate()}
/>
);
};

export default ConnectButton;

const NetworkDisplay = () => {
const { chainId, isActivating, isActive, connector } = useWeb3React();
const connectionType = getConnection(connector).type;

useEffect(() => {
if (chainId && connectionType !== networkConnection.type) {
networkConnection.connector.activate(chainId);
}
}, [chainId, connectionType, connector]);

return (
<div>
<NetworkStatus {...{ chainId, isActivating, isActive }} />
</div>
);
};

interface INetworkStatus {
isActivating: ReturnType<Web3ReactHooks["useIsActivating"]>;
isActive: ReturnType<Web3ReactHooks["useIsActive"]>;
chainId: ReturnType<Web3ReactHooks["useChainId"]>;
error?: Error;
}

const NetworkStatus = ({
isActivating,
isActive,
chainId,
error,
}: INetworkStatus) => {
const chainName = getChainNameById(chainId);
return (
<Container>
{error ? (
<div className="network-badge">
<Dot color="error" />
{error.name ?? "Error"}
</div>
) : isActivating ? (
<div className="network-badge">
<Dot color="warning" />
{chainName}
</div>
) : (
isActive && (
<div className="network-badge">
<Dot color="success" />
{chainName}
</div>
)
)}
</Container>
);
};

const Dot = styled.div<{ color: string }>`
width: 6px;
height: 6px;
border-radius: 50%;
background-color: ${(props) => props.theme[props.color]};
`;

const Container = styled.div`
color: white;

.network-badge {
display: flex;
align-items: center;
gap: 5px;
}
`;
49 changes: 32 additions & 17 deletions web/src/components/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
import React from "react";
import styled from "styled-components";

enum StatusColors {
Submitted = "primaryBlue",
Challenged = "warning",
Pending = "stroke",
Appealed = "error",
Registered = "success",
Removed = "secondaryText",
export interface Status {
color: string;
label: string;
}
interface IStatusBadgeProps {
status: keyof typeof StatusColors | string;

interface StatusColors {
[key: string]: Status;
}

const DEFAULT_STATUS_COLORS: StatusColors = {
Submitted: { color: "primaryBlue", label: "Submitted" },
Challenged: { color: "warning", label: "Challenged" },
Pending: { color: "stroke", label: "Pending" },
Appealed: { color: "error", label: "Appealed" },
Registered: { color: "success", label: "Registered" },
Removed: { color: "secondaryText", label: "Removed" },
};

interface StatusBadgeProps {
status: keyof StatusColors | string;
statusColors?: StatusColors;
}

const StatusBadge: React.FC<IStatusBadgeProps> = ({ status }) => {
let color: string = StatusColors.Submitted;
const StatusBadge: React.FC<StatusBadgeProps> = ({
status,
statusColors = DEFAULT_STATUS_COLORS,
}) => {
const color: string =
statusColors[status]?.color || statusColors.Submitted.color;
let label: string =
statusColors[status]?.label || statusColors.Submitted.label;

if (
typeof status === "string" &&
StatusColors[status as keyof typeof StatusColors]
) {
color = StatusColors[status as keyof typeof StatusColors];
if (typeof status === "string" && !statusColors[status]) {
label = status;
}

return (
<Container color={color}>
<Dot color={color} />
<p>{status}</p>
<p>{label}</p>
</Container>
);
};
Expand Down
14 changes: 14 additions & 0 deletions web/src/connections/IConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Web3ReactHooks } from "@web3-react/core";
import { Connector } from "@web3-react/types";

export interface Connection {
connector: Connector;
hooks: Web3ReactHooks;
type: ConnectionType;
priority: number;
}

export enum ConnectionType {
INJECTED = "INJECTED",
NETWORK = "NETWORK",
}
8 changes: 8 additions & 0 deletions web/src/connections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { injectedConnection } from "./metaMask";
import { networkConnection } from "./network";

const CONNECTIONS = [injectedConnection, networkConnection];
export default CONNECTIONS;

export * from "./network";
export * from "./metaMask";
18 changes: 18 additions & 0 deletions web/src/connections/metaMask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { initializeConnector } from "@web3-react/core";
import { MetaMask } from "@web3-react/metamask";
import { Connection, ConnectionType } from "./IConnection";

const onError = (error: Error) => {
console.debug(`web3-react error: ${error}`);
};

const [metaMask, hooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask({ actions, onError })
);

export const injectedConnection: Connection = {
connector: metaMask,
hooks: hooks,
type: ConnectionType.INJECTED,
priority: 1,
};
17 changes: 17 additions & 0 deletions web/src/connections/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { initializeConnector } from "@web3-react/core";
import { Network } from "@web3-react/network";

import { RPC_PROVIDERS } from "consts/providers";
import { Connection, ConnectionType } from "./IConnection";

const [network, hooks] = initializeConnector<Network>(
(actions) =>
new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 5 })
);

export const networkConnection: Connection = {
connector: network,
hooks: hooks,
type: ConnectionType.NETWORK,
priority: -1,
};
47 changes: 47 additions & 0 deletions web/src/connections/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Connector } from "@web3-react/types";
import CONNECTIONS from ".";
import { Connection, ConnectionType } from "./IConnection";
import { injectedConnection } from "./metaMask";
import { networkConnection } from "./network";

export function getConnection(c: Connector | ConnectionType) {
if (c instanceof Connector) {
const connection = CONNECTIONS.find(
(connection) => connection.connector === c
);
if (!connection) {
throw Error("unsupported connector");
}
return connection;
} else {
switch (c) {
case ConnectionType.INJECTED:
return injectedConnection;
case ConnectionType.NETWORK:
return networkConnection;
}
}
}

export function getConnectionName(
connectionType: ConnectionType,
hasMetaMaskExtension: boolean = isMetaMaskWallet()
) {
switch (connectionType) {
case ConnectionType.INJECTED:
return hasMetaMaskExtension ? "MetaMask" : "Browser Wallet";
case ConnectionType.NETWORK:
return "Network";
}
}

export const getOrderedConnections = (): Connection[] =>
[...CONNECTIONS].sort((a, b) => b.priority - a.priority);

export const isInjected = (): boolean => Boolean(window.ethereum);

export const isMetaMaskWallet = (): boolean =>
window.ethereum?.isMetaMask ?? false;

export const isNetworkType = (connector: Connector): boolean =>
getConnection(connector).type === networkConnection.type;
77 changes: 77 additions & 0 deletions web/src/consts/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { deepCopy } from "@ethersproject/properties";
// This is the only file which should instantiate new Providers.
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from "@ethersproject/providers";
import {
getChainNameById,
RPC_URLS,
SupportedChainId,
} from "./supportedChains";

const AVERAGE_L1_BLOCK_TIME_MS = 1200;

class AppJsonRpcProvider extends StaticJsonRpcProvider {
private _blockCache = new Map<string, Promise<any>>();
get blockCache() {
// If the blockCache has not yet been initialized this block, do so by
// setting a listener to clear it on the next block.
if (!this._blockCache.size) {
this.once("block", () => this._blockCache.clear());
}
return this._blockCache;
}

constructor(chainId: SupportedChainId) {
// Including networkish allows ethers to skip the initial detectNetwork call.
super(
RPC_URLS[chainId][0],
/* networkish= */ { chainId, name: getChainNameById[chainId] }
);

// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppJsonRpcProvider) need to update less frequently to be considered responsive.
this.pollingInterval = AVERAGE_L1_BLOCK_TIME_MS;
}

send(method: string, params: Array<any>): Promise<any> {
// Only cache eth_call's.
if (method !== "eth_call") return super.send(method, params);

// Only cache if params are serializable.
let key: string;
try {
key = `call:${JSON.stringify(params)}`;
} catch (error) {
console.warn(
"Cannot cache eth_call request because the params are not serializable"
);
return super.send(method, params);
}

const cached = this.blockCache.get(key);
if (cached) {
this.emit("debug", {
action: "request",
request: deepCopy({ method, params, id: "cache" }),
provider: this,
});
return cached;
}

const result = super.send(method, params);
this.blockCache.set(key, result);
return result;
}
}

/**
* These are the only JsonRpcProviders used directly by the interface.
*/
export const RPC_PROVIDERS: {
[key in SupportedChainId]: StaticJsonRpcProvider;
} = {
[SupportedChainId.MAINNET]: new AppJsonRpcProvider(SupportedChainId.MAINNET),
[SupportedChainId.GNOSIS]: new AppJsonRpcProvider(SupportedChainId.GNOSIS),
[SupportedChainId.GOERLI]: new AppJsonRpcProvider(SupportedChainId.GOERLI),
};
Loading