feat(#807): adds app for validator add and remove signer (#1402)

* Feat/807: ABI and classes for the contract methods

* Feat/807: Added a new multisig-signer app

* Feat/807: Added a new multisig-signer app

* Feat/800: Untested signer forms

* Feat/800: Moved reused bg video into ui-toolkit to use in multisig-signer project, and cleaned up some spacing that was overlooked in the stats theme changes

* Feat/800: Componentised a bit, made the app look ok

* Feat/800: Linting, prettifying, removing some unneeded tests, ensuring e2e tests run

* Feat/800: Bit of translation

* chore: fix type errors

* chore: some parts error handling

* feat: handle error and not found cases

* feat: add changes to remove signer form as well

* chore: rename component

* chore: fix type issues

* feat: add web3 connector logic

* feat: allow disconnecting and show connected eth wallet info

* Feat/800: Removed unused 'useApolloClient'

* Feat/800: Ensure bundle.nonce and bundle.signatures have '0x' prepended

* Feat/800: Removed unused e2e directory

* Feat/800: Removed unnecessary app test

* Feat/800: Removed unnecessary router

* Feat/800: Capturing GQL errors in Sentry

* Feat/800: Removing references to the unused e2e test directory

* Feat/807: Consistent react hook imports

* Feat/807: Removed unnecessary spreads

Co-authored-by: Dexter <dexter.edwards93@gmail.com>
This commit is contained in:
Sam Keen 2022-09-29 20:10:53 +01:00 committed by GitHub
parent 8fee3ca080
commit 4272fefb0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1596 additions and 10 deletions

View File

@ -30,6 +30,10 @@ An application for the status of the Vega network. Showing block height and othe
Hosting for static content being shared across apps, for example fonts.
### [Multisig-signer](./apps/multisig-signer)
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit)

View File

@ -0,0 +1,11 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@ -0,0 +1,4 @@
NX_VEGA_URL=https://api.n01.stagnet3.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
NX_VEGA_ENV=STAGNET3

View File

@ -0,0 +1,5 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
NX_VEGA_ENV=DEVNET

View File

@ -0,0 +1,5 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_URL=https://api.vega.xyz/query
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
NX_VEGA_ENV=MAINNET

View File

@ -0,0 +1,5 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_URL=https://api.n01.stagnet3.vega.xyz/graphql
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
NX_VEGA_ENV=STAGNET3

View File

@ -0,0 +1,5 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
NX_VEGA_ENV=TESTNET

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,46 @@
## Multisig-signer
## Development
First copy the configuration of the application you are starting:
```bash
cp .env.[environment] .env.local
```
Starting the app:
```bash
yarn nx serve multisig-signer
```
### Configuration
Example configurations are provided here:
- [Mainnet](./.env.mainnet)
- [Devnet](./.env.devnet)
- [Capsule](./.env.capsule)
- [Testnet](./.env.testnet)
- [Stagnet3](./.env.stagnet3)
For convenience, you can boot the app injecting one of the configurations above by running:
```bash
yarn nx run multisig-signer:serve --env={env} # e.g. stagnet3
```
There are a few different configuration options offered for this app:
| **Flag** | **Purpose** |
| -------------------------------- | ---------------------------------------------------------------------------------------------------- | --- | |
| `NX_VEGA_URL` | The GraphQl query endpoint of a [Vega data node](https://github.com/vegaprotocol/networks#data-node) |
| `NX_VEGA_ENV` | The name of the currently connected vega environment |
## Testing
To run the minimal set of unit tests, run the following:
```bash
yarn nx test multisig-signer
```

5
apps/multisig-signer/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="react-scripts" />
interface Window {
_env_?: Record<string, string>;
}

View File

@ -0,0 +1,12 @@
/* eslint-disable */
export default {
displayName: 'multisig-signer',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/multisig-signer',
setupFilesAfterEnv: ['./src/app/setup-tests.ts'],
};

View File

@ -0,0 +1,4 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@ -0,0 +1,10 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -0,0 +1,80 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/multisig-signer/src",
"projectType": "application",
"targets": {
"build": {
"executor": "./tools/executors/webpack:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/multisig-signer",
"index": "apps/multisig-signer/src/index.html",
"baseHref": "/",
"main": "apps/multisig-signer/src/main.tsx",
"polyfills": "apps/multisig-signer/src/polyfills.ts",
"tsConfig": "apps/multisig-signer/tsconfig.app.json",
"assets": ["apps/multisig-signer/src/assets"],
"styles": ["apps/multisig-signer/src/styles.css"],
"scripts": [],
"webpackConfig": "apps/multisig-signer/webpack.config.js"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/multisig-signer/src/environments/environment.ts",
"with": "apps/multisig-signer/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
},
"serve": {
"executor": "./tools/executors/webpack:serve",
"options": {
"port": 3000,
"buildTarget": "multisig-signer:build:development",
"hmr": true
},
"configurations": {
"production": {
"buildTarget": "multisig-signer:build:production",
"hmr": false
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/multisig-signer/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/multisig-signer"],
"options": {
"jestConfig": "apps/multisig-signer/jest.config.ts",
"passWithNoTests": true
}
},
"build-netlify": {
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"cp apps/multisig-signer/netlify.toml netlify.toml",
"nx build multisig-signer"
]
}
}
},
"tags": []
}

