Feat/103 deposits (#143)
* add web3 provider using web3-react package * add env setup, add guard for incorrect chain id * add lib for web3-provider * make wallet and ethereum connect dialogs look more consistent * add setup tests file for jest-dom * remove chain id config and just use appChainId prop, add disconnect button to invalid chainId state * switch handling of connect dialog state to the consuming app * rename web3-provider package to just web3 * envs for each environment so we can specify chainId * make web3container enforce connection before rendering childen * add web3 provider using web3-react package * make web3container enforce connection before rendering childen * add container for getting network params * Move ethereum config query to web3 container * add basic deposit form elements * add queries required for deposits, add asset default * add bridge contract and deposit transaction * break txhash * restrict etherscan link props, use etherscan link in transaction dialogs * use smart-contracts-sdk * split hooks and components into different files, fix find deposit logic, add styles and progress for tx dialogs * fix text colors for dark mode * improve tx dialogs, rename deposit query * position use buttons, fix select validation * fix type errors after not being in strict mode, add allowance checking * add deposit-limits component, fix types now that strict mode is enabled * make contract hooks have a single instance * split out dialogs into separate files, fix icon alignment * improve error types for use transaction hook, add number save min and max for the amount input * add validation for ethereum and vega addresses * add unit test for deposit form component * add icons and shared dialog styles so it better matches order transaction dialog * fix underline class, reset finalized deposit * fix type imports, use i18n function, regen types * only pass contract address to token contract hook * add vega env, refactor so retrieving asset contract address logic isn't duplicated * add faucet functionality, combine dialogs into single transaction-dialog * combine rendering logic into single func of transaction dialog, rever contract hooks to just useMemo * use to field rather than connected key * fix props and imports in deposit form test * share faucetable condition, pass it to token contract * pass contracts in as params to hooks to avoid multiple contract instances * refetch balance in wallet after deposit, add comments * use hook state for tracking deposit via partyid, add test for use ethereum transaction hook * add deposits lib * add last smart contract sdk package * fix asset import in test * tidy up ts-ignores * pass arg for faucetable token contract * add provider url to env vars and use in place of infura id, also update web3-connector to only allow the chain permitted by the app * add type guard for erc20 assets * fix intent shadow helper function, use arrow function for isEthereumError * update etherscan link to use env vars for url base * rename deposit related hooks to indicate read vs write calls * move ethereum error class and helpers to react-helpers * add use-ethereum-read-contract hook to contain fetch logic * remove unused import * move validation to lib, add hex check for vega public key * use map for transaction modal states, pass confirmed prop to transaction dialog for deposits * remove unused import for classnames
This commit is contained in:
parent
ac2348ad7e
commit
f244cd07d4
@ -19,6 +19,8 @@ NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV = "TESTNET"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 3
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
|
||||
|
@ -2,4 +2,5 @@
|
||||
NX_VEGA_ENV = "DEVNET"
|
||||
NX_VEGA_URL = "https://n04.d.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 3
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
|
@ -2,4 +2,5 @@
|
||||
NX_VEGA_ENV = "MAINNET"
|
||||
NX_VEGA_URL = "https://api.token.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 1
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://etherscan.io"
|
@ -2,4 +2,5 @@
|
||||
NX_VEGA_ENV = "STAGNET"
|
||||
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 3
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
|
@ -2,4 +2,5 @@
|
||||
NX_VEGA_ENV = "STAGNET2"
|
||||
NX_VEGA_URL = "https://n03.stagnet2.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 3
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
|
@ -2,4 +2,5 @@
|
||||
NX_VEGA_ENV = "TESTNET"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
NX_ETHEREUM_CHAIN_ID = 3
|
||||
NX_INFURA_ID = "4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
|
||||
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
|
1
apps/trading/components/page-query-container/index.ts
Normal file
1
apps/trading/components/page-query-container/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { PageQueryContainer } from './page-query-container';
|
27
apps/trading/components/web3-container/__generated__/NetworkParamsQuery.ts
generated
Normal file
27
apps/trading/components/web3-container/__generated__/NetworkParamsQuery.ts
generated
Normal file
@ -0,0 +1,27 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: NetworkParamsQuery
|
||||
// ====================================================
|
||||
|
||||
export interface NetworkParamsQuery_networkParameters {
|
||||
__typename: "NetworkParameter";
|
||||
/**
|
||||
* The name of the network parameter
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The value of the network parameter
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface NetworkParamsQuery {
|
||||
/**
|
||||
* return the full list of network parameters
|
||||
*/
|
||||
networkParameters: NetworkParamsQuery_networkParameters[] | null;
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import { fireEvent, render, screen, act } from '@testing-library/react';
|
||||
import { Web3Container } from './web3-container';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { NETWORK_PARAMS_QUERY, Web3Container } from './web3-container';
|
||||
import type { NetworkParametersQuery } from '@vegaprotocol/graphql';
|
||||
|
||||
const defaultHookValue = {
|
||||
isActive: false,
|
||||
@ -9,6 +12,32 @@ const defaultHookValue = {
|
||||
};
|
||||
let mockHookValue;
|
||||
|
||||
const mockEthereumConfig = {
|
||||
network_id: '3',
|
||||
chain_id: '3',
|
||||
confirmations: 3,
|
||||
collateral_bridge_contract: {
|
||||
address: 'bridge address',
|
||||
},
|
||||
};
|
||||
|
||||
const networkParamsQueryMock: MockedResponse<NetworkParametersQuery> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'blockchains.ethereumConfig',
|
||||
value: JSON.stringify(mockEthereumConfig),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const original = jest.requireActual('@web3-react/core');
|
||||
return {
|
||||
@ -17,19 +46,33 @@ jest.mock('@web3-react/core', () => {
|
||||
};
|
||||
});
|
||||
|
||||
function setup(mock = networkParamsQueryMock) {
|
||||
return render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<Web3Container>
|
||||
{({ ethereumConfig }) => (
|
||||
<div>
|
||||
<div>Child</div>
|
||||
<div>{ethereumConfig.collateral_bridge_contract.address}</div>
|
||||
</div>
|
||||
)}
|
||||
</Web3Container>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
test('Prompt to connect opens dialog', async () => {
|
||||
mockHookValue = defaultHookValue;
|
||||
await act(async () => {
|
||||
render(
|
||||
<Web3Container>
|
||||
<div>Child</div>
|
||||
</Web3Container>
|
||||
);
|
||||
});
|
||||
setup();
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Connect your Ethereum wallet')
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Child')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('web3-connector-list')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Connect your Ethereum wallet')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Connect'));
|
||||
expect(screen.getByTestId('web3-connector-list')).toBeInTheDocument();
|
||||
});
|
||||
@ -37,33 +80,84 @@ test('Prompt to connect opens dialog', async () => {
|
||||
test('Error message is shown', async () => {
|
||||
const message = 'Opps! An error';
|
||||
mockHookValue = { ...defaultHookValue, error: new Error(message) };
|
||||
await act(async () => {
|
||||
render(
|
||||
<Web3Container>
|
||||
<div>Child</div>
|
||||
</Web3Container>
|
||||
);
|
||||
});
|
||||
setup();
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(await screen.findByText(`Something went wrong: ${message}`));
|
||||
expect(screen.queryByText('Child')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(`Something went wrong: ${message}`));
|
||||
});
|
||||
|
||||
test('Chain id matches app configuration', async () => {
|
||||
test('Checks that chain ID matches app ID', async () => {
|
||||
const expectedChainId = 4;
|
||||
mockHookValue = {
|
||||
...defaultHookValue,
|
||||
isActive: true,
|
||||
chainId: expectedChainId,
|
||||
};
|
||||
await act(async () => {
|
||||
render(
|
||||
<Web3Container>
|
||||
<div>Child</div>
|
||||
</Web3Container>
|
||||
);
|
||||
});
|
||||
setup();
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(await screen.findByText(`This app only works on chain ID: 3`));
|
||||
expect(screen.queryByText('Child')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(`This app only works on chain ID: 3`));
|
||||
});
|
||||
|
||||
test('Passes ethereum config to children', async () => {
|
||||
mockHookValue = {
|
||||
...defaultHookValue,
|
||||
isActive: true,
|
||||
};
|
||||
setup();
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(
|
||||
mockEthereumConfig.collateral_bridge_contract.address
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Shows no config found message if the network parameter doesnt exist', async () => {
|
||||
const mock: MockedResponse<NetworkParametersQuery> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'nope',
|
||||
value: 'foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setup(mock);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('No ethereum config found')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Shows message if ethereum config could not be parsed', async () => {
|
||||
const mock: MockedResponse<NetworkParametersQuery> = {
|
||||
request: {
|
||||
query: NETWORK_PARAMS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
networkParameters: [
|
||||
{
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'blockchains.ethereumConfig',
|
||||
value: '"something invalid }',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setup(mock);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Could not parse config')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -1,41 +1,113 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { NetworkParamsQuery } from './__generated__/NetworkParamsQuery';
|
||||
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Web3Provider, Web3ConnectDialog } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Connectors } from '../../lib/web3-connectors';
|
||||
import { PageQueryContainer } from '../page-query-container';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface EthereumConfig {
|
||||
network_id: string;
|
||||
chain_id: string;
|
||||
confirmations: number;
|
||||
collateral_bridge_contract: {
|
||||
address: string;
|
||||
};
|
||||
multisig_control_contract: {
|
||||
address: string;
|
||||
deployment_block_height: number;
|
||||
};
|
||||
staking_bridge_contract: {
|
||||
address: string;
|
||||
deployment_block_height: number;
|
||||
};
|
||||
token_vesting_contract: {
|
||||
address: string;
|
||||
deployment_block_height: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const NETWORK_PARAMS_QUERY = gql`
|
||||
query NetworkParamsQuery {
|
||||
networkParameters {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Web3ContainerProps {
|
||||
children: ReactNode;
|
||||
children: (params: { ethereumConfig: EthereumConfig }) => ReactNode;
|
||||
}
|
||||
|
||||
export const Web3Container = ({ children }: Web3ContainerProps) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
return (
|
||||
<Web3Provider connectors={Connectors}>
|
||||
<Web3Content setDialogOpen={setDialogOpen}>{children}</Web3Content>
|
||||
<Web3ConnectDialog
|
||||
connectors={Connectors}
|
||||
dialogOpen={dialogOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
desiredChainId={Number(process.env['NX_ETHEREUM_CHAIN_ID'] || 3)}
|
||||
/>
|
||||
</Web3Provider>
|
||||
<PageQueryContainer<NetworkParamsQuery> query={NETWORK_PARAMS_QUERY}>
|
||||
{(data) => {
|
||||
const ethereumConfigParam = data.networkParameters?.find(
|
||||
(np) => np.key === 'blockchains.ethereumConfig'
|
||||
);
|
||||
|
||||
if (!ethereumConfigParam) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('No ethereum config found')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
let ethereumConfig: EthereumConfig;
|
||||
|
||||
try {
|
||||
ethereumConfig = JSON.parse(ethereumConfigParam.value);
|
||||
} catch {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Could not parse config')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Web3Provider connectors={Connectors}>
|
||||
<Web3Content
|
||||
appChainId={Number(ethereumConfig.chain_id)}
|
||||
setDialogOpen={setDialogOpen}
|
||||
>
|
||||
{children({ ethereumConfig })}
|
||||
</Web3Content>
|
||||
<Web3ConnectDialog
|
||||
connectors={Connectors}
|
||||
dialogOpen={dialogOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
desiredChainId={Number(ethereumConfig.chain_id)}
|
||||
/>
|
||||
</Web3Provider>
|
||||
);
|
||||
}}
|
||||
</PageQueryContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface Web3ContentProps {
|
||||
children: ReactNode;
|
||||
appChainId: number;
|
||||
setDialogOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const Web3Content = ({ children, setDialogOpen }: Web3ContentProps) => {
|
||||
const appChainId = Number(process.env['NX_ETHEREUM_CHAIN_ID'] || 3);
|
||||
export const Web3Content = ({
|
||||
children,
|
||||
appChainId,
|
||||
setDialogOpen,
|
||||
}: Web3ContentProps) => {
|
||||
const { isActive, error, connector, chainId } = useWeb3React();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof connector?.connectEagerly === 'function') {
|
||||
if (connector?.connectEagerly) {
|
||||
connector.connectEagerly();
|
||||
}
|
||||
}, [connector]);
|
||||
|
@ -6,17 +6,21 @@ export const metamask = initializeConnector<MetaMask>(
|
||||
(actions) => new MetaMask(actions)
|
||||
);
|
||||
|
||||
const infuraId = process.env['NX_INFURA_ID'];
|
||||
const CHAIN_ID = Number(process.env['NX_ETHEREUM_CHAIN_ID']);
|
||||
const PROVIDER_URL = process.env['NX_ETHEREUM_PROVIDER_URL'] as string;
|
||||
|
||||
if (isNaN(CHAIN_ID)) {
|
||||
throw new Error('Invalid Ethereum chain ID for environment');
|
||||
}
|
||||
|
||||
export const walletconnect = initializeConnector<WalletConnect>(
|
||||
(actions) =>
|
||||
new WalletConnect(actions, {
|
||||
rpc: {
|
||||
1: `https://mainnet.infura.io/v3/${infuraId}`,
|
||||
3: `https://ropsten.infura.io/v3/${infuraId}`,
|
||||
[CHAIN_ID]: PROVIDER_URL,
|
||||
},
|
||||
}),
|
||||
[1, 3]
|
||||
[CHAIN_ID]
|
||||
);
|
||||
|
||||
export const Connectors = {
|
||||
|
53
apps/trading/pages/portfolio/deposit/__generated__/DepositPage.ts
generated
Normal file
53
apps/trading/pages/portfolio/deposit/__generated__/DepositPage.ts
generated
Normal file
@ -0,0 +1,53 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: DepositPage
|
||||
// ====================================================
|
||||
|
||||
export interface DepositPage_assets_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface DepositPage_assets_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the erc20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type DepositPage_assets_source = DepositPage_assets_source_BuiltinAsset | DepositPage_assets_source_ERC20;
|
||||
|
||||
export interface DepositPage_assets {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an erc20 asset)
|
||||
*/
|
||||
source: DepositPage_assets_source;
|
||||
}
|
||||
|
||||
export interface DepositPage {
|
||||
/**
|
||||
* The list of all assets in use in the vega network
|
||||
*/
|
||||
assets: DepositPage_assets[] | null;
|
||||
}
|
70
apps/trading/pages/portfolio/deposit/deposit-container.tsx
Normal file
70
apps/trading/pages/portfolio/deposit/deposit-container.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { EthereumConfig } from '../../../components/web3-container/web3-container';
|
||||
import { gql } from '@apollo/client';
|
||||
import { PageQueryContainer } from '../../../components/page-query-container';
|
||||
import type {
|
||||
DepositPage,
|
||||
DepositPage_assets,
|
||||
} from './__generated__/DepositPage';
|
||||
import type { Asset } from '@vegaprotocol/deposits';
|
||||
import { DepositManager } from '@vegaprotocol/deposits';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const DEPOSIT_PAGE_QUERY = gql`
|
||||
query DepositPage {
|
||||
assets {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface DepositContainerProps {
|
||||
ethereumConfig: EthereumConfig;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const DepositContainer = ({
|
||||
ethereumConfig,
|
||||
assetId,
|
||||
}: DepositContainerProps) => {
|
||||
return (
|
||||
<PageQueryContainer<DepositPage> query={DEPOSIT_PAGE_QUERY}>
|
||||
{(data) => {
|
||||
if (!data.assets?.length) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('No assets on this network')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DepositManager
|
||||
bridgeAddress={ethereumConfig.collateral_bridge_contract.address}
|
||||
requiredConfirmations={ethereumConfig.confirmations}
|
||||
assets={data.assets.filter(isERC20Asset)}
|
||||
initialAssetId={assetId}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</PageQueryContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const isERC20Asset = (asset: DepositPage_assets): asset is Asset => {
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
@ -1,28 +1,34 @@
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
import { Web3Container } from '../../../components/web3-container';
|
||||
import { DepositContainer } from './deposit-container';
|
||||
|
||||
const Deposit = () => {
|
||||
const { query } = useRouter();
|
||||
|
||||
// AssetId can be specified in the query string to allow link to deposit a particular asset
|
||||
const assetId = useMemo(() => {
|
||||
if (query.assetId && Array.isArray(query.assetId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Array.isArray(query.assetId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return query.assetId;
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<Web3Container>
|
||||
<div>
|
||||
<h1>Deposit</h1>
|
||||
<Info />
|
||||
</div>
|
||||
{({ ethereumConfig }) => (
|
||||
<div className="max-w-[420px] p-24 mx-auto">
|
||||
<h1 className="text-h3 mb-12">Deposit</h1>
|
||||
<DepositContainer ethereumConfig={ethereumConfig} assetId={assetId} />
|
||||
</div>
|
||||
)}
|
||||
</Web3Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Info = () => {
|
||||
const { isActive, chainId, account } = useWeb3React();
|
||||
if (!isActive) {
|
||||
return <div>Not active</div>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<p>{chainId}</p>
|
||||
<p>{account ? account : 'No account'}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Deposit;
|
||||
|
@ -10,6 +10,9 @@ const Portfolio = () => {
|
||||
{keypair && <p>{t(`Keypair: ${keypair.name} ${keypair.pub}`)}</p>}
|
||||
<div className="flex gap-4">
|
||||
<AnchorButton href="/portfolio/deposit">{t('Deposit')}</AnchorButton>
|
||||
<AnchorButton href="/portfolio/deposit?assetId=8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4">
|
||||
{t('Deposit tEURO')}
|
||||
</AnchorButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
12
libs/deposits/.babelrc
Normal file
12
libs/deposits/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
18
libs/deposits/.eslintrc.json
Normal file
18
libs/deposits/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
7
libs/deposits/README.md
Normal file
7
libs/deposits/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# deposits
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test deposits` to execute the unit tests via [Jest](https://jestjs.io).
|
10
libs/deposits/jest.config.js
Normal file
10
libs/deposits/jest.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
displayName: 'deposits',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/deposits',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
4
libs/deposits/package.json
Normal file
4
libs/deposits/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/deposits",
|
||||
"version": "0.0.1"
|
||||
}
|
43
libs/deposits/project.json
Normal file
43
libs/deposits/project.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"root": "libs/deposits",
|
||||
"sourceRoot": "libs/deposits/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/deposits",
|
||||
"tsConfig": "libs/deposits/tsconfig.lib.json",
|
||||
"project": "libs/deposits/package.json",
|
||||
"entryFile": "libs/deposits/src/index.ts",
|
||||
"external": ["react/jsx-runtime"],
|
||||
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||
"compiler": "babel",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "libs/deposits/README.md",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/deposits/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/deposits"],
|
||||
"options": {
|
||||
"jestConfig": "libs/deposits/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
libs/deposits/src/config/index.ts
Normal file
1
libs/deposits/src/config/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const FAUCETABLE = process.env['NX_VEGA_ENV'] !== 'MAINNET';
|
1
libs/deposits/src/index.ts
Normal file
1
libs/deposits/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/deposit-manager';
|
63
libs/deposits/src/lib/__generated__/DepositEvent.ts
generated
Normal file
63
libs/deposits/src/lib/__generated__/DepositEvent.ts
generated
Normal file
@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { BusEventType, DepositStatus } from "./../../../../types/src/__generated__/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: DepositEvent
|
||||
// ====================================================
|
||||
|
||||
export interface DepositEvent_busEvents_event_TimeUpdate {
|
||||
__typename: "TimeUpdate" | "MarketEvent" | "TransferResponses" | "PositionResolution" | "Order" | "Trade" | "Account" | "Party" | "MarginLevels" | "Proposal" | "Vote" | "MarketData" | "NodeSignature" | "LossSocialization" | "SettlePosition" | "Market" | "Asset" | "MarketTick" | "SettleDistressed" | "AuctionEvent" | "RiskFactor" | "Withdrawal" | "OracleSpec" | "LiquidityProvision";
|
||||
}
|
||||
|
||||
export interface DepositEvent_busEvents_event_Deposit {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal id of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
}
|
||||
|
||||
export type DepositEvent_busEvents_event = DepositEvent_busEvents_event_TimeUpdate | DepositEvent_busEvents_event_Deposit;
|
||||
|
||||
export interface DepositEvent_busEvents {
|
||||
__typename: "BusEvent";
|
||||
/**
|
||||
* the id for this event
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* the block hash
|
||||
*/
|
||||
block: string;
|
||||
/**
|
||||
* the type of event we're dealing with
|
||||
*/
|
||||
type: BusEventType;
|
||||
/**
|
||||
* the payload - the wrapped event
|
||||
*/
|
||||
event: DepositEvent_busEvents_event;
|
||||
}
|
||||
|
||||
export interface DepositEvent {
|
||||
/**
|
||||
* Subscribe to event data from the event bus
|
||||
*/
|
||||
busEvents: DepositEvent_busEvents[] | null;
|
||||
}
|
||||
|
||||
export interface DepositEventVariables {
|
||||
partyId: string;
|
||||
}
|
207
libs/deposits/src/lib/deposit-form.spec.tsx
Normal file
207
libs/deposits/src/lib/deposit-form.spec.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DepositFormProps } from './deposit-form';
|
||||
import { DepositForm } from './deposit-form';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { Asset } from './deposit-manager';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@web3-react/core');
|
||||
|
||||
function generateAsset(): Asset {
|
||||
return {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
symbol: 'asset-symbol',
|
||||
name: 'asset-name',
|
||||
decimals: 2,
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: 'contract-address',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let asset: Asset;
|
||||
let props: DepositFormProps;
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
props = {
|
||||
assets: [asset],
|
||||
selectedAsset: undefined,
|
||||
onSelectAsset: jest.fn(),
|
||||
available: new BigNumber(5),
|
||||
submitApprove: jest.fn(),
|
||||
submitDeposit: jest.fn(),
|
||||
requestFaucet: jest.fn(),
|
||||
limits: {
|
||||
min: new BigNumber(0),
|
||||
max: new BigNumber(20),
|
||||
},
|
||||
allowance: new BigNumber(30),
|
||||
};
|
||||
});
|
||||
|
||||
test('Form validation', async () => {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ keypair: null });
|
||||
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({ account: undefined });
|
||||
|
||||
const { rerender } = render(<DepositForm {...props} />);
|
||||
|
||||
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
|
||||
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue('');
|
||||
expect(screen.getByLabelText('Asset')).toHaveValue('');
|
||||
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
|
||||
expect(screen.getByLabelText('Amount')).toHaveValue(null);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
screen.getByText('Deposit', { selector: '[type="submit"]' })
|
||||
);
|
||||
});
|
||||
|
||||
expect(props.submitDeposit).not.toHaveBeenCalled();
|
||||
const validationMessages = screen.getAllByRole('alert');
|
||||
expect(validationMessages).toHaveLength(4);
|
||||
validationMessages.forEach((el) => {
|
||||
expect(el).toHaveTextContent('Required');
|
||||
});
|
||||
|
||||
// Address validation
|
||||
const invalidEthereumAddress = '123';
|
||||
fireEvent.change(screen.getByLabelText('From (Ethereum address)'), {
|
||||
target: { value: invalidEthereumAddress },
|
||||
});
|
||||
expect(await screen.findByText('Invalid Ethereum address'));
|
||||
|
||||
const invalidVegaKey = 'abc';
|
||||
fireEvent.change(screen.getByLabelText('To (Vega key)'), {
|
||||
target: { value: invalidVegaKey },
|
||||
});
|
||||
expect(await screen.findByText('Invalid Vega key'));
|
||||
|
||||
// Max amount validation
|
||||
const amountMoreThanAvailable = '11';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amountMoreThanAvailable },
|
||||
});
|
||||
expect(await screen.findByText('Insufficient amount in Ethereum wallet'));
|
||||
|
||||
const amountMoreThanLimit = '21';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amountMoreThanLimit },
|
||||
});
|
||||
expect(await screen.findByText('Amount is above permitted maximum'));
|
||||
|
||||
rerender(
|
||||
<DepositForm
|
||||
{...props}
|
||||
limits={{ min: new BigNumber(0), max: new BigNumber(100) }}
|
||||
/>
|
||||
);
|
||||
|
||||
const amountMoreThanAllowance = '31';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amountMoreThanAllowance },
|
||||
});
|
||||
expect(await screen.findByText('Amount is above approved amount'));
|
||||
|
||||
// Min amount validation
|
||||
rerender(<DepositForm {...props} selectedAsset={asset} />); // Rerender with selected asset so we have asset.decimals
|
||||
|
||||
const amountLessThanMinViable = '0.00001';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amountLessThanMinViable },
|
||||
});
|
||||
expect(await screen.findByText('Amount is below permitted minimum'));
|
||||
|
||||
rerender(
|
||||
<DepositForm
|
||||
{...props}
|
||||
limits={{ max: new BigNumber(20), min: new BigNumber(10) }}
|
||||
/>
|
||||
);
|
||||
const amountLessThanLimit = '5';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amountLessThanLimit },
|
||||
});
|
||||
expect(await screen.findByText('Amount is below permitted minimum'));
|
||||
});
|
||||
|
||||
test('Approval', () => {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ keypair: null });
|
||||
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({ account: undefined });
|
||||
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
allowance={new BigNumber(0)}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByText(`Approve ${asset.symbol}`, { selector: '[type="button"]' })
|
||||
);
|
||||
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Deposit', async () => {
|
||||
const vegaKey =
|
||||
'f8885edfa7ffdb6ed996ca912e9258998e47bf3515c885cf3c63fb56b15de36f';
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ keypair: { pub: vegaKey } });
|
||||
|
||||
const account = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({ account });
|
||||
|
||||
const limits = {
|
||||
min: new BigNumber(10),
|
||||
max: new BigNumber(20),
|
||||
};
|
||||
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
allowance={new BigNumber(100)}
|
||||
available={new BigNumber(50)}
|
||||
limits={limits}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check deposit limits are displayed
|
||||
expect(
|
||||
screen.getByText('Minimum', { selector: 'th' }).nextElementSibling
|
||||
).toHaveTextContent(limits.min.toString());
|
||||
expect(
|
||||
screen.getByText('Maximum', { selector: 'th' }).nextElementSibling
|
||||
).toHaveTextContent(limits.max.toString());
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '15' },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
screen.getByText('Deposit', { selector: '[type="submit"]' })
|
||||
);
|
||||
});
|
||||
|
||||
expect(props.submitDeposit).toHaveBeenCalledWith({
|
||||
// @ts-ignore contract address definitely defined
|
||||
assetSource: asset.source.contractAddress,
|
||||
amount: '1500',
|
||||
vegaPublicKey: `0x${vegaKey}`,
|
||||
});
|
||||
});
|
315
libs/deposits/src/lib/deposit-form.tsx
Normal file
315
libs/deposits/src/lib/deposit-form.tsx
Normal file
@ -0,0 +1,315 @@
|
||||
import {
|
||||
addDecimal,
|
||||
removeDecimal,
|
||||
t,
|
||||
ethereumAddress,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
minSafe,
|
||||
maxSafe,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
InputError,
|
||||
Select,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { FAUCETABLE } from '../config';
|
||||
import type { Asset } from './deposit-manager';
|
||||
|
||||
interface FormFields {
|
||||
asset: string;
|
||||
from: string;
|
||||
to: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface DepositFormProps {
|
||||
assets: Asset[];
|
||||
selectedAsset?: Asset;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
available: BigNumber | undefined;
|
||||
submitApprove: () => Promise<void>;
|
||||
submitDeposit: (args: {
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}) => Promise<void>;
|
||||
requestFaucet: () => Promise<void>;
|
||||
limits: {
|
||||
min: BigNumber;
|
||||
max: BigNumber;
|
||||
} | null;
|
||||
allowance: BigNumber | undefined;
|
||||
}
|
||||
|
||||
export const DepositForm = ({
|
||||
assets,
|
||||
selectedAsset,
|
||||
onSelectAsset,
|
||||
available,
|
||||
submitApprove,
|
||||
submitDeposit,
|
||||
requestFaucet,
|
||||
limits,
|
||||
allowance,
|
||||
}: DepositFormProps) => {
|
||||
const { account } = useWeb3React();
|
||||
const { keypair } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
clearErrors,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
asset: selectedAsset?.id,
|
||||
from: account,
|
||||
to: keypair?.pub,
|
||||
},
|
||||
});
|
||||
|
||||
const onDeposit = async (fields: FormFields) => {
|
||||
if (!selectedAsset) {
|
||||
throw new Error('Asset not selected');
|
||||
}
|
||||
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: removeDecimal(fields.amount, selectedAsset.decimals),
|
||||
vegaPublicKey: `0x${fields.to}`,
|
||||
});
|
||||
};
|
||||
|
||||
const assetId = useWatch({ name: 'asset', control });
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const min = useMemo(() => {
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const minViableAmount = selectedAsset
|
||||
? new BigNumber(addDecimal('1', selectedAsset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
const min = limits
|
||||
? BigNumber.maximum(minViableAmount, limits.min)
|
||||
: minViableAmount;
|
||||
|
||||
return min;
|
||||
}, [limits, selectedAsset]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : new BigNumber(Infinity);
|
||||
const maxAvailable = available ? available : new BigNumber(Infinity);
|
||||
// A max limit of zero indicates that there is no limit
|
||||
let maxLimit = new BigNumber(Infinity);
|
||||
if (limits && limits.max.isGreaterThan(0)) {
|
||||
maxLimit = limits.max;
|
||||
}
|
||||
|
||||
return {
|
||||
approved: maxApproved,
|
||||
available: maxAvailable,
|
||||
limit: maxLimit,
|
||||
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
|
||||
};
|
||||
}, [limits, allowance, available]);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectAsset(assetId);
|
||||
}, [assetId, onSelectAsset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onDeposit)} noValidate={true}>
|
||||
<FormGroup
|
||||
label={t('From (Ethereum address)')}
|
||||
labelFor="ethereum-address"
|
||||
>
|
||||
<Input
|
||||
{...register('from', { validate: { required, ethereumAddress } })}
|
||||
id="ethereum-address"
|
||||
/>
|
||||
{errors.from?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.from.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label={t('Asset')} labelFor="asset" className="relative">
|
||||
<Select {...register('asset', { validate: { required } })} id="asset">
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{errors.asset?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.asset.message}
|
||||
</InputError>
|
||||
)}
|
||||
{FAUCETABLE && selectedAsset && (
|
||||
<UseButton onClick={requestFaucet}>
|
||||
{t(`Get ${selectedAsset.symbol}`)}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t('To (Vega key)')}
|
||||
labelFor="vega-key"
|
||||
className="relative"
|
||||
>
|
||||
<Input
|
||||
{...register('to', { validate: { required, vegaPublicKey } })}
|
||||
id="vega-key"
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.to.message}
|
||||
</InputError>
|
||||
)}
|
||||
{keypair?.pub && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('to', keypair.pub);
|
||||
clearErrors('to');
|
||||
}}
|
||||
>
|
||||
{t('Use connected')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && limits && (
|
||||
<FormGroup>
|
||||
<DepositLimits limits={limits} />
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
minSafe: (value) => minSafe(min)(value),
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
} else if (value.isGreaterThan(max.limit)) {
|
||||
return t('Amount is above permitted maximum');
|
||||
} else if (value.isGreaterThan(max.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return maxSafe(max.amount)(v);
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{account && selectedAsset && available && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', max.amount.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormButton
|
||||
selectedAsset={selectedAsset}
|
||||
amount={new BigNumber(amount || 0)}
|
||||
allowance={allowance}
|
||||
onApproveClick={submitApprove}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormButtonProps {
|
||||
selectedAsset?: Asset;
|
||||
amount: BigNumber;
|
||||
allowance: BigNumber | undefined;
|
||||
onApproveClick: () => void;
|
||||
}
|
||||
|
||||
const FormButton = ({
|
||||
selectedAsset,
|
||||
amount,
|
||||
allowance,
|
||||
onApproveClick,
|
||||
}: FormButtonProps) => {
|
||||
const approved =
|
||||
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
|
||||
let button = null;
|
||||
let message: ReactNode = '';
|
||||
|
||||
if (!selectedAsset) {
|
||||
button = (
|
||||
<Button type="submit" className="w-full">
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else if (approved) {
|
||||
message = (
|
||||
<>
|
||||
<Icon name="tick" /> <span>{t('Approved')}</span>
|
||||
</>
|
||||
);
|
||||
button = (
|
||||
<Button type="submit" className="w-full">
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
|
||||
button = (
|
||||
<Button onClick={onApproveClick} className="w-full">
|
||||
{t(`Approve ${selectedAsset.symbol}`)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{message && <p className="text-ui text-center mb-4">{message}</p>}
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UseButtonProps {
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const UseButton = ({ children, onClick }: UseButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto text-ui absolute top-0 right-0 underline"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
33
libs/deposits/src/lib/deposit-limits.tsx
Normal file
33
libs/deposits/src/lib/deposit-limits.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
|
||||
interface DepositLimitsProps {
|
||||
limits: {
|
||||
min: BigNumber;
|
||||
max: BigNumber;
|
||||
};
|
||||
}
|
||||
|
||||
export const DepositLimits = ({ limits }: DepositLimitsProps) => {
|
||||
const minLimit = limits.min.toString();
|
||||
const maxLimit = limits.max.isEqualTo(Infinity)
|
||||
? t('No limit')
|
||||
: limits.max.toString();
|
||||
return (
|
||||
<>
|
||||
<p className="text-ui font-bold">{t('Temporary deposit limits')}</p>
|
||||
<table className="w-full text-ui">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Minimum')}</th>
|
||||
<td className="text-right">{minLimit}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Maximum')}</th>
|
||||
<td className="text-right">{maxLimit}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
};
|
107
libs/deposits/src/lib/deposit-manager.tsx
Normal file
107
libs/deposits/src/lib/deposit-manager.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { DepositForm } from './deposit-form';
|
||||
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
import { useSubmitDeposit } from './use-submit-deposit';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useGetDepositLimits } from './use-get-deposit-limits';
|
||||
import { useGetAllowance } from './use-get-allowance';
|
||||
import { TransactionDialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import {
|
||||
useTokenContract,
|
||||
useBridgeContract,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface Asset {
|
||||
__typename: 'Asset';
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
source: {
|
||||
__typename: 'ERC20';
|
||||
contractAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DepositManagerProps {
|
||||
requiredConfirmations: number;
|
||||
bridgeAddress: string;
|
||||
assets: Asset[];
|
||||
initialAssetId?: string;
|
||||
}
|
||||
|
||||
export const DepositManager = ({
|
||||
requiredConfirmations,
|
||||
bridgeAddress,
|
||||
assets,
|
||||
initialAssetId,
|
||||
}: DepositManagerProps) => {
|
||||
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
|
||||
|
||||
// Find the asset object from the select box
|
||||
const asset = useMemo(() => {
|
||||
const asset = assets?.find((a) => a.id === assetId);
|
||||
return asset;
|
||||
}, [assets, assetId]);
|
||||
|
||||
const tokenContract = useTokenContract(
|
||||
asset?.source.contractAddress,
|
||||
process.env['NX_VEGA_ENV'] !== 'MAINNET'
|
||||
);
|
||||
const bridgeContract = useBridgeContract();
|
||||
|
||||
// Get users balance of the erc20 token selected
|
||||
const { balanceOf, refetch } = useGetBalanceOfERC20Token(tokenContract);
|
||||
|
||||
// Get temporary deposit limits
|
||||
const limits = useGetDepositLimits(bridgeContract, asset);
|
||||
|
||||
// Get allowance (approved spending limit of brdige contract) for the selected asset
|
||||
const allowance = useGetAllowance(tokenContract, bridgeAddress);
|
||||
|
||||
// Set up approve transaction
|
||||
const approve = useSubmitApproval(tokenContract, bridgeAddress);
|
||||
|
||||
// Set up deposit transaction
|
||||
const { confirmationEvent, ...deposit } = useSubmitDeposit(
|
||||
bridgeContract,
|
||||
requiredConfirmations
|
||||
);
|
||||
|
||||
// Set up faucet transaction
|
||||
const faucet = useSubmitFaucet(tokenContract);
|
||||
|
||||
// Update balance after confirmation event has been received
|
||||
useEffect(() => {
|
||||
if (confirmationEvent !== null) {
|
||||
refetch();
|
||||
}
|
||||
}, [confirmationEvent, refetch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DepositForm
|
||||
available={balanceOf}
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={(id) => setAssetId(id)}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={approve.perform}
|
||||
submitDeposit={deposit.perform}
|
||||
requestFaucet={faucet.perform}
|
||||
limits={limits}
|
||||
allowance={allowance}
|
||||
/>
|
||||
<TransactionDialog {...approve} name="approve" />
|
||||
<TransactionDialog {...faucet} name="faucet" />
|
||||
<TransactionDialog
|
||||
{...deposit}
|
||||
name="deposit"
|
||||
confirmed={Boolean(confirmationEvent)}
|
||||
// Must wait for additional confirmations for Vega to pick up the Ethereum transaction
|
||||
requiredConfirmations={requiredConfirmations}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
24
libs/deposits/src/lib/use-get-allowance.ts
Normal file
24
libs/deposits/src/lib/use-get-allowance.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useCallback } from 'react';
|
||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const useGetAllowance = (
|
||||
contract: ERC20Token | null,
|
||||
bridgeAddress: string
|
||||
) => {
|
||||
const { account } = useWeb3React();
|
||||
|
||||
const getAllowance = useCallback(() => {
|
||||
if (!contract || !account) {
|
||||
return;
|
||||
}
|
||||
return contract.allowance(account, bridgeAddress);
|
||||
}, [contract, account, bridgeAddress]);
|
||||
|
||||
const {
|
||||
state: { data },
|
||||
} = useEthereumReadContract(getAllowance);
|
||||
|
||||
return data;
|
||||
};
|
20
libs/deposits/src/lib/use-get-balance-of-erc20-token.ts
Normal file
20
libs/deposits/src/lib/use-get-balance-of-erc20-token.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useGetBalanceOfERC20Token = (contract: ERC20Token | null) => {
|
||||
const { account } = useWeb3React();
|
||||
|
||||
const getBalance = useCallback(() => {
|
||||
if (!contract || !account) {
|
||||
return;
|
||||
}
|
||||
|
||||
return contract.balanceOf(account);
|
||||
}, [contract, account]);
|
||||
|
||||
const { state, refetch } = useEthereumReadContract(getBalance);
|
||||
|
||||
return { balanceOf: state.data, refetch };
|
||||
};
|
37
libs/deposits/src/lib/use-get-deposit-limits.ts
Normal file
37
libs/deposits/src/lib/use-get-deposit-limits.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import { useCallback } from 'react';
|
||||
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import type { Asset } from './deposit-manager';
|
||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface Limits {
|
||||
min: BigNumber;
|
||||
max: BigNumber;
|
||||
}
|
||||
|
||||
export const useGetDepositLimits = (
|
||||
contract: VegaErc20Bridge | null,
|
||||
asset?: Asset
|
||||
): Limits | null => {
|
||||
const getLimits = useCallback(async () => {
|
||||
if (!contract || !asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
contract.getDepositMinimum(asset.source.contractAddress, asset.decimals),
|
||||
contract.getDepositMaximum(asset.source.contractAddress, asset.decimals),
|
||||
]);
|
||||
}, [asset, contract]);
|
||||
|
||||
const {
|
||||
state: { data },
|
||||
} = useEthereumReadContract<[BigNumber, BigNumber] | undefined>(getLimits);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return {
|
||||
min: data[0],
|
||||
max: data[1],
|
||||
};
|
||||
};
|
16
libs/deposits/src/lib/use-submit-approval.ts
Normal file
16
libs/deposits/src/lib/use-submit-approval.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const useSubmitApproval = (
|
||||
contract: ERC20Token | null,
|
||||
bridgeAddress: string
|
||||
) => {
|
||||
const transaction = useEthereumTransaction(() => {
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
return contract.approve(bridgeAddress);
|
||||
});
|
||||
|
||||
return transaction;
|
||||
};
|
95
libs/deposits/src/lib/use-submit-deposit.ts
Normal file
95
libs/deposits/src/lib/use-submit-deposit.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { gql, useSubscription } from '@apollo/client';
|
||||
import type {
|
||||
DepositEvent,
|
||||
DepositEventVariables,
|
||||
DepositEvent_busEvents_event_Deposit,
|
||||
} from './__generated__/DepositEvent';
|
||||
import { DepositStatus } from '@vegaprotocol/types';
|
||||
import { useState } from 'react';
|
||||
import { useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
||||
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
||||
|
||||
const DEPOSIT_EVENT_SUB = gql`
|
||||
subscription DepositEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Deposit]) {
|
||||
eventId
|
||||
block
|
||||
type
|
||||
event {
|
||||
... on Deposit {
|
||||
id
|
||||
txHash
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const useSubmitDeposit = (
|
||||
contract: VegaErc20Bridge | null,
|
||||
confirmations: number
|
||||
) => {
|
||||
const [confirmationEvent, setConfirmationEvent] =
|
||||
useState<DepositEvent_busEvents_event_Deposit | null>(null);
|
||||
// Store public key from contract arguments for use in the subscription,
|
||||
// NOTE: it may be different from the users connected key
|
||||
const [partyId, setPartyId] = useState<string | null>(null);
|
||||
|
||||
const transaction = useEthereumTransaction<{
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}>((args) => {
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
// New deposit started clear old confirmation event and start
|
||||
// tracking deposits for the new public key
|
||||
setConfirmationEvent(null);
|
||||
setPartyId(args.vegaPublicKey);
|
||||
|
||||
return contract.depositAsset(
|
||||
args.assetSource,
|
||||
args.amount,
|
||||
args.vegaPublicKey
|
||||
);
|
||||
}, confirmations);
|
||||
|
||||
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
|
||||
variables: { partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
onSubscriptionData: ({ subscriptionData }) => {
|
||||
if (!subscriptionData.data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDeposit = subscriptionData.data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Deposit') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
e.event.txHash === transaction.txHash &&
|
||||
// Note there is a bug in data node where the subscription is not emitted when the status
|
||||
// changes from 'Open' to 'Finalized' as a result the deposit UI will hang in a pending state right now
|
||||
// https://github.com/vegaprotocol/data-node/issues/460
|
||||
e.event.status === DepositStatus.Finalized
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matchingDeposit && matchingDeposit.event.__typename === 'Deposit') {
|
||||
setConfirmationEvent(matchingDeposit.event);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
confirmationEvent,
|
||||
};
|
||||
};
|
13
libs/deposits/src/lib/use-submit-faucet.ts
Normal file
13
libs/deposits/src/lib/use-submit-faucet.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const useSubmitFaucet = (contract: ERC20Token | null) => {
|
||||
const transaction = useEthereumTransaction(() => {
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
return contract.faucet();
|
||||
});
|
||||
|
||||
return transaction;
|
||||
};
|
1
libs/deposits/src/setup-tests.ts
Normal file
1
libs/deposits/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
25
libs/deposits/tsconfig.json
Normal file
25
libs/deposits/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
22
libs/deposits/tsconfig.lib.json
Normal file
22
libs/deposits/tsconfig.lib.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
19
libs/deposits/tsconfig.spec.json
Normal file
19
libs/deposits/tsconfig.spec.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
53
libs/market-list/src/lib/__generated__/Deposit.ts
generated
Normal file
53
libs/market-list/src/lib/__generated__/Deposit.ts
generated
Normal file
@ -0,0 +1,53 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: DepositPage
|
||||
// ====================================================
|
||||
|
||||
export interface DepositPage_assets_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface DepositPage_assets_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the erc20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type DepositPage_assets_source = DepositPage_assets_source_BuiltinAsset | DepositPage_assets_source_ERC20;
|
||||
|
||||
export interface DepositPage_assets {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an erc20 asset)
|
||||
*/
|
||||
source: DepositPage_assets_source;
|
||||
}
|
||||
|
||||
export interface DepositPage {
|
||||
/**
|
||||
* The list of all assets in use in the vega network
|
||||
*/
|
||||
assets: DepositPage_assets[] | null;
|
||||
}
|
63
libs/market-list/src/lib/__generated__/DepositEvent.ts
generated
Normal file
63
libs/market-list/src/lib/__generated__/DepositEvent.ts
generated
Normal file
@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { BusEventType, DepositStatus } from "./globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: DepositEvent
|
||||
// ====================================================
|
||||
|
||||
export interface DepositEvent_busEvents_event_TimeUpdate {
|
||||
__typename: "TimeUpdate" | "MarketEvent" | "TransferResponses" | "PositionResolution" | "Order" | "Trade" | "Account" | "Party" | "MarginLevels" | "Proposal" | "Vote" | "MarketData" | "NodeSignature" | "LossSocialization" | "SettlePosition" | "Market" | "Asset" | "MarketTick" | "SettleDistressed" | "AuctionEvent" | "RiskFactor" | "Withdrawal" | "OracleSpec" | "LiquidityProvision";
|
||||
}
|
||||
|
||||
export interface DepositEvent_busEvents_event_Deposit {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal id of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
}
|
||||
|
||||
export type DepositEvent_busEvents_event = DepositEvent_busEvents_event_TimeUpdate | DepositEvent_busEvents_event_Deposit;
|
||||
|
||||
export interface DepositEvent_busEvents {
|
||||
__typename: "BusEvent";
|
||||
/**
|
||||
* the id for this event
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* the block hash
|
||||
*/
|
||||
block: string;
|
||||
/**
|
||||
* the type of event we're dealing with
|
||||
*/
|
||||
type: BusEventType;
|
||||
/**
|
||||
* the payload - the wrapped event
|
||||
*/
|
||||
event: DepositEvent_busEvents_event;
|
||||
}
|
||||
|
||||
export interface DepositEvent {
|
||||
/**
|
||||
* Subscribe to event data from the event bus
|
||||
*/
|
||||
busEvents: DepositEvent_busEvents[] | null;
|
||||
}
|
||||
|
||||
export interface DepositEventVariables {
|
||||
partyId: string;
|
||||
}
|
53
libs/market-list/src/lib/__generated__/DepositPage.ts
generated
Normal file
53
libs/market-list/src/lib/__generated__/DepositPage.ts
generated
Normal file
@ -0,0 +1,53 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: DepositPage
|
||||
// ====================================================
|
||||
|
||||
export interface DepositPage_assets_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface DepositPage_assets_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the erc20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type DepositPage_assets_source = DepositPage_assets_source_BuiltinAsset | DepositPage_assets_source_ERC20;
|
||||
|
||||
export interface DepositPage_assets {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an erc20 asset)
|
||||
*/
|
||||
source: DepositPage_assets_source;
|
||||
}
|
||||
|
||||
export interface DepositPage {
|
||||
/**
|
||||
* The list of all assets in use in the vega network
|
||||
*/
|
||||
assets: DepositPage_assets[] | null;
|
||||
}
|
27
libs/market-list/src/lib/__generated__/NetworkParamsQuery.ts
generated
Normal file
27
libs/market-list/src/lib/__generated__/NetworkParamsQuery.ts
generated
Normal file
@ -0,0 +1,27 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: NetworkParamsQuery
|
||||
// ====================================================
|
||||
|
||||
export interface NetworkParamsQuery_networkParameters {
|
||||
__typename: "NetworkParameter";
|
||||
/**
|
||||
* The name of the network parameter
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The value of the network parameter
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface NetworkParamsQuery {
|
||||
/**
|
||||
* return the full list of network parameters
|
||||
*/
|
||||
networkParameters: NetworkParamsQuery_networkParameters[] | null;
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
export * from './use-apply-grid-transaction';
|
||||
export * from './use-ethereum-transaction';
|
||||
export * from './use-ethereum-read-contract';
|
||||
export * from './use-data-provider';
|
||||
export * from './use-theme-switcher';
|
||||
export * from './use-bridge-contract';
|
||||
export * from './use-token-contract';
|
||||
|
21
libs/react-helpers/src/hooks/use-bridge-contract.ts
Normal file
21
libs/react-helpers/src/hooks/use-bridge-contract.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Networks } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useBridgeContract = () => {
|
||||
const { provider } = useWeb3React();
|
||||
|
||||
const contract = useMemo(() => {
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
return new VegaErc20Bridge(
|
||||
process.env['NX_VEGA_ENV'] as Networks,
|
||||
provider,
|
||||
provider?.getSigner()
|
||||
);
|
||||
}, [provider]);
|
||||
|
||||
return contract;
|
||||
};
|
70
libs/react-helpers/src/hooks/use-ethereum-read-contract.ts
Normal file
70
libs/react-helpers/src/hooks/use-ethereum-read-contract.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
|
||||
interface State<T> {
|
||||
data?: T;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
LOADING = 'LOADING',
|
||||
ERROR = 'ERROR',
|
||||
FETCHED = 'FETCHED',
|
||||
}
|
||||
|
||||
type Action<T> =
|
||||
| { type: ActionType.LOADING }
|
||||
| { type: ActionType.FETCHED; payload: T }
|
||||
| { type: ActionType.ERROR; error: Error };
|
||||
|
||||
export const useEthereumReadContract = <T>(
|
||||
contractFunc: () => Promise<T> | undefined
|
||||
) => {
|
||||
const cancelRequest = useRef(false);
|
||||
|
||||
const initialState: State<T> = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
|
||||
switch (action.type) {
|
||||
case ActionType.LOADING:
|
||||
return { ...state, loading: true };
|
||||
case ActionType.FETCHED:
|
||||
return { ...state, data: action.payload, loading: false };
|
||||
case ActionType.ERROR:
|
||||
return { ...state, error: action.error, loading: false };
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
|
||||
const fetchCallback = useCallback(async () => {
|
||||
dispatch({ type: ActionType.LOADING });
|
||||
try {
|
||||
const result = contractFunc();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const response = await result;
|
||||
if (cancelRequest.current) return;
|
||||
dispatch({ type: ActionType.FETCHED, payload: response });
|
||||
} catch (error) {
|
||||
if (cancelRequest.current) return;
|
||||
dispatch({ type: ActionType.ERROR, error: error as Error });
|
||||
}
|
||||
}, [contractFunc]);
|
||||
|
||||
useEffect(() => {
|
||||
cancelRequest.current = false;
|
||||
fetchCallback();
|
||||
|
||||
return () => {
|
||||
cancelRequest.current = true;
|
||||
};
|
||||
}, [fetchCallback]);
|
||||
|
||||
return { state, refetch: fetchCallback };
|
||||
};
|
122
libs/react-helpers/src/hooks/use-ethereum-transaction.test.tsx
Normal file
122
libs/react-helpers/src/hooks/use-ethereum-transaction.test.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { TxState } from './use-ethereum-transaction';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEthereumTransaction } from './use-ethereum-transaction';
|
||||
import type { ethers } from 'ethers';
|
||||
import { EthereumError } from '../lib/ethereum-error';
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
class MockContract {
|
||||
static txHash = 'tx-hash';
|
||||
confirmations = 0;
|
||||
depositAsset(args: {
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}): Promise<ethers.ContractTransaction> {
|
||||
return Promise.resolve({
|
||||
hash: MockContract.txHash,
|
||||
wait: () => {
|
||||
this.confirmations++;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
confirmations: this.confirmations,
|
||||
} as ethers.ContractReceipt),
|
||||
100
|
||||
);
|
||||
});
|
||||
},
|
||||
} as ethers.ContractTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
const mockContract = new MockContract();
|
||||
const requiredConfirmations = 3;
|
||||
|
||||
function setup(perform: () => void) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(
|
||||
// @ts-ignore force MockContract
|
||||
() => useEthereumTransaction(perform, requiredConfirmations),
|
||||
{ wrapper }
|
||||
);
|
||||
}
|
||||
|
||||
test('Ethereum transaction flow', async () => {
|
||||
const { result } = setup(() => {
|
||||
return mockContract.depositAsset({
|
||||
assetSource: 'asset-source',
|
||||
amount: '100',
|
||||
vegaPublicKey: 'vega-key',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
status: TxState.Default,
|
||||
txHash: null,
|
||||
error: null,
|
||||
confirmations: 0,
|
||||
perform: expect.any(Function),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.perform();
|
||||
});
|
||||
|
||||
expect(result.current.status).toEqual(TxState.Requested);
|
||||
expect(result.current.confirmations).toBe(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual(TxState.Pending);
|
||||
expect(result.current.txHash).toEqual(MockContract.txHash);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current.confirmations).toBe(1);
|
||||
expect(result.current.status).toEqual(TxState.Pending);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current.confirmations).toBe(2);
|
||||
expect(result.current.status).toEqual(TxState.Pending);
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current.confirmations).toBe(3);
|
||||
|
||||
// Now complete as required confirmations have been surpassed
|
||||
expect(result.current.status).toEqual(TxState.Complete);
|
||||
});
|
||||
|
||||
test('Error handling', async () => {
|
||||
const { result } = setup(() => {
|
||||
throw new EthereumError(errorMsg, 500);
|
||||
});
|
||||
|
||||
const errorMsg = 'test-error';
|
||||
|
||||
act(() => {
|
||||
result.current.perform();
|
||||
});
|
||||
|
||||
expect(result.current.status).toEqual(TxState.Error);
|
||||
expect(result.current.error instanceof EthereumError).toBe(true);
|
||||
expect(result.current.error?.message).toBe(errorMsg);
|
||||
});
|
72
libs/react-helpers/src/hooks/use-ethereum-transaction.ts
Normal file
72
libs/react-helpers/src/hooks/use-ethereum-transaction.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { ethers } from 'ethers';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { EthereumError, isEthereumError } from '../lib/ethereum-error';
|
||||
|
||||
export enum TxState {
|
||||
Default = 'Default',
|
||||
Requested = 'Requested',
|
||||
Pending = 'Pending',
|
||||
Complete = 'Complete',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export type TxError = Error | EthereumError;
|
||||
|
||||
export const useEthereumTransaction = <TArgs = void>(
|
||||
performTransaction: (
|
||||
args: TArgs
|
||||
) => Promise<ethers.ContractTransaction> | null,
|
||||
requiredConfirmations = 1
|
||||
) => {
|
||||
const [confirmations, setConfirmations] = useState(0);
|
||||
const [status, setStatus] = useState(TxState.Default);
|
||||
const [txHash, setTxHash] = useState<string | null>(null);
|
||||
const [error, setError] = useState<TxError | null>(null);
|
||||
|
||||
const perform = useCallback(
|
||||
async (args: TArgs) => {
|
||||
setConfirmations(0);
|
||||
setStatus(TxState.Requested);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = performTransaction(args);
|
||||
|
||||
if (res === null) {
|
||||
setStatus(TxState.Default);
|
||||
return;
|
||||
}
|
||||
|
||||
const tx = await res;
|
||||
|
||||
let receipt: ethers.ContractReceipt | null = null;
|
||||
|
||||
setTxHash(tx.hash);
|
||||
setStatus(TxState.Pending);
|
||||
|
||||
for (let i = 1; i <= requiredConfirmations; i++) {
|
||||
receipt = await tx.wait(i);
|
||||
setConfirmations(receipt.confirmations);
|
||||
}
|
||||
|
||||
if (!receipt) {
|
||||
throw new Error('No receipt after confirmations are met');
|
||||
}
|
||||
|
||||
setStatus(TxState.Complete);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err);
|
||||
} else if (isEthereumError(err)) {
|
||||
setError(new EthereumError(err.message, err.code));
|
||||
} else {
|
||||
setError(new Error('Something went wrong'));
|
||||
}
|
||||
setStatus(TxState.Error);
|
||||
}
|
||||
},
|
||||
[performTransaction, requiredConfirmations]
|
||||
);
|
||||
|
||||
return { perform, status, error, confirmations, txHash };
|
||||
};
|
23
libs/react-helpers/src/hooks/use-token-contract.ts
Normal file
23
libs/react-helpers/src/hooks/use-token-contract.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useTokenContract = (
|
||||
contractAddress?: string,
|
||||
faucetable = false
|
||||
): ERC20Token | null => {
|
||||
const { provider } = useWeb3React();
|
||||
const contract = useMemo(() => {
|
||||
if (!provider || !contractAddress) {
|
||||
return null;
|
||||
}
|
||||
return new ERC20Token(
|
||||
contractAddress,
|
||||
provider,
|
||||
provider.getSigner(),
|
||||
faucetable
|
||||
);
|
||||
}, [provider, contractAddress, faucetable]);
|
||||
|
||||
return contract;
|
||||
};
|
@ -4,5 +4,7 @@ export * from './lib/grid-cells';
|
||||
export * from './lib/storage';
|
||||
export * from './lib/generic-data-provider';
|
||||
export * from './lib/i18n';
|
||||
export * from './lib/ethereum-error';
|
||||
export * from './lib/validate';
|
||||
|
||||
export * from './hooks';
|
||||
|
26
libs/react-helpers/src/lib/ethereum-error.ts
Normal file
26
libs/react-helpers/src/lib/ethereum-error.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export class EthereumError extends Error {
|
||||
code: number;
|
||||
|
||||
constructor(message: string, code: number) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.name = 'EthereumError';
|
||||
}
|
||||
}
|
||||
|
||||
export const isEthereumError = (err: unknown): err is EthereumError => {
|
||||
if (typeof err === 'object' && err !== null && 'code' in err) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isExpectedEthereumError = (error: unknown) => {
|
||||
const EXPECTED_ERRORS = [4001];
|
||||
|
||||
if (isEthereumError(error) && EXPECTED_ERRORS.indexOf(error.code) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
38
libs/react-helpers/src/lib/validate/index.ts
Normal file
38
libs/react-helpers/src/lib/validate/index.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { t } from '../i18n';
|
||||
|
||||
export const required = (value: string) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return t('Required');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ethereumAddress = (value: string) => {
|
||||
if (!ethers.utils.isAddress(value)) {
|
||||
return t('Invalid Ethereum address');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const vegaPublicKey = (value: string) => {
|
||||
if (value.length !== 64 || !/^[A-Fa-f0-9]*$/i.test(value)) {
|
||||
return t('Invalid Vega key');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const minSafe = (min: BigNumber) => (value: string) => {
|
||||
if (new BigNumber(value).isLessThan(min)) {
|
||||
return t('Amount is below permitted minimum');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const maxSafe = (max: BigNumber) => (value: string) => {
|
||||
if (new BigNumber(value).isGreaterThan(max)) {
|
||||
return t('Amount is above permitted maximum');
|
||||
}
|
||||
return true;
|
||||
};
|
9
libs/types/src/__generated__/globalTypes.ts
generated
9
libs/types/src/__generated__/globalTypes.ts
generated
@ -67,6 +67,15 @@ export enum BusEventType {
|
||||
Withdrawal = "Withdrawal",
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of a deposit
|
||||
*/
|
||||
export enum DepositStatus {
|
||||
Cancelled = "Cancelled",
|
||||
Finalized = "Finalized",
|
||||
Open = "Open",
|
||||
}
|
||||
|
||||
/**
|
||||
* The current state of a market
|
||||
*/
|
||||
|
@ -6,5 +6,6 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/ui-toolkit',
|
||||
setupFiles: ['./src/setup-test-env.ts'],
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
@ -32,7 +32,10 @@ export function Dialog({
|
||||
<DialogPrimitives.Portal>
|
||||
<DialogPrimitives.Overlay className="fixed inset-0 bg-black/50 dark:bg-white/15" />
|
||||
<DialogPrimitives.Content className={contentClasses}>
|
||||
<DialogPrimitives.Close className="p-12 absolute top-0 right-0">
|
||||
<DialogPrimitives.Close
|
||||
className="p-12 absolute top-0 right-0"
|
||||
data-testid="dialog-close"
|
||||
>
|
||||
<Icon name="cross" />
|
||||
</DialogPrimitives.Close>
|
||||
{title && <h1 className="text-h5 mb-12">{title}</h1>}
|
||||
|
@ -1,51 +1,24 @@
|
||||
import React from 'react';
|
||||
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { EtherscanLink } from '.';
|
||||
import { EthereumChainIds, EthereumChainNames } from '../../utils/web3';
|
||||
|
||||
export default {
|
||||
title: 'EtherscanLink',
|
||||
component: EtherscanLink,
|
||||
argTypes: {
|
||||
chainId: {
|
||||
options: Object.values(EthereumChainIds),
|
||||
control: {
|
||||
type: 'select', // Type 'select' is automatically inferred when 'options' is defined
|
||||
labels: EthereumChainNames,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof EtherscanLink>;
|
||||
|
||||
const Template: ComponentStory<typeof EtherscanLink> = (args) => (
|
||||
<EtherscanLink {...args} />
|
||||
);
|
||||
|
||||
export const MainnetTx = Template.bind({});
|
||||
MainnetTx.args = {
|
||||
chainId: EthereumChainIds.Mainnet,
|
||||
export const Transaction = Template.bind({});
|
||||
Transaction.args = {
|
||||
tx: 'foo',
|
||||
text: 'View transaction on Etherscan',
|
||||
};
|
||||
|
||||
export const RopstenTx = Template.bind({});
|
||||
RopstenTx.args = {
|
||||
chainId: EthereumChainIds.Ropsten,
|
||||
tx: 'foo',
|
||||
text: 'View transaction on Etherscan',
|
||||
};
|
||||
|
||||
export const MainnetAddress = Template.bind({});
|
||||
MainnetAddress.args = {
|
||||
chainId: EthereumChainIds.Mainnet,
|
||||
address: 'foo',
|
||||
text: 'View transaction on Etherscan',
|
||||
};
|
||||
|
||||
export const RopstenAddress = Template.bind({});
|
||||
RopstenAddress.args = {
|
||||
chainId: EthereumChainIds.Ropsten,
|
||||
export const Address = Template.bind({});
|
||||
Address.args = {
|
||||
address: 'foo',
|
||||
text: 'View transaction on Etherscan',
|
||||
};
|
||||
|
@ -1,64 +1,35 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { EtherscanLink } from '.';
|
||||
import { EthereumChainIds } from '../../utils/web3';
|
||||
|
||||
test('It renders a link with the text', () => {
|
||||
render(
|
||||
<EtherscanLink text="foo" chainId={EthereumChainIds.Mainnet} tx="tx" />
|
||||
);
|
||||
render(<EtherscanLink text="foo" tx="tx" />);
|
||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('It renders a link with the tx hash if no text is provided', () => {
|
||||
render(<EtherscanLink chainId={EthereumChainIds.Mainnet} tx="tx" />);
|
||||
render(<EtherscanLink tx="tx" />);
|
||||
expect(screen.getByText('tx')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('It renders a link with the address if no text is provided', () => {
|
||||
render(
|
||||
<EtherscanLink chainId={EthereumChainIds.Mainnet} address="address" />
|
||||
);
|
||||
render(<EtherscanLink address="address" />);
|
||||
expect(screen.getByText('address')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('It links to etherscan if network is mainnet', () => {
|
||||
render(
|
||||
<EtherscanLink chainId={EthereumChainIds.Mainnet} address="address" />
|
||||
);
|
||||
test('It links to etherscan address', () => {
|
||||
const hash = 'hash';
|
||||
render(<EtherscanLink address={hash} />);
|
||||
expect(screen.getByTestId('etherscan-link')).toHaveAttribute(
|
||||
'href',
|
||||
'https://etherscan.io/address/address'
|
||||
`${process.env['NX_ETHERSCAN_URL']}/address/${hash}`
|
||||
);
|
||||
});
|
||||
|
||||
test('It links to ropsten etherscan if network is ropsten', () => {
|
||||
render(
|
||||
<EtherscanLink chainId={EthereumChainIds.Ropsten} address="address" />
|
||||
);
|
||||
test('It links to etherscan transaction', () => {
|
||||
const hash = 'hash';
|
||||
render(<EtherscanLink tx={hash} />);
|
||||
expect(screen.getByTestId('etherscan-link')).toHaveAttribute(
|
||||
'href',
|
||||
'https://ropsten.etherscan.io/address/address'
|
||||
`${process.env['NX_ETHERSCAN_URL']}/tx/${hash}`
|
||||
);
|
||||
});
|
||||
|
||||
test("Doesn't render for address if chainid is null", () => {
|
||||
render(<EtherscanLink chainId={null} address="address" />);
|
||||
expect(screen.queryByTestId('etherscan-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Doesn't render for tx if chainid is null", () => {
|
||||
render(<EtherscanLink chainId={null} tx="tx" />);
|
||||
expect(screen.queryByTestId('etherscan-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Doesn't render for address if chainid is unknown", () => {
|
||||
// @ts-ignore wrong chaindId passed
|
||||
render(<EtherscanLink chainId={'foo'} address="address" />);
|
||||
expect(screen.queryByTestId('etherscan-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Doesn't render for tx if chainid is unknown", () => {
|
||||
// @ts-ignore wrong chaindId passed
|
||||
render(<EtherscanLink chainId={'foo'} tx="tx" />);
|
||||
expect(screen.queryByTestId('etherscan-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -1,16 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import type { AnchorHTMLAttributes } from 'react';
|
||||
import React from 'react';
|
||||
import type { EthereumChainId } from '../../utils/web3';
|
||||
|
||||
const etherscanUrls: Record<EthereumChainId, string> = {
|
||||
'0x1': 'https://etherscan.io',
|
||||
'0x3': 'https://ropsten.etherscan.io',
|
||||
'0x4': 'https://rinkeby.etherscan.io',
|
||||
'0x5': 'https://goerli.etherscan.io',
|
||||
'0x2a': 'https://kovan.etherscan.io',
|
||||
};
|
||||
const ETHERSCAN_URL = process.env['NX_ETHERSCAN_URL'] as string;
|
||||
|
||||
interface BaseEtherscanLinkProps {
|
||||
chainId: EthereumChainId | null;
|
||||
interface BaseEtherscanLinkProps
|
||||
extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
@ -30,23 +25,22 @@ type EtherscanLinkProps =
|
||||
* Form an HTML link tag pointing to an appropriate Etherscan page
|
||||
*/
|
||||
export const EtherscanLink = ({
|
||||
chainId,
|
||||
text,
|
||||
className,
|
||||
...props
|
||||
}: EtherscanLinkProps) => {
|
||||
let hash: string;
|
||||
let txLink: string | null;
|
||||
const createLink = React.useMemo(
|
||||
() => etherscanLinkCreator(chainId),
|
||||
[chainId]
|
||||
);
|
||||
const anchorClasses = classNames('underline', className);
|
||||
|
||||
if ('tx' in props) {
|
||||
hash = props.tx;
|
||||
txLink = createLink ? createLink.tx(hash) : null;
|
||||
} else {
|
||||
txLink = `${ETHERSCAN_URL}/tx/${hash}`;
|
||||
} else if ('address' in props) {
|
||||
hash = props.address;
|
||||
txLink = createLink ? createLink.address(hash) : null;
|
||||
txLink = `${ETHERSCAN_URL}/address/${hash}`;
|
||||
} else {
|
||||
throw new Error('Must provider either "tx" or "address" prop');
|
||||
}
|
||||
|
||||
const linkText = text ? text : hash;
|
||||
@ -62,28 +56,11 @@ export const EtherscanLink = ({
|
||||
href={txLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="etherscan-link"
|
||||
className={anchorClasses}
|
||||
>
|
||||
{linkText}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
function etherscanLinkCreator(chainId: EthereumChainId | null) {
|
||||
if (!chainId) return null;
|
||||
|
||||
const url = etherscanUrls[chainId];
|
||||
|
||||
return {
|
||||
tx: (tx: string) => {
|
||||
if (!url) return null;
|
||||
return `${url}/tx/${tx}`;
|
||||
},
|
||||
address: (address: string) => {
|
||||
if (!url) return null;
|
||||
return `${url}/address/${address}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
EtherscanLink.displayName = 'EtherScanLink';
|
||||
|
@ -14,6 +14,8 @@ export const Icon = ({ size = 16, name, className }: IconProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
'inline-block',
|
||||
'fill-current',
|
||||
'align-text-bottom',
|
||||
'fill-current',
|
||||
{
|
||||
'w-20': size === 20,
|
||||
'h-20': size === 20,
|
||||
|
@ -39,6 +39,7 @@ export const InputError = ({
|
||||
data-testid="input-error-text"
|
||||
className={effectiveClassName}
|
||||
{...props}
|
||||
role="alert"
|
||||
>
|
||||
<Icon name="warning-sign" className={iconClassName} />
|
||||
{children}
|
||||
|
@ -70,6 +70,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
if (appendIconName) {
|
||||
className += ' pr-28';
|
||||
}
|
||||
|
||||
const input = (
|
||||
<input
|
||||
{...props}
|
||||
|
@ -20,7 +20,7 @@ export const Loader = ({ size = 'large' }: LoaderProps) => {
|
||||
const items = size === 'small' ? 9 : 16;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center" data-testid="splash-loader">
|
||||
<div className="flex flex-col items-center" data-testid="loader">
|
||||
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||
{new Array(items).fill(null).map((_, i) => {
|
||||
return (
|
||||
|
@ -0,0 +1,94 @@
|
||||
import { EtherscanLink } from '../etherscan-link';
|
||||
import { t, TxState } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const ACTIVE_CLASSES = 'text-black dark:text-white';
|
||||
|
||||
export const ConfirmRow = ({ status }: { status: TxState }) => {
|
||||
if (status === TxState.Requested) {
|
||||
return (
|
||||
<p className="text-black dark:text-white">
|
||||
{t('Confirm transaction in wallet')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t('Confirmed in wallet')}</p>;
|
||||
};
|
||||
|
||||
interface TxRowProps {
|
||||
status: TxState;
|
||||
txHash: string | null;
|
||||
confirmations: number;
|
||||
requiredConfirmations: number;
|
||||
highlightComplete?: boolean;
|
||||
}
|
||||
|
||||
export const TxRow = ({
|
||||
status,
|
||||
txHash,
|
||||
confirmations,
|
||||
requiredConfirmations,
|
||||
highlightComplete = true,
|
||||
}: TxRowProps) => {
|
||||
if (status === TxState.Pending) {
|
||||
return (
|
||||
<p className={`flex justify-between ${ACTIVE_CLASSES}`}>
|
||||
<span>
|
||||
{t(
|
||||
`Awaiting Ethereum transaction ${confirmations}/${requiredConfirmations} confirmations...`
|
||||
)}
|
||||
</span>
|
||||
<EtherscanLink
|
||||
tx={txHash || ''}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
text={t('View on Etherscan')}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === TxState.Complete) {
|
||||
return (
|
||||
<p
|
||||
className={`flex justify-between ${
|
||||
highlightComplete ? ACTIVE_CLASSES : ''
|
||||
}`}
|
||||
>
|
||||
<span>{t('Ethereum transaction complete')}</span>
|
||||
<EtherscanLink
|
||||
tx={txHash || ''}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
text={t('View on Etherscan')}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t('Await Ethereum transaction')}</p>;
|
||||
};
|
||||
|
||||
interface ConfirmationEventRowProps {
|
||||
status: TxState;
|
||||
confirmed: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmationEventRow = ({
|
||||
status,
|
||||
confirmed,
|
||||
}: ConfirmationEventRowProps) => {
|
||||
if (status !== TxState.Complete) {
|
||||
return <p>{t('Vega confirmation')}</p>;
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
return (
|
||||
<p className="text-black dark:text-white">
|
||||
{t('Vega is confirming your transaction...')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-black dark:text-white">{t('Transaction confirmed')}</p>
|
||||
);
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface DialogWrapperProps {
|
||||
children: ReactNode;
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const DialogWrapper = ({
|
||||
children,
|
||||
icon,
|
||||
title,
|
||||
}: DialogWrapperProps) => {
|
||||
return (
|
||||
<div className="flex gap-12 max-w-full text-ui">
|
||||
<div className="pt-8 fill-current">{icon}</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-h4 text-black dark:text-white capitalize mb-12">
|
||||
{title}
|
||||
</h1>
|
||||
<div className="text-black-40 dark:text-white-40">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './transaction-dialog';
|
@ -0,0 +1,106 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { EthereumError, TxState } from '@vegaprotocol/react-helpers';
|
||||
import type { TransactionDialogProps } from './transaction-dialog';
|
||||
import { TransactionDialog } from './transaction-dialog';
|
||||
|
||||
jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({
|
||||
chainId: 3,
|
||||
}),
|
||||
}));
|
||||
|
||||
let props: TransactionDialogProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
name: 'test',
|
||||
status: TxState.Default,
|
||||
txHash: null,
|
||||
error: null,
|
||||
confirmations: 1,
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (moreProps?: Partial<TransactionDialogProps>) => {
|
||||
return <TransactionDialog {...props} {...moreProps} />;
|
||||
};
|
||||
|
||||
test('Opens when tx starts and closes if the user rejects the tx', () => {
|
||||
const { container, rerender } = render(generateJsx());
|
||||
|
||||
// Dialog closed by default
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
|
||||
rerender(generateJsx({ status: TxState.Pending }));
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
// User rejecting the tx closes the dialog
|
||||
rerender(
|
||||
generateJsx({
|
||||
status: TxState.Error,
|
||||
error: new EthereumError('User rejected', 4001),
|
||||
})
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('Doesn\t repoen if user dismissed the dialog', () => {
|
||||
const { container, rerender } = render(
|
||||
generateJsx({ status: TxState.Pending })
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('dialog-close'));
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
|
||||
rerender(generateJsx({ status: TxState.Complete }));
|
||||
|
||||
// Should still be closed even though tx updated
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('Dialog states', () => {
|
||||
const { rerender } = render(generateJsx({ status: TxState.Requested }));
|
||||
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirm transaction in wallet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Await Ethereum transaction')).toBeInTheDocument();
|
||||
|
||||
rerender(generateJsx({ status: TxState.Pending, confirmations: 0 }));
|
||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Awaiting Ethereum transaction 0/1 confirmations...')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('etherscan-link')).toBeInTheDocument();
|
||||
|
||||
rerender(generateJsx({ status: TxState.Complete, confirmations: 1 }));
|
||||
expect(screen.getByText(`${props.name} complete`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
||||
|
||||
const errorMsg = 'Something went wrong';
|
||||
rerender(generateJsx({ status: TxState.Error, error: new Error(errorMsg) }));
|
||||
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
||||
expect(screen.getByText(errorMsg)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Success state waits for confirmation event if provided', () => {
|
||||
const { rerender } = render(
|
||||
generateJsx({ status: TxState.Complete, confirmed: false })
|
||||
);
|
||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Vega is confirming your transaction...')
|
||||
).toBeInTheDocument();
|
||||
|
||||
// @ts-ignore enforce truthy on confirmation event
|
||||
rerender(generateJsx({ confirmed: true, status: TxState.Complete }));
|
||||
expect(
|
||||
screen.queryByText('Vega is confirming your transaction...')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Transaction confirmed')).toBeInTheDocument();
|
||||
});
|
@ -0,0 +1,133 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
t,
|
||||
TxState,
|
||||
isExpectedEthereumError,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
||||
import { DialogWrapper } from './dialog-wrapper';
|
||||
import { Loader } from '../loader';
|
||||
import { Intent } from '../../utils/intent';
|
||||
import { Dialog } from '../dialog';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
export interface TransactionDialogProps {
|
||||
name: string;
|
||||
status: TxState;
|
||||
error: Error | null;
|
||||
confirmations: number;
|
||||
txHash: string | null;
|
||||
requiredConfirmations?: number;
|
||||
// Undefined means this dialog isn't expecting an additional event for a complete state, a boolean
|
||||
// value means it is but hasn't been received yet
|
||||
confirmed?: boolean;
|
||||
}
|
||||
|
||||
export const TransactionDialog = ({
|
||||
name,
|
||||
status,
|
||||
error,
|
||||
confirmations,
|
||||
txHash,
|
||||
requiredConfirmations = 1,
|
||||
confirmed,
|
||||
}: TransactionDialogProps) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const dialogDismissed = useRef(false);
|
||||
|
||||
const renderContent = () => {
|
||||
if (status === TxState.Error) {
|
||||
return (
|
||||
<p className="text-black dark:text-white">{error && error.message}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmRow status={status} />
|
||||
<TxRow
|
||||
status={status}
|
||||
txHash={txHash}
|
||||
confirmations={confirmations}
|
||||
requiredConfirmations={requiredConfirmations}
|
||||
highlightComplete={false}
|
||||
/>
|
||||
{confirmed !== undefined && (
|
||||
<ConfirmationEventRow status={status} confirmed={confirmed} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getWrapperProps = () => {
|
||||
const propsMap = {
|
||||
[TxState.Error]: {
|
||||
title: t(`${name} failed`),
|
||||
icon: <Icon name="warning-sign" size={20} />,
|
||||
intent: Intent.Danger,
|
||||
},
|
||||
[TxState.Requested]: {
|
||||
title: t('Confirm transaction'),
|
||||
icon: <Icon name="hand-up" size={20} />,
|
||||
intent: Intent.Prompt,
|
||||
},
|
||||
[TxState.Pending]: {
|
||||
title: t(`${name} pending`),
|
||||
icon: <Loader />,
|
||||
intent: Intent.Progress,
|
||||
},
|
||||
[TxState.Complete]: {
|
||||
title: t(`${name} complete`),
|
||||
icon: <Icon name="tick" />,
|
||||
intent: Intent.Success,
|
||||
},
|
||||
};
|
||||
|
||||
// Dialog not showing
|
||||
if (status === TxState.Default) {
|
||||
return { intent: undefined, title: '', icon: null };
|
||||
}
|
||||
|
||||
// Confirmation event bool is required so
|
||||
if (confirmed !== undefined) {
|
||||
// Vega has confirmed Tx
|
||||
if (confirmed === true) {
|
||||
return propsMap[TxState.Complete];
|
||||
}
|
||||
// Tx is complete but still awaiting for Vega to confirm
|
||||
else if (status === TxState.Complete) {
|
||||
return propsMap[TxState.Pending];
|
||||
}
|
||||
}
|
||||
|
||||
return propsMap[status];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Close dialog if error is due to user rejecting the tx
|
||||
if (status === TxState.Error && isExpectedEthereumError(error)) {
|
||||
setDialogOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status !== TxState.Default && !dialogDismissed.current) {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
}, [status, error]);
|
||||
|
||||
const { intent, ...wrapperProps } = getWrapperProps();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onChange={(isOpen) => {
|
||||
setDialogOpen(isOpen);
|
||||
dialogDismissed.current = true;
|
||||
}}
|
||||
intent={intent}
|
||||
>
|
||||
<DialogWrapper {...wrapperProps}>{renderContent()}</DialogWrapper>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -1,12 +1,9 @@
|
||||
import * as EthereumUtils from './utils/web3';
|
||||
|
||||
// Components
|
||||
export { AgGridLazy, AgGridDynamic } from './components/ag-grid';
|
||||
export { AsyncRenderer } from './components/async-renderer';
|
||||
export { Button, AnchorButton } from './components/button';
|
||||
export { Callout } from './components/callout';
|
||||
export { CopyWithTooltip } from './components/copy-with-tooltip';
|
||||
export { EthereumUtils };
|
||||
export { EtherscanLink } from './components/etherscan-link';
|
||||
export { FormGroup } from './components/form-group';
|
||||
export { Icon } from './components/icon';
|
||||
@ -23,6 +20,7 @@ export { VegaLogo } from './components/vega-logo';
|
||||
export { Tooltip } from './components/tooltip';
|
||||
export { Indicator } from './components/indicator';
|
||||
export { Card } from './components/card';
|
||||
export { TransactionDialog } from './components/transaction-dialog';
|
||||
|
||||
// Utils
|
||||
export * from './utils/intent';
|
||||
|
1
libs/ui-toolkit/src/setup-test-env.ts
Normal file
1
libs/ui-toolkit/src/setup-test-env.ts
Normal file
@ -0,0 +1 @@
|
||||
process.env['NX_ETHERSCAN_URL'] = 'https://etherscan.io';
|
@ -1,5 +1,3 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export enum Intent {
|
||||
Danger = 'danger',
|
||||
Warning = 'warning',
|
||||
@ -19,14 +17,15 @@ export enum TailwindIntents {
|
||||
}
|
||||
|
||||
export const getIntentShadow = (intent?: Intent) => {
|
||||
return classNames('shadow-callout', {
|
||||
return {
|
||||
'shadow-callout': true,
|
||||
'shadow-intent-danger': intent === Intent.Danger,
|
||||
'shadow-intent-warning': intent === Intent.Warning,
|
||||
'shadow-intent-prompt': intent === Intent.Prompt,
|
||||
'shadow-black dark:shadow-white': intent === Intent.Progress,
|
||||
'shadow-intent-success': intent === Intent.Success,
|
||||
'shadow-intent-help': intent === Intent.Help,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getVariantBackground = (variant?: TailwindIntents) => {
|
||||
|
@ -1,23 +0,0 @@
|
||||
export type EthereumChainId = '0x1' | '0x3' | '0x4' | '0x5' | '0x2a';
|
||||
export type EthereumChainName =
|
||||
| 'Mainnet'
|
||||
| 'Ropsten'
|
||||
| 'Rinkeby'
|
||||
| 'Goerli'
|
||||
| 'Kovan';
|
||||
|
||||
export const EthereumChainNames: Record<EthereumChainId, EthereumChainName> = {
|
||||
'0x1': 'Mainnet',
|
||||
'0x3': 'Ropsten',
|
||||
'0x4': 'Rinkeby',
|
||||
'0x5': 'Goerli',
|
||||
'0x2a': 'Kovan',
|
||||
};
|
||||
|
||||
export const EthereumChainIds: Record<EthereumChainName, EthereumChainId> = {
|
||||
Mainnet: '0x1',
|
||||
Ropsten: '0x3',
|
||||
Rinkeby: '0x4',
|
||||
Goerli: '0x5',
|
||||
Kovan: '0x2a',
|
||||
};
|
@ -24,6 +24,7 @@
|
||||
"@sentry/tracing": "^6.19.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@vegaprotocol/smart-contracts-sdk": "^1.6.0",
|
||||
"@vegaprotocol/vegawallet-service-api-client": "^0.4.6",
|
||||
"@walletconnect/ethereum-provider": "^1.7.5",
|
||||
"@web3-react/core": "8.0.20-beta.0",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"paths": {
|
||||
"@vegaprotocol/cypress": ["libs/cypress/src/index.ts"],
|
||||
"@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"],
|
||||
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
|
||||
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
||||
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
|
||||
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],
|
||||
|
@ -3,6 +3,7 @@
|
||||
"projects": {
|
||||
"cypress": "libs/cypress",
|
||||
"deal-ticket": "libs/deal-ticket",
|
||||
"deposits": "libs/deposits",
|
||||
"explorer": "apps/explorer",
|
||||
"explorer-e2e": "apps/explorer-e2e",
|
||||
"market-list": "libs/market-list",
|
||||
|
89
yarn.lock
89
yarn.lock
@ -1584,6 +1584,13 @@
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/bytes@5.6.1":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7"
|
||||
integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/constants@5.6.0", "@ethersproject/constants@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.0.tgz#55e3eb0918584d3acc0688e9958b0cedef297088"
|
||||
@ -1678,6 +1685,13 @@
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/networks@5.6.1":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.1.tgz#7a21ed1f83e86121737b16841961ec99ccf5c9c7"
|
||||
integrity sha512-b2rrupf3kCTcc3jr9xOWBuHylSFtbpJf79Ga7QR98ienU2UqGimPGEsYMgbI29KHJfA5Us89XwGVmxrlxmSrMg==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz#04fcc2d7c6bff88393f5b4237d906a192426685a"
|
||||
@ -1718,6 +1732,31 @@
|
||||
bech32 "1.1.4"
|
||||
ws "7.4.6"
|
||||
|
||||
"@ethersproject/providers@5.6.2":
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.2.tgz#b9807b1c8c6f59fa2ee4b3cf6519724d07a9f422"
|
||||
integrity sha512-6/EaFW/hNWz+224FXwl8+HdMRzVHt8DpPmu5MZaIQqx/K/ELnC9eY236SMV7mleCM3NnEArFwcAAxH5kUUgaRg==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-provider" "^5.6.0"
|
||||
"@ethersproject/abstract-signer" "^5.6.0"
|
||||
"@ethersproject/address" "^5.6.0"
|
||||
"@ethersproject/basex" "^5.6.0"
|
||||
"@ethersproject/bignumber" "^5.6.0"
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
"@ethersproject/constants" "^5.6.0"
|
||||
"@ethersproject/hash" "^5.6.0"
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
"@ethersproject/networks" "^5.6.0"
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/random" "^5.6.0"
|
||||
"@ethersproject/rlp" "^5.6.0"
|
||||
"@ethersproject/sha2" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
"@ethersproject/transactions" "^5.6.0"
|
||||
"@ethersproject/web" "^5.6.0"
|
||||
bech32 "1.1.4"
|
||||
ws "7.4.6"
|
||||
|
||||
"@ethersproject/providers@^5.6.0":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.1.tgz#9a05f00ecbac59565bf6907c8d2af8ac33303b48"
|
||||
@ -5847,6 +5886,15 @@
|
||||
"@typescript-eslint/types" "5.14.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@vegaprotocol/smart-contracts-sdk@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@vegaprotocol/smart-contracts-sdk/-/smart-contracts-sdk-1.6.0.tgz#c689d5928148113dddbe39ed963c74817f432d5e"
|
||||
integrity sha512-cNFDywrtrvR3V5WQL5BadMS8pckRaetXyxA5ttLZEghlpkMcz5BQ51GuAfK0ufank8vY3xYN0m1DalbfADQIGw==
|
||||
dependencies:
|
||||
deprecated-decorator "^0.1.6"
|
||||
ethers "^5.5.2"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@vegaprotocol/vegawallet-service-api-client@^0.4.6":
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@vegaprotocol/vegawallet-service-api-client/-/vegawallet-service-api-client-0.4.6.tgz#476d3dc5b91ed79f739b672e7260a4d88210f93a"
|
||||
@ -9873,6 +9921,11 @@ depd@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||
|
||||
deprecated-decorator@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37"
|
||||
integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=
|
||||
|
||||
deps-sort@^2.0.0, deps-sort@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d"
|
||||
@ -10883,6 +10936,42 @@ etag@1.8.1, etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
ethers@^5.5.2:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.2.tgz#e75bac7f038c5e0fdde667dba62fc223924143a2"
|
||||
integrity sha512-EzGCbns24/Yluu7+ToWnMca3SXJ1Jk1BvWB7CCmVNxyOeM4LLvw2OLuIHhlkhQk1dtOcj9UMsdkxUh8RiG1dxQ==
|
||||
dependencies:
|
||||
"@ethersproject/abi" "5.6.0"
|
||||
"@ethersproject/abstract-provider" "5.6.0"
|
||||
"@ethersproject/abstract-signer" "5.6.0"
|
||||
"@ethersproject/address" "5.6.0"
|
||||
"@ethersproject/base64" "5.6.0"
|
||||
"@ethersproject/basex" "5.6.0"
|
||||
"@ethersproject/bignumber" "5.6.0"
|
||||
"@ethersproject/bytes" "5.6.1"
|
||||
"@ethersproject/constants" "5.6.0"
|
||||
"@ethersproject/contracts" "5.6.0"
|
||||
"@ethersproject/hash" "5.6.0"
|
||||
"@ethersproject/hdnode" "5.6.0"
|
||||
"@ethersproject/json-wallets" "5.6.0"
|
||||
"@ethersproject/keccak256" "5.6.0"
|
||||
"@ethersproject/logger" "5.6.0"
|
||||
"@ethersproject/networks" "5.6.1"
|
||||
"@ethersproject/pbkdf2" "5.6.0"
|
||||
"@ethersproject/properties" "5.6.0"
|
||||
"@ethersproject/providers" "5.6.2"
|
||||
"@ethersproject/random" "5.6.0"
|
||||
"@ethersproject/rlp" "5.6.0"
|
||||
"@ethersproject/sha2" "5.6.0"
|
||||
"@ethersproject/signing-key" "5.6.0"
|
||||
"@ethersproject/solidity" "5.6.0"
|
||||
"@ethersproject/strings" "5.6.0"
|
||||
"@ethersproject/transactions" "5.6.0"
|
||||
"@ethersproject/units" "5.6.0"
|
||||
"@ethersproject/wallet" "5.6.0"
|
||||
"@ethersproject/web" "5.6.0"
|
||||
"@ethersproject/wordlists" "5.6.0"
|
||||
|
||||
ethers@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.0.tgz#924eb965dc03963fad0a09ce687efdf49aca3b45"
|
||||
|
Loading…
Reference in New Issue
Block a user