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. 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 # 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [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 ( return (
<ThemeContext.Provider value={theme}> <ThemeContext.Provider value={theme}>
<NetworkLoader createClient={createClient}> <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"> <div className="layout-grid w-screen justify-self-center">
<Header theme={theme} toggleTheme={toggleTheme} /> <Header theme={theme} toggleTheme={toggleTheme} />
<StatsManager className="max-w-3xl px-24" /> <StatsManager className="max-w-3xl px-6" />
</div> </div>
</div> </div>
</NetworkLoader> </NetworkLoader>

View File

@ -1,5 +1,8 @@
import { VegaLogo, ThemeSwitcher } from '@vegaprotocol/ui-toolkit'; import {
import { VegaBackgroundVideo } from '../videos'; BackgroundVideo,
VegaLogo,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
interface ThemeToggleProps { interface ThemeToggleProps {
theme: 'light' | 'dark'; theme: 'light' | 'dark';
@ -8,11 +11,11 @@ interface ThemeToggleProps {
export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => { export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => {
return ( return (
<header className="relative overflow-hidden py-8 mb-40 md:mb-64"> <header className="relative overflow-hidden py-2 mb-10 md:mb-16">
<VegaBackgroundVideo /> <BackgroundVideo />
<div className="relative flex justify-center px-8 dark:bg-black bg-white"> <div className="relative flex justify-center px-2 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-20 flex items-center justify-between"> <div className="w-full max-w-3xl p-5 flex items-center justify-between">
<VegaLogo /> <VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} /> <ThemeSwitcher theme={theme} onToggle={toggleTheme} />
</div> </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-vesting';
export * from './token'; export * from './token';
export * from './token-faucetable'; 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 ( return (
<video <video
autoPlay 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 './ag-grid';
export * from './arrows'; export * from './arrows';
export * from './async-renderer'; export * from './async-renderer';
export * from './background-video';
export * from './button'; export * from './button';
export * from './callout'; export * from './callout';
export * from './checkbox'; export * from './checkbox';

View File

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