View File

@ -0,0 +1,108 @@
import * as Sentry from '@sentry/react';
import classnames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { BrowserTracing } from '@sentry/tracing';
import {
EnvironmentProvider,
NetworkLoader,
useEnvironment,
} from '@vegaprotocol/environment';
import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit';
import type { EthereumConfig } from '@vegaprotocol/web3';
import { useEthereumConfig, Web3Provider } from '@vegaprotocol/web3';
import { ThemeContext, useThemeSwitcher, t } from '@vegaprotocol/react-helpers';
import { createClient } from './lib/apollo-client';
import { ENV } from './config/env';
import { ContractsProvider } from './config/contracts/contracts-provider';
import {
AddSignerForm,
RemoveSignerForm,
Header,
ContractDetails,
} from './components';
import { createConnectors } from './lib/web3-connectors';
import { Web3Connector } from './components/web3-connector';
import { EthWalletContainer } from './components/eth-wallet-container';
import { useWeb3React } from '@web3-react/core';
const pageWrapperClasses = classnames(
'min-h-screen w-screen',
'grid grid-rows-[auto,1fr]',
'bg-white dark:bg-black',
'text-neutral-900 dark:text-neutral-100'
);
const ConnectedApp = ({ config }: { config: EthereumConfig | null }) => {
const { account, connector } = useWeb3React();
return (
<main className="w-full max-w-3xl px-5 justify-self-center">
<h1>{t('Multisig signer')}</h1>
<div className="mb-8">
<p>
Connected to Eth wallet: <Lozenge>{account}</Lozenge>
</p>
<Button onClick={() => connector.deactivate()}>Disconnect</Button>
</div>
<ContractDetails config={config} />
<h2>{t('Add or remove signer')}</h2>
<AddSignerForm />
<RemoveSignerForm />
</main>
);
};
function App() {
const { VEGA_ENV, ETHEREUM_PROVIDER_URL } = useEnvironment();
const { config, loading, error } = useEthereumConfig();
const [dialogOpen, setDialogOpen] = useState(false);
const [theme, toggleTheme] = useThemeSwitcher();
useEffect(() => {
Sentry.init({
dsn: ENV.dsn,
integrations: [new BrowserTracing()],
tracesSampleRate: 1,
environment: VEGA_ENV,
});
}, [VEGA_ENV]);
const Connectors = useMemo(() => {
if (config?.chain_id) {
return createConnectors(ETHEREUM_PROVIDER_URL, Number(config.chain_id));
}
return [];
}, [config?.chain_id, ETHEREUM_PROVIDER_URL]);
return (
<ThemeContext.Provider value={theme}>
<Web3Provider connectors={Connectors}>
<Web3Connector dialogOpen={dialogOpen} setDialogOpen={setDialogOpen}>
<div className={pageWrapperClasses}>
<AsyncRenderer loading={loading} data={config} error={error}>
<Header theme={theme} toggleTheme={toggleTheme} />
<EthWalletContainer
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
>
<ConnectedApp config={config} />
</EthWalletContainer>
</AsyncRenderer>
</div>
</Web3Connector>
</Web3Provider>
</ThemeContext.Provider>
);
}
const Wrapper = () => {
return (
<EnvironmentProvider>
<NetworkLoader createClient={createClient}>
<ContractsProvider>
<App />
</ContractsProvider>
</NetworkLoader>
</EnvironmentProvider>
);
};
export default Wrapper;

View File

@ -0,0 +1,45 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: AddSignerBundle
// ====================================================
export interface AddSignerBundle_erc20MultiSigSignerAddedBundles_edges_node {
__typename: "ERC20MultiSigSignerAddedBundle";
/**
* The ethereum address of the signer to be added
*/
newSigner: string;
/**
* The nonce used in the signing operation
*/
nonce: string;
/**
* The bundle of signatures from current validators to sign in the new signer
*/
signatures: string;
}
export interface AddSignerBundle_erc20MultiSigSignerAddedBundles_edges {
__typename: "ERC20MultiSigSignerAddedBundleEdge";
node: AddSignerBundle_erc20MultiSigSignerAddedBundles_edges_node;
}
export interface AddSignerBundle_erc20MultiSigSignerAddedBundles {
__typename: "ERC20MultiSigSignerAddedConnection";
edges: (AddSignerBundle_erc20MultiSigSignerAddedBundles_edges | null)[] | null;
}
export interface AddSignerBundle {
/**
* Get the signature bundle to add a particular validator to the signer list of the multisig contract
*/
erc20MultiSigSignerAddedBundles: AddSignerBundle_erc20MultiSigSignerAddedBundles;
}
export interface AddSignerBundleVariables {
nodeId: string;
}

View File

@ -0,0 +1,48 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: RemoveSignerBundle
// ====================================================
export interface RemoveSignerBundle_erc20MultiSigSignerRemovedBundles_edges_node {
__typename: "ERC20MultiSigSignerRemovedBundle";
/**
* The ethereum address of the signer to be removed
*/
oldSigner: string;
/**
* The nonce used in the signing operation
*/
nonce: string;
/**
* The bundle of signatures from current validators to sign in the new signer
*/
signatures: string;
}
export interface RemoveSignerBundle_erc20MultiSigSignerRemovedBundles_edges {
__typename: "ERC20MultiSigSignerRemovedBundleEdge";
node: RemoveSignerBundle_erc20MultiSigSignerRemovedBundles_edges_node;
}
export interface RemoveSignerBundle_erc20MultiSigSignerRemovedBundles {
__typename: "ERC20MultiSigSignerRemovedConnection";
/**
* The list of signer bundles for that validator
*/
edges: (RemoveSignerBundle_erc20MultiSigSignerRemovedBundles_edges | null)[] | null;
}
export interface RemoveSignerBundle {
/**
* Get the signatures bundle to remove a particular validator from signer list of the multisig contract
*/
erc20MultiSigSignerRemovedBundles: RemoveSignerBundle_erc20MultiSigSignerRemovedBundles;
}
export interface RemoveSignerBundleVariables {
nodeId: string;
}

View File

@ -0,0 +1,121 @@
import { useState } from 'react';
import { gql, useLazyQuery } from '@apollo/client';
import { captureException } from '@sentry/react';
import { t } from '@vegaprotocol/react-helpers';
import { useEthereumTransaction } from '@vegaprotocol/web3';
import {
FormGroup,
Input,
Button,
InputError,
Loader,
} from '@vegaprotocol/ui-toolkit';
import { prepend0x } from '@vegaprotocol/smart-contracts';
import { useContracts } from '../../config/contracts/contracts-context';
import type { FormEvent } from 'react';
import type {
AddSignerBundle,
AddSignerBundleVariables,
} from '../__generated__/AddSignerBundle';
import type { MultisigControl } from '@vegaprotocol/smart-contracts';
export const ADD_SIGNER_QUERY = gql`
query AddSignerBundle($nodeId: ID!) {
erc20MultiSigSignerAddedBundles(nodeId: $nodeId) {
edges {
node {
newSigner
nonce
signatures
}
}
}
}
`;
export const AddSignerForm = () => {
const { multisig } = useContracts();
const [address, setAddress] = useState('');
const [bundleNotFound, setBundleNotFound] = useState(false);
const [runQuery, { data, error, loading }] = useLazyQuery<
AddSignerBundle,
AddSignerBundleVariables
>(ADD_SIGNER_QUERY);
const { perform, Dialog } = useEthereumTransaction<
MultisigControl,
'add_signer'
>(multisig, 'add_signer');
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setBundleNotFound(false);
try {
if (address === '') {
return;
}
await runQuery({
variables: { nodeId: address },
});
const bundle = data?.erc20MultiSigSignerAddedBundles?.edges?.[0]?.node;
if (!bundle) {
if (!error) {
setBundleNotFound(true);
}
return;
}
await perform(
bundle.newSigner,
bundle.nonce.startsWith('0x') ? bundle.nonce : prepend0x(bundle.nonce),
bundle.signatures.startsWith('0x')
? bundle.signatures
: prepend0x(bundle.signatures)
);
} catch (err: unknown) {
captureException(err);
}
};
return (
<form onSubmit={(e) => handleSubmit(e)}>
<FormGroup
label={t('Add signer')}
labelFor="add-signer-input"
labelDescription={t('Public key of the signer to add')}
className="max-w-xl"
>
<div className="grid grid-cols-[1fr,auto] gap-2">
<Input
id="add-signer-input"
onChange={(e) => setAddress(e.target.value)}
data-testid="add-signer-input-input"
/>
<Button
type="submit"
data-testid="add-signer-submit"
disabled={loading}
>
{loading ? <Loader size="small" /> : t('Add')}
</Button>
</div>
<div>
{error && (
<InputError intent="danger">
{error?.message.includes('InvalidArgument')
? t('Invalid node id')
: error?.message}
</InputError>
)}
{bundleNotFound && !error && (
<InputError intent="danger">
{t(
'Bundle was not found, are you sure this validator needs to be added?'
)}
</InputError>
)}
</div>
</FormGroup>
<Dialog />
</form>
);
};

View File

@ -0,0 +1 @@
export * from './add-signer-form';

View File

@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import { useContracts } from '../../config/contracts/contracts-context';
import type { EthereumConfig } from '@vegaprotocol/web3';
interface ContractDetailsProps {
config: EthereumConfig | null;
}
export const ContractDetails = ({ config }: ContractDetailsProps) => {
const { multisig } = useContracts();
const [validSignerCount, setValidSignerCount] = useState(undefined);
useEffect(() => {
(async () => {
try {
const res = await multisig.get_valid_signer_count();
setValidSignerCount(res);
} catch (err) {
Sentry.captureException(err);
}
})();
}, [multisig]);
return (
<div className="mb-8">
<p>
Multisig contract address:{' '}
<Lozenge>{config?.multisig_control_contract?.address}</Lozenge>
</p>
<p>Valid signer count: {validSignerCount}</p>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './contract-details';

View File

@ -0,0 +1,25 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react';
export const EthWalletContainer = ({
dialogOpen,
setDialogOpen,
children,
}: {
dialogOpen: boolean;
setDialogOpen: (open: boolean) => void;
children: ReactElement;
}) => {
const { account } = useWeb3React();
if (!account) {
return (
<div className="w-full text-center">
<Button onClick={() => setDialogOpen(true)}>
Connect Ethereum Wallet
</Button>
</div>
);
}
return children;
};

View File

@ -0,0 +1 @@
export * from './eth-wallet-container';

View File

@ -0,0 +1,24 @@
import {
BackgroundVideo,
ThemeSwitcher,
VegaLogo,
} from '@vegaprotocol/ui-toolkit';
interface HeaderProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Header = ({ theme, toggleTheme }: HeaderProps) => {
return (
<header className="relative overflow-hidden py-2 mb-8">
<BackgroundVideo />
<div className="relative flex justify-center px-2 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-5 flex items-center justify-between">
<VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
</div>
</div>
</header>
);
};

View File

@ -0,0 +1 @@
export * from './header';

View File

@ -0,0 +1,4 @@
export * from './add-signer-form';
export * from './remove-signer-form';
export * from './header';
export * from './contract-details';

View File

@ -0,0 +1 @@
export * from './remove-signer-form';

View File

@ -0,0 +1,121 @@
import { useState } from 'react';
import { gql, useLazyQuery } from '@apollo/client';
import { captureException } from '@sentry/react';
import { t } from '@vegaprotocol/react-helpers';
import { useEthereumTransaction } from '@vegaprotocol/web3';
import {
FormGroup,
Input,
Button,
InputError,
Loader,
} from '@vegaprotocol/ui-toolkit';
import { prepend0x } from '@vegaprotocol/smart-contracts';
import { useContracts } from '../../config/contracts/contracts-context';
import type { FormEvent } from 'react';
import type {
RemoveSignerBundle,
RemoveSignerBundleVariables,
} from '../__generated__/RemoveSignerBundle';
import type { MultisigControl } from '@vegaprotocol/smart-contracts';
const REMOVE_SIGNER_QUERY = gql`
query RemoveSignerBundle($nodeId: ID!) {
erc20MultiSigSignerRemovedBundles(nodeId: $nodeId) {
edges {
node {
oldSigner
nonce
signatures
}
}
}
}
`;
export const RemoveSignerForm = () => {
const { multisig } = useContracts();
const [address, setAddress] = useState('');
const [bundleNotFound, setBundleNotFound] = useState(false);
const [runQuery, { data, error, loading }] = useLazyQuery<
RemoveSignerBundle,
RemoveSignerBundleVariables
>(REMOVE_SIGNER_QUERY);
const { perform, Dialog } = useEthereumTransaction<
MultisigControl,
'remove_signer'
>(multisig, 'remove_signer');
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setBundleNotFound(false);
try {
if (address === '') {
return;
}
await runQuery({
variables: { nodeId: address },
});
const bundle = data?.erc20MultiSigSignerRemovedBundles?.edges?.[0]?.node;
if (!bundle) {
if (!error) {
setBundleNotFound(true);
}
return;
}
await perform(
bundle.oldSigner,
bundle.nonce.startsWith('0x') ? bundle.nonce : prepend0x(bundle.nonce),
bundle.signatures.startsWith('0x')
? bundle.signatures
: prepend0x(bundle.signatures)
);
} catch (err: unknown) {
captureException(err);
}
};
return (
<form onSubmit={(e) => handleSubmit(e)}>
<FormGroup
label={t('Remove signer')}
labelFor="remove-signer-input"
labelDescription={t('Public key of the signer to remove')}
className="max-w-xl"
>
<div className="grid grid-cols-[1fr,auto] gap-2">
<Input
id="remove-signer-input"
onChange={(e) => setAddress(e.target.value)}
data-testid="remove-signer-input-input"
/>
<Button
type="submit"
data-testid="remove-signer-submit"
disabled={loading}
>
{loading ? <Loader size="small" /> : t('Remove')}
</Button>
</div>
<div>
{error && (
<InputError intent="danger">
{error?.message.includes('InvalidArgument')
? t('Invalid node id')
: error?.message}
</InputError>
)}
{bundleNotFound && !error && (
<InputError intent="danger">
{t(
'Bundle was not found, are you sure this validator needs to be removed?'
)}
</InputError>
)}
</div>
</FormGroup>
<Dialog />
</form>
);
};

View File

@ -0,0 +1 @@
export * from './web3-connector';

View File

@ -0,0 +1,94 @@
import { useEnvironment } from '@vegaprotocol/environment';
import { useEthereumConfig } from '@vegaprotocol/web3';
import { Button, Splash, AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Web3ConnectDialog } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react';
import { useEffect, useMemo } from 'react';
import { createConnectors } from '../../lib/web3-connectors';
interface Web3ConnectorProps {
children: ReactElement;
dialogOpen: boolean;
setDialogOpen: (open: boolean) => void;
}
export function Web3Connector({
children,
dialogOpen,
setDialogOpen,
}: Web3ConnectorProps) {
const { ETHEREUM_PROVIDER_URL } = useEnvironment();
const { config, loading, error } = useEthereumConfig();
const Connectors = useMemo(() => {
if (config?.chain_id) {
return createConnectors(ETHEREUM_PROVIDER_URL, Number(config.chain_id));
}
return undefined;
}, [config?.chain_id, ETHEREUM_PROVIDER_URL]);
const appChainId = Number(config?.chain_id);
return (
<AsyncRenderer loading={loading} error={error} data={config}>
<Web3Content appChainId={appChainId} setDialogOpen={setDialogOpen}>
{children}
</Web3Content>
{Connectors && (
<Web3ConnectDialog
connectors={Connectors}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
desiredChainId={appChainId}
/>
)}
</AsyncRenderer>
);
}
interface Web3ContentProps {
children: ReactElement;
appChainId: number;
setDialogOpen: (isOpen: boolean) => void;
}
export const Web3Content = ({
children,
appChainId,
setDialogOpen,
}: Web3ContentProps) => {
const { error, connector, chainId } = useWeb3React();
useEffect(() => {
if (connector?.connectEagerly) {
connector.connectEagerly();
}
// wallet connect doesnt handle connectEagerly being called when connector is also in the
// deps array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connector]);
if (error) {
return (
<Splash>
<div className="flex flex-col items-center gap-12">
<p className="text-white">Something went wrong: {error.message}</p>
<Button onClick={() => connector.deactivate()}>Disconnect</Button>
</div>
</Splash>
);
}
if (chainId !== undefined && chainId !== appChainId) {
return (
<Splash>
<div className="flex flex-col items-center gap-12">
<p className="text-white">
This app only works on chain ID: {appChainId}
</p>
<Button onClick={() => connector.deactivate()}>Disconnect</Button>
</div>
</Splash>
);
}
return children;
};

View File

@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';
import type { MultisigControl } from '@vegaprotocol/smart-contracts';
export interface ContractsContextShape {
multisig: MultisigControl;
}
export const ContractsContext = createContext<
ContractsContextShape | undefined
>(undefined);
export function useContracts() {
const context = useContext(ContractsContext);
if (context === undefined) {
throw new Error('useContracts must be used within ContractsProvider');
}
return context;
}

View File

@ -0,0 +1,60 @@
import { useEffect, useState } from 'react';
import { MultisigControl } from '@vegaprotocol/smart-contracts';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { ethers } from 'ethers';
import type { ContractsContextShape } from './contracts-context';
import { ContractsContext } from './contracts-context';
import { useEthereumConfig } from '@vegaprotocol/web3';
import { useEnvironment } from '@vegaprotocol/environment';
/**
* Provides Vega Ethereum contract instances to its children.
*/
export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
const { config } = useEthereumConfig();
const { VEGA_ENV, ETHEREUM_PROVIDER_URL } = useEnvironment();
const [contracts, setContracts] = useState<ContractsContextShape | null>(
null
);
// Create instances of contract classes. If we have an account use a signer for the
// contracts so that we can sign transactions, otherwise use the provider for just
// reading data
useEffect(() => {
let cancelled = false;
const run = async () => {
if (config) {
const provider = new ethers.providers.JsonRpcProvider(
ETHEREUM_PROVIDER_URL,
Number(config.chain_id)
);
if (provider && config) {
if (!cancelled) {
setContracts({
multisig: new MultisigControl(
config.multisig_control_contract.address,
provider
),
});
}
}
}
};
run();
return () => {
// TODO: hacky quick fix for release to prevent race condition, find a better fix for this.
cancelled = true;
};
}, [config, VEGA_ENV, ETHEREUM_PROVIDER_URL]);
if (!contracts) {
return <Splash>Error: cannot get data on multisig contract</Splash>;
}
return (
<ContractsContext.Provider value={contracts}>
{children}
</ContractsContext.Provider>
);
};

View File

@ -0,0 +1,12 @@
const windowOrDefault = (key: string) => {
if (window._env_ && window._env_[key]) {
return window._env_[key] as string;
}
return (process.env[key] as string) || '';
};
export const ENV = {
dsn: windowOrDefault('NX_SENTRY_DSN'),
flags: {},
dataSources: {},
};

View File

@ -0,0 +1,3 @@
import { ENV } from './env';
export default ENV.flags;

View File

@ -0,0 +1,3 @@
import { ENV } from './env';
export const DATA_SOURCES = ENV.dataSources;

View File

@ -0,0 +1,54 @@
import * as Sentry from '@sentry/react';
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
export function createClient(base?: string) {
if (!base) {
throw new Error('Base must be passed into createClient!');
}
const urlHTTP = new URL(base);
const urlWS = new URL(base);
// Replace http with ws, preserving if its a secure connection eg. https => wss
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
const cache = new InMemoryCache({
typePolicies: {
Query: {},
Account: {
keyFields: false,
fields: {
balanceFormatted: {},
},
},
Node: {
keyFields: false,
},
},
});
const retryLink = new RetryLink({
delay: {
initial: 300,
max: 10000,
jitter: true,
},
});
const httpLink = new HttpLink({
uri: urlHTTP.href,
credentials: 'same-origin',
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
console.log(graphQLErrors);
console.log(networkError);
Sentry.captureException(graphQLErrors);
});
return new ApolloClient({
connectToDevTools: process.env['NODE_ENV'] === 'development',
link: from([errorLink, retryLink, httpLink]),
cache,
});
}

View File

@ -0,0 +1,34 @@
import { ethers } from 'ethers';
import type { Web3ReactHooks } from '@web3-react/core';
import { initializeConnector } from '@web3-react/core';
import { MetaMask } from '@web3-react/metamask';
import { WalletConnect } from '@web3-react/walletconnect';
import type { Connector } from '@web3-react/types';
const [metamask, metamaskHooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask(actions)
);
export const createDefaultProvider = (providerUrl: string, chainId: number) => {
return new ethers.providers.JsonRpcProvider(providerUrl, chainId);
};
export const createConnectors = (providerUrl: string, chainId: number) => {
if (isNaN(chainId)) {
throw new Error('Invalid Ethereum chain ID for environment');
}
const [walletconnect, walletconnectHooks] =
initializeConnector<WalletConnect>(
(actions) =>
new WalletConnect(actions, {
rpc: {
[chainId]: providerUrl,
},
}),
[chainId]
);
return [
[metamask, metamaskHooks],
[walletconnect, walletconnectHooks],
] as [Connector, Web3ReactHooks][];
};

View File

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

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

View File

@ -0,0 +1 @@
window._env_ = {};

View File

@ -0,0 +1,3 @@
export const environment = {
production: true,
};

View File

@ -0,0 +1,6 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Multisig Signer</title>
<base href="/" />
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<script src="./assets/env-config.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,14 @@
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './app/app';
import { StrictMode } from 'react';
const rootElement = document.getElementById('root');
const root = rootElement && createRoot(rootElement);
root?.render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';

View File

@ -0,0 +1,16 @@
/* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply text-2xl uppercase mb-4;
}
h2 {
@apply text-xl mb-4;
}
p {
@apply mb-2;
}
}

View File

@ -0,0 +1,17 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
module.exports = {
content: [
join(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
'libs/ui-toolkit/src/utils/shared.ts',
...createGlobPatternsForDependencies(__dirname),
],
darkMode: 'class',
theme: {
extend: theme,
},
plugins: [vegaCustomClasses],
};

View File

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"lib": ["es5", "es6", "dom", "dom.iterable"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,24 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node", "@testing-library/jest-dom"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts",
"jest.config.ts"
],
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
]
}

View File

@ -0,0 +1,17 @@
const SentryPlugin = require('@sentry/webpack-plugin');
module.exports = (config, context) => {
const additionalPlugins = process.env.SENTRY_AUTH_TOKEN
? [
new SentryPlugin({
include: './dist/apps/multisig-signer',
project: 'multisig-signer',
}),
]
: [];
return {
...config,
plugins: [...additionalPlugins, ...config.plugins],
};
};

View File

@ -11,10 +11,10 @@ function App() {
return (
<ThemeContext.Provider value={theme}>
<NetworkLoader createClient={createClient}>
<div className="w-screen min-h-screen grid pb-24 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100">
<div className="w-screen min-h-screen grid pb-6 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100">
<div className="layout-grid w-screen justify-self-center">
<Header theme={theme} toggleTheme={toggleTheme} />
<StatsManager className="max-w-3xl px-24" />
<StatsManager className="max-w-3xl px-6" />
</div>
</div>
</NetworkLoader>

View File

@ -1,5 +1,8 @@
import { VegaLogo, ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { VegaBackgroundVideo } from '../videos';
import {
BackgroundVideo,
VegaLogo,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
interface ThemeToggleProps {
theme: 'light' | 'dark';
@ -8,11 +11,11 @@ interface ThemeToggleProps {
export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => {
return (
<header className="relative overflow-hidden py-8 mb-40 md:mb-64">
<VegaBackgroundVideo />
<header className="relative overflow-hidden py-2 mb-10 md:mb-16">
<BackgroundVideo />
<div className="relative flex justify-center px-8 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-20 flex items-center justify-between">
<div className="relative flex justify-center px-2 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-5 flex items-center justify-between">
<VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
</div>

View File

@ -1 +0,0 @@
export { VegaBackgroundVideo } from './vega-background-video';

View File

@ -0,0 +1,276 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "NonceBurnt",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "new_signer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "SignerAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "old_signer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "SignerRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "new_threshold",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "ThresholdSet",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "new_signer",
"type": "address"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signatures",
"type": "bytes"
}
],
"name": "add_signer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signatures",
"type": "bytes"
}
],
"name": "burn_nonce",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "get_current_threshold",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "get_valid_signer_count",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "is_nonce_used",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "signer_address",
"type": "address"
}
],
"name": "is_valid_signer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "old_signer",
"type": "address"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signatures",
"type": "bytes"
}
],
"name": "remove_signer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "new_threshold",
"type": "uint16"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "signatures",
"type": "bytes"
}
],
"name": "set_threshold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "signers",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "signatures",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "message",
"type": "bytes"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
}
],
"name": "verify_signatures",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

View File

@ -6,3 +6,4 @@ export * from './staking-bridge';
export * from './token-vesting';
export * from './token';
export * from './token-faucetable';
export * from './multisig-control';

View File

@ -0,0 +1,55 @@
import { ethers } from 'ethers';
import abi from '../abis/multisig_abi.json';
export class MultisigControl {
public contract: ethers.Contract;
public address: string;
constructor(
address: string,
signerOrProvider: ethers.Signer | ethers.providers.Provider
) {
this.contract = new ethers.Contract(address, abi, signerOrProvider);
this.address = address;
}
add_signer(newSigner: string, nonce: string, signatures: string) {
return this.contract.add_signer(newSigner, nonce, signatures);
}
burn_nonce(nonce: string, signatures: string) {
return this.contract.burn_nonce(nonce, signatures);
}
get_current_threshold() {
return this.contract.get_current_threshold();
}
get_valid_signer_count() {
return this.contract.get_valid_signer_count();
}
is_nonce_used(nonce: string) {
return this.contract.is_nonce_used(nonce);
}
is_valid_signer(signerAddress: string) {
return this.contract.is_valid_signer(signerAddress);
}
remove_signer(oldSigner: string, nonce: string, signatures: string) {
return this.contract.remove_signer(oldSigner, nonce, signatures);
}
set_threshold(newThreshold: string, nonce: string, signatures: string) {
return this.contract.set_threshold(newThreshold, nonce, signatures);
}
signers(address: string) {
return this.contract.signers(address);
}
verify_signatures(nonce: string, message: string, signatures: string) {
return this.contract.verify_signatures(nonce, message, signatures);
}
}

View File

@ -0,0 +1,9 @@
import { render } from '@testing-library/react';
import { BackgroundVideo } from './background-video';
describe('Background video', () => {
it('should render successfully', () => {
const { baseElement } = render(<BackgroundVideo />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,9 @@
import type { Story, Meta } from '@storybook/react';
import { BackgroundVideo } from './background-video';
export default {
component: BackgroundVideo,
title: 'BackgroundVideo',
} as Meta;
export const Default: Story = () => <BackgroundVideo />;

View File

@ -1,4 +1,4 @@
export const VegaBackgroundVideo = () => {
export const BackgroundVideo = () => {
return (
<video
autoPlay

View File

@ -0,0 +1 @@
export * from './background-video';

View File

@ -2,6 +2,7 @@ export * from './accordion';
export * from './ag-grid';
export * from './arrows';
export * from './async-renderer';
export * from './background-video';
export * from './button';
export * from './callout';
export * from './checkbox';

View File

@ -20,6 +20,7 @@
"market-depth": "libs/market-depth",
"market-info": "libs/market-info",
"market-list": "libs/market-list",
"multisig-signer": "apps/multisig-signer",
"network-info": "libs/network-info",
"network-stats": "libs/network-stats",
"orders": "libs/orders",